Skip to content

Commit

Permalink
feat(ai): Add percent life lost damage simulator
Browse files Browse the repository at this point in the history
  • Loading branch information
vincent4vx committed Dec 23, 2023
1 parent a5c9be3 commit 318a0d3
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 1 deletion.
5 changes: 5 additions & 0 deletions src/main/java/fr/quatrevieux/araknemu/game/GameModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.InvokeMonsterSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.MoveBackSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.PercentLifeDamageSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.PercentLifeLostDamageSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.PunishmentSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.RemovePointsSimulator;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.effect.SetStateSimulator;
Expand Down Expand Up @@ -932,6 +933,10 @@ private void configureServices(ContainerConfigurator configurator) {
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(82, new FixedStealLifeSimulator());
simulator.register(131, new DamageOnActionPointUseSimulator());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

/**
* Simulator for damage depending on the life of the caster effect
* Unlike {@link PercentLifeDamageSimulator}, this effect is related to an element, so it can be reduced by armor
* 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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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.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.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
* This effect is related to an element, so it can be reduced by armor
*
* @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;
}

@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) {
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* 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.quatrevieux.araknemu.data.constant.Characteristic;
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.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.castable.CastScope;
import fr.quatrevieux.araknemu.game.fight.castable.effect.Element;
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;
import fr.quatrevieux.araknemu.game.spell.Spell;
import fr.quatrevieux.araknemu.game.spell.SpellConstraints;
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.CircleArea;
import fr.quatrevieux.araknemu.game.spell.effect.target.SpellEffectTarget;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.assertEquals;

class PercentLifeLostDamageSimulatorTest extends FightBaseCase {
private Fight fight;
private PlayerFighter fighter;
private Fighter target;
private FighterAI ai;
private PercentLifeLostDamageSimulator simulator;

@Override
@BeforeEach
public void setUp() throws Exception {
super.setUp();

fight = createFight();
fighter = player.fighter();
target = other.fighter();
target.init();
fighter.init();
target.life().alterMax(target, 1000);
fighter.life().alter(target, -100);
ai = new FighterAI(fighter, fight, new NullGenerator());
simulator = new PercentLifeLostDamageSimulator(Element.EARTH);
}

@Test
void simulateSimple() {
assertEquals(-10, simulate().enemiesLife());
assertEquals(0, simulate().selfLife());

fighter.life().alter(fighter, 50);
assertEquals(-5, simulate().enemiesLife());
}

@Test
void simulateWithResistance() {
target.characteristics().alter(Characteristic.RESISTANCE_PERCENT_EARTH, 25);
target.characteristics().alter(Characteristic.RESISTANCE_EARTH, 5);

assertEquals(-2, simulate().enemiesLife());
assertEquals(0, simulate().selfLife());

fighter.life().alter(fighter, -100);
assertEquals(-10, simulate().enemiesLife());

simulator = new PercentLifeLostDamageSimulator(Element.WATER);
assertEquals(-20, simulate().enemiesLife());
}

@Test
void simulateBuff() {
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<Fighter, FightCell> scope = makeCastScope(fighter, spell, effect, target.cell());
simulator.simulate(simulation, ai, scope.effects().get(0));

assertEquals(-15, simulation.enemiesLife());

Mockito.when(effect.duration()).thenReturn(5);
simulation = new CastSimulation(spell, fighter, target.cell());
scope = makeCastScope(fighter, spell, effect, target.cell());
simulator.simulate(simulation, ai, scope.effects().get(0));
assertEquals(-37.5, simulation.enemiesLife());

Mockito.when(effect.duration()).thenReturn(20);
simulation = new CastSimulation(spell, fighter, target.cell());
scope = makeCastScope(fighter, spell, effect, target.cell());
simulator.simulate(simulation, ai, scope.effects().get(0));
assertEquals(-75, simulation.enemiesLife());

target.characteristics().alter(Characteristic.RESISTANCE_PERCENT_EARTH, 25);
target.characteristics().alter(Characteristic.RESISTANCE_EARTH, 5);

simulation = new CastSimulation(spell, fighter, target.cell());
simulator.simulate(simulation, ai, scope.effects().get(0));
assertEquals(-15, simulation.enemiesLife());
}

@Test
void simulateInfiniteBuff() {
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(-1);
Mockito.when(spell.constraints()).thenReturn(constraints);
Mockito.when(constraints.freeCell()).thenReturn(false);

CastSimulation simulation = new CastSimulation(spell, fighter, target.cell());

CastScope<Fighter, FightCell> scope = makeCastScope(fighter, spell, effect, target.cell());
simulator.simulate(simulation, ai, scope.effects().get(0));

assertEquals(-75, simulation.enemiesLife());
}

@Test
void simulateArea() {
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 CircleArea(new EffectArea(EffectArea.Type.CIRCLE, 10)));
Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT);
Mockito.when(spell.constraints()).thenReturn(constraints);
Mockito.when(constraints.freeCell()).thenReturn(false);

CastSimulation simulation = new CastSimulation(spell, fighter, other.fighter().cell());

CastScope<Fighter, FightCell> scope = makeCastScope(fighter, spell, effect, other.fighter().cell());
simulator.simulate(simulation, ai, scope.effects().get(0));

assertEquals(-10, simulation.selfLife());
assertEquals(-10, simulation.enemiesLife());
}

private CastSimulation simulate() {
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(spell.constraints()).thenReturn(constraints);
Mockito.when(constraints.freeCell()).thenReturn(false);

CastSimulation simulation = new CastSimulation(spell, fighter, target.cell());

CastScope<Fighter, FightCell> scope = makeCastScope(fighter, spell, effect, target.cell());
simulator.simulate(simulation, ai, scope.effects().get(0));

return simulation;
}
}

0 comments on commit 318a0d3

Please sign in to comment.