Skip to content

Commit

Permalink
Merge pull request #353 from vincent4vx/feature-ai-buffs
Browse files Browse the repository at this point in the history
feat(ai): Handle armor buff on damage
  • Loading branch information
vincent4vx authored Jun 29, 2024
2 parents 6b40625 + 286b469 commit bffce79
Show file tree
Hide file tree
Showing 17 changed files with 960 additions and 178 deletions.
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;
}
}
Loading

0 comments on commit bffce79

Please sign in to comment.