Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ai): Handle armor buff on damage #353

Merged
merged 2 commits into from
Jun 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 23 additions & 23 deletions src/main/java/fr/quatrevieux/araknemu/game/GameModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@
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;
import fr.quatrevieux.araknemu.game.fight.turn.action.util.CriticalityStrategy;
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;
Expand All @@ -38,6 +42,7 @@
*/
public final class Simulator {
private final Map<Integer, EffectSimulator> simulators = new HashMap<>();
private final Map<Integer, BuffEffectSimulator> buffSimulators = new HashMap<>();
private final CriticalityStrategy criticalityStrategy;

public Simulator(CriticalityStrategy criticalityStrategy) {
Expand All @@ -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 <E> The type of the simulator
*/
public <E extends BuffEffectSimulator & EffectSimulator> void registerEffectAndBuff(int effectId, E simulator) {
simulators.put(effectId, simulator);
buffSimulators.put(effectId, simulator);
}

/**
* Simulate the spell cast
*
Expand Down Expand Up @@ -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
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
* 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<? extends FighterData, ? extends BattlefieldCell> 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<? extends FighterData> 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<? extends FighterData> targets) {
for (FighterData target : targets) {
simulation.addDamage(applyResistancesAndArmor(damage, simulation, target), target);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<? extends FighterData, ? extends BattlefieldCell> effect) {
if (effect.effect().duration() == 0) {
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
* 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;

Check warning on line 41 in src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/BuffEffectSimulator.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/fr/quatrevieux/araknemu/game/fight/ai/simulation/effect/BuffEffectSimulator.java#L41

Added line #L41 was not covered by tests
}
}
Loading
Loading