Skip to content

Commit

Permalink
Merge pull request #354 from vincent4vx/feature-ai-return-spell-and-d…
Browse files Browse the repository at this point in the history
…amage

feat(ai): Handle reflected damage and spell return effect buffs
  • Loading branch information
vincent4vx authored Jul 5, 2024
2 parents bffce79 + 4f17e80 commit f3530ae
Show file tree
Hide file tree
Showing 15 changed files with 556 additions and 47 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
strategy:
matrix:
java_version: [8, 11, 14, 17]
fail-fast: false

steps:
- uses: actions/checkout@v2
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/fr/quatrevieux/araknemu/game/GameModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.PercentLifeDamageSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.PercentLifeLostDamageSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.PunishmentSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.ReflectDamageSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.RemovePointsSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.SetStateSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.SkipTurnSimulator;
Expand Down Expand Up @@ -1036,8 +1037,8 @@ private void configureServices(ContainerConfigurator configurator) {

// Armors
simulator.registerEffectAndBuff(105, new ArmorSimulator());
simulator.register(106, new SpellReturnSimulator(20));
simulator.register(107, new AlterCharacteristicSimulator(5)); // Reflect damage. Considered as simple characteristic boost to simplify the code
simulator.registerEffectAndBuff(106, new SpellReturnSimulator(20));
simulator.registerEffectAndBuff(107, new ReflectDamageSimulator(5));
simulator.registerEffectAndBuff(265, new ArmorSimulator());

// Heal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,14 @@ public SpellScore score(Spell spell, Characteristics characteristics) {
*
* @return The modified damage
*
* @see BuffEffectSimulator#onReduceableDamage(Buff, FighterData, Damage) The called buff method
* @see BuffEffectSimulator#onReduceableDamage(CastSimulation simulation, Buff, FighterData, Damage) The called buff method
*/
public Damage applyReduceableDamageBuffs(FighterData target, Damage damage) {
public Damage applyReduceableDamageBuffs(CastSimulation simulation, FighterData target, Damage damage) {
for (Buff buff : target.buffs()) {
final @Nullable BuffEffectSimulator buffSimulator = buffSimulators.get(buff.effect().effect());

if (buffSimulator != null) {
damage = buffSimulator.onReduceableDamage(buff, target, damage);
damage = buffSimulator.onReduceableDamage(simulation, buff, target, damage);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,16 @@ private Interval applyResistances(Interval baseDamageInterval, FighterData targe
return baseDamageInterval.map(value -> Asserter.castNonNegative(createDamage(value, target).value()));
}

private Interval applyResistancesAndArmor(Interval baseDamageInterval, CastSimulation simulation, FighterData target) {
return baseDamageInterval.map(value -> Asserter.castNonNegative(createDamageWithArmor(value, simulation, target).value()));
private IntervalAndReflectedDamage applyResistancesAndArmor(Interval baseDamageInterval, CastSimulation simulation, FighterData target) {
final int baseCounterDamage = Math.max(target.characteristics().get(Characteristic.COUNTER_DAMAGE), 0);
final Damage min = createDamageWithArmor(baseDamageInterval.min(), simulation, target);
final Damage max = createDamageWithArmor(baseDamageInterval.max(), simulation, target);
final Interval reflectedDamage = Interval.of(min.reflectedDamage(), max.reflectedDamage()).map(v -> v + baseCounterDamage);

return new IntervalAndReflectedDamage(
Interval.of(Math.max(min.value(), 0), Math.max(max.value(), 0)),
reflectedDamage
);
}

private Damage createDamage(@NonNegative int baseDamage, FighterData target) {
Expand All @@ -91,14 +99,7 @@ private Damage createDamage(@NonNegative int baseDamage, FighterData target) {
}

private Damage createDamageWithArmor(@NonNegative int baseDamage, CastSimulation simulation, FighterData target) {
final Damage damage = simulator.applyReduceableDamageBuffs(target, createDamage(baseDamage, target));
final int reflectedDamage = damage.reflectedDamage() + target.characteristics().get(Characteristic.COUNTER_DAMAGE);

if (reflectedDamage > 0) {
simulation.addDamage(Interval.of(reflectedDamage), simulation.caster());
}

return damage;
return simulator.applyReduceableDamageBuffs(simulation, target, createDamage(baseDamage, target));
}

private void simulatePoison(CastSimulation simulation, Interval damage, @GTENegativeOne int duration, Collection<? extends FighterData> targets) {
Expand All @@ -111,7 +112,20 @@ private void simulatePoison(CastSimulation simulation, Interval damage, @GTENega

private void simulateDamage(CastSimulation simulation, Interval damage, Collection<? extends FighterData> targets) {
for (FighterData target : targets) {
simulation.addDamage(applyResistancesAndArmor(damage, simulation, target), target);
final IntervalAndReflectedDamage value = applyResistancesAndArmor(damage, simulation, target);

simulation.addDamage(value.interval, target);
simulation.addDamage(value.reflectedDamage, simulation.caster());
}
}

private static final class IntervalAndReflectedDamage {
private final Interval interval;
private final Interval reflectedDamage;

private IntervalAndReflectedDamage(Interval interval, Interval reflectedDamage) {
this.interval = interval;
this.reflectedDamage = reflectedDamage;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public void score(SpellScore score, SpellEffect effect, Characteristics characte
}

@Override
public Damage onReduceableDamage(Buff buff, FighterData target, Damage damage) {
public Damage onReduceableDamage(CastSimulation simulation, Buff buff, FighterData target, Damage damage) {
if (!supportsElement(buff, damage.element())) {
return damage;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package fr.quatrevieux.araknemu.game.fight.ai.simulation.effect;

import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation;
import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff;
import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.Damage;
import fr.quatrevieux.araknemu.game.fight.fighter.FighterData;
Expand All @@ -35,9 +36,9 @@ public interface BuffEffectSimulator {
* @param damage Computed damage before reduction
*
* @return The reduced damage
* @see fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator#applyReduceableDamageBuffs(FighterData, Damage) Caller of this method
* @see fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator#applyReduceableDamageBuffs(CastSimulation, FighterData, Damage) Caller of this method
*/
public default Damage onReduceableDamage(Buff buff, FighterData target, Damage damage) {
public default Damage onReduceableDamage(CastSimulation simulation, Buff buff, FighterData target, Damage damage) {
return damage;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* This file is part of Araknemu.
*
* Araknemu is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Araknemu is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Araknemu. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (c) 2017-2024 Vincent Quatrevieux
*/

package fr.quatrevieux.araknemu.game.fight.ai.simulation.effect;

import fr.quatrevieux.araknemu.data.constant.Characteristic;
import fr.quatrevieux.araknemu.game.fight.ai.AI;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.SpellScore;
import fr.quatrevieux.araknemu.game.fight.castable.CastScope;
import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff;
import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.Damage;
import fr.quatrevieux.araknemu.game.fight.fighter.FighterData;
import fr.quatrevieux.araknemu.game.fight.map.BattlefieldCell;
import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect;
import fr.quatrevieux.araknemu.game.world.creature.characteristics.Characteristics;

/**
* Handle simulator for reflect damage effect
*
* For simulation, will be considered as simple characteristic boost to simplify the code.
* Also provides a buff effect simulator for compute reflected damage.
*
* @see fr.quatrevieux.araknemu.game.fight.castable.effect.handler.armor.ReflectDamageHandler The actual effect handler
*/
public final class ReflectDamageSimulator implements EffectSimulator, BuffEffectSimulator {
private final EffectSimulator baseSimulator;

/**
* @param multiplier The value multiplier for buff score. Forwards to {@link AlterCharacteristicSimulator}.
*/
public ReflectDamageSimulator(int multiplier) {
this.baseSimulator = new AlterCharacteristicSimulator(multiplier);
}

@Override
public void simulate(CastSimulation simulation, AI ai, CastScope.EffectScope<? extends FighterData, ? extends BattlefieldCell> effect) {
baseSimulator.simulate(simulation, ai, effect);
}

@Override
public void score(SpellScore score, SpellEffect effect, Characteristics characteristics) {
baseSimulator.score(score, effect, characteristics);
}

@Override
public Damage onReduceableDamage(CastSimulation simulation, Buff buff, FighterData target, Damage damage) {
final int wisdom = buff.target().characteristics().get(Characteristic.WISDOM);
final int baseReflect = buff.effect().min();
final int reflect = baseReflect + baseReflect * wisdom / 100;

if (reflect > 0) {
damage.reflect(reflect);
}

return damage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import fr.quatrevieux.araknemu.game.fight.ai.simulation.SpellScore;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.util.Formula;
import fr.quatrevieux.araknemu.game.fight.castable.CastScope;
import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff;
import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.Damage;
import fr.quatrevieux.araknemu.game.fight.fighter.FighterData;
import fr.quatrevieux.araknemu.game.fight.map.BattlefieldCell;
import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect;
Expand All @@ -35,7 +37,7 @@
*
* @see fr.quatrevieux.araknemu.game.fight.castable.effect.handler.armor.SpellReturnHandler
*/
public final class SpellReturnSimulator implements EffectSimulator {
public final class SpellReturnSimulator implements EffectSimulator, BuffEffectSimulator {
private final int multiplier;

public SpellReturnSimulator(int multiplier) {
Expand All @@ -56,6 +58,31 @@ public void score(SpellScore score, SpellEffect effect, Characteristics characte
score.addBoost(Asserter.castNonNegative((int) score(effect)));
}

@Override
public Damage onReduceableDamage(CastSimulation simulation, Buff buff, FighterData target, Damage damage) {
final SpellEffect effect = buff.effect();
final int level = Math.max(effect.min(), effect.max());
final double percent = effect.special();

// Ignore the effect if it's too random, or doesn't apply to the current cast
if (percent < 90 || level < simulation.spell().level()) {
return damage;
}

final int currentDamage = damage.value();

if (currentDamage <= 0) {
return damage;
}

// Simulate return effect by reducing all damage, and mark it as reflected
// This is not the actual behavior (i.e. change the target), but it's enough for simulation
return damage
.reflect(currentDamage)
.reduce(currentDamage)
;
}

private double score(SpellEffect effect) {
final int level = Math.max(effect.min(), effect.max());
final double percent = effect.special() / 100d;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ void applyReduceableDamageBuffsWithoutBuff() {
simulator = new Simulator(new BaseCriticalityStrategy());

Damage damage = new Damage(10, Element.EARTH);
Damage returned = simulator.applyReduceableDamageBuffs(other.fighter(), damage);
Damage returned = simulator.applyReduceableDamageBuffs(new CastSimulation(Mockito.mock(Spell.class), fighter, fight.map().get(123)), other.fighter(), damage);

assertSame(damage, returned);
assertEquals(10, returned.value());
Expand All @@ -372,14 +372,14 @@ void applyReduceableDamageBuffsWithBuffNotMatching() {
simulator = new Simulator(new BaseCriticalityStrategy());
simulator.registerBuff(101, new BuffEffectSimulator() {
@Override
public Damage onReduceableDamage(Buff buff, FighterData target, Damage damage) {
public Damage onReduceableDamage(CastSimulation simulation, Buff buff, FighterData target, Damage damage) {
called.set(true);
return damage;
}
});

Damage damage = new Damage(10, Element.EARTH);
Damage returned = simulator.applyReduceableDamageBuffs(other.fighter(), damage);
Damage returned = simulator.applyReduceableDamageBuffs(new CastSimulation(Mockito.mock(Spell.class), fighter, fight.map().get(123)), other.fighter(), damage);

assertSame(damage, returned);
assertEquals(10, returned.value());
Expand All @@ -403,15 +403,15 @@ void applyReduceableDamageBuffsWithBuffMatching() {
simulator = new Simulator(new BaseCriticalityStrategy());
simulator.registerBuff(100, new BuffEffectSimulator() {
@Override
public Damage onReduceableDamage(Buff buff, FighterData target, Damage damage) {
public Damage onReduceableDamage(CastSimulation simulation, Buff buff, FighterData target, Damage damage) {
called.set(true);

return damage.reduce(5);
}
});

Damage damage = new Damage(10, Element.EARTH);
Damage returned = simulator.applyReduceableDamageBuffs(other.fighter(), damage);
Damage returned = simulator.applyReduceableDamageBuffs(new CastSimulation(Mockito.mock(Spell.class), fighter, fight.map().get(123)), other.fighter(), damage);

assertSame(damage, returned);
assertEquals(5, returned.value());
Expand All @@ -434,13 +434,13 @@ void applyReduceableDamageBuffsWithBuffMatchingReturnedNewInstance() {
simulator = new Simulator(new BaseCriticalityStrategy());
simulator.registerBuff(100, new BuffEffectSimulator() {
@Override
public Damage onReduceableDamage(Buff buff, FighterData target, Damage damage) {
public Damage onReduceableDamage(CastSimulation simulation, Buff buff, FighterData target, Damage damage) {
return new Damage(buff.effect().min(), Element.NEUTRAL);
}
});

Damage damage = new Damage(10, Element.EARTH);
Damage returned = simulator.applyReduceableDamageBuffs(other.fighter(), damage);
Damage returned = simulator.applyReduceableDamageBuffs(new CastSimulation(Mockito.mock(Spell.class), fighter, fight.map().get(123)), other.fighter(), damage);

assertNotSame(damage, returned);
assertEquals(20, returned.value());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,9 @@ void onReduceableDamageSuccess() {
);

ArmorSimulator simulator = new ArmorSimulator();
CastSimulation simulation = new CastSimulation(Mockito.mock(Spell.class), fighter, fight.map().get(123));

assertSame(damage, simulator.onReduceableDamage(buff, other.fighter(), damage));
assertSame(damage, simulator.onReduceableDamage(simulation, buff, other.fighter(), damage));
assertEquals(7, damage.value());
assertEquals(3, damage.reducedDamage());
}
Expand All @@ -242,8 +243,9 @@ void onReduceableDamageShouldBeBoostedByIntelligence() {
);

ArmorSimulator simulator = new ArmorSimulator();
CastSimulation simulation = new CastSimulation(Mockito.mock(Spell.class), fighter, fight.map().get(123));

assertSame(damage, simulator.onReduceableDamage(buff, other.fighter(), damage));
assertSame(damage, simulator.onReduceableDamage(simulation, buff, other.fighter(), damage));
assertEquals(6, damage.value());
assertEquals(4, damage.reducedDamage());
}
Expand All @@ -262,8 +264,9 @@ void onReduceableDamageShouldIgnoreNegativeReduce() {
);

ArmorSimulator simulator = new ArmorSimulator();
CastSimulation simulation = new CastSimulation(Mockito.mock(Spell.class), fighter, fight.map().get(123));

assertSame(damage, simulator.onReduceableDamage(buff, other.fighter(), damage));
assertSame(damage, simulator.onReduceableDamage(simulation, buff, other.fighter(), damage));
assertEquals(10, damage.value());
assertEquals(0, damage.reducedDamage());
}
Expand All @@ -282,8 +285,9 @@ void onReduceableDamageShouldBeBoostedByDamageElement() {
);

ArmorSimulator simulator = new ArmorSimulator();
CastSimulation simulation = new CastSimulation(Mockito.mock(Spell.class), fighter, fight.map().get(123));

assertSame(damage, simulator.onReduceableDamage(buff, other.fighter(), damage));
assertSame(damage, simulator.onReduceableDamage(simulation, buff, other.fighter(), damage));
assertEquals(6, damage.value());
assertEquals(4, damage.reducedDamage());
}
Expand All @@ -301,14 +305,15 @@ void onReduceableDamageShouldFilterDamageElement() {
);

ArmorSimulator simulator = new ArmorSimulator();
CastSimulation simulation = new CastSimulation(Mockito.mock(Spell.class), fighter, fight.map().get(123));

Damage damage = new Damage(10, Element.EARTH);
assertSame(damage, simulator.onReduceableDamage(buff, other.fighter(), damage));
assertSame(damage, simulator.onReduceableDamage(simulation, buff, other.fighter(), damage));
assertEquals(10, damage.value());
assertEquals(0, damage.reducedDamage());

damage = new Damage(10, Element.WATER);
assertSame(damage, simulator.onReduceableDamage(buff, other.fighter(), damage));
assertSame(damage, simulator.onReduceableDamage(simulation, buff, other.fighter(), damage));
assertEquals(7, damage.value());
assertEquals(3, damage.reducedDamage());
}
Expand Down
Loading

0 comments on commit f3530ae

Please sign in to comment.