From c8dff52edc3e909207bd47cd53a8934d2316dc8e Mon Sep 17 00:00:00 2001 From: Vincent Quatrevieux Date: Sat, 27 Jan 2024 16:31:17 +0100 Subject: [PATCH] feat(fight): Apply attenuation of effect by distance from center of area effect --- .../game/fight/castable/BaseCastScope.java | 41 ++- .../fight/castable/CastTargetResolver.java | 60 +--- .../game/fight/castable/CastTargets.java | 292 ++++++++++++++++++ .../fight/castable/effect/EffectsUtils.java | 45 +++ .../AbstractAttenuableAreaEffectHandler.java | 82 +++++ .../effect/handler/damage/Damage.java | 16 +- .../effect/handler/damage/DamageApplier.java | 8 +- .../effect/handler/damage/DamageHandler.java | 11 +- .../handler/damage/StealLifeHandler.java | 11 +- .../effect/handler/heal/HealHandler.java | 34 +- .../fight/module/CommonEffectsModule.java | 2 +- .../game/fight/castable/CastScopeTest.java | 138 ++++++++- .../game/fight/castable/CastTargetsTest.java | 186 +++++++++++ .../castable/effect/EffectsUtilsTest.java | 19 ++ .../fight/castable/effect/FunctionalTest.java | 4 +- .../handler/damage/DamageApplierTest.java | 71 +++-- .../handler/damage/DamageHandlerTest.java | 95 +++++- .../effect/handler/damage/DamageTest.java | 19 ++ .../handler/damage/StealLifeHandlerTest.java | 8 +- .../effect/handler/heal/HealHandlerTest.java | 6 +- .../MaximizeTargetEffectsHandlerTest.java | 2 +- .../MinimizeCastedEffectsHandlerTest.java | 2 +- .../modifier/MultiplyDamageHandlerTest.java | 2 +- 23 files changed, 1025 insertions(+), 129 deletions(-) create mode 100644 src/main/java/fr/quatrevieux/araknemu/game/fight/castable/CastTargets.java create mode 100644 src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/AbstractAttenuableAreaEffectHandler.java create mode 100644 src/test/java/fr/quatrevieux/araknemu/game/fight/castable/CastTargetsTest.java diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/BaseCastScope.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/BaseCastScope.java index 1db6503e5..fae67d22f 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/BaseCastScope.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/BaseCastScope.java @@ -19,10 +19,12 @@ package fr.quatrevieux.araknemu.game.fight.castable; +import fr.arakne.utils.maps.CoordinateCell; import fr.quatrevieux.araknemu.game.fight.fighter.FighterData; import fr.quatrevieux.araknemu.game.fight.map.BattlefieldCell; import fr.quatrevieux.araknemu.game.spell.Spell; import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect; +import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.Pure; @@ -179,9 +181,9 @@ public final List effects() { public final class EffectScope implements CastScope.EffectScope { private final SpellEffect effect; - private final Collection targets; + private final CastTargets targets; - public EffectScope(SpellEffect effect, Collection targets) { + public EffectScope(SpellEffect effect, CastTargets targets) { this.effect = effect; this.targets = targets; } @@ -235,5 +237,40 @@ public Collection targets() { public Set cells() { return effect.area().resolve(target, from); } + + /** + * Iterate over each target and distance from the center of the area effect + * Like {@link #targets()}, target mapping is resolved, and dead targets are ignored + * + * @param consumer The callback. Takes as first argument the target, and as second argument the distance. Return false to stop iteration + */ + public void forEachTargetAndDistance(TargetDistanceConsumer consumer) { + final CoordinateCell baseCell = target.coordinate(); + + targets.forEach((target, cell) -> { + final F actualTarget = resolveTarget(target); + + if (actualTarget == null || actualTarget.dead()) { + return true; + } + + final int distance = baseCell.distance(cell); + + return consumer.accept(actualTarget, distance); + }); + } + } + + @FunctionalInterface + public interface TargetDistanceConsumer { + /** + * Handle a target + * + * @param target The target + * @param distance The distance from the center of the area effect + * + * @return true to continue the iteration, false to stop + */ + public boolean accept(F target, @NonNegative int distance); } } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/CastTargetResolver.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/CastTargetResolver.java index 1d735fcf0..38c1afed6 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/CastTargetResolver.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/CastTargetResolver.java @@ -24,10 +24,6 @@ import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect; import org.checkerframework.checker.nullness.qual.Nullable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; - /** * Perform target resolution for a casted effect */ @@ -37,22 +33,6 @@ public final class CastTargetResolver { */ private CastTargetResolver() {} - /** - * Resolve targets of an effect - * - * @param caster The action caster - * @param target The target cell - * @param action Action to perform - * @param effect The effect to resolve - * - * @return List of fighters - * - * @see fr.quatrevieux.araknemu.game.spell.effect.target.EffectTarget - */ - public static Collection resolveFromEffect(F caster, BattlefieldCell target, Castable action, SpellEffect effect) { - return resolveFromEffect(caster, caster.cell(), target, action, effect); - } - /** * Resolve targets of an effect * @@ -66,13 +46,13 @@ public static Collection resolveFromEffect(F caster, * * @see fr.quatrevieux.araknemu.game.spell.effect.target.EffectTarget */ - public static Collection resolveFromEffect(F caster, BattlefieldCell from, BattlefieldCell target, Castable action, SpellEffect effect) { + public static CastTargets resolveFromEffect(F caster, BattlefieldCell from, BattlefieldCell target, Castable action, SpellEffect effect) { if (effect.target().onlyCaster()) { - return Collections.singleton(caster); + return CastTargets.one(caster, target); } if (action.constraints().freeCell()) { - return Collections.emptyList(); + return CastTargets.empty(); } return resolveFromEffectArea(caster, from, target, effect); @@ -82,10 +62,9 @@ public static Collection resolveFromEffect(F caster, * Perform resolution from effect target and effect area */ @SuppressWarnings("cast.unsafe") // @Nullable cast cause a compiler crash on java 8 - private static Collection resolveFromEffectArea(F caster, BattlefieldCell from, BattlefieldCell target, SpellEffect effect) { - // Use lazy instantiation and do not use stream API to optimise memory allocations - F firstTarget = null; - Collection targets = null; + private static CastTargets resolveFromEffectArea(F caster, BattlefieldCell from, BattlefieldCell target, SpellEffect effect) { + // Do not use stream API to optimise memory allocations + final CastTargets.Builder builder = new CastTargets.Builder<>(); for (BattlefieldCell cell : effect.area().resolve(target, from)) { final @Nullable F resolvedTarget = (/*@Nullable*/ F) cell.fighter(); @@ -94,32 +73,9 @@ private static Collection resolveFromEffectArea(F cas continue; } - // Found the first target - if (firstTarget == null) { - firstTarget = resolvedTarget; - continue; - } - - // Multiple targets are found : instantiate the collection - if (targets == null) { - targets = new ArrayList<>(); - targets.add(firstTarget); - } - - targets.add(resolvedTarget); - } - - // There is multiple targets - if (targets != null) { - return targets; - } - - // There is only one target : create a singleton - if (firstTarget != null) { - return Collections.singleton(firstTarget); + builder.add(resolvedTarget, cell); } - // No targets are resolved - return Collections.emptyList(); + return builder.build(); } } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/CastTargets.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/CastTargets.java new file mode 100644 index 000000000..2b79c6487 --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/CastTargets.java @@ -0,0 +1,292 @@ +/* + * 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.castable; + +import fr.quatrevieux.araknemu.game.fight.fighter.FighterData; +import fr.quatrevieux.araknemu.game.fight.map.BattlefieldCell; +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.index.qual.Positive; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.util.NullnessUtil; + +import java.util.Collections; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Store targets of a casted effect + * It associates a target fighter with its cell + * + * The goal of this class is to minimize memory allocations, and provide an immutable "collection" of targets + */ +@SuppressWarnings({"accessing.nullable", "array.access.unsafe.high.range"}) +public final class CastTargets implements Iterable { + @SuppressWarnings("argument") + private static final CastTargets EMPTY = new CastTargets<>(null, null, null, null, 0); + + private final @Nullable F fighter; + private final @Nullable BattlefieldCell cell; + private final F @Nullable [] fighters; + private final BattlefieldCell @Nullable [] cells; + private final @NonNegative int size; + + private CastTargets(@Nullable F fighter, @Nullable BattlefieldCell cell, F @Nullable [] fighters, BattlefieldCell @Nullable [] cells, @NonNegative int size) { + this.fighter = fighter; + this.cell = cell; + this.fighters = fighters; + this.cells = cells; + this.size = size; + } + + @Override + public Iterator iterator() { + switch (size) { + case 0: + return Collections.emptyIterator(); + + case 1: + return new SingletonIterator(); + + default: + return new ManyIterator(); + } + } + + /** + * Iterate over targets and cells + * + * @param consumer The consumer. Takes the fighter and its cell as arguments, and returns true if the iteration should continue, false otherwise + */ + @SuppressWarnings("array.access.unsafe.high") + public void forEach(TargetConsumer consumer) { + final @NonNegative int size = this.size; + + if (size == 0) { + return; + } + + if (size == 1) { + consumer.accept( + NullnessUtil.castNonNull(fighter), + NullnessUtil.castNonNull(cell) + ); + return; + } + + final F[] fighters = NullnessUtil.castNonNull(this.fighters); + final BattlefieldCell[] cells = NullnessUtil.castNonNull(this.cells); + + for (int i = 0; i < size; i++) { + final boolean ok = consumer.accept( + NullnessUtil.castNonNull(fighters[i]), + NullnessUtil.castNonNull(cells[i]) + ); + + if (!ok) { + break; + } + } + } + + /** + * Get an empty target + * + * @return The empty instance. + * @param The fighter type + */ + public static CastTargets empty() { + return (CastTargets) EMPTY; + } + + /** + * Create a singleton target + * + * @param fighter The target fighter + * @param cell The target cell. This cell should not the fighter cell but the target cell + * + * @return The new instance + * + * @param The fighter type + */ + public static CastTargets one(F fighter, BattlefieldCell cell) { + return new CastTargets<>(fighter, cell, (F[]) null, (BattlefieldCell[]) null, 1); + } + + /** + * Create a builder for multiple targets + * + * @return The builder instance + * + * @param The fighter type + */ + public static Builder builder() { + return new Builder<>(); + } + + /** + * Create with multiple targets + * + * @param fighters Array of fighters + * @param cells Array of cells. Should be the same size as fighters + * @param size The size of both arrays + * + * @return The new instance + * + * @param The fighter type + */ + private static CastTargets many(F[] fighters, BattlefieldCell[] cells, @NonNegative int size) { + return new CastTargets<>(null, null, fighters, cells, size); + } + + @FunctionalInterface + public interface TargetConsumer { + public boolean accept(F fighter, BattlefieldCell cell); + } + + /** + * Builder for CastTargets + * + * @param The fighter type + */ + public static final class Builder { + private static final @Positive int INITIAL_CAPACITY = 4; + + private @Nullable F fighter = null; + private @Nullable BattlefieldCell cell = null; + private F @Nullable [] fighters = null; + private BattlefieldCell @Nullable [] cells = null; + private @NonNegative int size = 0; + + /** + * Add a target + * + * @param fighter The target fighter. + * @param cell The cell of the fighter. + */ + public void add(F fighter, BattlefieldCell cell) { + switch (size) { + case 0: + singleton(fighter, cell); + break; + + case 1: + pair(fighter, cell); + break; + + default: + push(fighter, cell); + } + } + + /** + * Build the CastTargets instance + */ + public CastTargets build() { + switch (size) { + case 0: + return CastTargets.empty(); + + case 1: + return CastTargets.one(NullnessUtil.castNonNull(fighter), NullnessUtil.castNonNull(cell)); + + default: + return CastTargets.many(NullnessUtil.castNonNull(fighters), NullnessUtil.castNonNull(cells), size); + } + } + + private void singleton(F fighter, BattlefieldCell cell) { + this.fighter = fighter; + this.cell = cell; + size = 1; + } + + @SuppressWarnings({"unchecked", "array.access.unsafe.high.constant", "array.access.unsafe.high"}) + private void pair(F fighter, BattlefieldCell cell) { + fighters = (F[]) new FighterData[INITIAL_CAPACITY]; + cells = new BattlefieldCell[INITIAL_CAPACITY]; + + fighters[0] = NullnessUtil.castNonNull(this.fighter); + cells[0] = NullnessUtil.castNonNull(this.cell); + + fighters[1] = fighter; + cells[1] = cell; + + size = 2; + } + + @SuppressWarnings({"dereference.of.nullable", "argument", "unchecked", "array.access.unsafe.high"}) + private void push(F fighter, BattlefieldCell cell) { + final int lastSize = size; + + if (lastSize == fighters.length) { + final F[] newFighters = (F[]) new FighterData[lastSize * 2]; + final BattlefieldCell[] newCells = new BattlefieldCell[lastSize * 2]; + + System.arraycopy(fighters, 0, newFighters, 0, lastSize); + System.arraycopy(cells, 0, newCells, 0, lastSize); + + fighters = newFighters; + cells = newCells; + } + + fighters[lastSize] = fighter; + cells[lastSize] = cell; + + size++; + } + } + + private final class ManyIterator implements Iterator { + private @NonNegative int index = 0; + + @Override + public boolean hasNext() { + return index < size; + } + + @Override + public F next() { + if (index >= size) { + throw new NoSuchElementException(); + } + + return NullnessUtil.castNonNull(fighters[index++]); + } + } + + private final class SingletonIterator implements Iterator { + private boolean hasNext = true; + + @Override + public boolean hasNext() { + return hasNext; + } + + @Override + public F next() { + if (!hasNext) { + throw new NoSuchElementException(); + } + + hasNext = false; + return NullnessUtil.castNonNull(fighter); + } + } +} diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/EffectsUtils.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/EffectsUtils.java index 43a5b6cfd..834807450 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/EffectsUtils.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/EffectsUtils.java @@ -19,6 +19,9 @@ package fr.quatrevieux.araknemu.game.fight.castable.effect; +import fr.quatrevieux.araknemu.util.Asserter; +import org.checkerframework.checker.index.qual.NonNegative; + /** * Utility class for effects * @@ -56,4 +59,46 @@ public static boolean isLooseApEffect(int id) { || id == 168 ; } + + /** + * Compute the attenuation of an effect based on distance from the center of the area + * + * For each unit of distance, the effect is attenuated by 10% (from the previous distance), + * so this gives the following results : + * - Distance 0: 100% + * - Distance 1: 90% + * - Distance 2: 81% + * - Distance 5: 59% + * - Distance 10: 34% + * + * The formula used is : {@code value * 0.9 ** distance} + * + * @param value The base effect value + * @param distance The distance from the center of the area + * + * @return The attenuated effect value + */ + public static @NonNegative int applyDistanceAttenuation(@NonNegative int value, @NonNegative int distance) { + // Usage of Math.pow is around 5x slower than performing the calculation manually using integer + // So we use a switch to optimize the most common cases (nearly all classes spells have area <= 4) + switch (distance) { + case 0: + return value; + + case 1: + return value * 9 / 10; + + case 2: + return value * 81 / 100; + + case 3: + return value * 729 / 1000; + + case 4: + return value * 6561 / 10000; + + default: + return Asserter.castNonNegative((int) (value * Math.pow(0.9, distance))); + } + } } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/AbstractAttenuableAreaEffectHandler.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/AbstractAttenuableAreaEffectHandler.java new file mode 100644 index 000000000..7fdec4993 --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/AbstractAttenuableAreaEffectHandler.java @@ -0,0 +1,82 @@ +/* + * 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.castable.effect.handler; + +import fr.quatrevieux.araknemu.game.fight.Fight; +import fr.quatrevieux.araknemu.game.fight.castable.BaseCastScope; +import fr.quatrevieux.araknemu.game.fight.castable.FightCastScope; +import fr.quatrevieux.araknemu.game.fight.castable.effect.EffectValue; +import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; +import fr.quatrevieux.araknemu.game.fight.fighter.FighterData; +import fr.quatrevieux.araknemu.game.fight.map.FightCell; +import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect; +import org.checkerframework.checker.index.qual.NonNegative; + +/** + * Base implementation for effect handler which has its effect value diminished by the distance from + * the center of the area effect. + * + * Note: Only a single "dice" is rolled for all targets, so all targets have the same base value (except the reduction) + * + * Only method {@link EffectHandler#handle(FightCastScope, BaseCastScope.EffectScope)} is implemented, and marked as final. + * So you need to implement : + * - {@link EffectHandler#buff(FightCastScope, BaseCastScope.EffectScope)} for applying the effect as buff + * - {@link AbstractAttenuableAreaEffectHandler#applyOnTarget(FightCastScope, SpellEffect, Fighter, EffectValue, int)} for applying the effect on each target + * + * Note: The application of the effect is stopped if the fight is not active anymore (e.g. last target died by the effect) + */ +public abstract class AbstractAttenuableAreaEffectHandler implements EffectHandler { + private final Fight fight; + + public AbstractAttenuableAreaEffectHandler(Fight fight) { + this.fight = fight; + } + + @Override + public final void handle(FightCastScope cast, BaseCastScope.EffectScope effect) { + final Fight fight = this.fight; + final SpellEffect spellEffect = effect.effect(); + final EffectValue.Context context = EffectValue.preRoll(spellEffect, cast.caster()); + + effect.forEachTargetAndDistance((target, distance) -> { + if (!fight.active()) { + return false; + } + + return applyOnTarget(cast, spellEffect, target, context.forTarget(target), distance); + }); + } + + /** + * Apply the effect on the target + * + * @param cast The cast action arguments + * @param effect The spell effect to apply + * @param target The target of the effect + * @param value The pre-roll value. Buff hooks are already applied + * @param distance The distance from the center of the area effect + * + * @return true to continue the application of the effect on the next target, false to stop + * + * @see EffectValue#preRoll(SpellEffect, FighterData) Called to get the pre-roll value + * @see EffectValue.Context#forTarget(FighterData) Called to get the effect value instance for the target + */ + protected abstract boolean applyOnTarget(FightCastScope cast, SpellEffect effect, Fighter target, EffectValue value, @NonNegative int distance); +} diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/Damage.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/Damage.java index 8dd069798..7def8c873 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/Damage.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/Damage.java @@ -19,7 +19,9 @@ package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage; +import fr.quatrevieux.araknemu.game.fight.castable.effect.EffectsUtils; import fr.quatrevieux.araknemu.game.fight.castable.effect.Element; +import org.checkerframework.checker.index.qual.NonNegative; /** * Compute suffered damage @@ -36,6 +38,7 @@ public final class Damage implements MultipliableDamage { private int percent = 100; private int reduce = 0; private int returned = 0; + private @NonNegative int distance = 0; public Damage(int value, Element element) { this.value = value; @@ -57,7 +60,7 @@ public int value() { return 0; } - return base * multiply; + return EffectsUtils.applyDistanceAttenuation(base, distance) * multiply; } /** @@ -107,6 +110,17 @@ public Damage reflect(int value) { return this; } + /** + * Set the distance between the center of the effect and target + * + * Each distance reduce damage by 10% (cumulated), so distance 1 reduce damage by 10%, distance 2 by 19%, etc... + */ + public Damage distance(@NonNegative int distance) { + this.distance = distance; + + return this; + } + /** * Get the damage reduction value from armor buff effects */ diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageApplier.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageApplier.java index bc142aa59..70fb82655 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageApplier.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageApplier.java @@ -29,6 +29,7 @@ import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect; import fr.quatrevieux.araknemu.network.game.out.fight.action.ActionEffect; import fr.quatrevieux.araknemu.util.Asserter; +import org.checkerframework.checker.index.qual.NonNegative; /** * Apply simple damage to fighter @@ -60,6 +61,7 @@ public DamageApplier(Element element, Fight fight) { * @param effect The effect to apply * @param target The target * @param value The pre-roll value. Must be configured for the given caster and target. + * @param distance The distance between the center of the effect and the current target. Should be 0 for single cell effect. * * @return The real damage value * @@ -67,16 +69,18 @@ public DamageApplier(Element element, Fight fight) { * @see fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buffs#onDirectDamage(Fighter, Damage) The called buff hook * @see fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buffs#onDirectDamageApplied(Fighter, int) Buff called when damage are applied */ - public int apply(Fighter caster, SpellEffect effect, Fighter target, EffectValue value) { + public int apply(Fighter caster, SpellEffect effect, Fighter target, EffectValue value, @NonNegative int distance) { final Damage damage = computeDamage(caster, effect, target, value); + damage.distance(distance); + return applyDirectDamage(caster, damage, target); } /** * Apply a fixed (i.e. precomputed) amount of damage on the target * - * Like {@link DamageApplier#apply(Fighter, SpellEffect, Fighter, EffectValue)} : + * Like {@link DamageApplier#apply(Fighter, SpellEffect, Fighter, EffectValue, int)} : * - resistance are applied * - direct damage buff are called * - returned damage are applied diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageHandler.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageHandler.java index 84c049794..8a649a4d3 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageHandler.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageHandler.java @@ -25,15 +25,16 @@ 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.AbstractPreRollEffectHandler; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.AbstractAttenuableAreaEffectHandler; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.EffectHandler; import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect; +import org.checkerframework.checker.index.qual.NonNegative; /** * Handle simple damage effect */ -public final class DamageHandler extends AbstractPreRollEffectHandler implements EffectHandler, BuffHook { +public final class DamageHandler extends AbstractAttenuableAreaEffectHandler implements EffectHandler, BuffHook { private final DamageApplier applier; public DamageHandler(Element element, Fight fight) { @@ -43,8 +44,10 @@ public DamageHandler(Element element, Fight fight) { } @Override - protected void applyOnTarget(FightCastScope cast, SpellEffect effect, Fighter target, EffectValue value) { - applier.apply(cast.caster(), effect, target, value); + protected boolean applyOnTarget(FightCastScope cast, SpellEffect effect, Fighter target, EffectValue value, @NonNegative int distance) { + applier.apply(cast.caster(), effect, target, value, distance); + + return true; } @Override diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/StealLifeHandler.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/StealLifeHandler.java index 256ba7a8f..9f169cf0a 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/StealLifeHandler.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/StealLifeHandler.java @@ -25,15 +25,16 @@ 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.AbstractPreRollEffectHandler; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.AbstractAttenuableAreaEffectHandler; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.EffectHandler; import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect; +import org.checkerframework.checker.index.qual.NonNegative; /** * Handle steal life */ -public final class StealLifeHandler extends AbstractPreRollEffectHandler implements EffectHandler, BuffHook { +public final class StealLifeHandler extends AbstractAttenuableAreaEffectHandler implements EffectHandler, BuffHook { private final DamageApplier applier; public StealLifeHandler(Element element, Fight fight) { @@ -43,10 +44,12 @@ public StealLifeHandler(Element element, Fight fight) { } @Override - protected void applyOnTarget(FightCastScope cast, SpellEffect effect, Fighter target, EffectValue value) { + protected boolean applyOnTarget(FightCastScope cast, SpellEffect effect, Fighter target, EffectValue value, @NonNegative int distance) { final Fighter caster = cast.caster(); - applyCasterHeal(applier.apply(caster, effect, target, value), caster); + applyCasterHeal(applier.apply(caster, effect, target, value, distance), caster); + + return true; } @Override diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealHandler.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealHandler.java index 49b5dc760..15f4d93c2 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealHandler.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealHandler.java @@ -20,25 +20,40 @@ package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.heal; import fr.quatrevieux.araknemu.data.constant.Characteristic; +import fr.quatrevieux.araknemu.game.fight.Fight; import fr.quatrevieux.araknemu.game.fight.castable.FightCastScope; import fr.quatrevieux.araknemu.game.fight.castable.effect.EffectValue; +import fr.quatrevieux.araknemu.game.fight.castable.effect.EffectsUtils; 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.AbstractAttenuableAreaEffectHandler; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.EffectHandler; import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect; +import org.checkerframework.checker.index.qual.NonNegative; /** * Handle basic heal effect * * This effect is boosted by {@link Characteristic#INTELLIGENCE} and {@link Characteristic#HEALTH_BOOST} */ -public final class HealHandler implements EffectHandler, BuffHook { +public final class HealHandler extends AbstractAttenuableAreaEffectHandler implements EffectHandler, BuffHook { + public HealHandler(Fight fight) { + super(fight); + } + @Override - public void handle(FightCastScope cast, FightCastScope.EffectScope effect) { - for (Fighter target : effect.targets()) { - apply(cast.caster(), effect.effect(), target); - } + protected boolean applyOnTarget(FightCastScope cast, SpellEffect effect, Fighter target, EffectValue value, @NonNegative int distance) { + final Fighter caster = cast.caster(); + + value + .percent(caster.characteristics().get(Characteristic.INTELLIGENCE)) + .fixed(caster.characteristics().get(Characteristic.HEALTH_BOOST)) + ; + + target.life().alter(caster, EffectsUtils.applyDistanceAttenuation(value.value(), distance)); + + return true; } @Override @@ -50,17 +65,16 @@ public void buff(FightCastScope cast, FightCastScope.EffectScope effect) { @Override public boolean onStartTurn(Buff buff) { - apply(buff.caster(), buff.effect(), buff.target()); + final Fighter caster = buff.caster(); + final SpellEffect effect = buff.effect(); + final Fighter target = buff.target(); - return true; - } - - private void apply(Fighter caster, SpellEffect effect, Fighter target) { final EffectValue value = EffectValue.create(effect, caster, target) .percent(caster.characteristics().get(Characteristic.INTELLIGENCE)) .fixed(caster.characteristics().get(Characteristic.HEALTH_BOOST)) ; target.life().alter(caster, value.value()); + return true; } } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/module/CommonEffectsModule.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/module/CommonEffectsModule.java index 488d3af63..c78fd61fd 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/module/CommonEffectsModule.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/module/CommonEffectsModule.java @@ -140,7 +140,7 @@ public void effects(EffectsHandler handler) { handler.register(81, new HealOnDamageHandler()); handler.register(90, new GivePercentLifeHandler()); - handler.register(108, new HealHandler()); + handler.register(108, new HealHandler(fight)); handler.register(143, new FixedHealHandler()); handler.register(786, new HealOnAttackHandler()); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/CastScopeTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/CastScopeTest.java index c8a30cf54..406862daf 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/CastScopeTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/CastScopeTest.java @@ -34,11 +34,13 @@ import fr.quatrevieux.araknemu.game.spell.effect.area.CircleArea; import fr.quatrevieux.araknemu.game.spell.effect.area.LineArea; import fr.quatrevieux.araknemu.game.spell.effect.target.SpellEffectTarget; +import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; @@ -110,11 +112,12 @@ void withEffectsWillResolveTarget() { Mockito.when(spell.constraints()).thenReturn(constraints); Mockito.when(constraints.freeCell()).thenReturn(false); - CastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); assertCount(1, scope.effects()); assertEquals(effect, scope.effects().get(0).effect()); assertEquals(Collections.singleton(target), scope.effects().get(0).targets()); + assertForEachTargetAndDistance(scope.effects().get(0), Pair.of(target, 0)); assertEquals(Collections.singleton(target.cell()), scope.effects().get(0).cells()); } @@ -129,11 +132,12 @@ void withEffectsWithFreeCellConstraintWillNotResolveTargets() { Mockito.when(spell.constraints()).thenReturn(constraints); Mockito.when(constraints.freeCell()).thenReturn(true); - CastScope scope = FightCastScope.simple(spell, caster, fight.map().get(123), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, caster, fight.map().get(123), Collections.singletonList(effect)); assertCount(1, scope.effects()); assertEquals(effect, scope.effects().get(0).effect()); assertEquals(Collections.emptyList(), scope.effects().get(0).targets()); + assertForEachTargetAndDistance(scope.effects().get(0)); } @Test @@ -214,9 +218,10 @@ void resolveTargetsWithAreaTwoFighters() { Mockito.when(spell.constraints()).thenReturn(constraints); Mockito.when(constraints.freeCell()).thenReturn(false); - CastScope scope = FightCastScope.simple(spell, caster, fight.map().get(123), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, caster, fight.map().get(123), Collections.singletonList(effect)); assertEquals(Arrays.asList(caster, target), scope.effects().get(0).targets()); + assertForEachTargetAndDistance(scope.effects().get(0), Pair.of(caster, 2), Pair.of(target, 4)); } @Test @@ -230,9 +235,10 @@ void resolveTargetsWithAreaOneFighter() { Mockito.when(spell.constraints()).thenReturn(constraints); Mockito.when(constraints.freeCell()).thenReturn(false); - CastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); assertEquals(Collections.singleton(target), scope.effects().get(0).targets()); + assertForEachTargetAndDistance(scope.effects().get(0), Pair.of(target, 0)); } @Test @@ -246,9 +252,10 @@ void resolveTargetsWithAreaNoTargets() { Mockito.when(spell.constraints()).thenReturn(constraints); Mockito.when(constraints.freeCell()).thenReturn(false); - CastScope scope = FightCastScope.simple(spell, caster, fight.map().get(2), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, caster, fight.map().get(2), Collections.singletonList(effect)); assertEquals(Collections.emptyList(), scope.effects().get(0).targets()); + assertForEachTargetAndDistance(scope.effects().get(0)); } @Test @@ -427,10 +434,87 @@ void targets() { Mockito.when(spell.constraints()).thenReturn(constraints); Mockito.when(constraints.freeCell()).thenReturn(false); - CastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); assertCollectionEquals(scope.targets(), target, caster); - assertCollectionEquals(scope.effects().get(0).targets(), target, caster); + assertEquals(Arrays.asList(target, caster), scope.effects().get(0).targets()); + assertForEachTargetAndDistance(scope.effects().get(0), Pair.of(target, 0), Pair.of(caster, 6)); + } + + @Test + void targetsMultipleShouldBeOrderedByDistance() { + Fight fight = fightBuilder() + .addSelf(fb -> fb.cell(277)) + .addEnemy(fb -> fb.cell(263)) + .addEnemy(fb -> fb.cell(249)) + .addEnemy(fb -> fb.cell(234)) + .build(true) + ; + + fight.nextState(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + 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); + + List fighters = fight.turnList().fighters(); + + FightCastScope scope = FightCastScope.simple(spell, fighters.get(0), fight.map().get(263), Collections.singletonList(effect)); + + assertCollectionEquals(scope.targets(), fighters.get(0), fighters.get(1), fighters.get(2), fighters.get(3)); + assertEquals( + Arrays.asList( + fighters.get(1), fighters.get(3), fighters.get(0), fighters.get(2) + ), + scope.effects().get(0).targets() + ); + assertForEachTargetAndDistance( + scope.effects().get(0), + Pair.of(fighters.get(1), 0), + Pair.of(fighters.get(3), 1), + Pair.of(fighters.get(0), 1), + Pair.of(fighters.get(2), 2) + ); + } + + @Test + void forEachTargetWithReturnFalse() { + Fight fight = fightBuilder() + .addSelf(fb -> fb.cell(277)) + .addEnemy(fb -> fb.cell(263)) + .addEnemy(fb -> fb.cell(249)) + .addEnemy(fb -> fb.cell(234)) + .build(true) + ; + + fight.nextState(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + 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); + + List fighters = fight.turnList().fighters(); + + FightCastScope scope = FightCastScope.simple(spell, fighters.get(0), fight.map().get(263), Collections.singletonList(effect)); + + List> actual = new ArrayList<>(); + + scope.effects().get(0).forEachTargetAndDistance((fighter, distance) -> { + actual.add(Pair.of(fighter, distance)); + return false; + }); + + assertEquals(Arrays.asList(Pair.of(fighters.get(1), 0)), actual); } @Test @@ -444,12 +528,13 @@ void targetsWithDeadFighter() { Mockito.when(spell.constraints()).thenReturn(constraints); Mockito.when(constraints.freeCell()).thenReturn(false); - CastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); target.init(); target.life().kill(caster); assertCollectionEquals(scope.effects().get(0).targets(), caster); + assertForEachTargetAndDistance(scope.effects().get(0), Pair.of(caster, 6)); } @Test @@ -463,12 +548,13 @@ void replaceTarget() { Mockito.when(spell.constraints()).thenReturn(constraints); Mockito.when(constraints.freeCell()).thenReturn(false); - CastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); scope.replaceTarget(target, caster); assertCollectionEquals(scope.targets(), caster, target); assertCollectionEquals(scope.effects().get(0).targets(), caster, caster); + assertForEachTargetAndDistance(scope.effects().get(0), Pair.of(caster, 0), Pair.of(caster, 6)); } @Test @@ -494,13 +580,14 @@ void replaceTargetChaining() { List fighters = fight.turnList().fighters(); - CastScope scope = FightCastScope.simple(spell, fighters.get(0), fight.map().get(263), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, fighters.get(0), fight.map().get(263), Collections.singletonList(effect)); scope.replaceTarget(fighters.get(1), fighters.get(2)); scope.replaceTarget(fighters.get(2), fighters.get(3)); assertCollectionEquals(scope.targets(), fighters.get(1), fighters.get(2), fighters.get(3)); assertCollectionEquals(scope.effects().get(0).targets(), fighters.get(3)); + assertForEachTargetAndDistance(scope.effects().get(0), Pair.of(fighters.get(3), 0)); } @Test @@ -526,7 +613,7 @@ void replaceTargetChainingWithRecursionOnFirstTarget() { List fighters = fight.turnList().fighters(); - CastScope scope = FightCastScope.simple(spell, fighters.get(0), fight.map().get(263), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, fighters.get(0), fight.map().get(263), Collections.singletonList(effect)); scope.replaceTarget(fighters.get(1), fighters.get(2)); scope.replaceTarget(fighters.get(2), fighters.get(3)); @@ -534,6 +621,7 @@ void replaceTargetChainingWithRecursionOnFirstTarget() { assertCollectionEquals(scope.targets(), fighters.get(1), fighters.get(2), fighters.get(3)); assertCollectionEquals(scope.effects().get(0).targets(), fighters.get(1)); + assertForEachTargetAndDistance(scope.effects().get(0), Pair.of(fighters.get(1), 0)); } @Test @@ -559,7 +647,7 @@ void replaceTargetChainingWithRecursionOnMiddleTarget() { List fighters = fight.turnList().fighters(); - CastScope scope = FightCastScope.simple(spell, fighters.get(0), fight.map().get(263), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, fighters.get(0), fight.map().get(263), Collections.singletonList(effect)); scope.replaceTarget(fighters.get(1), fighters.get(2)); scope.replaceTarget(fighters.get(2), fighters.get(3)); @@ -567,6 +655,7 @@ void replaceTargetChainingWithRecursionOnMiddleTarget() { assertCollectionEquals(scope.targets(), fighters.get(1), fighters.get(2), fighters.get(3)); assertCollectionEquals(scope.effects().get(0).targets(), fighters.get(2)); + assertForEachTargetAndDistance(scope.effects().get(0), Pair.of(fighters.get(2), 0)); } @Test @@ -592,7 +681,7 @@ void removeTargetWithReplaceTargetChain() { List fighters = fight.turnList().fighters(); - CastScope scope = FightCastScope.simple(spell, fighters.get(0), fight.map().get(263), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, fighters.get(0), fight.map().get(263), Collections.singletonList(effect)); scope.replaceTarget(fighters.get(1), fighters.get(2)); scope.replaceTarget(fighters.get(2), fighters.get(3)); @@ -600,6 +689,7 @@ void removeTargetWithReplaceTargetChain() { assertCollectionEquals(scope.targets(), fighters.get(1), fighters.get(2), fighters.get(3)); assertTrue(scope.effects().get(0).targets().isEmpty()); + assertForEachTargetAndDistance(scope.effects().get(0)); } @Test @@ -613,17 +703,19 @@ void removeTarget() { Mockito.when(spell.constraints()).thenReturn(constraints); Mockito.when(constraints.freeCell()).thenReturn(false); - CastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); scope.removeTarget(target); assertCollectionEquals(scope.targets(), caster, target); assertCollectionEquals(scope.effects().get(0).targets(), caster); + assertForEachTargetAndDistance(scope.effects().get(0), Pair.of(caster, 6)); scope.removeTarget(caster); assertCollectionEquals(scope.targets(), caster, target); assertTrue(scope.effects().get(0).targets().isEmpty()); + assertForEachTargetAndDistance(scope.effects().get(0)); } @Test @@ -637,9 +729,10 @@ void effectTargetsFilter() { Mockito.when(spell.constraints()).thenReturn(constraints); Mockito.when(constraints.freeCell()).thenReturn(false); - CastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); assertCollectionEquals(scope.effects().get(0).targets(), target); + assertForEachTargetAndDistance(scope.effects().get(0), Pair.of(target, 0)); } @Test @@ -653,8 +746,21 @@ void effectTargetsOnlyCaster() { Mockito.when(spell.constraints()).thenReturn(constraints); Mockito.when(constraints.freeCell()).thenReturn(false); - CastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); + FightCastScope scope = FightCastScope.simple(spell, caster, target.cell(), Collections.singletonList(effect)); assertEquals(Collections.singleton(caster), scope.effects().get(0).targets()); + assertForEachTargetAndDistance(scope.effects().get(0), Pair.of(caster, 0)); + } + + private void assertForEachTargetAndDistance(FightCastScope.EffectScope effectScope, Pair ... targets) { + final List> actual = new ArrayList<>(); + + effectScope.forEachTargetAndDistance((fighter, distance) -> { + actual.add(Pair.of(fighter, distance)); + + return true; + }); + + assertEquals(Arrays.asList(targets), actual); } } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/CastTargetsTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/CastTargetsTest.java new file mode 100644 index 000000000..d53130524 --- /dev/null +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/CastTargetsTest.java @@ -0,0 +1,186 @@ +/* + * 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.castable; + +import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; +import fr.quatrevieux.araknemu.game.fight.map.BattlefieldCell; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class CastTargetsTest { + @Test + void empty() { + assertSame(CastTargets.empty(), CastTargets.empty()); + + assertEquals(Collections.emptyList(), toList(CastTargets.empty())); + assertForEach(CastTargets.empty()); + + assertSame(CastTargets.empty(), CastTargets.builder().build()); + + Iterator iterator = CastTargets.empty().iterator(); + assertFalse(iterator.hasNext()); + assertThrows(NoSuchElementException.class, iterator::next); + } + + @Test + void singleton() { + Fighter fighter = Mockito.mock(Fighter.class); + BattlefieldCell cell = Mockito.mock(BattlefieldCell.class); + + CastTargets targets = CastTargets.one(fighter, cell); + + assertEquals(Collections.singletonList(fighter), toList(targets)); + assertForEach(targets, Pair.of(fighter, cell)); + + CastTargets.Builder builder = CastTargets.builder(); + builder.add(fighter, cell); + + targets = builder.build(); + + assertEquals(Collections.singletonList(fighter), toList(targets)); + assertForEach(targets, Pair.of(fighter, cell)); + + Iterator iterator = targets.iterator(); + assertTrue(iterator.hasNext()); + assertSame(fighter, iterator.next()); + assertFalse(iterator.hasNext()); + assertThrows(NoSuchElementException.class, iterator::next); + } + + @Test + void pair() { + Fighter fighter1 = Mockito.mock(Fighter.class); + BattlefieldCell cell1 = Mockito.mock(BattlefieldCell.class); + + Fighter fighter2 = Mockito.mock(Fighter.class); + BattlefieldCell cell2 = Mockito.mock(BattlefieldCell.class); + + CastTargets.Builder builder = CastTargets.builder(); + + builder.add(fighter1, cell1); + builder.add(fighter2, cell2); + + CastTargets targets = builder.build(); + + assertEquals(Arrays.asList(fighter1, fighter2), toList(targets)); + assertForEach(targets, Pair.of(fighter1, cell1), Pair.of(fighter2, cell2)); + + Iterator iterator = targets.iterator(); + assertTrue(iterator.hasNext()); + assertSame(fighter1, iterator.next()); + assertTrue(iterator.hasNext()); + assertSame(fighter2, iterator.next()); + assertFalse(iterator.hasNext()); + assertThrows(NoSuchElementException.class, iterator::next); + } + + @Test + void buildManyWithoutRealloc() { + Fighter fighter1 = Mockito.mock(Fighter.class); + BattlefieldCell cell1 = Mockito.mock(BattlefieldCell.class); + + Fighter fighter2 = Mockito.mock(Fighter.class); + BattlefieldCell cell2 = Mockito.mock(BattlefieldCell.class); + + Fighter fighter3 = Mockito.mock(Fighter.class); + BattlefieldCell cell3 = Mockito.mock(BattlefieldCell.class); + + CastTargets.Builder builder = CastTargets.builder(); + + builder.add(fighter1, cell1); + builder.add(fighter2, cell2); + builder.add(fighter3, cell3); + + CastTargets targets = builder.build(); + + assertEquals(Arrays.asList(fighter1, fighter2, fighter3), toList(targets)); + assertForEach(targets, Pair.of(fighter1, cell1), Pair.of(fighter2, cell2), Pair.of(fighter3, cell3)); + } + + @Test + void buildMany() { + List fighters = Stream.generate(() -> Mockito.mock(Fighter.class)).limit(100).collect(Collectors.toList()); + List cells = Stream.generate(() -> Mockito.mock(BattlefieldCell.class)).limit(100).collect(Collectors.toList()); + + CastTargets.Builder builder = CastTargets.builder(); + + for(int i = 0; i < fighters.size(); ++i){ + builder.add(fighters.get(i), cells.get(i)); + } + + CastTargets targets = builder.build(); + + assertEquals(fighters, toList(targets)); + assertForEach(targets, fighters.stream().map(fighter -> Pair.of(fighter, cells.get(fighters.indexOf(fighter)))).toArray(Pair[]::new)); + } + + @Test + void forEachWithReturnFalse() { + Fighter fighter1 = Mockito.mock(Fighter.class); + BattlefieldCell cell1 = Mockito.mock(BattlefieldCell.class); + + Fighter fighter2 = Mockito.mock(Fighter.class); + BattlefieldCell cell2 = Mockito.mock(BattlefieldCell.class); + + CastTargets.Builder builder = CastTargets.builder(); + + builder.add(fighter1, cell1); + builder.add(fighter2, cell2); + + CastTargets targets = builder.build(); + + List> actual = new ArrayList<>(); + + targets.forEach((fighter, cell) -> { + actual.add(Pair.of(fighter, cell)); + return false; + }); + + assertEquals(Collections.singletonList(Pair.of(fighter1, cell1)), actual); + } + + public List toList(CastTargets targets){ + List list = new ArrayList<>(); + for(Fighter fighter : targets){ + list.add(fighter); + } + return list; + } + + public void assertForEach(CastTargets targets, Pair ... pairs){ + List> actual = new ArrayList<>(); + + targets.forEach((fighter, cell) -> actual.add(Pair.of(fighter, cell))); + + assertEquals(Arrays.asList(pairs), actual); + } +} diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/EffectsUtilsTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/EffectsUtilsTest.java index 699450bbb..88d52d6f4 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/EffectsUtilsTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/EffectsUtilsTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -36,4 +37,22 @@ void isLooseApEffect() { assertTrue(EffectsUtils.isLooseApEffect(101)); assertFalse(EffectsUtils.isLooseApEffect(123)); } + + @Test + void applyDistanceAttenuation() { + assertEquals(1000, EffectsUtils.applyDistanceAttenuation(1000, 0)); + assertEquals(900, EffectsUtils.applyDistanceAttenuation(1000, 1)); + assertEquals(810, EffectsUtils.applyDistanceAttenuation(1000, 2)); + assertEquals(729, EffectsUtils.applyDistanceAttenuation(1000, 3)); + assertEquals(656, EffectsUtils.applyDistanceAttenuation(1000, 4)); + assertEquals(590, EffectsUtils.applyDistanceAttenuation(1000, 5)); + assertEquals(531, EffectsUtils.applyDistanceAttenuation(1000, 6)); + assertEquals(478, EffectsUtils.applyDistanceAttenuation(1000, 7)); + assertEquals(430, EffectsUtils.applyDistanceAttenuation(1000, 8)); + assertEquals(387, EffectsUtils.applyDistanceAttenuation(1000, 9)); + assertEquals(348, EffectsUtils.applyDistanceAttenuation(1000, 10)); + assertEquals(205, EffectsUtils.applyDistanceAttenuation(1000, 15)); + assertEquals(98, EffectsUtils.applyDistanceAttenuation(1000, 22)); + assertEquals(11, EffectsUtils.applyDistanceAttenuation(1000, 42)); + } } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/FunctionalTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/FunctionalTest.java index 59e96ad6a..2f03bdf21 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/FunctionalTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/FunctionalTest.java @@ -24,7 +24,6 @@ import fr.quatrevieux.araknemu.game.fight.FightBaseCase; import fr.quatrevieux.araknemu.game.fight.ai.FighterAI; import fr.quatrevieux.araknemu.game.fight.ai.factory.AiFactory; -import fr.quatrevieux.araknemu.game.fight.castable.Castable; import fr.quatrevieux.araknemu.game.fight.castable.closeCombat.CastableWeapon; import fr.quatrevieux.araknemu.game.fight.castable.closeCombat.CloseCombatValidator; import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff; @@ -65,7 +64,6 @@ import fr.quatrevieux.araknemu.network.game.out.game.AddSprites; import fr.quatrevieux.araknemu.network.game.out.game.UpdateCells; import fr.quatrevieux.araknemu.network.game.out.info.Error; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -1841,7 +1839,7 @@ void carrierShouldTakeSpellEffectsInPlaceOfCarried() { castNormal(223, fight.map().get(284)); // Météorite assertTrue(target.life().isFull()); - assertBetween(11, 40, caster.life().max() - caster.life().current()); + assertBetween(9, 36, caster.life().max() - caster.life().current()); // target is 1 cell away from the center, so -10% damage is applied } @Test diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageApplierTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageApplierTest.java index 3f48facfc..3d359e2fa 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageApplierTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageApplierTest.java @@ -75,7 +75,7 @@ void applyFixedWithoutBoost() { DamageApplier applier = new DamageApplier(Element.AIR, fight); EffectValue effectValue = EffectValue.create(effect, caster, target); - int value = applier.apply(caster, effect, target, effectValue); + int value = applier.apply(caster, effect, target, effectValue, 0); assertEquals(-10, value); assertEquals(10, target.life().max() - target.life().current()); @@ -83,6 +83,35 @@ void applyFixedWithoutBoost() { requestStack.assertLast(ActionEffect.alterLifePoints(caster, target, -10)); } + @Test + void applyWithDistance() { + SpellEffect effect = Mockito.mock(SpellEffect.class); + + Mockito.when(effect.min()).thenReturn(10); + + DamageApplier applier = new DamageApplier(Element.AIR, fight); + EffectValue effectValue = EffectValue.create(effect, caster, target); + + int value = applier.apply(caster, effect, target, effectValue, 0); + + assertEquals(-10, value); + assertEquals(10, target.life().max() - target.life().current()); + requestStack.assertLast(ActionEffect.alterLifePoints(caster, target, -10)); + + target.life().alterMax(target, 1000); + target.life().alter(target, 1000); + + assertEquals(-9, applier.apply(caster, effect, target, effectValue, 1)); + assertEquals(-8, applier.apply(caster, effect, target, effectValue, 2)); + assertEquals(-7, applier.apply(caster, effect, target, effectValue, 3)); + assertEquals(-6, applier.apply(caster, effect, target, effectValue, 4)); + assertEquals(-5, applier.apply(caster, effect, target, effectValue, 5)); + assertEquals(-5, applier.apply(caster, effect, target, effectValue, 6)); + assertEquals(-3, applier.apply(caster, effect, target, effectValue, 10)); + assertEquals(-2, applier.apply(caster, effect, target, effectValue, 15)); + assertEquals(-1, applier.apply(caster, effect, target, effectValue, 20)); + } + @Test void applyRandomWithoutBoost() { SpellEffect effect = Mockito.mock(SpellEffect.class); @@ -93,7 +122,7 @@ void applyRandomWithoutBoost() { DamageApplier applier = new DamageApplier(Element.AIR, fight); EffectValue effectValue = EffectValue.create(effect, caster, target); - int value = applier.apply(caster, effect, target, effectValue); + int value = applier.apply(caster, effect, target, effectValue, 0); assertBetween(-15, -10, value); assertEquals(value, target.life().current() - target.life().max()); @@ -114,7 +143,7 @@ void applyWithBoost() { player.properties().characteristics().base().set(Characteristic.PERCENT_DAMAGE, 25); player.properties().characteristics().base().set(Characteristic.FIXED_DAMAGE, 10); - int value = applier.apply(caster, effect, target, effectValue); + int value = applier.apply(caster, effect, target, effectValue, 0); assertEquals(-27, value); } @@ -130,12 +159,12 @@ void applyWithPhysicalBoost() { DamageApplier applier = new DamageApplier(Element.AIR, fight); EffectValue effectValue = EffectValue.create(effect, caster, target); - int value = applier.apply(caster, effect, target, effectValue); + int value = applier.apply(caster, effect, target, effectValue, 0); assertEquals(-15, value); applier = new DamageApplier(Element.EARTH, fight); effectValue = EffectValue.create(effect, caster, target); - value = applier.apply(caster, effect, target, effectValue); + value = applier.apply(caster, effect, target, effectValue, 0); assertEquals(-25, value); } @@ -151,10 +180,10 @@ void applyWithTrapBoost() { player.properties().characteristics().base().set(Characteristic.PERCENT_TRAP_BOOST, 25); player.properties().characteristics().base().set(Characteristic.TRAP_BOOST, 10); - assertEquals(-15, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target))); // Without trap boost + assertEquals(-15, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0)); // Without trap boost Mockito.when(effect.trap()).thenReturn(true); - assertEquals(-27, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target))); // With trap boost + assertEquals(-27, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0)); // With trap boost } @Test @@ -168,7 +197,7 @@ void applyWithResistance() { other.properties().characteristics().base().set(Characteristic.RESISTANCE_PERCENT_AIR, 25); other.properties().characteristics().base().set(Characteristic.RESISTANCE_AIR, 5); - int value = applier.apply(caster, effect, target, EffectValue.create(effect, caster, target)); + int value = applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0); assertEquals(-2, value); } @@ -183,7 +212,7 @@ void applyWithTooHighResistance() { other.properties().characteristics().base().set(Characteristic.RESISTANCE_AIR, 100); - int value = applier.apply(caster, effect, target, EffectValue.create(effect, caster, target)); + int value = applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0); assertEquals(0, value); } @@ -196,7 +225,7 @@ void applyWithValueHigherThanTargetLife() { DamageApplier applier = new DamageApplier(Element.AIR, fight); - int value = applier.apply(caster, effect, target, EffectValue.create(effect, caster, target)); + int value = applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0); assertEquals(-50, value); assertTrue(target.dead()); @@ -221,7 +250,7 @@ public void onDamage(Buff buff, Damage value) { }) ); - int value = applier.apply(caster, effect, target, EffectValue.create(effect, caster, target)); + int value = applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0); assertEquals(-3, value); @@ -262,7 +291,7 @@ public void onDirectDamageApplied(Buff buff, Fighter caster, @Positive int damag target.buffs().add(buff); - int value = applier.apply(caster, effect, target, EffectValue.create(effect, caster, target)); + int value = applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0); assertEquals(-10, value); @@ -338,7 +367,7 @@ public void onReflectedDamage(Buff buff, ReflectedDamage damage) { DamageApplier applier = new DamageApplier(Element.AIR, fight); requestStack.clear(); - assertEquals(-10, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target))); + assertEquals(-10, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0)); assertEquals(-10, target.life().current() - target.life().max()); assertEquals(-5, caster.life().current() - caster.life().max()); @@ -363,7 +392,7 @@ void applyWithCounterDamageShouldNotExceedHalfOfDamage() { DamageApplier applier = new DamageApplier(Element.AIR, fight); requestStack.clear(); - assertEquals(-10, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target))); + assertEquals(-10, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0)); assertEquals(-10, target.life().current() - target.life().max()); assertEquals(-5, caster.life().current() - caster.life().max()); @@ -386,7 +415,7 @@ void applyWithCounterDamageShouldTakeResistanceInAccount() { DamageApplier applier = new DamageApplier(Element.AIR, fight); requestStack.clear(); - assertEquals(-20, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target))); + assertEquals(-20, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0)); assertEquals(-20, target.life().current() - target.life().max()); assertEquals(-7, caster.life().current() - caster.life().max()); @@ -421,7 +450,7 @@ public void onReflectedDamage(Buff buff, ReflectedDamage damage) { DamageApplier applier = new DamageApplier(Element.AIR, fight); requestStack.clear(); - assertEquals(-10, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target))); + assertEquals(-10, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0)); assertEquals(-10, target.life().current() - target.life().max()); assertEquals(caster.life().current(), caster.life().max()); assertEquals(-5, newTarget.life().current() - newTarget.life().max()); @@ -443,7 +472,7 @@ void applyWithCounterDamageShouldIgnoreSelfTarget() { DamageApplier applier = new DamageApplier(Element.AIR, fight); requestStack.clear(); - assertEquals(-10, applier.apply(caster, effect, caster, EffectValue.create(effect, caster, target))); + assertEquals(-10, applier.apply(caster, effect, caster, EffectValue.create(effect, caster, target), 0)); assertEquals(-10, caster.life().current() - caster.life().max()); requestStack.assertAll( @@ -467,7 +496,7 @@ void applyWithCounterDamageNoDamageShouldBeIgnored() { DamageApplier applier = new DamageApplier(Element.AIR, fight); requestStack.clear(); - assertEquals(0, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target))); + assertEquals(0, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0)); assertEquals(0, target.life().current() - target.life().max()); assertEquals(0, caster.life().current() - caster.life().max()); @@ -498,7 +527,7 @@ public void onReflectedDamage(Buff buff, ReflectedDamage damage) { DamageApplier applier = new DamageApplier(Element.AIR, fight); requestStack.clear(); - assertEquals(-10, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target))); + assertEquals(-10, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0)); assertEquals(-10, target.life().current() - target.life().max()); assertEquals(-5, caster.life().current() - caster.life().max()); @@ -535,7 +564,7 @@ public void onDirectDamageApplied(Buff buff, Fighter caster, @Positive int damag DamageApplier applier = new DamageApplier(Element.AIR, fight); requestStack.clear(); - assertEquals(10, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target))); + assertEquals(10, applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0)); assertEquals(-10, target.life().current() - target.life().max()); assertFalse(appliedDamageHookCalled.get()); @@ -813,7 +842,7 @@ void applyShouldCallOnCastDamageOnCaster() { caster.buffs().add(buff); - int value = applier.apply(caster, effect, target, EffectValue.create(effect, caster, target)); + int value = applier.apply(caster, effect, target, EffectValue.create(effect, caster, target), 0); assertEquals(-10, value); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageHandlerTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageHandlerTest.java index c8524065d..8b1be4bae 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageHandlerTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageHandlerTest.java @@ -170,9 +170,9 @@ void applyToEmptyCellWithArea() { int damage = target.life().max() - target.life().current(); - assertEquals(10, damage); + assertEquals(8, damage); - requestStack.assertLast(ActionEffect.alterLifePoints(caster, target, -10)); + requestStack.assertLast(ActionEffect.alterLifePoints(caster, target, -8)); } @Test @@ -190,10 +190,99 @@ void applyWithAreaMultipleFighters() { FightCastScope scope = makeCastScope(caster, spell, effect, fight.map().get(122)); handler.handle(scope, scope.effects().get(0)); - requestStack.assertOne(ActionEffect.alterLifePoints(caster, target, -10)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, target, -8)); requestStack.assertOne(ActionEffect.alterLifePoints(caster, caster, -10)); } + @Test + void applyWithAreaShouldReduceDamageOnHigherDistance() { + fight = fightBuilder() + .addSelf(fb -> fb.cell(152)) + .addEnemy(fb -> fb.cell(166)) // 1 + .addEnemy(fb -> fb.cell(151)) // 2 + .addEnemy(fb -> fb.cell(165)) // 3 + .addEnemy(fb -> fb.cell(179)) // 4 + .addEnemy(fb -> fb.cell(222)) // 5 + .addEnemy(fb -> fb.cell(250)) // 7 + .addEnemy(fb -> fb.cell(292)) // 10 + .addEnemy(fb -> fb.cell(320)) // 12 + .build(true) + ; + + fight.nextState(); + + handler = new DamageHandler(Element.AIR, fight); + caster = (PlayerFighter) fight.map().get(152).fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.min()).thenReturn(20); + Mockito.when(effect.area()).thenReturn(new CircleArea(new EffectArea(EffectArea.Type.CIRCLE, 20))); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + FightCastScope scope = makeCastScope(caster, spell, effect, caster.cell()); + handler.handle(scope, scope.effects().get(0)); + + requestStack.assertOne(ActionEffect.alterLifePoints(caster, caster, -20)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, fight.map().get(166).fighter(), -18)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, fight.map().get(151).fighter(), -16)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, fight.map().get(165).fighter(), -14)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, fight.map().get(179).fighter(), -13)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, fight.map().get(222).fighter(), -11)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, fight.map().get(250).fighter(), -9)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, fight.map().get(292).fighter(), -6)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, fight.map().get(320).fighter(), -5)); + } + + @Test + void applyWithAreaShouldApplySameDamageOnSameDistanceFromTarget() { + fight = fightBuilder() + .addSelf(fb -> fb.cell(298)) + .addEnemy(fb -> fb.cell(312)) // 1 + .addEnemy(fb -> fb.cell(313)) // 1 + .addEnemy(fb -> fb.cell(340)) // 3 + .addEnemy(fb -> fb.cell(342)) // 3 + .build(true) + ; + + fight.nextState(); + + handler = new DamageHandler(Element.AIR, fight); + caster = (PlayerFighter) fight.map().get(298).fighter(); + + 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.max()).thenReturn(20); + Mockito.when(effect.area()).thenReturn(new CircleArea(new EffectArea(EffectArea.Type.CIRCLE, 20))); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + FightCastScope scope = makeCastScope(caster, spell, effect, caster.cell()); + handler.handle(scope, scope.effects().get(0)); + + int centerDamage = caster.life().max() - caster.life().current(); + int firstDamage = fight.map().get(312).fighter().life().max() - fight.map().get(312).fighter().life().current(); + int secondDamage = fight.map().get(313).fighter().life().max() - fight.map().get(313).fighter().life().current(); + int thirdDamage = fight.map().get(340).fighter().life().max() - fight.map().get(340).fighter().life().current(); + int fourthDamage = fight.map().get(342).fighter().life().max() - fight.map().get(342).fighter().life().current(); + + assertBetween(10, 20, centerDamage); + + assertEquals(firstDamage, secondDamage); + assertTrue(centerDamage > secondDamage); + + assertEquals(thirdDamage, fourthDamage); + assertTrue(secondDamage > fourthDamage); + } + @Test void applyWithAreaMultipleFightersShouldStopApplyingDamageIfFightEnds() { SpellEffect effect = Mockito.mock(SpellEffect.class); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageTest.java index 0874dbb1c..b7b419988 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/DamageTest.java @@ -78,4 +78,23 @@ void reduce() { assertEquals(7, damage.percent(20).reduce(5).value()); assertEquals(5, damage.reducedDamage()); } + + @Test + void distance() { + assertEquals(15, damage.distance(0).value()); + assertEquals(13, damage.distance(1).value()); + assertEquals(12, damage.distance(2).value()); + assertEquals(10, damage.distance(3).value()); + assertEquals(9, damage.distance(4).value()); + assertEquals(8, damage.distance(5).value()); + assertEquals(7, damage.distance(6).value()); + assertEquals(7, damage.distance(7).value()); + assertEquals(6, damage.distance(8).value()); + assertEquals(5, damage.distance(9).value()); + assertEquals(5, damage.distance(10).value()); + assertEquals(3, damage.distance(15).value()); + assertEquals(1, damage.distance(20).value()); + assertEquals(1, damage.distance(25).value()); + assertEquals(0, damage.distance(26).value()); + } } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/StealLifeHandlerTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/StealLifeHandlerTest.java index 228822826..cef06d6ce 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/StealLifeHandlerTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/damage/StealLifeHandlerTest.java @@ -240,8 +240,8 @@ void applyToEmptyCellWithArea() { FightCastScope scope = makeCastScope(caster, spell, effect, fight.map().get(122)); handler.handle(scope, scope.effects().get(0)); - requestStack.assertOne(ActionEffect.alterLifePoints(caster, target, -10)); - requestStack.assertOne(ActionEffect.alterLifePoints(caster, caster, 5)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, target, -8)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, caster, 4)); } @Test @@ -259,8 +259,8 @@ void applyWithAreaMultipleFighters() { FightCastScope scope = makeCastScope(caster, spell, effect, fight.map().get(122)); handler.handle(scope, scope.effects().get(0)); - requestStack.assertOne(ActionEffect.alterLifePoints(caster, target, -10)); - requestStack.assertOne(ActionEffect.alterLifePoints(caster, caster, 5)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, target, -8)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, caster, 4)); requestStack.assertOne(ActionEffect.alterLifePoints(caster, caster, -10)); requestStack.assertOne(ActionEffect.alterLifePoints(caster, caster, 5)); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealHandlerTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealHandlerTest.java index c559df2b8..1e351c060 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealHandlerTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/heal/HealHandlerTest.java @@ -64,7 +64,7 @@ public void setUp() throws Exception { target.life().alter(target, -30); lastTargetLife = target.life().current(); - handler = new HealHandler(); + handler = new HealHandler(fight); player.properties().characteristics().base().set(Characteristic.INTELLIGENCE, 0); @@ -165,7 +165,7 @@ void applyToEmptyCellWithArea() { FightCastScope scope = makeCastScope(caster, spell, effect, fight.map().get(122)); handler.handle(scope, scope.effects().get(0)); - assertEquals(10, computeHeal()); + assertEquals(8, computeHeal()); requestStack.assertLast(ActionEffect.alterLifePoints(caster, target, computeHeal())); } @@ -188,7 +188,7 @@ void applyWithAreaMultipleFighters() { FightCastScope scope = makeCastScope(caster, spell, effect, fight.map().get(122)); handler.handle(scope, scope.effects().get(0)); - requestStack.assertOne(ActionEffect.alterLifePoints(caster, target, 10)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, target, 8)); requestStack.assertOne(ActionEffect.alterLifePoints(caster, caster, 10)); } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/modifier/MaximizeTargetEffectsHandlerTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/modifier/MaximizeTargetEffectsHandlerTest.java index 3b41043a3..2a4200468 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/modifier/MaximizeTargetEffectsHandlerTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/modifier/MaximizeTargetEffectsHandlerTest.java @@ -129,7 +129,7 @@ void functional() { Mockito.when(damageEffect.min()).thenReturn(5); Mockito.when(damageEffect.max()).thenReturn(10); - assertEquals(-10, new DamageApplier(Element.AIR, fight).apply(caster, damageEffect, target, EffectValue.create(damageEffect, caster, target))); + assertEquals(-10, new DamageApplier(Element.AIR, fight).apply(caster, damageEffect, target, EffectValue.create(damageEffect, caster, target), 0)); assertEquals(10, target.life().max() - target.life().current()); } } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/modifier/MinimizeCastedEffectsHandlerTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/modifier/MinimizeCastedEffectsHandlerTest.java index 72b331104..fef7a39eb 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/modifier/MinimizeCastedEffectsHandlerTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/modifier/MinimizeCastedEffectsHandlerTest.java @@ -129,7 +129,7 @@ void functional() { Mockito.when(damageEffect.min()).thenReturn(5); Mockito.when(damageEffect.max()).thenReturn(10); - assertEquals(-5, new DamageApplier(Element.AIR, fight).apply(target, damageEffect, caster, EffectValue.create(damageEffect, caster, target))); + assertEquals(-5, new DamageApplier(Element.AIR, fight).apply(target, damageEffect, caster, EffectValue.create(damageEffect, caster, target), 0)); assertEquals(5, caster.life().max() - caster.life().current()); } } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/modifier/MultiplyDamageHandlerTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/modifier/MultiplyDamageHandlerTest.java index e1604153b..f7bb97d40 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/modifier/MultiplyDamageHandlerTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/modifier/MultiplyDamageHandlerTest.java @@ -131,7 +131,7 @@ void functional() { SpellEffect damageEffect = Mockito.mock(SpellEffect.class); Mockito.when(damageEffect.min()).thenReturn(5); - assertEquals(-21, new DamageApplier(Element.EARTH, fight).apply(caster, damageEffect, target, EffectValue.create(damageEffect, caster, target))); + assertEquals(-21, new DamageApplier(Element.EARTH, fight).apply(caster, damageEffect, target, EffectValue.create(damageEffect, caster, target), 0)); assertEquals(21, target.life().max() - target.life().current()); } }