Skip to content

Commit

Permalink
feat(ai): Use armor buff on other damage effects
Browse files Browse the repository at this point in the history
  • Loading branch information
vincent4vx committed Jun 29, 2024
1 parent c19eb60 commit 286b469
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 164 deletions.
18 changes: 9 additions & 9 deletions src/main/java/fr/quatrevieux/araknemu/game/GameModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -952,15 +952,15 @@ private void configureServices(ContainerConfigurator configurator) {
simulator.register(99, new DamageSimulator(simulator, Element.FIRE));
simulator.register(100, new DamageSimulator(simulator, 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(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
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 @@ -21,62 +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 {
private final Simulator simulator;
public final class DamageSimulator extends AbstractElementalDamageSimulator {
private final Element element;

public DamageSimulator(Simulator simulator, Element element) {
this.simulator = simulator;
super(simulator, element);

this.element = element;
}

@Override
public void simulate(CastSimulation simulation, AI ai, CastScope.EffectScope<? extends FighterData, ? extends BattlefieldCell> 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 int duration = spellEffect.duration();
final Interval damage = value.map(base -> computeDamage(base, simulation, target, duration == 0));

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
Expand All @@ -88,23 +66,4 @@ public void score(SpellScore score, SpellEffect effect, Characteristics characte

score.addDamage(value * boost / 100);
}

private @NonNegative int computeDamage(@NonNegative int value, CastSimulation simulation, FighterData target, boolean direct) {
Damage damage = new Damage(value, element)
.percent(target.characteristics().get(element.percentResistance()))
.fixed(target.characteristics().get(element.fixedResistance()))
;

if (direct) {
damage = simulator.applyReduceableDamageBuffs(target, damage);
}

final int reflectedDamage = damage.reflectedDamage() + target.characteristics().get(Characteristic.COUNTER_DAMAGE);

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

return Asserter.castNonNegative(damage.value());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,72 +20,28 @@
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
* Unlike {@link FixedDamageSimulator}, this effect is related to an element, so it can be reduced by armor
*
* @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<? extends FighterData, ? extends BattlefieldCell> 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<? 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(applyResistances(damage, target), target);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -42,53 +34,18 @@
* @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<? extends FighterData, ? extends BattlefieldCell> 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());

return Interval.of(effect.min(), Math.max(effect.max(), effect.min()))
.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<? 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(applyResistances(damage, target), target);
}
}
}
Loading

0 comments on commit 286b469

Please sign in to comment.