diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyActiveFighter.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyActiveFighter.java index 9471d402a..cd9f6fc87 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyActiveFighter.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyActiveFighter.java @@ -87,6 +87,11 @@ public FighterLife life() { return fighter.life(); } + @Override + public int level() { + return fighter.level(); + } + @Override public Buffs buffs() { return fighter.buffs(); diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyPassiveFighter.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyPassiveFighter.java index 3eb351c65..98a7bc6e2 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyPassiveFighter.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/proxy/ProxyPassiveFighter.java @@ -76,6 +76,11 @@ public States states() { return fighter.states(); } + @Override + public int level() { + return fighter.level(); + } + @Override public FighterCharacteristics characteristics() { return fighter.characteristics(); diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/CastScope.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/CastScope.java index bfa31bacd..ed4ab8865 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/CastScope.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/CastScope.java @@ -108,6 +108,13 @@ public void replaceTarget(PassiveFighter originalTarget, PassiveFighter newTarge targetMapping.put(originalTarget, newTarget); } + /** + * Remove a target of the cast + */ + public void removeTarget(PassiveFighter target) { + targetMapping.remove(target); + } + /** * Get list of effects to apply */ @@ -218,7 +225,7 @@ public SpellEffect effect() { public Collection targets() { return targets.stream() .map(targetMapping::get) - .filter(fighter -> !fighter.dead()) + .filter(fighter -> fighter != null && !fighter.dead()) .collect(Collectors.toList()) ; } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/BuffHook.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/BuffHook.java index 6a9a5a218..4390e7237 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/BuffHook.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/BuffHook.java @@ -21,6 +21,7 @@ import fr.quatrevieux.araknemu.game.fight.castable.CastScope; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.Damage; +import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter; /** * Hook action for apply buff effects @@ -60,6 +61,24 @@ public default void onCastTarget(Buff buff, CastScope cast) {} */ public default void onDamage(Buff buff, Damage value) {} + /** + * The fighter will take damages + */ + public default void onDirectDamage(Buff buff, ActiveFighter caster, Damage value) { + onDamage(buff, value); + } + + /** + * The fighter will take damages by a buff (i.e. poison) + * + * @param buff The current buff + * @param poison The poison buff + * @param value The damage to apply + */ + public default void onBuffDamage(Buff buff, Buff poison, Damage value) { + onDamage(buff, value); + } + /** * The fighter life has been altered * diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/BuffList.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/BuffList.java index 1e25fc343..9aca2b83c 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/BuffList.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/BuffList.java @@ -21,6 +21,7 @@ import fr.quatrevieux.araknemu.game.fight.castable.CastScope; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.Damage; +import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter; import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; import fr.quatrevieux.araknemu.game.fight.fighter.PassiveFighter; import fr.quatrevieux.araknemu.network.game.out.fight.AddBuff; @@ -98,9 +99,16 @@ public void onCastTarget(CastScope cast) { } @Override - public void onDamage(Damage value) { + public void onDirectDamage(ActiveFighter caster, Damage value) { for (Buff buff : buffs) { - buff.hook().onDamage(buff, value); + buff.hook().onDirectDamage(buff, caster, value); + } + } + + @Override + public void onBuffDamage(Buff poison, Damage value) { + for (Buff buff : buffs) { + buff.hook().onBuffDamage(buff, poison, value); } } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/Buffs.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/Buffs.java index c511293ee..f9543d2fd 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/Buffs.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/Buffs.java @@ -21,6 +21,7 @@ import fr.quatrevieux.araknemu.game.fight.castable.CastScope; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.damage.Damage; +import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter; import fr.quatrevieux.araknemu.game.fight.fighter.PassiveFighter; /** @@ -38,9 +39,14 @@ public interface Buffs extends Iterable { public void onCastTarget(CastScope cast); /** - * @see BuffHook#onDamage(Buff, Damage) + * @see BuffHook#onDirectDamage(Buff, ActiveFighter, Damage) */ - public void onDamage(Damage value); + public void onDirectDamage(ActiveFighter caster, Damage value); + + /** + * @see BuffHook#onBuffDamage(Buff, Buff, Damage) + */ + public void onBuffDamage(Buff poison, Damage value); /** * @see BuffHook#onLifeAltered(Buff, int) 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 9b0934522..519fc9ec4 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 @@ -47,28 +47,67 @@ public DamageApplier(Element element, Fight fight) { } /** - * Apply a damage effect to a fighter + * Apply a direct damage effect to a fighter + * + * Note: do not use this method for a buff, it will call the invalid buff hook * * @param caster The spell caster * @param effect The effect to apply * @param target The target * * @return The real damage value + * + * @see DamageApplier#apply(Buff) For apply a buff damage (i.e. poison) + * @see fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buffs#onDirectDamage(ActiveFighter, Damage) The called buff hook */ public int apply(ActiveFighter caster, SpellEffect effect, PassiveFighter target) { + final Damage damage = computeDamage(caster, effect, target); + target.buffs().onDirectDamage(caster, damage); + + return applyDamage(caster, damage, target); + } + + /** + * Apply a damage buff effect + * + * @param buff Buff to apply + * + * @return The real damage value + * + * @see fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buffs#onBuffDamage(Buff, Damage) The called buff hook + */ + public int apply(Buff buff) { + final PassiveFighter target = buff.target(); + final ActiveFighter caster = buff.caster(); + + final Damage damage = computeDamage(caster, buff.effect(), target); + target.buffs().onBuffDamage(buff, damage); + + return applyDamage(caster, damage, target); + } + + /** + * Create the damage object + */ + private Damage computeDamage(ActiveFighter caster, SpellEffect effect, PassiveFighter target) { final EffectValue value = new EffectValue(effect) .percent(caster.characteristics().get(element.boost())) .percent(caster.characteristics().get(Characteristic.PERCENT_DAMAGE)) .fixed(caster.characteristics().get(Characteristic.FIXED_DAMAGE)) ; - final Damage damage = new Damage(value.value(), element) + return new Damage(value.value(), element) .percent(target.characteristics().get(element.percentResistance())) .fixed(target.characteristics().get(element.fixedResistance())) ; + } - target.buffs().onDamage(damage); - + /** + * Apply the damage object to the target + * + * @return The life change value. Negative for damage, positive for heal. + */ + private int applyDamage(ActiveFighter caster, Damage damage, PassiveFighter target) { if (damage.reducedDamage() > 0) { fight.send(ActionEffect.reducedDamage(target, damage.reducedDamage())); } @@ -77,15 +116,4 @@ public int apply(ActiveFighter caster, SpellEffect effect, PassiveFighter target return target.life().alter(caster, -damage.value()); } - - /** - * Apply a damage buff effect - * - * @param buff Buff to apply - * - * @return The realm damage value - */ - public int apply(Buff buff) { - return apply(buff.caster(), buff.effect(), buff.target()); - } } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/AvoidDamageByMovingBackHandler.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/AvoidDamageByMovingBackHandler.java new file mode 100644 index 000000000..c94831591 --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/AvoidDamageByMovingBackHandler.java @@ -0,0 +1,87 @@ +/* + * 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-2021 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting; + +import fr.arakne.utils.value.helper.RandomUtil; +import fr.quatrevieux.araknemu.game.fight.Fight; +import fr.quatrevieux.araknemu.game.fight.castable.CastScope; +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.EffectHandler; +import fr.quatrevieux.araknemu.game.fight.fighter.PassiveFighter; + +/** + * Buff effect which permit to "cancel" a damage effect by moving back + * + * This effect is hooked before cast the spell, and remove the target if the effect is a damage, + * and it's launch in close combat (i.e. distance = 1) + * + * Note: this effect is only applied on direct damage + */ +public final class AvoidDamageByMovingBackHandler implements EffectHandler, BuffHook { + private final MoveBackApplier applier; + private final RandomUtil random = new RandomUtil(); + + public AvoidDamageByMovingBackHandler(Fight fight) { + applier = new MoveBackApplier(fight); + } + + @Override + public void handle(CastScope cast, CastScope.EffectScope effect) { + throw new UnsupportedOperationException("Avoid damage by moving back is a buff effect"); + } + + @Override + public void buff(CastScope cast, CastScope.EffectScope effect) { + for (PassiveFighter target : effect.targets()) { + target.buffs().add(new Buff(effect.effect(), cast.action(), cast.caster(), target, this)); + } + } + + @Override + public void onCastTarget(Buff buff, CastScope cast) { + if (!isDamageCast(cast) || buff.target().cell().coordinate().distance(cast.caster().cell()) != 1) { + return; + } + + if (!random.bool(buff.effect().min())) { + return; + } + + applier.apply(cast.caster(), buff.target(), buff.effect().max()); + cast.removeTarget(buff.target()); + } + + /** + * Check if the action is direct damage attack + * + * @param cast The action to check + * + * @return true if the cast can be dodged + */ + private boolean isDamageCast(CastScope cast) { + return cast.effects().stream() + .map(CastScope.EffectScope::effect) + // Should return only direct damage effects + .anyMatch(effect -> EffectsUtils.isDamageEffect(effect.effect()) && effect.duration() == 0) + ; + } +} diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveBackApplier.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveBackApplier.java new file mode 100644 index 000000000..e79a21c2b --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveBackApplier.java @@ -0,0 +1,150 @@ +/* + * 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-2021 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting; + +import fr.arakne.utils.maps.MapCell; +import fr.arakne.utils.maps.constant.Direction; +import fr.arakne.utils.maps.path.Decoder; +import fr.arakne.utils.value.helper.RandomUtil; +import fr.quatrevieux.araknemu.game.fight.Fight; +import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter; +import fr.quatrevieux.araknemu.game.fight.fighter.PassiveFighter; +import fr.quatrevieux.araknemu.game.fight.map.FightCell; +import fr.quatrevieux.araknemu.network.game.out.fight.action.ActionEffect; + +import java.util.Optional; + +/** + * Apply the move back effect to one fighter + * This class will perform the move, and apply blocking damage + */ +public final class MoveBackApplier { + public static final int BASE_DAMAGE = 8; + public static final int MAX_DAMAGE = 8; + + private final Fight fight; + private final int baseDamage; + private final int maxDamage; + + private final Decoder decoder; + private final RandomUtil random = new RandomUtil(); + + public MoveBackApplier(Fight fight) { + this(fight, BASE_DAMAGE, MAX_DAMAGE); + } + + /** + * @param fight The current fight instance + * @param baseDamage The base damage per remaining distance + * @param maxDamage The maximum damage of the random part per remaining distance + */ + public MoveBackApplier(Fight fight, int baseDamage, int maxDamage) { + this.fight = fight; + this.baseDamage = baseDamage; + this.maxDamage = maxDamage; + this.decoder = new Decoder<>(fight.map()); + } + + /** + * Apply the effect to the given target + * + * @param caster The spell caster + * @param target The spell target + * @param distance The move back distance + */ + public void apply(ActiveFighter caster, PassiveFighter target, int distance) { + final Direction direction = caster.cell().coordinate().directionTo(target.cell()); + FightCell destination = target.cell(); + + // Check if a cell block the movement + for (; distance > 0; --distance) { + final Optional nextCell = decoder + .nextCellByDirection(destination, direction) + .filter(MapCell::walkable) + ; + + if (!nextCell.isPresent()) { + break; + } + + destination = nextCell.get(); + } + + // Fighter has moved + if (!destination.equals(target.cell())) { + target.move(destination); + fight.send(ActionEffect.slide(caster, target, destination)); + } + + if (distance > 0) { + applyBlockingDamageChain(caster, target, destination, direction, distance); + } + } + + /** + * Apply blocked move back damage on each fighter on the path + * The damage are divided by two to each following fighter + * + * @param caster The move back caster + * @param target The base target + * @param lastCell The destination cell (where the target is blocked) + * @param direction The move direction + * @param distance Remain move distance + */ + private void applyBlockingDamageChain(ActiveFighter caster, PassiveFighter target, FightCell lastCell, Direction direction, int distance) { + int damage = computeDamage(caster, distance); + + if (damage <= 0) { + return; + } + + target.life().alter(caster, -damage); + + // Divide damage by 2 on each fighter + for (damage /= 2; damage > 0; damage /= 2) { + final Optional nextCell = decoder + .nextCellByDirection(lastCell, direction) + .filter(cell -> cell.fighter().isPresent()) + ; + + // Out of map, or no player is present here : stop the chain + if (!nextCell.isPresent()) { + return; + } + + lastCell = nextCell.get(); + lastCell.fighter().get().life().alter(caster, -damage); + } + } + + /** + * Compute the damage in life point for a blocked move back + * + * https://www.dofus.com/fr/forum/1750-dofus/234052-version-beta-1-27?page=3#entry1657700 + * + * @param caster The spell caster + * @param distance The remaining distance + * + * @return The damage + */ + private int computeDamage(ActiveFighter caster, int distance) { + return (baseDamage + random.rand(1, maxDamage) * caster.level() / 50) * distance; + } +} diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveBackHandler.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveBackHandler.java new file mode 100644 index 000000000..442eb4b41 --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveBackHandler.java @@ -0,0 +1,63 @@ +/* + * 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-2021 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting; + +import fr.arakne.utils.maps.CoordinateCell; +import fr.quatrevieux.araknemu.game.fight.Fight; +import fr.quatrevieux.araknemu.game.fight.castable.CastScope; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.EffectHandler; +import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter; +import fr.quatrevieux.araknemu.game.fight.fighter.PassiveFighter; +import fr.quatrevieux.araknemu.game.fight.map.FightCell; + +import java.util.Comparator; + +/** + * Move back targets + * + * - Start to move the most distant targets + * - If the target is blocked, apply damage + * - Apply damage divided by 2 on following fighters, if the obstacle is a fighter + */ +public final class MoveBackHandler implements EffectHandler { + private final MoveBackApplier applier; + + public MoveBackHandler(Fight fight) { + applier = new MoveBackApplier(fight); + } + + @Override + public void handle(CastScope cast, CastScope.EffectScope effect) { + final ActiveFighter caster = cast.caster(); + final int distance = effect.effect().min(); + final CoordinateCell casterCell = caster.cell().coordinate(); + + // Apply to most distant targets before, to ensure that they will not block mutually + effect.targets().stream() + .sorted(Comparator.comparingInt(target -> casterCell.distance(target.cell())).reversed()) + .forEach(target -> applier.apply(caster, target, distance)) + ; + } + + @Override + public void buff(CastScope cast, CastScope.EffectScope effect) { + throw new UnsupportedOperationException("Cannot use move back as buff effect"); + } +} diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveFrontHandler.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveFrontHandler.java new file mode 100644 index 000000000..002dd0e7a --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveFrontHandler.java @@ -0,0 +1,89 @@ +/* + * 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-2021 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting; + +import fr.arakne.utils.maps.MapCell; +import fr.arakne.utils.maps.constant.Direction; +import fr.arakne.utils.maps.path.Decoder; +import fr.quatrevieux.araknemu.game.fight.Fight; +import fr.quatrevieux.araknemu.game.fight.castable.CastScope; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.EffectHandler; +import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter; +import fr.quatrevieux.araknemu.game.fight.fighter.PassiveFighter; +import fr.quatrevieux.araknemu.game.fight.map.FightCell; +import fr.quatrevieux.araknemu.network.game.out.fight.action.ActionEffect; + +import java.util.Optional; + +/** + * Move front target + * + * Unlike move back, this effect do not handle area effects, and do not apply any damage when blocked + */ +public final class MoveFrontHandler implements EffectHandler { + private final Fight fight; + private final Decoder decoder; + + public MoveFrontHandler(Fight fight) { + this.fight = fight; + this.decoder = new Decoder<>(fight.map()); + } + + @Override + public void handle(CastScope cast, CastScope.EffectScope effect) { + final ActiveFighter caster = cast.caster(); + + for (PassiveFighter target : effect.targets()) { + apply(caster, target, effect.effect().min()); + } + } + + private void apply(ActiveFighter caster, PassiveFighter target, int distance) { + final FightCell startCell = target.cell(); + final Direction direction = startCell.coordinate().directionTo(caster.cell()); + + FightCell destination = startCell; + + // Check if a cell block the movement + for (int i = 0; i < distance; ++i) { + final Optional nextCell = decoder + .nextCellByDirection(destination, direction) + .filter(MapCell::walkable) + ; + + if (!nextCell.isPresent()) { + break; + } + + destination = nextCell.get(); + } + + // Fighter has moved + if (!destination.equals(startCell)) { + target.move(destination); + fight.send(ActionEffect.slide(caster, target, destination)); + } + } + + @Override + public void buff(CastScope cast, CastScope.EffectScope effect) { + throw new UnsupportedOperationException("Cannot use move back as buff effect"); + } +} diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveToTargetCellHandler.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveToTargetCellHandler.java new file mode 100644 index 000000000..4de47628c --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveToTargetCellHandler.java @@ -0,0 +1,67 @@ +/* + * 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-2021 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting; + +import fr.arakne.utils.maps.CoordinateCell; +import fr.arakne.utils.maps.constant.Direction; +import fr.arakne.utils.maps.path.Decoder; +import fr.quatrevieux.araknemu.game.fight.Fight; +import fr.quatrevieux.araknemu.game.fight.castable.CastScope; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.EffectHandler; +import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter; +import fr.quatrevieux.araknemu.game.fight.map.FightCell; + +/** + * Move the adjacent fighter to the target cell + * + * - Get the direction between caster and target cell + * - Get the adjacent fighter for the given direction + * - Compute the distance between the target fighter and the target cell + * - Perform a move back on the target fighter with the computed distance + */ +public final class MoveToTargetCellHandler implements EffectHandler { + private final Decoder decoder; + private final MoveBackApplier applier; + + public MoveToTargetCellHandler(Fight fight) { + this.decoder = new Decoder<>(fight.map()); + this.applier = new MoveBackApplier(fight); + } + + @Override + public void handle(CastScope cast, CastScope.EffectScope effect) { + final ActiveFighter caster = cast.caster(); + final CoordinateCell casterCell = caster.cell().coordinate(); + + // Remove 1 because the distance should be computed from the target fighter cell + final int distance = casterCell.distance(cast.target()) - 1; + final Direction direction = casterCell.directionTo(cast.target()); + + decoder.nextCellByDirection(casterCell.cell(), direction) + .flatMap(FightCell::fighter) + .ifPresent(target -> applier.apply(caster, target, distance)) + ; + } + + @Override + public void buff(CastScope cast, CastScope.EffectScope effect) { + throw new UnsupportedOperationException("Cannot use move back as buff effect"); + } +} diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/Fighter.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/Fighter.java index 8aadf7bca..fbe8942aa 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/Fighter.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/Fighter.java @@ -80,11 +80,6 @@ public default void attach(Object value) { attach(value.getClass(), value); } - /** - * Get the fighter level - */ - public int level(); - /** * Get the fight */ diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/PassiveFighter.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/PassiveFighter.java index d03391c55..c49737b8e 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/PassiveFighter.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/PassiveFighter.java @@ -42,6 +42,11 @@ public interface PassiveFighter { */ public void move(FightCell cell); + /** + * Get the fighter level + */ + public int level(); + /** * Get the fighter life */ 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 c1dd9f5ca..a2920e3b1 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 @@ -42,6 +42,10 @@ import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.misc.PushStateHandler; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.misc.RemoveStateHandler; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.misc.SkipTurnHandler; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting.AvoidDamageByMovingBackHandler; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting.MoveBackHandler; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting.MoveFrontHandler; +import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting.MoveToTargetCellHandler; import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting.TeleportHandler; /** @@ -57,6 +61,10 @@ public CommonEffectsModule(Fight fight) { @Override public void effects(EffectsHandler handler) { handler.register(4, new TeleportHandler(fight)); + handler.register(5, new MoveBackHandler(fight)); + handler.register(6, new MoveFrontHandler(fight)); + handler.register(9, new AvoidDamageByMovingBackHandler(fight)); + handler.register(783, new MoveToTargetCellHandler(fight)); handler.register(132, new DispelHandler(fight)); diff --git a/src/main/java/fr/quatrevieux/araknemu/game/handler/fight/KickFighter.java b/src/main/java/fr/quatrevieux/araknemu/game/handler/fight/KickFighter.java index 494b78acc..a78184562 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/handler/fight/KickFighter.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/handler/fight/KickFighter.java @@ -31,7 +31,7 @@ * Kick a fighter during placement state * * Unlike leave, the kicked fighter will not be punished. - * Only the team leader can kick a fighter, expect himself. + * Only the team leader can kick a fighter, except himself. * * @see PlacementState#kick(Fighter) */ diff --git a/src/main/java/fr/quatrevieux/araknemu/network/game/out/fight/action/ActionEffect.java b/src/main/java/fr/quatrevieux/araknemu/network/game/out/fight/action/ActionEffect.java index 626dd4b58..d148d6fe1 100644 --- a/src/main/java/fr/quatrevieux/araknemu/network/game/out/fight/action/ActionEffect.java +++ b/src/main/java/fr/quatrevieux/araknemu/network/game/out/fight/action/ActionEffect.java @@ -223,4 +223,15 @@ public static ActionEffect removeState(PassiveFighter fighter, int state) { public static ActionEffect dispelBuffs(PassiveFighter caster, PassiveFighter target) { return new ActionEffect(132, caster, target.id()); } + + /** + * The fighter has been slided (i.e. move back or front) by the caster + * + * @param caster The spell caster + * @param target The target (which has moved) + * @param destination The destination cell + */ + public static ActionEffect slide(PassiveFighter caster, PassiveFighter target, FightCell destination) { + return new ActionEffect(5, caster, target.id(), destination.id()); + } } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java b/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java index 9e9317d74..b45f9c273 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java @@ -485,6 +485,10 @@ public GameDataSet pushFunctionalSpells() throws SQLException, ContainerExceptio "(121, 'Mot Curatif', 702, '11,1,1', '108,5,11,,0,0,1d7+4|108,16,,,0,0,0d0+16|6|0|0|50|100|false|false|false|false|0|1|0|0|PaPa||50;18;19;3;1;41|1|false', '108,7,13,,0,0,1d7+6|108,20,,,0,0,0d0+20|6|0|0|50|100|false|false|false|false|0|1|0|0|PaPa||50;18;19;3;1;41|1|false', '108,9,15,,0,0,1d7+8|108,24,,,0,0,0d0+24|5|0|0|50|100|false|false|false|false|0|1|0|0|PaPa||50;18;19;3;1;41|1|false', '108,11,17,,0,0,1d7+10|108,28,,,0,0,0d0+28|5|0|0|45|100|false|false|false|false|0|1|0|0|PaPa||50;18;19;3;1;41|1|false', '108,13,19,,0,0,1d7+12|108,32,,,0,0,0d0+32|4|0|0|40|100|false|false|false|false|0|1|0|0|PaPa||50;18;19;3;1;41|1|false', '108,17,25,,0,0,1d9+16|108,40,,,0,0,0d0+40|3|0|0|40|100|false|false|false|false|0|1|0|0|PaPa||50;18;19;3;1;41|101|false', '')", "(131, 'Mot de Régénération', 702, '11,1,1', '108,1,4,,2,0,1d4+0|108,5,,,2,0,0d0+5|3|0|0|50|100|true|true|false|false|0|0|1|0|PaPa||50;18;19;3;1;41|26|false', '108,1,4,,3,0,1d4+0|108,5,,,3,0,0d0+5|3|0|0|50|100|true|true|false|false|0|0|1|0|PaPa||50;18;19;3;1;41|26|false', '108,1,4,,3,0,1d4+0|108,5,,,3,0,0d0+5|3|0|1|50|100|true|true|false|false|0|0|1|0|PaPa||50;18;19;3;1;41|26|false', '108,1,4,,4,0,1d4+0|108,5,,,4,0,0d0+5|3|0|1|50|100|true|true|false|false|0|0|1|0|PaPa||50;18;19;3;1;41|26|false', '108,1,4,,4,0,1d4+0|108,5,,,4,0,0d0+5|3|0|2|50|100|true|true|false|false|0|0|1|0|PaPa||50;18;19;3;1;41|26|false', '108,3,6,,5,0,1d4+2|108,6,,,5,0,0d0+6|3|0|3|50|100|true|true|false|false|0|0|1|0|PaPa||50;18;19;3;1;41|126|false', '')", "(1556, 'Fourberie', -1, '0,0,0', '81,6,,,2,0,0d0+6|81,7,,,2,0,0d0+7|4|0|1|30|100|false|true|false|false|0|0|0|8|PaPa||18;19;3;1;41|30|false', '81,8,,,2,0,0d0+8|81,10,,,2,0,0d0+10|4|0|1|30|100|false|true|false|false|0|0|0|8|PaPa||18;19;3;1;41|30|false', '81,10,,,3,0,0d0+10|81,12,,,3,0,0d0+12|4|0|1|30|100|false|true|false|false|0|0|0|8|PaPa||18;19;3;1;41|30|false', '81,12,,,3,0,0d0+12|81,15,,,3,0,0d0+15|4|0|1|30|100|false|true|false|false|0|0|0|8|PaPa||18;19;3;1;41|30|false', '81,15,,,3,0,0d0+15|81,18,,,3,0,0d0+18|4|0|1|30|100|false|true|false|false|0|0|0|8|PaPa||18;19;3;1;41|30|false', '81,20,,,3,0,0d0+20|108,25,,,3,0,0d0+25|4|0|1|30|100|false|true|false|false|0|0|0|8|PaPa||18;19;3;1;41|30|false', '')", + "(444, 'Dérobade', 0, '10,1,1', '9,100,1,,1,0|9,100,1,,2,0|3|0|1|50|100|false|false|false|true|0|0|0|10|PaPa||18;19;3;1;41|3|false', '9,100,1,,1,0|9,100,1,,2,0|3|0|2|50|100|false|false|false|true|0|0|0|9|PaPa||18;19;3;1;41|3|false', '9,100,1,,1,0|9,100,1,,2,0|3|0|3|50|100|false|false|false|true|0|0|0|8|PaPa||18;19;3;1;41|3|false', '9,100,1,,1,0|9,100,1,,2,0|3|0|4|50|100|false|false|false|true|0|0|0|7|PaPa||18;19;3;1;41|3|false', '9,100,1,,1,0|9,100,1,,2,0|3|0|5|50|100|false|false|false|true|0|0|0|6|PaPa||18;19;3;1;41|3|false', '9,100,1,,1,0|9,100,1,,2,0|3|0|6|50|100|false|false|false|true|0|0|0|5|PaPa||18;19;3;1;41|103|false', '4')", + "(128, 'Mot de Frayeur', 707, '11,1,1', '5,1,,,0,0||3|1|1|0|100|true|true|false|false|0|0|3|0|Pa||18;19;3;1;41|1|false', '5,1,,,0,0||3|1|2|0|100|true|true|false|false|0|0|3|0|Pa||18;19;3;1;41|1|false', '5,1,,,0,0||2|1|2|0|100|true|true|false|false|0|0|3|0|Pa||18;19;3;1;41|1|false', '5,1,,,0,0||2|1|3|0|100|true|true|false|false|0|0|3|0|Pa||18;19;3;1;41|1|false', '5,1,,,0,0||2|1|4|0|100|true|true|false|false|0|0|3|0|Pa||18;19;3;1;41|1|false', '5,1,,,0,0||1|1|5|0|100|true|true|false|false|0|0|3|0|Pa||18;19;3;1;41|101|false', '')", + "(67, 'Peur', 403, '21,2,1', '783,,,,0,0||2|2|2|0|100|true|false|true|false|0|0|0|0|Pa||18;19;3;1;41|60|false', '783,,,,0,0||2|2|3|0|100|true|false|true|false|0|0|0|0|Pa||18;19;3;1;41|60|false', '783,,,,0,0||2|2|4|0|100|true|false|true|false|0|0|0|0|Pa||18;19;3;1;41|60|false', '783,,,,0,0||2|2|5|0|100|true|false|true|false|0|0|0|0|Pa||18;19;3;1;41|60|false', '783,,,,0,0||2|2|6|0|100|true|false|true|false|0|0|0|0|Pa||18;19;3;1;41|60|false', '783,,,,0,0||2|2|7|0|100|true|false|true|false|0|0|0|0|Pa||18;19;3;1;41|160|false', '')", + "(434, 'Attirance', 1052, '51,1,1', '6,2,,,0,0||3|2|9|0|100|true|true|false|true|0|0|1|0|Pa||18;19;3;1;41|1|false', '6,3,,,0,0||3|2|10|0|100|true|true|false|true|0|0|1|0|Pa||18;19;3;1;41|1|false', '6,4,,,0,0||3|2|11|0|100|true|true|false|true|0|0|1|0|Pa||18;19;3;1;41|1|false', '6,5,,,0,0||3|2|12|0|100|true|true|false|true|0|0|1|0|Pa||18;19;3;1;41|1|false', '6,6,,,0,0||3|2|13|0|100|true|true|false|true|0|0|1|0|Pa||18;19;3;1;41|1|false', '6,7,,,0,0||3|2|14|0|100|true|true|false|true|0|0|1|0|Pa||18;19;3;1;41|101|false', '')", }, ",") + ";" ); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/FightBaseCase.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/FightBaseCase.java index a0f237003..c50084522 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/FightBaseCase.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/FightBaseCase.java @@ -38,6 +38,7 @@ import fr.quatrevieux.araknemu.game.fight.fighter.FighterFactory; import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter; import fr.quatrevieux.araknemu.game.fight.map.FightCell; +import fr.quatrevieux.araknemu.game.fight.module.CommonEffectsModule; import fr.quatrevieux.araknemu.game.fight.module.StatesModule; import fr.quatrevieux.araknemu.game.fight.state.*; import fr.quatrevieux.araknemu.game.fight.team.FightTeam; @@ -356,6 +357,7 @@ public Fight build(boolean init) { ); fight.register(new StatesModule(fight)); + fight.register(new CommonEffectsModule(fight)); fight.dispatcher().add(new Listener() { @Override public void on(FightStarted event) { 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 0bf8fd83e..2ee0f0e7b 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 @@ -384,6 +384,31 @@ void replaceTarget() { assertContainsAll(scope.effects().get(0).targets(), caster); } + @Test + void removeTarget() { + 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); + + CastScope scope = new CastScope(spell, caster, target.cell()); + + scope.withEffects(Collections.singletonList(effect)); + scope.removeTarget(target); + + assertContainsAll(scope.targets(), caster); + assertContainsAll(scope.effects().get(0).targets(), caster); + + scope.removeTarget(caster); + + assertTrue(scope.targets().isEmpty()); + assertTrue(scope.effects().get(0).targets().isEmpty()); + } + @Test void effectTargetsFilter() { SpellEffect effect = Mockito.mock(SpellEffect.class); 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 a8e9f06a8..f6697c873 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 @@ -23,7 +23,6 @@ import fr.quatrevieux.araknemu.game.fight.Fight; import fr.quatrevieux.araknemu.game.fight.FightBaseCase; 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.spell.SpellConstraintsValidator; import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter; import fr.quatrevieux.araknemu.game.fight.map.FightCell; @@ -35,13 +34,10 @@ import fr.quatrevieux.araknemu.game.fight.turn.action.util.CriticalityStrategy; import fr.quatrevieux.araknemu.game.spell.Spell; import fr.quatrevieux.araknemu.game.spell.SpellService; -import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect; import fr.quatrevieux.araknemu.network.game.out.fight.action.ActionEffect; import fr.quatrevieux.araknemu.network.game.out.fight.action.FightAction; -import fr.quatrevieux.araknemu.network.game.out.info.Error; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import java.util.Collections; import java.util.Optional; @@ -379,6 +375,54 @@ void healOnDamage() { requestStack.assertOne(ActionEffect.alterLifePoints(fighter1, fighter1, heal)); } + @Test + void avoidDamageByMovingBack() { + fighter1.move(fight.map().get(150)); + fighter2.move(fight.map().get(165)); + + castNormal(444, fighter1.cell()); // Dérobade + fighter1.turn().stop(); + + castNormal(183, fighter1.cell()); // Simple attack + + assertEquals(fighter1.life().max(), fighter1.life().max()); + assertEquals(135, fighter1.cell().id()); + requestStack.assertOne(ActionEffect.slide(fighter2, fighter1, fight.map().get(135))); + } + + @Test + void moveBack() { + fighter1.move(fight.map().get(150)); + fighter2.move(fight.map().get(165)); + + castNormal(128, fighter2.cell()); // Mot de Frayeur + + assertEquals(180, fighter2.cell().id()); + requestStack.assertOne(ActionEffect.slide(fighter1, fighter2, fight.map().get(180))); + } + + @Test + void moveToTargetCell() { + fighter1.move(fight.map().get(291)); + fighter2.move(fight.map().get(277)); + + castNormal(67, fight.map().get(235)); // Peur + + assertEquals(235, fighter2.cell().id()); + requestStack.assertOne(ActionEffect.slide(fighter1, fighter2, fight.map().get(235))); + } + + @Test + void moveFront() { + fighter1.move(fight.map().get(305)); + fighter2.move(fight.map().get(193)); + + castNormal(434, fight.map().get(193)); // Attirance + + assertEquals(277, fighter2.cell().id()); + requestStack.assertOne(ActionEffect.slide(fighter1, fighter2, fight.map().get(277))); + } + private void passTurns(int number) { for (; number > 0; --number) { fighter1.turn().stop(); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/BuffListTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/BuffListTest.java index 3e8cf9929..d435f21e6 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/BuffListTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/buff/BuffListTest.java @@ -23,6 +23,7 @@ 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.ActiveFighter; import fr.quatrevieux.araknemu.game.spell.Spell; import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect; import fr.quatrevieux.araknemu.network.game.out.fight.AddBuff; @@ -187,7 +188,7 @@ void onCastTarget() { } @Test - void onDamage() { + void onDirectDamage() { BuffHook hook1, hook2, hook3; Buff buff1 = new Buff(Mockito.mock(SpellEffect.class), Mockito.mock(Spell.class), other.fighter(), player.fighter(), hook1 = Mockito.mock(BuffHook.class)); @@ -199,12 +200,35 @@ void onDamage() { list.add(buff3); Damage damage = new Damage(10, Element.NEUTRAL); + ActiveFighter fighter = Mockito.mock(ActiveFighter.class); - list.onDamage(damage); + list.onDirectDamage(fighter, damage); - Mockito.verify(hook1).onDamage(buff1, damage); - Mockito.verify(hook2).onDamage(buff2, damage); - Mockito.verify(hook3).onDamage(buff3, damage); + Mockito.verify(hook1).onDirectDamage(buff1, fighter, damage); + Mockito.verify(hook2).onDirectDamage(buff2, fighter, damage); + Mockito.verify(hook3).onDirectDamage(buff3, fighter, damage); + } + + @Test + void onBuffDamage() { + BuffHook hook1, hook2, hook3; + + Buff buff1 = new Buff(Mockito.mock(SpellEffect.class), Mockito.mock(Spell.class), other.fighter(), player.fighter(), hook1 = Mockito.mock(BuffHook.class)); + Buff buff2 = new Buff(Mockito.mock(SpellEffect.class), Mockito.mock(Spell.class), other.fighter(), player.fighter(), hook2 = Mockito.mock(BuffHook.class)); + Buff buff3 = new Buff(Mockito.mock(SpellEffect.class), Mockito.mock(Spell.class), other.fighter(), player.fighter(), hook3 = Mockito.mock(BuffHook.class)); + + list.add(buff1); + list.add(buff2); + list.add(buff3); + + Damage damage = new Damage(10, Element.NEUTRAL); + Buff buff = new Buff(Mockito.mock(SpellEffect.class), Mockito.mock(Spell.class), other.fighter(), player.fighter(), Mockito.mock(BuffHook.class)); + + list.onBuffDamage(buff, damage); + + Mockito.verify(hook1).onBuffDamage(buff1, buff, damage); + Mockito.verify(hook2).onBuffDamage(buff2, buff, damage); + Mockito.verify(hook3).onBuffDamage(buff3, buff, damage); } @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 5dbf11674..859a1fc57 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 @@ -25,6 +25,7 @@ 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.fighter.ActiveFighter; import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter; import fr.quatrevieux.araknemu.game.spell.Spell; import fr.quatrevieux.araknemu.game.spell.effect.SpellEffect; @@ -33,7 +34,10 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import java.util.concurrent.atomic.AtomicReference; + import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; class DamageApplierTest extends FightBaseCase { @@ -176,4 +180,69 @@ public void onDamage(Buff buff, Damage value) { requestStack.assertOne(ActionEffect.alterLifePoints(caster, target, -3)); requestStack.assertOne(ActionEffect.reducedDamage(target, 7)); } + + @Test + void applyDirectDamageShouldCallBuffHook() { + SpellEffect effect = Mockito.mock(SpellEffect.class); + + Mockito.when(effect.min()).thenReturn(10); + + DamageApplier applier = new DamageApplier(Element.AIR, fight); + + AtomicReference calledBuff = new AtomicReference<>(); + AtomicReference calledCaster = new AtomicReference<>(); + AtomicReference calledDamage = new AtomicReference<>(); + + Buff buff = new Buff(Mockito.mock(SpellEffect.class), Mockito.mock(Spell.class), target, target, new BuffHook() { + @Override + public void onDirectDamage(Buff buff, ActiveFighter caster, Damage value) { + calledBuff.set(buff); + calledCaster.set(caster); + calledDamage.set(value); + } + }); + + target.buffs().add(buff); + + int value = applier.apply(caster, effect, target); + + assertEquals(-10, value); + + assertSame(buff, calledBuff.get()); + assertSame(caster, calledCaster.get()); + assertEquals(10, calledDamage.get().value()); + } + + @Test + void applyBuffDamageShouldCallBuffHook() { + SpellEffect effect = Mockito.mock(SpellEffect.class); + + Mockito.when(effect.min()).thenReturn(10); + + DamageApplier applier = new DamageApplier(Element.AIR, fight); + + AtomicReference calledBuff = new AtomicReference<>(); + AtomicReference calledPoison = new AtomicReference<>(); + AtomicReference calledDamage = new AtomicReference<>(); + + Buff buff = new Buff(Mockito.mock(SpellEffect.class), Mockito.mock(Spell.class), target, target, new BuffHook() { + @Override + public void onBuffDamage(Buff buff, Buff poison, Damage value) { + calledBuff.set(buff); + calledPoison.set(poison); + calledDamage.set(value); + } + }); + + target.buffs().add(buff); + + Buff toApply = new Buff(effect, Mockito.mock(Spell.class), caster, target, Mockito.mock(BuffHook.class)); + int value = applier.apply(toApply); + + assertEquals(-10, value); + + assertSame(buff, calledBuff.get()); + assertSame(toApply, calledPoison.get()); + assertEquals(10, calledDamage.get().value()); + } } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/AvoidDamageByMovingBackHandlerTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/AvoidDamageByMovingBackHandlerTest.java new file mode 100644 index 000000000..7e06177d0 --- /dev/null +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/AvoidDamageByMovingBackHandlerTest.java @@ -0,0 +1,325 @@ +/* + * 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-2021 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting; + +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.castable.CastScope; +import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff; +import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter; +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 fr.quatrevieux.araknemu.network.game.out.fight.action.ActionEffect; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Optional; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.*; + +class AvoidDamageByMovingBackHandlerTest extends FightBaseCase { + private Fight fight; + private AvoidDamageByMovingBackHandler handler; + + @Test + void handleDirectEffectNotSupported() { + configureFight(fb -> fb + .addSelf(b -> b.cell(165)) + .addEnemy(b -> b.player(other).cell(150)) + ); + + PlayerFighter caster = player.fighter(); + PlayerFighter target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.min()).thenReturn(3); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + assertThrows(UnsupportedOperationException.class, () -> handler.handle(scope, scope.effects().get(0))); + } + + @Test + void buffWillAddBuffToList() { + configureFight(fb -> fb + .addSelf(b -> b.cell(165)) + .addEnemy(b -> b.player(other).cell(150)) + ); + + PlayerFighter caster = player.fighter(); + PlayerFighter target = other.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.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.duration()).thenReturn(5); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.buff(scope, scope.effects().get(0)); + + Optional found = target.buffs().stream().filter(buff -> buff.effect().equals(effect)).findFirst(); + + assertTrue(found.isPresent()); + assertEquals(caster, found.get().caster()); + assertEquals(target, found.get().target()); + assertEquals(effect, found.get().effect()); + assertEquals(spell, found.get().action()); + assertEquals(handler, found.get().hook()); + assertEquals(5, found.get().remainingTurns()); + } + + @Test + void buffWithAreaMultipleFighters() { + configureFight(fb -> fb + .addSelf(b -> b.cell(165)) + .addEnemy(b -> b.player(other).cell(150)) + ); + + PlayerFighter caster = player.fighter(); + PlayerFighter target = other.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.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); + + CastScope scope = makeCastScope(caster, spell, effect, fight.map().get(122)); + handler.buff(scope, scope.effects().get(0)); + + assertTrue(caster.buffs().stream().anyMatch(buff -> buff.effect().equals(effect))); + assertTrue(target.buffs().stream().anyMatch(buff -> buff.effect().equals(effect))); + } + + @Test + void buffWithDirectDamage() { + configureFight(fb -> fb + .addSelf(b -> b.cell(165)) + .addEnemy(b -> b.player(other).cell(150)) + ); + + PlayerFighter caster = player.fighter(); + PlayerFighter target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.min()).thenReturn(100); + Mockito.when(effect.max()).thenReturn(2); + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.duration()).thenReturn(5); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.buff(scope, scope.effects().get(0)); + + requestStack.clear(); + + SpellEffect damageEffect = Mockito.mock(SpellEffect.class); + Mockito.when(damageEffect.effect()).thenReturn(100); + Mockito.when(damageEffect.min()).thenReturn(10); + Mockito.when(damageEffect.area()).thenReturn(new CellArea()); + Mockito.when(damageEffect.target()).thenReturn(SpellEffectTarget.DEFAULT); + + CastScope damageScope = makeCastScope(caster, spell, damageEffect, target.cell()); + fight.effects().apply(damageScope); + + assertEquals(target.life().max(), target.life().current()); + assertEquals(120, target.cell().id()); + assertFalse(fight.map().get(150).fighter().isPresent()); + + requestStack.assertOne(ActionEffect.slide(caster, target, fight.map().get(120))); + } + + @Test + void attackFromDistantCellShouldBeIgnored() { + configureFight(fb -> fb + .addSelf(b -> b.cell(180)) + .addEnemy(b -> b.player(other).cell(150)) + ); + + PlayerFighter caster = player.fighter(); + PlayerFighter target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.min()).thenReturn(100); + Mockito.when(effect.max()).thenReturn(2); + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.duration()).thenReturn(5); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.buff(scope, scope.effects().get(0)); + + requestStack.clear(); + + SpellEffect damageEffect = Mockito.mock(SpellEffect.class); + Mockito.when(damageEffect.effect()).thenReturn(100); + Mockito.when(damageEffect.min()).thenReturn(10); + Mockito.when(damageEffect.area()).thenReturn(new CellArea()); + Mockito.when(damageEffect.target()).thenReturn(SpellEffectTarget.DEFAULT); + + CastScope damageScope = makeCastScope(caster, spell, damageEffect, target.cell()); + fight.effects().apply(damageScope); + + assertEquals(15, target.life().max() - target.life().current()); + assertEquals(150, target.cell().id()); + + requestStack.assertOne(ActionEffect.alterLifePoints(caster, target, -15)); + } + + @Test + void buffWithPoisonDamageShouldBeIgnored() { + configureFight(fb -> fb + .addSelf(b -> b.cell(165)) + .addEnemy(b -> b.player(other).cell(150)) + ); + + PlayerFighter caster = player.fighter(); + PlayerFighter target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.min()).thenReturn(100); + Mockito.when(effect.max()).thenReturn(2); + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.duration()).thenReturn(5); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.buff(scope, scope.effects().get(0)); + + requestStack.clear(); + + SpellEffect damageEffect = Mockito.mock(SpellEffect.class); + Mockito.when(damageEffect.effect()).thenReturn(100); + Mockito.when(damageEffect.min()).thenReturn(10); + Mockito.when(damageEffect.duration()).thenReturn(5); + Mockito.when(damageEffect.area()).thenReturn(new CellArea()); + Mockito.when(damageEffect.target()).thenReturn(SpellEffectTarget.DEFAULT); + + CastScope damageScope = makeCastScope(caster, spell, damageEffect, target.cell()); + fight.effects().apply(damageScope); + + assertTrue(target.buffs().stream().anyMatch(buff -> buff.effect().equals(damageEffect))); + assertEquals(150, target.cell().id()); + } + + @Test + void randomChance() { + configureFight(fb -> fb + .addSelf(b -> b.cell(165)) + .addEnemy(b -> b.player(other).cell(150).currentLife(1000).maxLife(1000)) + ); + + PlayerFighter caster = player.fighter(); + PlayerFighter target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.min()).thenReturn(50); + Mockito.when(effect.max()).thenReturn(1); + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.duration()).thenReturn(5); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.buff(scope, scope.effects().get(0)); + + SpellEffect damageEffect = Mockito.mock(SpellEffect.class); + Mockito.when(damageEffect.effect()).thenReturn(100); + Mockito.when(damageEffect.min()).thenReturn(10); + Mockito.when(damageEffect.area()).thenReturn(new CellArea()); + Mockito.when(damageEffect.target()).thenReturn(SpellEffectTarget.DEFAULT); + + + int lastLife = target.life().current(); + int lostLifeCount = 0; + int moveBackCount = 0; + + for (int i = 0; i < 100; ++i) { + CastScope damageScope = makeCastScope(caster, spell, damageEffect, target.cell()); + fight.effects().apply(damageScope); + + if (lastLife > target.life().current()) { + ++lostLifeCount; + lastLife = target.life().current(); + } + + if (target.cell().id() != 150) { + target.move(fight.map().get(150)); + ++moveBackCount; + } + } + + assertBetween(40, 60, lostLifeCount); + assertEquals(100 - lostLifeCount, moveBackCount); + } + + private void configureFight(Consumer configurator) { + FightBuilder builder = fightBuilder(); + configurator.accept(builder); + + fight = builder.build(true); + handler = new AvoidDamageByMovingBackHandler(fight); + + fight.nextState(); + + requestStack.clear(); + } +} diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveBackApplierTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveBackApplierTest.java new file mode 100644 index 000000000..684d8e092 --- /dev/null +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveBackApplierTest.java @@ -0,0 +1,175 @@ +/* + * 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-2021 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting; + +import fr.quatrevieux.araknemu.game.fight.Fight; +import fr.quatrevieux.araknemu.game.fight.FightBaseCase; +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.network.game.out.fight.action.ActionEffect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; + +class MoveBackApplierTest extends FightBaseCase { + private Fight fight; + private PlayerFighter caster; + private PlayerFighter target; + private MoveBackApplier applier; + + @Test + void applySuccess() { + configureFight(fb -> fb + .addSelf(b -> b.cell(165)) + .addEnemy(b -> b.player(other).cell(150)) + ); + + caster = player.fighter(); + target = other.fighter(); + + FightCell lastCell = target.cell(); + FightCell destination = fight.map().get(105); + + applier.apply(caster, target, 3); + + requestStack.assertLast(ActionEffect.slide(caster, target, destination)); + + assertFalse(lastCell.fighter().isPresent()); + assertSame(target, destination.fighter().get()); + assertSame(destination, target.cell()); + } + + @ParameterizedTest + @MethodSource("provideDistances") + void applyBlocked(int distance, int min, int max) { + configureFight(fb -> fb + .addSelf(b -> b.cell(210)) + .addEnemy(b -> b.player(other).cell(182).currentLife(1000).maxLife(1000)) + ); + + caster = player.fighter(); + target = other.fighter(); + + applier.apply(caster, target, distance); + FightCell destination = fight.map().get(168); + + int damage = target.life().max() - target.life().current(); + assertBetween(min, max, damage); + + requestStack.assertOne(ActionEffect.slide(caster, target, destination)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, target, -damage)); + } + + public static Stream provideDistances() { + return Stream.of( + Arguments.of(2, 9, 16), + Arguments.of(3, 18, 32), + Arguments.of(4, 27, 48), + Arguments.of(10, 81, 144) + ); + } + + @Test + void applyBlockedByFighterShouldGetHalfDamage() { + configureFight(fb -> fb + .addSelf(b -> b.cell(210)) + .addEnemy(b -> b.cell(182).currentLife(1000).maxLife(1000)) + .addEnemy(b -> b.cell(168).currentLife(1000).maxLife(1000)) + ); + + caster = player.fighter(); + + List enemies = new ArrayList<>(fight.team(1).fighters()); + + applier.apply(caster, enemies.get(0), 3); + + int damage = enemies.get(0).life().max() - enemies.get(0).life().current(); + assertBetween(27, 48, damage); + + assertEquals(damage / 2, enemies.get(1).life().max() - enemies.get(1).life().current()); + } + + @Test + void applyBlockedChainDamage() { + configureFight(fb -> fb + .addSelf(b -> b.cell(305)) + .addEnemy(b -> b.cell(291)) + .addEnemy(b -> b.cell(277)) + .addEnemy(b -> b.cell(263)) + .addEnemy(b -> b.cell(249)) + .addEnemy(b -> b.cell(235)) + ); + + caster = player.fighter(); + + List enemies = new ArrayList<>(fight.team(1).fighters()); + + // Remove random damage + new MoveBackApplier(fight, 8, 1).apply(caster, enemies.get(0), 1); + + assertEquals(9, enemies.get(0).life().max() - enemies.get(0).life().current()); + assertEquals(4, enemies.get(1).life().max() - enemies.get(1).life().current()); + assertEquals(2, enemies.get(2).life().max() - enemies.get(2).life().current()); + assertEquals(1, enemies.get(3).life().max() - enemies.get(3).life().current()); + assertEquals(0, enemies.get(4).life().max() - enemies.get(4).life().current()); + } + + @Test + void applyBlockedWithoutMovementShouldNotPerformMove() { + configureFight(fb -> fb + .addSelf(b -> b.cell(210)) + .addEnemy(b -> b.cell(168).currentLife(1000).maxLife(1000)) + ); + + caster = player.fighter(); + + List enemies = new ArrayList<>(fight.team(1).fighters()); + + applier.apply(caster, enemies.get(0), 3); + + int damage = enemies.get(0).life().max() - enemies.get(0).life().current(); + + requestStack.assertAll(ActionEffect.alterLifePoints(caster, enemies.get(0), -damage)); + assertSame(fight.map().get(168), enemies.get(0).cell()); + } + + private void configureFight(Consumer configurator) { + FightBuilder builder = fightBuilder(); + configurator.accept(builder); + + fight = builder.build(true); + applier = new MoveBackApplier(fight); + + fight.nextState(); + + requestStack.clear(); + } +} diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveBackHandlerTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveBackHandlerTest.java new file mode 100644 index 000000000..3a054ea7b --- /dev/null +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveBackHandlerTest.java @@ -0,0 +1,269 @@ +/* + * 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-2021 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting; + +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.castable.CastScope; +import fr.quatrevieux.araknemu.game.fight.fighter.Fighter; +import fr.quatrevieux.araknemu.game.fight.fighter.PassiveFighter; +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.LineArea; +import fr.quatrevieux.araknemu.game.spell.effect.target.SpellEffectTarget; +import fr.quatrevieux.araknemu.network.game.out.fight.action.ActionEffect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MoveBackHandlerTest extends FightBaseCase { + private Fight fight; + private PlayerFighter caster; + private PlayerFighter target; + private MoveBackHandler handler; + + @Test + void buffNotSupported() { + configureFight(fb -> fb + .addSelf(b -> b.cell(165)) + .addEnemy(b -> b.player(other).cell(150)) + ); + + caster = player.fighter(); + target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.min()).thenReturn(3); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + + assertThrows(UnsupportedOperationException.class, () -> handler.buff(scope, scope.effects().get(0))); + } + + @Test + void applySuccess() { + configureFight(fb -> fb + .addSelf(b -> b.cell(165)) + .addEnemy(b -> b.player(other).cell(150)) + ); + + caster = player.fighter(); + target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.min()).thenReturn(3); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + FightCell lastCell = target.cell(); + FightCell destination = fight.map().get(105); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.handle(scope, scope.effects().get(0)); + + requestStack.assertLast(ActionEffect.slide(caster, target, destination)); + + assertFalse(lastCell.fighter().isPresent()); + assertSame(target, destination.fighter().get()); + assertSame(destination, target.cell()); + } + + @ParameterizedTest + @MethodSource("provideDistances") + void applyBlocked(int distance, int min, int max) { + configureFight(fb -> fb + .addSelf(b -> b.cell(210)) + .addEnemy(b -> b.player(other).cell(182).currentLife(1000).maxLife(1000)) + ); + + caster = player.fighter(); + target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.min()).thenReturn(distance); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + FightCell destination = fight.map().get(168); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.handle(scope, scope.effects().get(0)); + + int damage = target.life().max() - target.life().current(); + assertBetween(min, max, damage); + + requestStack.assertOne(ActionEffect.slide(caster, target, destination)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, target, -damage)); + } + + public static Stream provideDistances() { + return Stream.of( + Arguments.of(2, 9, 16), + Arguments.of(3, 18, 32), + Arguments.of(4, 27, 48), + Arguments.of(10, 81, 144) + ); + } + + @Test + void applyBlockedByFighterShouldGetHalfDamage() { + configureFight(fb -> fb + .addSelf(b -> b.cell(210)) + .addEnemy(b -> b.cell(182).currentLife(1000).maxLife(1000)) + .addEnemy(b -> b.cell(168).currentLife(1000).maxLife(1000)) + ); + + caster = player.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.min()).thenReturn(3); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, fight.map().get(182)); + handler.handle(scope, scope.effects().get(0)); + + List enemies = new ArrayList<>(fight.team(1).fighters()); + + int damage = enemies.get(0).life().max() - enemies.get(0).life().current(); + assertBetween(27, 48, damage); + + assertEquals(damage / 2, enemies.get(1).life().max() - enemies.get(1).life().current()); + } + + @Test + void applyBlockedWithoutMovementShouldNotPerformMove() { + configureFight(fb -> fb + .addSelf(b -> b.cell(210)) + .addEnemy(b -> b.cell(168).currentLife(1000).maxLife(1000)) + ); + + caster = player.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.min()).thenReturn(3); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, fight.map().get(168)); + handler.handle(scope, scope.effects().get(0)); + + List enemies = new ArrayList<>(fight.team(1).fighters()); + + int damage = enemies.get(0).life().max() - enemies.get(0).life().current(); + + requestStack.assertAll(ActionEffect.alterLifePoints(caster, enemies.get(0), -damage)); + assertSame(fight.map().get(168), enemies.get(0).cell()); + } + + @Test + void applyWithArea() { + configureFight(fb -> fb + .addSelf(b -> b.cell(340)) + .addEnemy(b -> b.cell(312)) + .addEnemy(b -> b.cell(298)) + .addEnemy(b -> b.cell(284)) + ); + + caster = player.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.area()).thenReturn(new LineArea(new EffectArea(EffectArea.Type.LINE, 5))); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.min()).thenReturn(2); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, fight.map().get(312)); + handler.handle(scope, scope.effects().get(0)); + + List enemies = new ArrayList<>(fight.team(1).fighters()); + + // Start with most distant target + requestStack.assertAll( + ActionEffect.slide(caster, enemies.get(2), fight.map().get(256)), + ActionEffect.slide(caster, enemies.get(1), fight.map().get(270)), + ActionEffect.slide(caster, enemies.get(0), fight.map().get(284)) + ); + + assertEquals(fight.map().get(284), enemies.get(0).cell()); + assertEquals(fight.map().get(270), enemies.get(1).cell()); + assertEquals(fight.map().get(256), enemies.get(2).cell()); + } + + private void configureFight(Consumer configurator) { + FightBuilder builder = fightBuilder(); + configurator.accept(builder); + + fight = builder.build(true); + handler = new MoveBackHandler(fight); + + fight.nextState(); + + requestStack.clear(); + } +} diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveFrontHandlerTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveFrontHandlerTest.java new file mode 100644 index 000000000..f79c6a0ab --- /dev/null +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveFrontHandlerTest.java @@ -0,0 +1,178 @@ +/* + * 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-2021 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting; + +import fr.quatrevieux.araknemu.game.fight.Fight; +import fr.quatrevieux.araknemu.game.fight.FightBaseCase; +import fr.quatrevieux.araknemu.game.fight.castable.CastScope; +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.target.SpellEffectTarget; +import fr.quatrevieux.araknemu.network.game.out.fight.action.ActionEffect; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MoveFrontHandlerTest extends FightBaseCase { + private Fight fight; + private PlayerFighter caster; + private PlayerFighter target; + private MoveFrontHandler handler; + + @Test + void buffNotSupported() { + configureFight(fb -> fb + .addSelf(b -> b.cell(277)) + .addEnemy(b -> b.player(other).cell(221)) + ); + + caster = player.fighter(); + target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.min()).thenReturn(3); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + + assertThrows(UnsupportedOperationException.class, () -> handler.buff(scope, scope.effects().get(0))); + } + + @Test + void applySuccess() { + configureFight(fb -> fb + .addSelf(b -> b.cell(277)) + .addEnemy(b -> b.player(other).cell(221)) + ); + + caster = player.fighter(); + target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.min()).thenReturn(3); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + FightCell lastCell = target.cell(); + FightCell destination = fight.map().get(263); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.handle(scope, scope.effects().get(0)); + + requestStack.assertLast(ActionEffect.slide(caster, target, destination)); + + assertFalse(lastCell.fighter().isPresent()); + assertSame(target, destination.fighter().get()); + assertSame(destination, target.cell()); + } + + @Test + void applyBlocked() { + configureFight(fb -> fb + .addSelf(b -> b.cell(236)) + .addEnemy(b -> b.player(other).cell(131)) + ); + + caster = player.fighter(); + target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.min()).thenReturn(4); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + FightCell destination = fight.map().get(161); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + handler.handle(scope, scope.effects().get(0)); + + requestStack.assertOne(ActionEffect.slide(caster, target, destination)); + } + + @Test + void applyBlockedWithoutMovementShouldNotPerformMove() { + configureFight(fb -> fb + .addSelf(b -> b.cell(206)) + .addEnemy(b -> b.cell(191)) + ); + + caster = player.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.min()).thenReturn(3); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + requestStack.clear(); + + CastScope scope = makeCastScope(caster, spell, effect, fight.map().get(191)); + handler.handle(scope, scope.effects().get(0)); + + List enemies = new ArrayList<>(fight.team(1).fighters()); + + requestStack.assertEmpty(); + assertSame(fight.map().get(191), enemies.get(0).cell()); + } + + private void configureFight(Consumer configurator) { + FightBuilder builder = fightBuilder(); + configurator.accept(builder); + + fight = builder.build(true); + handler = new MoveFrontHandler(fight); + + fight.nextState(); + + requestStack.clear(); + } +} \ No newline at end of file diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveToTargetCellHandlerTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveToTargetCellHandlerTest.java new file mode 100644 index 000000000..7dece366b --- /dev/null +++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/shifting/MoveToTargetCellHandlerTest.java @@ -0,0 +1,161 @@ +/* + * 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-2021 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.shifting; + +import fr.quatrevieux.araknemu.game.fight.Fight; +import fr.quatrevieux.araknemu.game.fight.FightBaseCase; +import fr.quatrevieux.araknemu.game.fight.castable.CastScope; +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.target.SpellEffectTarget; +import fr.quatrevieux.araknemu.network.game.out.fight.action.ActionEffect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MoveToTargetCellHandlerTest extends FightBaseCase { + private Fight fight; + private PlayerFighter caster; + private PlayerFighter target; + private MoveToTargetCellHandler handler; + + @Test + void buffNotSupported() { + configureFight(fb -> fb + .addSelf(b -> b.cell(165)) + .addEnemy(b -> b.player(other).cell(150)) + ); + + caster = player.fighter(); + target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + Mockito.when(effect.area()).thenReturn(new CellArea()); + Mockito.when(effect.target()).thenReturn(SpellEffectTarget.DEFAULT); + Mockito.when(effect.min()).thenReturn(3); + Mockito.when(spell.constraints()).thenReturn(constraints); + Mockito.when(constraints.freeCell()).thenReturn(false); + + CastScope scope = makeCastScope(caster, spell, effect, target.cell()); + + assertThrows(UnsupportedOperationException.class, () -> handler.buff(scope, scope.effects().get(0))); + } + + @Test + void applySuccess() { + configureFight(fb -> fb + .addSelf(b -> b.cell(235)) + .addEnemy(b -> b.player(other).cell(221)) + ); + + caster = player.fighter(); + target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + 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); + + FightCell lastCell = target.cell(); + FightCell destination = fight.map().get(179); + + CastScope scope = makeCastScope(caster, spell, effect, destination); + handler.handle(scope, scope.effects().get(0)); + + requestStack.assertLast(ActionEffect.slide(caster, target, destination)); + + assertFalse(lastCell.fighter().isPresent()); + assertSame(target, destination.fighter().get()); + assertSame(destination, target.cell()); + } + + @ParameterizedTest + @MethodSource("provideTargets") + void applyBlocked(int targetCell, int min, int max) { + configureFight(fb -> fb + .addSelf(b -> b.cell(196)) + .addEnemy(b -> b.player(other).cell(182).currentLife(1000).maxLife(1000)) + ); + + caster = player.fighter(); + target = other.fighter(); + + SpellEffect effect = Mockito.mock(SpellEffect.class); + Spell spell = Mockito.mock(Spell.class); + SpellConstraints constraints = Mockito.mock(SpellConstraints.class); + + 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); + + FightCell destination = fight.map().get(168); + + CastScope scope = makeCastScope(caster, spell, effect, fight.map().get(targetCell)); + handler.handle(scope, scope.effects().get(0)); + + int damage = target.life().max() - target.life().current(); + assertBetween(min, max, damage); + + requestStack.assertOne(ActionEffect.slide(caster, target, destination)); + requestStack.assertOne(ActionEffect.alterLifePoints(caster, target, -damage)); + } + + public static Stream provideTargets() { + return Stream.of( + Arguments.of(126, 27, 48), + Arguments.of(112, 36, 64), + Arguments.of(98, 45, 80), + Arguments.of(42, 81, 144) + ); + } + + private void configureFight(Consumer configurator) { + FightBuilder builder = fightBuilder(); + configurator.accept(builder); + + fight = builder.build(true); + handler = new MoveToTargetCellHandler(fight); + + fight.nextState(); + + requestStack.clear(); + } +} diff --git a/src/test/java/fr/quatrevieux/araknemu/network/game/out/fight/action/ActionEffectTest.java b/src/test/java/fr/quatrevieux/araknemu/network/game/out/fight/action/ActionEffectTest.java index 8da7c9d61..60874b36e 100644 --- a/src/test/java/fr/quatrevieux/araknemu/network/game/out/fight/action/ActionEffectTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/network/game/out/fight/action/ActionEffectTest.java @@ -230,4 +230,17 @@ void dispelBuffs() { assertEquals("GA;132;456;460", ActionEffect.dispelBuffs(fighter, fighter2).toString()); } -} \ No newline at end of file + + @Test + void slide() { + Fighter fighter = Mockito.mock(Fighter.class); + Fighter fighter2 = Mockito.mock(Fighter.class); + FightCell cell = Mockito.mock(FightCell.class); + + Mockito.when(fighter.id()).thenReturn(456); + Mockito.when(fighter2.id()).thenReturn(460); + Mockito.when(cell.id()).thenReturn(123); + + assertEquals("GA;5;456;460,123", ActionEffect.slide(fighter, fighter2, cell).toString()); + } +}