diff --git a/src/main/java/fr/quatrevieux/araknemu/game/GameModule.java b/src/main/java/fr/quatrevieux/araknemu/game/GameModule.java index 63651e984..ed7dbe37c 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/GameModule.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/GameModule.java @@ -940,27 +940,27 @@ private void configureServices(ContainerConfigurator configurator) { configurator.persist(Simulator.class, container -> { final Simulator simulator = new Simulator(container.get(CriticalityStrategy.class)); - simulator.register(91, new StealLifeSimulator(Element.WATER)); - simulator.register(92, new StealLifeSimulator(Element.EARTH)); - simulator.register(93, new StealLifeSimulator(Element.AIR)); - simulator.register(94, new StealLifeSimulator(Element.FIRE)); - simulator.register(95, new StealLifeSimulator(Element.NEUTRAL)); - - simulator.register(96, new DamageSimulator(Element.WATER)); - simulator.register(97, new DamageSimulator(Element.EARTH)); - simulator.register(98, new DamageSimulator(Element.AIR)); - simulator.register(99, new DamageSimulator(Element.FIRE)); - simulator.register(100, new DamageSimulator(Element.NEUTRAL)); - - simulator.register(85, new PercentLifeDamageSimulator(Element.WATER)); - simulator.register(86, new PercentLifeDamageSimulator(Element.EARTH)); - simulator.register(87, new PercentLifeDamageSimulator(Element.AIR)); - simulator.register(88, new PercentLifeDamageSimulator(Element.FIRE)); - simulator.register(89, new PercentLifeDamageSimulator(Element.NEUTRAL)); - simulator.register(671, new PercentLifeDamageSimulator(Element.NEUTRAL)); // The actual effect is applied as "indirect damage" but it works mostly like a simple percent life damage. - - simulator.register(276, new PercentLifeLostDamageSimulator(Element.EARTH)); - simulator.register(279, new PercentLifeLostDamageSimulator(Element.NEUTRAL)); + simulator.register(91, new StealLifeSimulator(simulator, Element.WATER)); + simulator.register(92, new StealLifeSimulator(simulator, Element.EARTH)); + simulator.register(93, new StealLifeSimulator(simulator, Element.AIR)); + simulator.register(94, new StealLifeSimulator(simulator, Element.FIRE)); + simulator.register(95, new StealLifeSimulator(simulator, Element.NEUTRAL)); + + simulator.register(96, new DamageSimulator(simulator, Element.WATER)); + simulator.register(97, new DamageSimulator(simulator, Element.EARTH)); + simulator.register(98, new DamageSimulator(simulator, Element.AIR)); + simulator.register(99, new DamageSimulator(simulator, Element.FIRE)); + simulator.register(100, new DamageSimulator(simulator, Element.NEUTRAL)); + + simulator.register(85, new PercentLifeDamageSimulator(simulator, Element.WATER)); + simulator.register(86, new PercentLifeDamageSimulator(simulator, Element.EARTH)); + simulator.register(87, new PercentLifeDamageSimulator(simulator, Element.AIR)); + simulator.register(88, new PercentLifeDamageSimulator(simulator, Element.FIRE)); + simulator.register(89, new PercentLifeDamageSimulator(simulator, Element.NEUTRAL)); + simulator.register(671, new PercentLifeDamageSimulator(simulator, Element.NEUTRAL)); // The actual effect is applied as "indirect damage" but it works mostly like a simple percent life damage. + + simulator.register(276, new PercentLifeLostDamageSimulator(simulator, Element.EARTH)); + simulator.register(279, new PercentLifeLostDamageSimulator(simulator, Element.NEUTRAL)); simulator.register(82, new FixedStealLifeSimulator()); simulator.register(131, new DamageOnActionPointUseSimulator()); @@ -1035,10 +1035,10 @@ private void configureServices(ContainerConfigurator configurator) { simulator.register(320, new StealCharacteristicSimulator(5)); // sight // Armors - simulator.register(105, new ArmorSimulator()); + 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.register(265, new ArmorSimulator()); + simulator.registerEffectAndBuff(265, new ArmorSimulator()); // Heal simulator.register(90, new GivePercentLifeSimulator()); diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/Simulator.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/Simulator.java index 9908210f2..2bf4dc0a3 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/Simulator.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/Simulator.java @@ -20,8 +20,11 @@ package fr.quatrevieux.araknemu.game.fight.ai.simulation; import fr.quatrevieux.araknemu.game.fight.ai.AI; +import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.BuffEffectSimulator; import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.EffectSimulator; 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.ActiveFighter; import fr.quatrevieux.araknemu.game.fight.fighter.FighterData; import fr.quatrevieux.araknemu.game.fight.map.BattlefieldCell; @@ -29,6 +32,7 @@ import fr.quatrevieux.araknemu.game.spell.Spell; import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect; import fr.quatrevieux.araknemu.game.world.creature.characteristics.Characteristics; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.HashMap; import java.util.Map; @@ -38,6 +42,7 @@ */ public final class Simulator { private final Map simulators = new HashMap<>(); + private final Map buffSimulators = new HashMap<>(); private final CriticalityStrategy criticalityStrategy; public Simulator(CriticalityStrategy criticalityStrategy) { @@ -54,6 +59,32 @@ public void register(int effectId, EffectSimulator simulator) { simulators.put(effectId, simulator); } + /** + * Register a buff effect simulator + * + * The simulator is called only if the buff effect corresponds to this id. + * The buff effect id is retrieved using {@code buff.effect().effect()}. + * + * @param effectId The buff effect to simulate + * @param simulator The simulator + */ + public void registerBuff(int effectId, BuffEffectSimulator simulator) { + buffSimulators.put(effectId, simulator); + } + + /** + * Register the instance as both effect and buff simulator + * + * @param effectId The effect id to simulate + * @param simulator The simulator + * + * @param The type of the simulator + */ + public void registerEffectAndBuff(int effectId, E simulator) { + simulators.put(effectId, simulator); + buffSimulators.put(effectId, simulator); + } + /** * Simulate the spell cast * @@ -113,6 +144,28 @@ public SpellScore score(Spell spell, Characteristics characteristics) { return score; } + /** + * Simulate buff effects on reduceable damage effect + * + * @param target The target of the damage + * @param damage The computed damage + * + * @return The modified damage + * + * @see BuffEffectSimulator#onReduceableDamage(Buff, FighterData, Damage) The called buff method + */ + public Damage applyReduceableDamageBuffs(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); + } + } + + return damage; + } + /** * Simulate a cast result * diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/AbstractElementalDamageSimulator.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/AbstractElementalDamageSimulator.java new file mode 100644 index 000000000..59c48de8f --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/AbstractElementalDamageSimulator.java @@ -0,0 +1,117 @@ +/* + * 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 . + * + * Copyright (c) 2017-2023 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.ai.simulation.effect; + +import fr.arakne.utils.value.Interval; +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.Simulator; +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.Element; +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.util.Asserter; +import org.checkerframework.checker.index.qual.GTENegativeOne; +import org.checkerframework.checker.index.qual.NonNegative; + +import java.util.Collection; + +/** + * Base class for simulator of damage related to an element. + * + * Those damage can be reduced by armor and resistances. + * Armor buffs are called when the damage is applied directly to the target (i.e. not a poison effect). + */ +public abstract class AbstractElementalDamageSimulator implements EffectSimulator { + private final Simulator simulator; + private final Element element; + + public AbstractElementalDamageSimulator(Simulator simulator, Element element) { + this.simulator = simulator; + this.element = element; + } + + @Override + public final void simulate(CastSimulation simulation, AI ai, CastScope.EffectScope effect) { + final SpellEffect spellEffect = effect.effect(); + final Interval baseDamage = computeBaseDamage(ai.fighter(), spellEffect); + + if (spellEffect.duration() == 0) { + simulateDamage(simulation, baseDamage, effect.targets()); + } else { + simulatePoison(simulation, baseDamage, spellEffect.duration(), effect.targets()); + } + } + + /** + * Return the interval of base damage. + * Base damage are computed with all boosts, but without resistances or armor. + * + * @param caster The spell caster + * @param effect The spell effect + * + * @return The interval of base damage + */ + protected abstract Interval computeBaseDamage(FighterData caster, SpellEffect effect); + + private Interval applyResistances(Interval baseDamageInterval, FighterData target) { + 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 Damage createDamage(@NonNegative int baseDamage, FighterData target) { + return new Damage(baseDamage, element) + .percent(target.characteristics().get(element.percentResistance())) + .fixed(target.characteristics().get(element.fixedResistance())) + ; + } + + 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; + } + + private void simulatePoison(CastSimulation simulation, Interval damage, @GTENegativeOne int duration, Collection targets) { + final int capedDuration = Formula.capedDuration(duration); + + for (FighterData target : targets) { + simulation.addPoison(applyResistances(damage, target), capedDuration, target); + } + } + + private void simulateDamage(CastSimulation simulation, Interval damage, Collection targets) { + for (FighterData target : targets) { + simulation.addDamage(applyResistancesAndArmor(damage, simulation, target), target); + } + } +} diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/ArmorSimulator.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/ArmorSimulator.java index 90f2cbcfe..81be1a666 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/ArmorSimulator.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/ArmorSimulator.java @@ -26,6 +26,8 @@ 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.Element; +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; @@ -34,7 +36,7 @@ import java.util.EnumSet; import java.util.Set; -public final class ArmorSimulator implements EffectSimulator { +public final class ArmorSimulator implements EffectSimulator, BuffEffectSimulator { @Override public void simulate(CastSimulation simulation, AI ai, CastScope.EffectScope effect) { if (effect.effect().duration() == 0) { @@ -71,4 +73,36 @@ public void score(SpellScore score, SpellEffect effect, Characteristics characte score.addBoost(effect.min() * boost / 100); } + + @Override + public Damage onReduceableDamage(Buff buff, FighterData target, Damage damage) { + if (!supportsElement(buff, damage.element())) { + return damage; + } + + final Characteristics characteristics = target.characteristics(); + + final int boost = 200 + characteristics.get(Characteristic.INTELLIGENCE) + characteristics.get(damage.element().boost()); + final int reduce = buff.effect().min() * boost / 200; + + if (reduce < 0) { + return damage; + } + + damage.reduce(reduce); + + return damage; + } + + /** + * Check if the armor supports the damage element + * + * @param buff The buff + * @param element The damage element + * + * @return true if the armor supports the element + */ + private boolean supportsElement(Buff buff, Element element) { + return buff.effect().special() == 0 || Element.fromBitSet(buff.effect().special()).contains(element); + } } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/BuffEffectSimulator.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/BuffEffectSimulator.java new file mode 100644 index 000000000..db141f431 --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/BuffEffectSimulator.java @@ -0,0 +1,43 @@ +/* + * 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 . + * + * Copyright (c) 2017-2024 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.ai.simulation.effect; + +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; + +/** + * Base type for simulate a buff effect on different hooks + */ +public interface BuffEffectSimulator { + /** + * Apply armor effect, or any other effect that can reduce the damage + * + * @param buff The armor buff + * @param target The simulated target + * @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 + */ + public default Damage onReduceableDamage(Buff buff, FighterData target, Damage damage) { + return damage; + } +} diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/DamageSimulator.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/DamageSimulator.java index b618a4b76..6da1f62fc 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/DamageSimulator.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/DamageSimulator.java @@ -21,59 +21,40 @@ import fr.arakne.utils.value.Interval; 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.Simulator; 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.EffectValue; import fr.quatrevieux.araknemu.game.fight.castable.effect.Element; -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; -import fr.quatrevieux.araknemu.util.Asserter; -import org.checkerframework.checker.index.qual.NonNegative; /** * Simulate simple damage effect * * @see fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.DamageHandler */ -public final class DamageSimulator implements EffectSimulator { +public final class DamageSimulator extends AbstractElementalDamageSimulator { private final Element element; - public DamageSimulator(Element element) { + public DamageSimulator(Simulator simulator, Element element) { + super(simulator, element); + this.element = element; } @Override - public void simulate(CastSimulation simulation, AI ai, CastScope.EffectScope effect) { - final FighterData caster = simulation.caster(); + protected Interval computeBaseDamage(FighterData caster, SpellEffect effect) { final int boost = caster.characteristics().get(element.boost()); final int percent = caster.characteristics().get(Characteristic.PERCENT_DAMAGE); final int fixed = caster.characteristics().get(Characteristic.FIXED_DAMAGE); - for (FighterData target : effect.targets()) { - final SpellEffect spellEffect = effect.effect(); - final Interval value = EffectValue.create(spellEffect, simulation.caster(), target) - .percent(boost) - .percent(percent) - .fixed(fixed) - .interval() - ; - - final Interval damage = value.map(base -> computeDamage(base, target)); - final int duration = spellEffect.duration(); - - if (duration == 0) { - simulation.addDamage(damage, target); - } else { - // Limit duration to 10 - simulation.addPoison(damage, Formula.capedDuration(effect.effect().duration()), target); - } - } + return new EffectValue(effect) + .percent(boost) + .percent(percent) + .fixed(fixed) + .interval() + ; } @Override @@ -85,13 +66,4 @@ public void score(SpellScore score, SpellEffect effect, Characteristics characte score.addDamage(value * boost / 100); } - - private @NonNegative int computeDamage(@NonNegative int value, FighterData target) { - final Damage damage = new Damage(value, element) - .percent(target.characteristics().get(element.percentResistance())) - .fixed(target.characteristics().get(element.fixedResistance())) - ; - - return Asserter.castNonNegative(damage.value()); - } } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeDamageSimulator.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeDamageSimulator.java index 4f8565b58..6a88357aa 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeDamageSimulator.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeDamageSimulator.java @@ -20,19 +20,10 @@ package fr.quatrevieux.araknemu.game.fight.ai.simulation.effect; import fr.arakne.utils.value.Interval; -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.effect.util.Formula; -import fr.quatrevieux.araknemu.game.fight.castable.CastScope; +import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; import fr.quatrevieux.araknemu.game.fight.castable.effect.Element; -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.util.Asserter; -import org.checkerframework.checker.index.qual.GTENegativeOne; - -import java.util.Collection; /** * Simulator for damage depending on the life of the caster effect @@ -40,52 +31,17 @@ * * @see fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.PercentLifeDamageHandler The simulated effect */ -public final class PercentLifeDamageSimulator implements EffectSimulator { - private final Element element; - - public PercentLifeDamageSimulator(Element element) { - this.element = element; +public final class PercentLifeDamageSimulator extends AbstractElementalDamageSimulator { + public PercentLifeDamageSimulator(Simulator simulator, Element element) { + super(simulator, element); } @Override - public void simulate(CastSimulation simulation, AI ai, CastScope.EffectScope effect) { - final SpellEffect spellEffect = effect.effect(); - final Interval baseDamage = damage(ai.fighter(), spellEffect); - - if (spellEffect.duration() == 0) { - simulateDamage(simulation, baseDamage, effect.targets()); - } else { - simulatePoison(simulation, baseDamage, spellEffect.duration(), effect.targets()); - } - } - - private Interval damage(FighterData caster, SpellEffect effect) { + protected Interval computeBaseDamage(FighterData caster, SpellEffect effect) { final int currentLife = caster.life().current(); return Interval.of(effect.min(), Math.max(effect.max(), effect.min())) .map(value -> value * currentLife / 100) ; } - - private Interval applyResistances(Interval damage, FighterData target) { - return damage.map(value -> Asserter.castNonNegative(new Damage(value, element) - .percent(target.characteristics().get(element.percentResistance())) - .fixed(target.characteristics().get(element.fixedResistance())) - .value() - )); - } - - private void simulatePoison(CastSimulation simulation, Interval damage, @GTENegativeOne int duration, Collection targets) { - final int capedDuration = Formula.capedDuration(duration); - - for (FighterData target : targets) { - simulation.addPoison(applyResistances(damage, target), capedDuration, target); - } - } - - private void simulateDamage(CastSimulation simulation, Interval damage, Collection targets) { - for (FighterData target : targets) { - simulation.addDamage(applyResistances(damage, target), target); - } - } } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeLostDamageSimulator.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeLostDamageSimulator.java index 3f3225177..c07c1321e 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeLostDamageSimulator.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeLostDamageSimulator.java @@ -20,20 +20,12 @@ package fr.quatrevieux.araknemu.game.fight.ai.simulation.effect; import fr.arakne.utils.value.Interval; -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.effect.util.Formula; -import fr.quatrevieux.araknemu.game.fight.castable.CastScope; +import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; import fr.quatrevieux.araknemu.game.fight.castable.effect.Element; -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.Life; import fr.quatrevieux.araknemu.util.Asserter; -import org.checkerframework.checker.index.qual.GTENegativeOne; - -import java.util.Collection; /** * Simulator for damage depending on the life of the caster effect @@ -42,26 +34,13 @@ * @see fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.PercentLifeLostDamageHandler The simulated effect * @see PercentLifeDamageSimulator The opposite effect */ -public final class PercentLifeLostDamageSimulator implements EffectSimulator { - private final Element element; - - public PercentLifeLostDamageSimulator(Element element) { - this.element = element; +public final class PercentLifeLostDamageSimulator extends AbstractElementalDamageSimulator { + public PercentLifeLostDamageSimulator(Simulator simulator, Element element) { + super(simulator, element); } @Override - public void simulate(CastSimulation simulation, AI ai, CastScope.EffectScope effect) { - final SpellEffect spellEffect = effect.effect(); - final Interval baseDamage = damage(ai.fighter(), spellEffect); - - if (spellEffect.duration() == 0) { - simulateDamage(simulation, baseDamage, effect.targets()); - } else { - simulatePoison(simulation, baseDamage, spellEffect.duration(), effect.targets()); - } - } - - private Interval damage(FighterData caster, SpellEffect effect) { + protected Interval computeBaseDamage(FighterData caster, SpellEffect effect) { final Life casterLife = caster.life(); final int lostLife = Asserter.castNonNegative(casterLife.max() - casterLife.current()); @@ -69,26 +48,4 @@ private Interval damage(FighterData caster, SpellEffect effect) { .map(value -> value * lostLife / 100) ; } - - private Interval applyResistances(Interval damage, FighterData target) { - return damage.map(value -> Asserter.castNonNegative(new Damage(value, element) - .percent(target.characteristics().get(element.percentResistance())) - .fixed(target.characteristics().get(element.fixedResistance())) - .value() - )); - } - - private void simulatePoison(CastSimulation simulation, Interval damage, @GTENegativeOne int duration, Collection targets) { - final int capedDuration = Formula.capedDuration(duration); - - for (FighterData target : targets) { - simulation.addPoison(applyResistances(damage, target), capedDuration, target); - } - } - - private void simulateDamage(CastSimulation simulation, Interval damage, Collection targets) { - for (FighterData target : targets) { - simulation.addDamage(applyResistances(damage, target), target); - } - } } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/StealLifeSimulator.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/StealLifeSimulator.java index b6de9f434..9657d00eb 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/StealLifeSimulator.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/StealLifeSimulator.java @@ -22,6 +22,7 @@ import fr.arakne.utils.value.Interval; 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.Simulator; 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.Element; @@ -38,8 +39,8 @@ public final class StealLifeSimulator implements EffectSimulator { private final DamageSimulator simulator; - public StealLifeSimulator(Element element) { - this.simulator = new DamageSimulator(element); + public StealLifeSimulator(Simulator simulator, Element element) { + this.simulator = new DamageSimulator(simulator, element); } @Override diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/EffectValue.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/EffectValue.java index 579ad850f..0ac27307d 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/EffectValue.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/EffectValue.java @@ -57,7 +57,7 @@ enum State { private @NonNegative int multiply = 1; private @NonNegative int value = 0; - EffectValue(SpellEffect effect) { + public EffectValue(SpellEffect effect) { this.effect = effect; } @@ -191,6 +191,7 @@ protected EffectValue clone() { * @param target The target on which {@link fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buffs#onEffectValueTarget(EffectValue)} will be called * * @return The configured effect + * @todo Use Fighter instead of FighterData */ public static EffectValue create(SpellEffect effect, FighterData caster, FighterData target) { final EffectValue value = new EffectValue(effect); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/SpellEffectStub.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/SpellEffectStub.java new file mode 100644 index 000000000..ad89dbee2 --- /dev/null +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/SpellEffectStub.java @@ -0,0 +1,174 @@ +/* + * 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 . + * + * Copyright (c) 2017-2024 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight; + +import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect; +import fr.quatrevieux.araknemu.game.spell.effect.area.CellArea; +import fr.quatrevieux.araknemu.game.spell.effect.area.SpellEffectArea; +import fr.quatrevieux.araknemu.game.spell.effect.target.EffectTarget; +import fr.quatrevieux.araknemu.game.spell.effect.target.SpellEffectTarget; +import org.checkerframework.checker.index.qual.GTENegativeOne; +import org.checkerframework.checker.index.qual.NonNegative; + +public class SpellEffectStub implements SpellEffect { + private int id; + private int min; + private int max; + private int special; + private int duration; + private int probability; + private String text; + private SpellEffectArea area; + private EffectTarget target; + private int boost; + private boolean trap; + + public SpellEffectStub(int id, int min, int max, int special, int duration, int probability, String text, SpellEffectArea area, EffectTarget target, int boost, boolean trap) { + this.id = id; + this.min = min; + this.max = max; + this.special = special; + this.duration = duration; + this.probability = probability; + this.text = text; + this.area = area; + this.target = target; + this.boost = boost; + this.trap = trap; + } + + public SpellEffectStub(int id, int min) { + this(id, min, 0, 0, 0, 0, "", new CellArea(), SpellEffectTarget.DEFAULT, 0, false); + } + + @Override + public int effect() { + return id; + } + + @Override + public @NonNegative int min() { + return min; + } + + @Override + public @NonNegative int max() { + return max; + } + + @Override + public int special() { + return special; + } + + @Override + public @GTENegativeOne int duration() { + return duration; + } + + @Override + public @NonNegative int probability() { + return probability; + } + + @Override + public String text() { + return text; + } + + @Override + public SpellEffectArea area() { + return area; + } + + @Override + public EffectTarget target() { + return target; + } + + @Override + public int boost() { + return boost; + } + + @Override + public boolean trap() { + return trap; + } + + public SpellEffectStub setId(int id) { + this.id = id; + return this; + } + + public SpellEffectStub setMin(int min) { + this.min = min; + return this; + } + + public SpellEffectStub setMax(int max) { + this.max = max; + return this; + } + + public SpellEffectStub setSpecial(int special) { + this.special = special; + return this; + } + + public SpellEffectStub setDuration(int duration) { + this.duration = duration; + return this; + } + + public SpellEffectStub setProbability(int probability) { + this.probability = probability; + return this; + } + + public SpellEffectStub setText(String text) { + this.text = text; + return this; + } + + public SpellEffectStub setArea(SpellEffectArea area) { + this.area = area; + return this; + } + + public SpellEffectStub setTarget(EffectTarget target) { + this.target = target; + return this; + } + + public SpellEffectStub setBoost(int boost) { + this.boost = boost; + return this; + } + + public SpellEffectStub setTrap(boolean trap) { + this.trap = trap; + return this; + } + + public static SpellEffectStub fixed(int id, int min) { + return new SpellEffectStub(id, min, 0, 0, 0, 0, "", new CellArea(), SpellEffectTarget.DEFAULT, 0, false); + } +} diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/SimulatorTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/SimulatorTest.java index 277fb9b3c..7d68f618d 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/SimulatorTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/SimulatorTest.java @@ -22,9 +22,16 @@ import fr.quatrevieux.araknemu.data.constant.Characteristic; import fr.quatrevieux.araknemu.game.fight.Fight; import fr.quatrevieux.araknemu.game.fight.FightBaseCase; +import fr.quatrevieux.araknemu.game.fight.SpellEffectStub; import fr.quatrevieux.araknemu.game.fight.ai.FighterAI; import fr.quatrevieux.araknemu.game.fight.ai.action.logic.NullGenerator; import fr.quatrevieux.araknemu.game.fight.ai.factory.ChainAiFactory; +import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.BuffEffectSimulator; +import fr.quatrevieux.araknemu.game.fight.castable.effect.Element; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.BuffHook; +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.fighter.invocation.DoubleFighter; import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter; import fr.quatrevieux.araknemu.game.fight.module.AiModule; @@ -36,8 +43,12 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import java.util.concurrent.atomic.AtomicBoolean; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; class SimulatorTest extends FightBaseCase { @@ -331,6 +342,111 @@ void multipleEffects() { assertFalse(score.isSuicide()); } + @Test + void applyReduceableDamageBuffsWithoutBuff() { + simulator = new Simulator(new BaseCriticalityStrategy()); + + Damage damage = new Damage(10, Element.EARTH); + Damage returned = simulator.applyReduceableDamageBuffs(other.fighter(), damage); + + assertSame(damage, returned); + assertEquals(10, returned.value()); + assertEquals(0, returned.reflectedDamage()); + assertEquals(0, returned.reducedDamage()); + } + + @Test + void applyReduceableDamageBuffsWithBuffNotMatching() { + other.fighter().buffs().add( + new Buff( + new SpellEffectStub(100, 10), + Mockito.mock(Spell.class), + other.fighter(), + other.fighter(), + new BuffHook() {} + ) + ); + + AtomicBoolean called = new AtomicBoolean(false); + + simulator = new Simulator(new BaseCriticalityStrategy()); + simulator.registerBuff(101, new BuffEffectSimulator() { + @Override + public Damage onReduceableDamage(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); + + assertSame(damage, returned); + assertEquals(10, returned.value()); + assertFalse(called.get()); + } + + @Test + void applyReduceableDamageBuffsWithBuffMatching() { + other.fighter().buffs().add( + new Buff( + new SpellEffectStub(100, 10), + Mockito.mock(Spell.class), + other.fighter(), + other.fighter(), + new BuffHook() {} + ) + ); + + AtomicBoolean called = new AtomicBoolean(false); + + simulator = new Simulator(new BaseCriticalityStrategy()); + simulator.registerBuff(100, new BuffEffectSimulator() { + @Override + public Damage onReduceableDamage(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); + + assertSame(damage, returned); + assertEquals(5, returned.value()); + assertEquals(5, returned.reducedDamage()); + assertTrue(called.get()); + } + + @Test + void applyReduceableDamageBuffsWithBuffMatchingReturnedNewInstance() { + other.fighter().buffs().add( + new Buff( + new SpellEffectStub(100, 20), + Mockito.mock(Spell.class), + other.fighter(), + other.fighter(), + new BuffHook() {} + ) + ); + + simulator = new Simulator(new BaseCriticalityStrategy()); + simulator.registerBuff(100, new BuffEffectSimulator() { + @Override + public Damage onReduceableDamage(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); + + assertNotSame(damage, returned); + assertEquals(20, returned.value()); + assertEquals(Element.NEUTRAL, returned.element()); + } + private Spell getSpell(int id, int level) { return container.get(SpellService.class).get(id).level(level); } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/ArmorSimulatorTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/ArmorSimulatorTest.java index 47c55b2aa..b269f0f34 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/ArmorSimulatorTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/ArmorSimulatorTest.java @@ -23,12 +23,16 @@ import fr.quatrevieux.araknemu.data.value.EffectArea; import fr.quatrevieux.araknemu.game.fight.Fight; import fr.quatrevieux.araknemu.game.fight.FightBaseCase; +import fr.quatrevieux.araknemu.game.fight.SpellEffectStub; import fr.quatrevieux.araknemu.game.fight.ai.FighterAI; import fr.quatrevieux.araknemu.game.fight.ai.action.logic.NullGenerator; 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.Element; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.BuffHook; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.Damage; import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter; import fr.quatrevieux.araknemu.game.fight.map.FightCell; @@ -43,6 +47,7 @@ import org.mockito.Mockito; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; class ArmorSimulatorTest extends FightBaseCase { private Fight fight; @@ -205,6 +210,109 @@ void score() { assertEquals(20, score.boost()); } + @Test + void onReduceableDamageSuccess() { + Damage damage = new Damage(10, Element.EARTH); + Buff buff = new Buff( + SpellEffectStub.fixed(105, 3), + Mockito.mock(Spell.class), + other.fighter(), + other.fighter(), + new BuffHook() {} + ); + + ArmorSimulator simulator = new ArmorSimulator(); + + assertSame(damage, simulator.onReduceableDamage(buff, other.fighter(), damage)); + assertEquals(7, damage.value()); + assertEquals(3, damage.reducedDamage()); + } + + @Test + void onReduceableDamageShouldBeBoostedByIntelligence() { + other.fighter().characteristics().alter(Characteristic.INTELLIGENCE, 100); + + Damage damage = new Damage(10, Element.EARTH); + Buff buff = new Buff( + SpellEffectStub.fixed(105, 3), + Mockito.mock(Spell.class), + other.fighter(), + other.fighter(), + new BuffHook() {} + ); + + ArmorSimulator simulator = new ArmorSimulator(); + + assertSame(damage, simulator.onReduceableDamage(buff, other.fighter(), damage)); + assertEquals(6, damage.value()); + assertEquals(4, damage.reducedDamage()); + } + + @Test + void onReduceableDamageShouldIgnoreNegativeReduce() { + other.fighter().characteristics().alter(Characteristic.INTELLIGENCE, -1000); + + Damage damage = new Damage(10, Element.EARTH); + Buff buff = new Buff( + SpellEffectStub.fixed(105, 3), + Mockito.mock(Spell.class), + other.fighter(), + other.fighter(), + new BuffHook() {} + ); + + ArmorSimulator simulator = new ArmorSimulator(); + + assertSame(damage, simulator.onReduceableDamage(buff, other.fighter(), damage)); + assertEquals(10, damage.value()); + assertEquals(0, damage.reducedDamage()); + } + + @Test + void onReduceableDamageShouldBeBoostedByDamageElement() { + other.fighter().characteristics().alter(Characteristic.STRENGTH, 100); + + Damage damage = new Damage(10, Element.EARTH); + Buff buff = new Buff( + SpellEffectStub.fixed(105, 3), + Mockito.mock(Spell.class), + other.fighter(), + other.fighter(), + new BuffHook() {} + ); + + ArmorSimulator simulator = new ArmorSimulator(); + + assertSame(damage, simulator.onReduceableDamage(buff, other.fighter(), damage)); + assertEquals(6, damage.value()); + assertEquals(4, damage.reducedDamage()); + } + + @Test + void onReduceableDamageShouldFilterDamageElement() { + other.fighter().characteristics().alter(Characteristic.STRENGTH, 100); + + Buff buff = new Buff( + SpellEffectStub.fixed(105, 3).setSpecial(4), // Water + Mockito.mock(Spell.class), + other.fighter(), + other.fighter(), + new BuffHook() {} + ); + + ArmorSimulator simulator = new ArmorSimulator(); + + Damage damage = new Damage(10, Element.EARTH); + assertSame(damage, simulator.onReduceableDamage(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)); + assertEquals(7, damage.value()); + assertEquals(3, damage.reducedDamage()); + } + private CastSimulation performSimulation(Spell spell, SpellEffect effect) { ArmorSimulator simulator = new ArmorSimulator(); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/DamageSimulatorTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/DamageSimulatorTest.java index eec395021..017719bda 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/DamageSimulatorTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/DamageSimulatorTest.java @@ -23,13 +23,20 @@ import fr.quatrevieux.araknemu.data.value.EffectArea; import fr.quatrevieux.araknemu.game.fight.Fight; import fr.quatrevieux.araknemu.game.fight.FightBaseCase; +import fr.quatrevieux.araknemu.game.fight.SpellEffectStub; import fr.quatrevieux.araknemu.game.fight.ai.FighterAI; import fr.quatrevieux.araknemu.game.fight.ai.action.logic.NullGenerator; import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation; +import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; 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.Element; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.BuffEffect; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.BuffHook; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.Damage; import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; +import fr.quatrevieux.araknemu.game.fight.fighter.FighterData; import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter; import fr.quatrevieux.araknemu.game.fight.map.FightCell; import fr.quatrevieux.araknemu.game.spell.Spell; @@ -89,7 +96,7 @@ void simulateWithBoost() { @Test void simulateBuff() { - DamageSimulator simulator = new DamageSimulator(Element.EARTH); + DamageSimulator simulator = new DamageSimulator(container.get(Simulator.class), Element.EARTH); SpellEffect effect = Mockito.mock(SpellEffect.class); Spell spell = Mockito.mock(Spell.class); @@ -124,7 +131,7 @@ void simulateBuff() { @Test void simulateInfiniteBuff() { - DamageSimulator simulator = new DamageSimulator(Element.EARTH); + DamageSimulator simulator = new DamageSimulator(container.get(Simulator.class), Element.EARTH); SpellEffect effect = Mockito.mock(SpellEffect.class); Spell spell = Mockito.mock(Spell.class); @@ -147,7 +154,7 @@ void simulateInfiniteBuff() { @Test void simulateArea() { - DamageSimulator simulator = new DamageSimulator(Element.EARTH); + DamageSimulator simulator = new DamageSimulator(container.get(Simulator.class), Element.EARTH); SpellEffect effect = Mockito.mock(SpellEffect.class); Spell spell = Mockito.mock(Spell.class); @@ -168,11 +175,59 @@ void simulateArea() { assertEquals(-15, simulation.enemiesLife()); } + @Test + void simulateWithCounterDamage() { + other.fighter().characteristics().alter(Characteristic.COUNTER_DAMAGE, 5); + + CastSimulation simulation = doSimulation(); + assertEquals(-15, simulation.enemiesLife()); + assertEquals(-5, simulation.selfLife()); + } + + @Test + void simulateWithReduceDamageBuff() { + other.fighter().buffs().add( + new Buff( + SpellEffectStub.fixed(105, 5), + Mockito.mock(Spell.class), + fighter, + other.fighter(), + new BuffHook() {} + ) + ); + + assertEquals(-10, simulate()); + } + + @Test + void simulateWithCounterDamageBuff() { + // @todo use actual simulator when it will be implemented + container.get(Simulator.class).registerBuff(107, new BuffEffectSimulator() { + @Override + public Damage onReduceableDamage(Buff buff, FighterData target, Damage damage) { + return damage.reflect(buff.effect().min()); + } + }); + + other.fighter().buffs().add( + new Buff( + SpellEffectStub.fixed(107, 5), + Mockito.mock(Spell.class), + fighter, + other.fighter(), + new BuffHook() {} + ) + ); + + assertEquals(-15, doSimulation().enemiesLife()); + assertEquals(-5, doSimulation().selfLife()); + } + @Test void score() { fighter.player().properties().characteristics().base().set(Characteristic.STRENGTH, 0); - DamageSimulator simulator = new DamageSimulator(Element.EARTH); + DamageSimulator simulator = new DamageSimulator(container.get(Simulator.class), Element.EARTH); SpellEffect effect = Mockito.mock(SpellEffect.class); Mockito.when(effect.min()).thenReturn(10); @@ -192,6 +247,10 @@ void score() { } private double simulate() { + return doSimulation().enemiesLife(); + } + + private CastSimulation doSimulation() { SpellEffect effect = Mockito.mock(SpellEffect.class); Spell spell = Mockito.mock(Spell.class); SpellConstraints constraints = Mockito.mock(SpellConstraints.class); @@ -205,8 +264,8 @@ private double simulate() { CastSimulation simulation = new CastSimulation(spell, fighter, target.cell()); CastScope scope = makeCastScope(fighter, spell, effect, target.cell()); - new DamageSimulator(Element.EARTH).simulate(simulation, ai, scope.effects().get(0)); + new DamageSimulator(container.get(Simulator.class), Element.EARTH).simulate(simulation, ai, scope.effects().get(0)); - return simulation.enemiesLife(); + return simulation; } } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeDamageSimulatorTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeDamageSimulatorTest.java index 01aa9caf4..26426b579 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeDamageSimulatorTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeDamageSimulatorTest.java @@ -23,12 +23,18 @@ import fr.quatrevieux.araknemu.data.value.EffectArea; import fr.quatrevieux.araknemu.game.fight.Fight; import fr.quatrevieux.araknemu.game.fight.FightBaseCase; +import fr.quatrevieux.araknemu.game.fight.SpellEffectStub; import fr.quatrevieux.araknemu.game.fight.ai.FighterAI; import fr.quatrevieux.araknemu.game.fight.ai.action.logic.NullGenerator; import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation; +import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; import fr.quatrevieux.araknemu.game.fight.castable.CastScope; import fr.quatrevieux.araknemu.game.fight.castable.effect.Element; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.BuffHook; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.Damage; import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; +import fr.quatrevieux.araknemu.game.fight.fighter.FighterData; import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter; import fr.quatrevieux.araknemu.game.fight.map.FightCell; import fr.quatrevieux.araknemu.game.spell.Spell; @@ -62,7 +68,7 @@ public void setUp() throws Exception { fighter.init(); target.life().alterMax(target, 1000); ai = new FighterAI(fighter, fight, new NullGenerator()); - simulator = new PercentLifeDamageSimulator(Element.EARTH); + simulator = new PercentLifeDamageSimulator(container.get(Simulator.class), Element.EARTH); } @Test @@ -85,10 +91,84 @@ void simulateWithResistance() { fighter.life().damage(fighter, 100); assertEquals(-9, simulate().enemiesLife()); - simulator = new PercentLifeDamageSimulator(Element.WATER); + simulator = new PercentLifeDamageSimulator(container.get(Simulator.class), Element.WATER); assertEquals(-19, simulate().enemiesLife()); } + @Test + void simulateWithArmorBuff() { + target.buffs().add(new Buff( + SpellEffectStub.fixed(105, 10), + Mockito.mock(Spell.class), + target, + target, + new BuffHook() {} + )); + + assertEquals(-19, simulate().enemiesLife()); + assertEquals(0, simulate().selfLife()); + } + + @Test + void simulateWithCounterDamageCharacteristic() { + target.characteristics().alter(Characteristic.COUNTER_DAMAGE, 10); + + assertEquals(-29, simulate().enemiesLife()); + assertEquals(-10, simulate().selfLife()); + } + + @Test + void simulateWithCounterDamageBuff() { + container.get(Simulator.class).registerBuff(999, new BuffEffectSimulator() { + @Override + public Damage onReduceableDamage(Buff buff, FighterData target, Damage damage) { + return damage.reflect(10); + } + }); + + target.buffs().add( + new Buff( + SpellEffectStub.fixed(999, 10), + Mockito.mock(Spell.class), + target, + target, + new BuffHook() {} + ) + ); + + assertEquals(-29, simulate().enemiesLife()); + assertEquals(-10, simulate().selfLife()); + } + + @Test + void simulatePoisonShouldIgnoreArmor() { + target.buffs().add(new Buff( + SpellEffectStub.fixed(105, 10), + Mockito.mock(Spell.class), + target, + target, + new BuffHook() {} + )); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.min()).thenReturn(10); + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.duration()).thenReturn(2); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastSimulation simulation = new CastSimulation(spell, fighter, target.cell()); + + CastScope scope = makeCastScope(fighter, spell, effect, target.cell()); + simulator.simulate(simulation, ai, scope.effects().get(0)); + + assertEquals(-43.5, simulation.enemiesLife()); + } + @Test void simulateBuff() { SpellEffect effect = Mockito.mock(SpellEffect.class); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeLostDamageSimulatorTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeLostDamageSimulatorTest.java index 8c22c2648..e8317ce70 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeLostDamageSimulatorTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/PercentLifeLostDamageSimulatorTest.java @@ -23,12 +23,18 @@ import fr.quatrevieux.araknemu.data.value.EffectArea; import fr.quatrevieux.araknemu.game.fight.Fight; import fr.quatrevieux.araknemu.game.fight.FightBaseCase; +import fr.quatrevieux.araknemu.game.fight.SpellEffectStub; import fr.quatrevieux.araknemu.game.fight.ai.FighterAI; import fr.quatrevieux.araknemu.game.fight.ai.action.logic.NullGenerator; import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation; +import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; import fr.quatrevieux.araknemu.game.fight.castable.CastScope; import fr.quatrevieux.araknemu.game.fight.castable.effect.Element; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.BuffHook; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.Damage; import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; +import fr.quatrevieux.araknemu.game.fight.fighter.FighterData; import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter; import fr.quatrevieux.araknemu.game.fight.map.FightCell; import fr.quatrevieux.araknemu.game.spell.Spell; @@ -63,7 +69,7 @@ public void setUp() throws Exception { target.life().alterMax(target, 1000); fighter.life().damage(target, 100); ai = new FighterAI(fighter, fight, new NullGenerator()); - simulator = new PercentLifeLostDamageSimulator(Element.EARTH); + simulator = new PercentLifeLostDamageSimulator(container.get(Simulator.class), Element.EARTH); } @Test @@ -75,6 +81,54 @@ void simulateSimple() { assertEquals(-5, simulate().enemiesLife()); } + @Test + void simulateWithArmorBuff() { + target.buffs().add(new Buff( + SpellEffectStub.fixed(105, 5), + Mockito.mock(Spell.class), + target, + target, + new BuffHook() {} + )); + + assertEquals(-5, simulate().enemiesLife()); + assertEquals(0, simulate().selfLife()); + + fighter.life().heal(fighter, 50); + assertEquals(0, simulate().enemiesLife()); + } + + @Test + void simulateWithCounterDamageCharacteristic() { + target.characteristics().alter(Characteristic.COUNTER_DAMAGE, 10); + + assertEquals(-10, simulate().enemiesLife()); + assertEquals(-10, simulate().selfLife()); + } + + @Test + void simulateWithCounterDamageBuff() { + container.get(Simulator.class).registerBuff(999, new BuffEffectSimulator() { + @Override + public Damage onReduceableDamage(Buff buff, FighterData target, Damage damage) { + return damage.reflect(10); + } + }); + + target.buffs().add( + new Buff( + SpellEffectStub.fixed(999, 10), + Mockito.mock(Spell.class), + target, + target, + new BuffHook() {} + ) + ); + + assertEquals(-10, simulate().enemiesLife()); + assertEquals(-10, simulate().selfLife()); + } + @Test void simulateWithResistance() { target.characteristics().alter(Characteristic.RESISTANCE_PERCENT_EARTH, 25); @@ -86,7 +140,7 @@ void simulateWithResistance() { fighter.life().damage(fighter, 100); assertEquals(-10, simulate().enemiesLife()); - simulator = new PercentLifeLostDamageSimulator(Element.WATER); + simulator = new PercentLifeLostDamageSimulator(container.get(Simulator.class), Element.WATER); assertEquals(-20, simulate().enemiesLife()); } @@ -130,6 +184,35 @@ void simulateBuff() { assertEquals(-15, simulation.enemiesLife()); } + @Test + void simulatePoisonShouldIgnoreArmorBuff() { + target.buffs().add(new Buff( + SpellEffectStub.fixed(105, 5), + Mockito.mock(Spell.class), + target, + target, + new BuffHook() {} + )); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.min()).thenReturn(10); + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.duration()).thenReturn(2); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastSimulation simulation = new CastSimulation(spell, fighter, target.cell()); + + CastScope scope = makeCastScope(fighter, spell, effect, target.cell()); + simulator.simulate(simulation, ai, scope.effects().get(0)); + + assertEquals(-15, simulation.enemiesLife()); + } + @Test void simulateInfiniteBuff() { SpellEffect effect = Mockito.mock(SpellEffect.class); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/StealLifeSimulatorTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/StealLifeSimulatorTest.java index ad187dc15..5f28466f8 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/StealLifeSimulatorTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/StealLifeSimulatorTest.java @@ -23,12 +23,16 @@ import fr.quatrevieux.araknemu.data.value.EffectArea; import fr.quatrevieux.araknemu.game.fight.Fight; import fr.quatrevieux.araknemu.game.fight.FightBaseCase; +import fr.quatrevieux.araknemu.game.fight.SpellEffectStub; import fr.quatrevieux.araknemu.game.fight.ai.FighterAI; import fr.quatrevieux.araknemu.game.fight.ai.action.logic.NullGenerator; import fr.quatrevieux.araknemu.game.fight.ai.simulation.CastSimulation; +import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator; 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.Element; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.BuffHook; import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter; import fr.quatrevieux.araknemu.game.fight.map.FightCell; @@ -70,6 +74,30 @@ void simulateSimple() { assertEquals(7, simulate().selfLife()); } + @Test + void simulateWithCounterDamage() { + other.fighter().characteristics().alter(Characteristic.COUNTER_DAMAGE, 10); + + assertEquals(-15, simulate().enemiesLife()); + assertEquals(-3, simulate().selfLife()); + } + + @Test + void simulateWithArmorBuff() { + other.fighter().buffs().add( + new Buff( + SpellEffectStub.fixed(105, 10), + Mockito.mock(Spell.class), + other.fighter(), + other.fighter(), + new BuffHook() {} + ) + ); + + assertEquals(-5, simulate().enemiesLife()); + assertEquals(2, simulate().selfLife()); + } + @Test void simulateWithResistance() { target.characteristics().alter(Characteristic.RESISTANCE_EARTH, 5); @@ -86,7 +114,7 @@ void simulateWithBoost() { @Test void simulateBuff() { - StealLifeSimulator simulator = new StealLifeSimulator(Element.EARTH); + StealLifeSimulator simulator = new StealLifeSimulator(container.get(Simulator.class), Element.EARTH); SpellEffect effect = Mockito.mock(SpellEffect.class); Spell spell = Mockito.mock(Spell.class); @@ -121,7 +149,7 @@ void simulateBuff() { @Test void simulateArea() { - StealLifeSimulator simulator = new StealLifeSimulator(Element.EARTH); + StealLifeSimulator simulator = new StealLifeSimulator(container.get(Simulator.class), Element.EARTH); SpellEffect effect = Mockito.mock(SpellEffect.class); Spell spell = Mockito.mock(Spell.class); @@ -146,7 +174,7 @@ void simulateArea() { void score() { fighter.player().properties().characteristics().base().set(Characteristic.STRENGTH, 0); - StealLifeSimulator simulator = new StealLifeSimulator(Element.EARTH); + StealLifeSimulator simulator = new StealLifeSimulator(container.get(Simulator.class), Element.EARTH); SpellEffect effect = Mockito.mock(SpellEffect.class); Mockito.when(effect.min()).thenReturn(10); @@ -179,7 +207,7 @@ private CastSimulation simulate() { CastSimulation simulation = new CastSimulation(spell, fighter, target.cell()); CastScope scope = makeCastScope(fighter, spell, effect, target.cell()); - new StealLifeSimulator(Element.EARTH).simulate(simulation, ai, scope.effects().get(0)); + new StealLifeSimulator(container.get(Simulator.class), Element.EARTH).simulate(simulation, ai, scope.effects().get(0)); return simulation; }