diff --git a/src/main/java/fr/quatrevieux/araknemu/game/GameModule.java b/src/main/java/fr/quatrevieux/araknemu/game/GameModule.java
index 0c5690c9c..dbb3da69f 100644
--- a/src/main/java/fr/quatrevieux/araknemu/game/GameModule.java
+++ b/src/main/java/fr/quatrevieux/araknemu/game/GameModule.java
@@ -133,8 +133,10 @@
import fr.quatrevieux.araknemu.game.fight.FightService;
import fr.quatrevieux.araknemu.game.fight.ai.factory.AiFactory;
import fr.quatrevieux.araknemu.game.fight.ai.factory.ChainAiFactory;
+import fr.quatrevieux.araknemu.game.fight.ai.factory.DoubleAiFactory;
import fr.quatrevieux.araknemu.game.fight.ai.factory.MonsterAiFactory;
import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Aggressive;
+import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Blocking;
import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Fixed;
import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Runaway;
import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Support;
@@ -895,7 +897,8 @@ private void configureServices(ContainerConfigurator configurator) {
configurator.persist(
AiFactory.class,
container -> new ChainAiFactory(
- container.get(MonsterAiFactory.class)
+ container.get(MonsterAiFactory.class),
+ container.get(DoubleAiFactory.class)
)
);
@@ -984,11 +987,17 @@ private void configureServices(ContainerConfigurator configurator) {
factory.register("SUPPORT", new Support(simulator));
factory.register("TACTICAL", new Tactical(simulator));
factory.register("FIXED", new Fixed(simulator));
+ factory.register("BLOCKING", new Blocking());
return factory;
}
);
+ configurator.persist(
+ DoubleAiFactory.class,
+ container -> new DoubleAiFactory(new Blocking())
+ );
+
configurator.persist(ExchangeFactory.class, container -> new DefaultExchangeFactory(
new PlayerExchangeFactories(),
new NpcExchangeFactories()
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/FighterSprite.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/FighterSprite.java
new file mode 100644
index 000000000..a26aefe10
--- /dev/null
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/FighterSprite.java
@@ -0,0 +1,38 @@
+/*
+ * 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-2023 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight;
+
+import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
+import fr.quatrevieux.araknemu.game.world.creature.Sprite;
+
+/**
+ * Extends the base sprite for fighters, allowing temporary alterations
+ */
+public interface FighterSprite extends Sprite {
+ /**
+ * Duplicate the sprite with the given fighter
+ * The returned sprite should be same as current one, but with new fighter cell, orientation, and characteristics.
+ *
+ * @param fighter The new fighter
+ *
+ * @return The new sprite instance
+ */
+ public FighterSprite withFighter(Fighter fighter);
+}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/BlockNearestEnemy.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/BlockNearestEnemy.java
new file mode 100644
index 000000000..33deeff2c
--- /dev/null
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/BlockNearestEnemy.java
@@ -0,0 +1,73 @@
+/*
+ * 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-2023 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.ai.action;
+
+import fr.arakne.utils.maps.CoordinateCell;
+import fr.quatrevieux.araknemu.game.fight.ai.AI;
+import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.FighterData;
+import fr.quatrevieux.araknemu.game.fight.map.BattlefieldCell;
+import fr.quatrevieux.araknemu.game.fight.turn.action.Action;
+
+import java.util.Comparator;
+import java.util.Optional;
+
+/**
+ * Try to block the nearest enemy of the invoker
+ * If the invoker is hidden, this generator will act like {@link MoveNearEnemy}
+ *
+ * @param the fighter type
+ */
+public final class BlockNearestEnemy implements ActionGenerator {
+ private final ActionGenerator moveGenerator;
+
+ @SuppressWarnings("methodref.receiver.bound")
+ public BlockNearestEnemy() {
+ moveGenerator = new MoveNearFighter<>(this::resolve);
+ }
+
+ @Override
+ public void initialize(AI ai) {
+ moveGenerator.initialize(ai);
+ }
+
+ @Override
+ public Optional generate(AI ai, AiActionFactory actions) {
+ return moveGenerator.generate(ai, actions);
+ }
+
+ private Optional extends FighterData> resolve(AI ai) {
+ final FighterData invoker = ai.fighter().invoker();
+
+ if (invoker == null || invoker.hidden()) {
+ return ai.enemy();
+ }
+
+ final CoordinateCell currentCell = invoker.cell().coordinate();
+
+ return ai.helper().enemies().stream()
+ .filter(fighter -> !fighter.hidden())
+ .min(Comparator
+ .comparingInt(f -> currentCell.distance(f.cell()))
+ .thenComparingInt(f -> f.life().current())
+ )
+ ;
+ }
+}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearEnemy.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearEnemy.java
index af035bb86..f352df1e0 100644
--- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearEnemy.java
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearEnemy.java
@@ -19,16 +19,9 @@
package fr.quatrevieux.araknemu.game.fight.ai.action;
-import fr.arakne.utils.maps.path.Pathfinder;
import fr.quatrevieux.araknemu.game.fight.ai.AI;
-import fr.quatrevieux.araknemu.game.fight.ai.util.AIHelper;
import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter;
-import fr.quatrevieux.araknemu.game.fight.map.BattlefieldCell;
import fr.quatrevieux.araknemu.game.fight.turn.action.Action;
-import fr.quatrevieux.araknemu.util.Asserter;
-import org.checkerframework.checker.index.qual.Positive;
-import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
-import org.checkerframework.checker.nullness.util.NullnessUtil;
import java.util.Optional;
@@ -36,56 +29,19 @@
* Try to move near the selected enemy
*/
public final class MoveNearEnemy implements ActionGenerator {
- private @MonotonicNonNull Pathfinder pathfinder;
- private @MonotonicNonNull AIHelper helper;
+ private final ActionGenerator moveGenerator;
+
+ public MoveNearEnemy() {
+ this.moveGenerator = new MoveNearFighter<>(AI::enemy);
+ }
@Override
public void initialize(AI ai) {
- this.helper = ai.helper();
- this.pathfinder = helper.cells().pathfinder()
- .targetDistance(1)
- .walkablePredicate(cell -> true) // Fix #94 Ignore inaccessible cell (handled by cell cost)
- .cellWeightFunction(this::cellCost)
- ;
+ moveGenerator.initialize(ai);
}
@Override
public Optional generate(AI ai, AiActionFactory actions) {
- if (helper == null || !helper.canMove()) {
- return Optional.empty();
- }
-
- final int movementPoints = helper.movementPoints();
- final BattlefieldCell currentCell = ai.fighter().cell();
-
- return ai.enemy()
- .map(enemy -> NullnessUtil.castNonNull(pathfinder).findPath(currentCell, enemy.cell()).truncate(movementPoints + 1))
- .map(path -> path.keepWhile(step -> step.cell().equals(currentCell) || step.cell().walkable())) // Truncate path to first unwalkable cell (may occur if the enemy cell is inaccessible or if other fighters block the path)
- .filter(path -> path.size() > 1)
- .map(path -> actions.move(path))
- ;
- }
-
- /**
- * Compute the cell cost for optimize the path finding
- */
- private @Positive int cellCost(BattlefieldCell cell) {
- // Fix #94 : Some cells are not accessible, but walkable/targetable using teleport.
- // In this case the pathfinder will fail, so instead of ignoring unwalkable cells, simply set a very high cost,
- // which allows the AI to generate a path to an inaccessible cell without throws a PathException
- if (!cell.walkableIgnoreFighter()) {
- return 1000;
- }
-
- // A fighter is on the cell : the cell is not walkable
- // But the fighter may leave the place at the next turn
- // The cost is higher than a simple detour, but permit to resolve a path blocked by a fighter
- if (cell.hasFighter()) {
- return 15;
- }
-
- // Add a cost of 3 for each enemy around the cell
- // This cost corresponds to the detour cost + 1
- return 1 + Asserter.castNonNegative(3 * (int) NullnessUtil.castNonNull(helper).enemies().adjacent(cell).count());
+ return moveGenerator.generate(ai, actions);
}
}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearFighter.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearFighter.java
new file mode 100644
index 000000000..b279b04a9
--- /dev/null
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearFighter.java
@@ -0,0 +1,98 @@
+/*
+ * 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-2023 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.ai.action;
+
+import fr.arakne.utils.maps.path.Pathfinder;
+import fr.quatrevieux.araknemu.game.fight.ai.AI;
+import fr.quatrevieux.araknemu.game.fight.ai.util.AIHelper;
+import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.FighterData;
+import fr.quatrevieux.araknemu.game.fight.map.BattlefieldCell;
+import fr.quatrevieux.araknemu.game.fight.turn.action.Action;
+import fr.quatrevieux.araknemu.util.Asserter;
+import org.checkerframework.checker.index.qual.Positive;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.util.NullnessUtil;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * Try to move near the selected fighter
+ */
+public final class MoveNearFighter implements ActionGenerator {
+ private @MonotonicNonNull Pathfinder pathfinder;
+ private @MonotonicNonNull AIHelper helper;
+ private final Function, Optional extends FighterData>> fighterResolver;
+
+ public MoveNearFighter(Function, Optional extends FighterData>> fighterResolver) {
+ this.fighterResolver = fighterResolver;
+ }
+
+ @Override
+ public void initialize(AI ai) {
+ this.helper = ai.helper();
+ this.pathfinder = helper.cells().pathfinder()
+ .targetDistance(1)
+ .walkablePredicate(cell -> true) // Fix #94 Ignore inaccessible cell (handled by cell cost)
+ .cellWeightFunction(this::cellCost)
+ ;
+ }
+
+ @Override
+ public Optional generate(AI ai, AiActionFactory actions) {
+ if (helper == null || !helper.canMove()) {
+ return Optional.empty();
+ }
+
+ final int movementPoints = helper.movementPoints();
+ final BattlefieldCell currentCell = ai.fighter().cell();
+
+ return fighterResolver.apply(ai)
+ .map(enemy -> NullnessUtil.castNonNull(pathfinder).findPath(currentCell, enemy.cell()).truncate(movementPoints + 1))
+ .map(path -> path.keepWhile(step -> step.cell().equals(currentCell) || step.cell().walkable())) // Truncate path to first unwalkable cell (may occur if the enemy cell is inaccessible or if other fighters block the path)
+ .filter(path -> path.size() > 1)
+ .map(path -> actions.move(path))
+ ;
+ }
+
+ /**
+ * Compute the cell cost for optimize the path finding
+ */
+ private @Positive int cellCost(BattlefieldCell cell) {
+ // Fix #94 : Some cells are not accessible, but walkable/targetable using teleport.
+ // In this case the pathfinder will fail, so instead of ignoring unwalkable cells, simply set a very high cost,
+ // which allows the AI to generate a path to an inaccessible cell without throws a PathException
+ if (!cell.walkableIgnoreFighter()) {
+ return 1000;
+ }
+
+ // A fighter is on the cell : the cell is not walkable
+ // But the fighter may leave the place at the next turn
+ // The cost is higher than a simple detour, but permit to resolve a path blocked by a fighter
+ if (cell.hasFighter()) {
+ return 15;
+ }
+
+ // Add a cost of 3 for each enemy around the cell
+ // This cost corresponds to the detour cost + 1
+ return 1 + Asserter.castNonNegative(3 * (int) NullnessUtil.castNonNull(helper).enemies().adjacent(cell).count());
+ }
+}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilder.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilder.java
index a16c1b287..015e41fc0 100644
--- a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilder.java
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilder.java
@@ -22,6 +22,7 @@
import fr.quatrevieux.araknemu.game.fight.ai.AI;
import fr.quatrevieux.araknemu.game.fight.ai.action.ActionGenerator;
import fr.quatrevieux.araknemu.game.fight.ai.action.Attack;
+import fr.quatrevieux.araknemu.game.fight.ai.action.BlockNearestEnemy;
import fr.quatrevieux.araknemu.game.fight.ai.action.Boost;
import fr.quatrevieux.araknemu.game.fight.ai.action.Debuff;
import fr.quatrevieux.araknemu.game.fight.ai.action.Heal;
@@ -278,6 +279,17 @@ public final GeneratorBuilder moveNearEnemy() {
return add(new MoveNearEnemy<>());
}
+ /**
+ * Try to move to the nearest enemy of the invoker
+ *
+ * @return The builder instance
+ *
+ * @see BlockNearestEnemy The used action generator
+ */
+ public final GeneratorBuilder blockNearestEnemy() {
+ return add(new BlockNearestEnemy<>());
+ }
+
/**
* Try to teleport near the selected enemy
*
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/DoubleAiFactory.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/DoubleAiFactory.java
new file mode 100644
index 000000000..b55782097
--- /dev/null
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/DoubleAiFactory.java
@@ -0,0 +1,53 @@
+/*
+ * 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-2023 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.ai.factory;
+
+import fr.quatrevieux.araknemu.game.fight.ai.AI;
+import fr.quatrevieux.araknemu.game.fight.fighter.PlayableFighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.operation.FighterOperation;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.Optional;
+
+/**
+ * AI factory for {@link DoubleFighter}
+ */
+public final class DoubleAiFactory implements AiFactory {
+ private final AiFactory factory;
+
+ public DoubleAiFactory(AiFactory factory) {
+ this.factory = factory;
+ }
+
+ @Override
+ public Optional> create(PlayableFighter fighter) {
+ return Optional.ofNullable(fighter.apply(new Resolver()).ai);
+ }
+
+ private class Resolver implements FighterOperation {
+ private @Nullable AI ai;
+
+ @Override
+ public void onDouble(DoubleFighter fighter) {
+ factory.create(fighter).ifPresent(ai -> this.ai = ai);
+ }
+ }
+}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Blocking.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Blocking.java
new file mode 100644
index 000000000..b8e1f2864
--- /dev/null
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/Blocking.java
@@ -0,0 +1,35 @@
+/*
+ * 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-2019 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.ai.factory.type;
+
+import fr.quatrevieux.araknemu.game.fight.ai.action.builder.GeneratorBuilder;
+import fr.quatrevieux.araknemu.game.fight.ai.factory.AbstractAiBuilderFactory;
+import fr.quatrevieux.araknemu.game.fight.fighter.PlayableFighter;
+
+/**
+ * AI for blocking invocation
+ * This AI will move to the enemy nearest of the invoker to block it
+ */
+public final class Blocking extends AbstractAiBuilderFactory {
+ @Override
+ public void configure(GeneratorBuilder builder, PlayableFighter fighter) {
+ builder.blockNearestEnemy();
+ }
+}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/invocations/CreateDoubleHandler.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/invocations/CreateDoubleHandler.java
new file mode 100644
index 000000000..99a766ab2
--- /dev/null
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/invocations/CreateDoubleHandler.java
@@ -0,0 +1,68 @@
+/*
+ * 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-2023 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.invocations;
+
+import fr.quatrevieux.araknemu.game.fight.Fight;
+import fr.quatrevieux.araknemu.game.fight.castable.FightCastScope;
+import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.EffectHandler;
+import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.FighterFactory;
+import fr.quatrevieux.araknemu.game.fight.fighter.PlayableFighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter;
+import fr.quatrevieux.araknemu.network.game.out.fight.action.ActionEffect;
+import fr.quatrevieux.araknemu.network.game.out.fight.turn.FighterTurnOrder;
+
+/**
+ * Create a double of the caster
+ * This spell effect does not take any parameters
+ *
+ * @see DoubleFighter
+ */
+public final class CreateDoubleHandler implements EffectHandler {
+ private final FighterFactory fighterFactory;
+ private final Fight fight;
+
+ public CreateDoubleHandler(FighterFactory fighterFactory, Fight fight) {
+ this.fighterFactory = fighterFactory;
+ this.fight = fight;
+ }
+
+ @Override
+ public void buff(FightCastScope cast, FightCastScope.EffectScope effect) {
+ throw new UnsupportedOperationException("CreateDoubleHandler cannot be used as buff");
+ }
+
+ @Override
+ public void handle(FightCastScope cast, FightCastScope.EffectScope effect) {
+ final Fighter caster = cast.caster();
+
+ final PlayableFighter invocation = fighterFactory.generate(id -> new DoubleFighter(
+ id,
+ cast.caster()
+ ));
+
+ fight.fighters().joinTurnList(invocation, cast.target());
+
+ invocation.init();
+
+ fight.send(ActionEffect.addDouble(caster, invocation));
+ fight.send(ActionEffect.packet(cast.caster(), new FighterTurnOrder(fight.turnList())));
+ }
+}
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 d5238ec5c..a017ea8b1 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
@@ -22,6 +22,7 @@
import fr.arakne.utils.maps.constant.Direction;
import fr.quatrevieux.araknemu.core.event.Dispatcher;
import fr.quatrevieux.araknemu.game.fight.Fight;
+import fr.quatrevieux.araknemu.game.fight.FighterSprite;
import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.BuffList;
import fr.quatrevieux.araknemu.game.fight.castable.weapon.CastableWeapon;
import fr.quatrevieux.araknemu.game.fight.fighter.operation.FighterOperation;
@@ -147,6 +148,9 @@ public default void attach(Object value) {
*/
public O apply(O operation);
+ @Override
+ public FighterSprite sprite();
+
/**
* Check if the fighter is the team leader
*/
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/DoubleFighter.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/DoubleFighter.java
new file mode 100644
index 000000000..132d3871e
--- /dev/null
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/DoubleFighter.java
@@ -0,0 +1,117 @@
+/*
+ * This file is part of Araknemu.
+ *
+ * Araknemu is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Araknemu is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Araknemu. If not, see .
+ *
+ * Copyright (c) 2017-2023 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.fighter.invocation;
+
+import fr.quatrevieux.araknemu.game.fight.FighterSprite;
+import fr.quatrevieux.araknemu.game.fight.castable.weapon.CastableWeapon;
+import fr.quatrevieux.araknemu.game.fight.exception.FightException;
+import fr.quatrevieux.araknemu.game.fight.fighter.AbstractPlayableFighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.BaseFighterLife;
+import fr.quatrevieux.araknemu.game.fight.fighter.EmptySpellList;
+import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.FighterCharacteristics;
+import fr.quatrevieux.araknemu.game.fight.fighter.FighterData;
+import fr.quatrevieux.araknemu.game.fight.fighter.FighterLife;
+import fr.quatrevieux.araknemu.game.fight.fighter.FighterSpellList;
+import fr.quatrevieux.araknemu.game.fight.fighter.operation.FighterOperation;
+import fr.quatrevieux.araknemu.game.fight.team.FightTeam;
+import org.checkerframework.checker.index.qual.Positive;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+/**
+ * Create a double of a fighter as invocation
+ * The double will inherit the characteristics of the invoker, but not the spells
+ */
+public final class DoubleFighter extends AbstractPlayableFighter {
+ private final int id;
+ private final Fighter invoker;
+ private final FightTeam team;
+ private final BaseFighterLife life;
+ private final FighterCharacteristics characteristics;
+ private final FighterSprite sprite;
+
+ @SuppressWarnings({"assignment", "argument"})
+ public DoubleFighter(int id, Fighter invoker) {
+ this.id = id;
+ this.invoker = invoker;
+ this.team = invoker.team();
+
+ this.life = new BaseFighterLife(this, invoker.life().current(), invoker.life().max());
+ this.characteristics = new DoubleFighterCharacteristics(this, invoker);
+ this.sprite = invoker.sprite().withFighter(this);
+ }
+
+ @Override
+ public O apply(O operation) {
+ operation.onDouble(this);
+
+ return operation;
+ }
+
+ @Override
+ public @Positive int level() {
+ return invoker.level();
+ }
+
+ @Override
+ public boolean ready() {
+ return true;
+ }
+
+ @Override
+ public FightTeam team() {
+ return team;
+ }
+
+ @Override
+ public CastableWeapon weapon() {
+ throw new FightException("The fighter do not have any weapon");
+ }
+
+ @Override
+ public int id() {
+ return id;
+ }
+
+ @Override
+ public FighterSprite sprite() {
+ return sprite;
+ }
+
+ @Override
+ public FighterSpellList spells() {
+ return EmptySpellList.INSTANCE;
+ }
+
+ @Override
+ public FighterCharacteristics characteristics() {
+ return characteristics;
+ }
+
+ @Override
+ public FighterLife life() {
+ return life;
+ }
+
+ @Override
+ public @NonNull FighterData invoker() {
+ return invoker;
+ }
+}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/DoubleFighterCharacteristics.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/DoubleFighterCharacteristics.java
new file mode 100644
index 000000000..9069cb57a
--- /dev/null
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/DoubleFighterCharacteristics.java
@@ -0,0 +1,75 @@
+/*
+ * 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-2023 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.fighter.invocation;
+
+import fr.quatrevieux.araknemu.data.constant.Characteristic;
+import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.FighterCharacteristics;
+import fr.quatrevieux.araknemu.game.fight.fighter.event.FighterCharacteristicChanged;
+import fr.quatrevieux.araknemu.game.world.creature.characteristics.Characteristics;
+import fr.quatrevieux.araknemu.game.world.creature.characteristics.DefaultCharacteristics;
+import fr.quatrevieux.araknemu.game.world.creature.characteristics.MutableCharacteristics;
+
+/**
+ * Characteristics for a double
+ * This implements will simply retrieve the base characteristics from the invoker (i.e. {@link FighterCharacteristics#initial()})
+ * and handle buffs.
+ */
+public final class DoubleFighterCharacteristics implements FighterCharacteristics {
+ private final Fighter fighter;
+ private final Characteristics base;
+
+ private final MutableCharacteristics buffs = new DefaultCharacteristics();
+
+ /**
+ * @param fighter The double fighter
+ * @param invoker The invoker
+ */
+ public DoubleFighterCharacteristics(Fighter fighter, Fighter invoker) {
+ this.fighter = fighter;
+ this.base = invoker.characteristics().initial();
+ }
+
+ @Override
+ public int initiative() {
+ return 0; // initiative is not used for invocations
+ }
+
+ @Override
+ public int discernment() {
+ return 0; // monster do not have discernment
+ }
+
+ @Override
+ public int get(Characteristic characteristic) {
+ return base.get(characteristic) + buffs.get(characteristic);
+ }
+
+ @Override
+ public void alter(Characteristic characteristic, int value) {
+ buffs.add(characteristic, value);
+ fighter.dispatch(new FighterCharacteristicChanged(characteristic, value));
+ }
+
+ @Override
+ public Characteristics initial() {
+ return base;
+ }
+}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/InvocationFighter.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/InvocationFighter.java
index 091e7756c..ca315da98 100644
--- a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/InvocationFighter.java
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/InvocationFighter.java
@@ -19,6 +19,7 @@
package fr.quatrevieux.araknemu.game.fight.fighter.invocation;
+import fr.quatrevieux.araknemu.game.fight.FighterSprite;
import fr.quatrevieux.araknemu.game.fight.castable.weapon.CastableWeapon;
import fr.quatrevieux.araknemu.game.fight.exception.FightException;
import fr.quatrevieux.araknemu.game.fight.fighter.AbstractPlayableFighter;
@@ -32,7 +33,6 @@
import fr.quatrevieux.araknemu.game.fight.fighter.operation.FighterOperation;
import fr.quatrevieux.araknemu.game.fight.team.FightTeam;
import fr.quatrevieux.araknemu.game.monster.Monster;
-import fr.quatrevieux.araknemu.game.world.creature.Sprite;
import org.checkerframework.checker.index.qual.Positive;
import org.checkerframework.checker.nullness.qual.NonNull;
@@ -96,7 +96,7 @@ public int id() {
}
@Override
- public Sprite sprite() {
+ public FighterSprite sprite() {
return sprite;
}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/StaticInvocationFighter.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/StaticInvocationFighter.java
index 52133a3e4..9c667f270 100644
--- a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/StaticInvocationFighter.java
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/StaticInvocationFighter.java
@@ -19,6 +19,7 @@
package fr.quatrevieux.araknemu.game.fight.fighter.invocation;
+import fr.quatrevieux.araknemu.game.fight.FighterSprite;
import fr.quatrevieux.araknemu.game.fight.castable.weapon.CastableWeapon;
import fr.quatrevieux.araknemu.game.fight.exception.FightException;
import fr.quatrevieux.araknemu.game.fight.fighter.AbstractFighter;
@@ -32,7 +33,6 @@
import fr.quatrevieux.araknemu.game.fight.fighter.operation.FighterOperation;
import fr.quatrevieux.araknemu.game.fight.team.FightTeam;
import fr.quatrevieux.araknemu.game.monster.Monster;
-import fr.quatrevieux.araknemu.game.world.creature.Sprite;
import org.checkerframework.checker.index.qual.Positive;
import org.checkerframework.checker.nullness.qual.NonNull;
@@ -94,7 +94,7 @@ public int id() {
}
@Override
- public Sprite sprite() {
+ public FighterSprite sprite() {
return sprite;
}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/monster/MonsterFighter.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/monster/MonsterFighter.java
index 891246add..df3247ea5 100644
--- a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/monster/MonsterFighter.java
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/monster/MonsterFighter.java
@@ -19,6 +19,7 @@
package fr.quatrevieux.araknemu.game.fight.fighter.monster;
+import fr.quatrevieux.araknemu.game.fight.FighterSprite;
import fr.quatrevieux.araknemu.game.fight.castable.weapon.CastableWeapon;
import fr.quatrevieux.araknemu.game.fight.exception.FightException;
import fr.quatrevieux.araknemu.game.fight.fighter.AbstractPlayableFighter;
@@ -32,7 +33,6 @@
import fr.quatrevieux.araknemu.game.fight.team.FightTeam;
import fr.quatrevieux.araknemu.game.monster.Monster;
import fr.quatrevieux.araknemu.game.monster.reward.MonsterReward;
-import fr.quatrevieux.araknemu.game.world.creature.Sprite;
import org.checkerframework.checker.index.qual.Positive;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -67,7 +67,7 @@ public int id() {
}
@Override
- public Sprite sprite() {
+ public FighterSprite sprite() {
return sprite;
}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/monster/MonsterFighterSprite.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/monster/MonsterFighterSprite.java
index 202244b79..00185076f 100644
--- a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/monster/MonsterFighterSprite.java
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/monster/MonsterFighterSprite.java
@@ -21,9 +21,9 @@
import fr.arakne.utils.maps.constant.Direction;
import fr.quatrevieux.araknemu.data.constant.Characteristic;
+import fr.quatrevieux.araknemu.game.fight.FighterSprite;
import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
import fr.quatrevieux.araknemu.game.monster.Monster;
-import fr.quatrevieux.araknemu.game.world.creature.Sprite;
/**
* Sprite for monster
@@ -32,7 +32,7 @@
*
* https://github.com/Emudofus/Dofus/blob/1.29/dofus/aks/Game.as#L520
*/
-public final class MonsterFighterSprite implements Sprite {
+public final class MonsterFighterSprite implements FighterSprite {
private final Fighter fighter;
private final Monster monster;
@@ -97,4 +97,9 @@ public String toString() {
fighter.team().number()
;
}
+
+ @Override
+ public FighterSprite withFighter(Fighter fighter) {
+ return new MonsterFighterSprite(fighter, monster);
+ }
}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/operation/FighterOperation.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/operation/FighterOperation.java
index f44f9f837..47b638ae5 100644
--- a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/operation/FighterOperation.java
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/operation/FighterOperation.java
@@ -20,6 +20,7 @@
package fr.quatrevieux.araknemu.game.fight.fighter.operation;
import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter;
import fr.quatrevieux.araknemu.game.fight.fighter.invocation.InvocationFighter;
import fr.quatrevieux.araknemu.game.fight.fighter.invocation.StaticInvocationFighter;
import fr.quatrevieux.araknemu.game.fight.fighter.monster.MonsterFighter;
@@ -60,6 +61,13 @@ public default void onStaticInvocation(StaticInvocationFighter fighter) {
onGenericFighter(fighter);
}
+ /**
+ * Apply the operation to a DoubleFighter
+ */
+ public default void onDouble(DoubleFighter fighter) {
+ onGenericFighter(fighter);
+ }
+
/**
* Apply the operation to a generic fighter type
*/
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/player/PlayerFighter.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/player/PlayerFighter.java
index 85e1918b7..bd6d61c32 100644
--- a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/player/PlayerFighter.java
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/player/PlayerFighter.java
@@ -19,6 +19,7 @@
package fr.quatrevieux.araknemu.game.fight.fighter.player;
+import fr.quatrevieux.araknemu.game.fight.FighterSprite;
import fr.quatrevieux.araknemu.game.fight.castable.weapon.CastableWeapon;
import fr.quatrevieux.araknemu.game.fight.event.FighterReadyStateChanged;
import fr.quatrevieux.araknemu.game.fight.exception.FightException;
@@ -36,7 +37,6 @@
import fr.quatrevieux.araknemu.game.player.inventory.slot.WeaponSlot;
import fr.quatrevieux.araknemu.game.spell.boost.DispatcherSpellsBoosts;
import fr.quatrevieux.araknemu.game.spell.boost.SimpleSpellsBoosts;
-import fr.quatrevieux.araknemu.game.world.creature.Sprite;
import fr.quatrevieux.araknemu.network.game.GameSession;
import org.checkerframework.checker.index.qual.Positive;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@@ -86,7 +86,7 @@ public int id() {
}
@Override
- public Sprite sprite() {
+ public FighterSprite sprite() {
return sprite;
}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/player/PlayerFighterSprite.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/player/PlayerFighterSprite.java
index 2858b279c..b5a31d52e 100644
--- a/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/player/PlayerFighterSprite.java
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/fighter/player/PlayerFighterSprite.java
@@ -21,21 +21,22 @@
import fr.arakne.utils.maps.constant.Direction;
import fr.quatrevieux.araknemu.data.constant.Characteristic;
+import fr.quatrevieux.araknemu.game.fight.FighterSprite;
+import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
import fr.quatrevieux.araknemu.game.player.sprite.SpriteInfo;
-import fr.quatrevieux.araknemu.game.world.creature.Sprite;
/**
- * Sprite for fighter
+ * Sprite for player fighter
*
* The sprite type ID MUST be the class id
*
* https://github.com/Emudofus/Dofus/blob/1.29/dofus/aks/Game.as#L764
*/
-public final class PlayerFighterSprite implements Sprite {
- private final PlayerFighter fighter;
+public final class PlayerFighterSprite implements FighterSprite {
+ private final Fighter fighter;
private final SpriteInfo spriteInfo;
- public PlayerFighterSprite(PlayerFighter fighter, SpriteInfo spriteInfo) {
+ public PlayerFighterSprite(Fighter fighter, SpriteInfo spriteInfo) {
this.fighter = fighter;
this.spriteInfo = spriteInfo;
}
@@ -81,7 +82,7 @@ public String toString() {
spriteInfo.race().ordinal() + ";" +
spriteInfo.gfxId() + "^" + spriteInfo.size() + ";" +
spriteInfo.gender().ordinal() + ";" +
- fighter.properties().experience().level() + ";" +
+ fighter.level() + ";" +
"0,0,0,0;" + // @todo alignment
spriteInfo.colors().toHexString(";") + ";" +
spriteInfo.accessories() + ";" +
@@ -99,4 +100,9 @@ public String toString() {
";" // @todo mount
;
}
+
+ @Override
+ public FighterSprite withFighter(Fighter fighter) {
+ return new PlayerFighterSprite(fighter, spriteInfo);
+ }
}
diff --git a/src/main/java/fr/quatrevieux/araknemu/game/fight/module/MonsterInvocationModule.java b/src/main/java/fr/quatrevieux/araknemu/game/fight/module/MonsterInvocationModule.java
index 77e19f422..de787df1f 100644
--- a/src/main/java/fr/quatrevieux/araknemu/game/fight/module/MonsterInvocationModule.java
+++ b/src/main/java/fr/quatrevieux/araknemu/game/fight/module/MonsterInvocationModule.java
@@ -22,6 +22,7 @@
import fr.quatrevieux.araknemu.core.event.Listener;
import fr.quatrevieux.araknemu.game.fight.Fight;
import fr.quatrevieux.araknemu.game.fight.castable.effect.EffectsHandler;
+import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.invocations.CreateDoubleHandler;
import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.invocations.MonsterInvocationHandler;
import fr.quatrevieux.araknemu.game.fight.castable.effect.handler.invocations.StaticInvocationHandler;
import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
@@ -50,8 +51,9 @@ public MonsterInvocationModule(MonsterService monsterService, FighterFactory fig
@Override
public void effects(EffectsHandler handler) {
- // moving creatures
+ handler.register(180, new CreateDoubleHandler(fighterFactory, fight));
handler.register(181, new MonsterInvocationHandler(monsterService, fighterFactory, fight));
+
handler.register(185, new StaticInvocationHandler(monsterService, fighterFactory, fight));
}
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 edce954a2..d8851d94c 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
@@ -353,6 +353,16 @@ public static ActionEffect addInvocation(FighterData caster, FighterData invocat
return new ActionEffect(181, caster, "+" + invocation.sprite());
}
+ /**
+ * Add a new double invocation to the fight
+ *
+ * @param caster Invoker
+ * @param invocation Invocation to add
+ */
+ public static ActionEffect addDouble(FighterData caster, FighterData invocation) {
+ return new ActionEffect(180, caster, "+" + invocation.sprite());
+ }
+
/**
* Add an invoked static creature to the fight
* Unlike {@link #addInvocation(FighterData, FighterData)}, the client will take this creature in account
diff --git a/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java b/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java
index 910ebc550..ba8b54789 100644
--- a/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java
+++ b/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java
@@ -541,6 +541,7 @@ public GameDataSet pushFunctionalSpells() throws SQLException, ContainerExceptio
"(16, 'Science du bâton', 101, '10,1,1', '142,5,,,5,0,0d0+5|142,7,,,5,0,0d0+7|3|0|1|40|100|false|false|false|true|0|0|0|6|PaPa||18;19;3;1;41|42|false', '142,6,,,5,0,0d0+6|142,8,,,5,0,0d0+8|3|0|2|40|100|false|false|false|true|0|0|0|6|PaPa||18;19;3;1;41|42|false', '142,8,,,5,0,0d0+8|142,11,,,5,0,0d0+11|3|0|3|40|100|false|false|false|true|0|0|0|6|PaPa||18;19;3;1;41|42|false', '142,10,,,5,0,0d0+10|142,15,,,5,0,0d0+15|3|0|4|40|100|false|false|false|true|0|0|0|6|PaPa||18;19;3;1;41|42|false', '142,15,,,5,0,0d0+15|142,20,,,5,0,0d0+20|3|0|5|40|100|false|false|false|true|0|0|0|6|PaPa||18;19;3;1;41|42|false', '142,25,,,5,0,0d0+25|142,30,,,5,0,0d0+30|2|0|6|40|100|false|false|false|true|0|0|0|6|PaPa||18;19;3;1;41|142|false', '')",
"(197, 'Puissance Sylvestre', 0, '10,1,1', '149,,,8005,2,0;169,100,,,2,0,0d0+100;183,1000,,,2,0,0d0+1000;184,1000,,,2,0,0d0+1000;168,100,,,2,0,0d0+100;108,10,,,2,0,0d0+10||6|0|1|0|100|true|false|false|true|0|0|0|15|PaPaPaPaPaPa||8;18;19;3;1;41|17|false', '149,,,8005,2,0;169,100,,,2,0,0d0+100;183,1000,,,2,0,0d0+1000;184,1000,,,2,0,0d0+1000;168,100,,,2,0,0d0+100;108,11,,,2,0,0d0+11||5|0|2|0|100|true|false|false|true|0|0|0|14|PaPaPaPaPaPa||8;18;19;3;1;41|17|false', '149,,,8005,3,0;169,100,,,3,0,0d0+100;183,1000,,,3,0,0d0+1000;184,1000,,,3,0,0d0+1000;168,100,,,3,0,0d0+100;108,12,,,3,0,0d0+12||5|0|3|0|100|true|false|false|true|0|0|0|13|PaPaPaPaPaPa||8;18;19;3;1;41|17|false', '149,,,8005,3,0;169,100,,,3,0,0d0+100;183,1000,,,3,0,0d0+1000;184,1000,,,3,0,0d0+1000;168,100,,,3,0,0d0+100;108,13,,,3,0,0d0+13||4|0|4|0|100|true|false|false|true|0|0|0|12|PaPaPaPaPaPa||8;18;19;3;1;41|17|false', '149,,,8005,4,0;169,100,,,4,0,0d0+100;183,1000,,,4,0,0d0+1000;184,1000,,,4,0,0d0+1000;168,100,,,4,0,0d0+100;108,14,,,4,0,0d0+14||3|0|5|0|100|true|false|false|true|0|0|0|11|PaPaPaPaPaPa||8;18;19;3;1;41|17|false', '149,,,8005,4,0;169,100,,,4,0,0d0+100;183,1000,,,4,0,0d0+1000;184,1000,,,4,0,0d0+1000;168,100,,,4,0,0d0+100;108,16,,,4,0,0d0+16||2|0|6|0|100|true|false|false|true|0|0|0|10|PaPaPaPaPaPa||8;18;19;3;1;41|117|false', '4;4;4;4;4;4;5;5;5;5;5;5')",
"(186, 'Arbre', 1100, '11,1,1', '185,282,1,,0,0||6|1|1|0|100|false|true|true|true|0|0|0|10|Pa||18;19;3;1;41|42|false', '185,282,2,,0,0||5|1|2|0|100|false|true|true|true|0|0|0|9|Pa||18;19;3;1;41|42|false', '185,282,3,,0,0||5|1|3|0|100|false|true|true|true|0|0|0|8|Pa||18;19;3;1;41|42|false', '185,282,4,,0,0||5|1|4|0|100|false|true|true|true|0|0|0|7|Pa||18;19;3;1;41|42|false', '185,282,5,,0,0||4|1|5|0|100|false|true|true|true|0|0|0|6|Pa||18;19;3;1;41|42|false', '185,282,6,,0,0||3|1|6|0|100|false|true|true|true|0|0|0|3|Pa||18;19;3;1;41|142|false', '')",
+ "(74, 'Double', 1100, '10,1,1', '180,,,,0,0||5|1|1|0|100|true|true|true|false|0|0|0|15|Pa||18;19;3;1;41|13|false', '180,,,,0,0||5|1|1|0|100|true|true|true|false|0|0|0|13|Pa||18;19;3;1;41|13|false', '180,,,,0,0||5|1|1|0|100|true|true|true|false|0|0|0|11|Pa||18;19;3;1;41|13|false', '180,,,,0,0||5|1|1|0|100|true|true|true|false|0|0|0|9|Pa||18;19;3;1;41|13|false', '180,,,,0,0||4|1|1|0|100|true|true|true|false|0|0|0|8|Pa||18;19;3;1;41|13|false', '180,,,,0,0||2|1|1|0|100|true|true|true|false|0|0|0|6|Pa||18;19;3;1;41|113|false', '')",
}, ",") + ";"
);
diff --git a/src/test/java/fr/quatrevieux/araknemu/game/GameModuleTest.java b/src/test/java/fr/quatrevieux/araknemu/game/GameModuleTest.java
index 7760b64dd..9a9124639 100644
--- a/src/test/java/fr/quatrevieux/araknemu/game/GameModuleTest.java
+++ b/src/test/java/fr/quatrevieux/araknemu/game/GameModuleTest.java
@@ -68,6 +68,7 @@
import fr.quatrevieux.araknemu.game.fight.FightService;
import fr.quatrevieux.araknemu.game.fight.ai.factory.AiFactory;
import fr.quatrevieux.araknemu.game.fight.ai.factory.ChainAiFactory;
+import fr.quatrevieux.araknemu.game.fight.ai.factory.DoubleAiFactory;
import fr.quatrevieux.araknemu.game.fight.ai.factory.MonsterAiFactory;
import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator;
import fr.quatrevieux.araknemu.game.fight.fighter.DefaultFighterFactory;
@@ -162,6 +163,7 @@ void instances() throws ContainerException, SQLException {
assertInstanceOf(ChallengeType.class, container.get(ChallengeType.class));
assertInstanceOf(ChainAiFactory.class, container.get(AiFactory.class));
assertInstanceOf(MonsterAiFactory.class, container.get(MonsterAiFactory.class));
+ assertInstanceOf(DoubleAiFactory.class, container.get(DoubleAiFactory.class));
assertInstanceOf(ActivityService.class, container.get(ActivityService.class));
assertInstanceOf(DefaultExchangeFactory.class, container.get(ExchangeFactory.class));
assertInstanceOf(BankService.class, container.get(BankService.class));
diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/AiBaseCase.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/AiBaseCase.java
index 29d4e50cd..27623cb33 100644
--- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/AiBaseCase.java
+++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/AiBaseCase.java
@@ -51,6 +51,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
+import java.util.function.Function;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -105,6 +106,27 @@ public void configureFight(Consumer configurator) {
}
}
+ public void configureFighterAi(PlayableFighter fighter) {
+ this.fighter = fighter;
+
+ while (fight.turnList().currentFighter() != fighter) {
+ fight.turnList().current().get().stop();
+ }
+
+ ai = new FighterAI(fighter, fight, new DummyGenerator());
+ ai.start(turn = fight.turnList().current().get());
+
+ if (action == null && actionFactory != null) {
+ GeneratorBuilder aiBuilder = new GeneratorBuilder<>();
+ actionFactory.configure(aiBuilder, fighter);
+ action = aiBuilder.build();
+ }
+
+ if (action != null) {
+ action.initialize(ai);
+ }
+ }
+
public Optional generateAction() {
lastAction = null;
final Optional generated = action.generate(ai, new FightAiActionFactoryAdapter(ai.fighter(), fight, fight.actions()));
diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/BlockNearestEnemyTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/BlockNearestEnemyTest.java
new file mode 100644
index 000000000..2f2750ec9
--- /dev/null
+++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/BlockNearestEnemyTest.java
@@ -0,0 +1,94 @@
+/*
+ * This file is part of Araknemu.
+ *
+ * Araknemu is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Araknemu is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Araknemu. If not, see .
+ *
+ * Copyright (c) 2017-2023 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.ai.action;
+
+import fr.quatrevieux.araknemu.data.constant.Characteristic;
+import fr.quatrevieux.araknemu.game.fight.ai.AiBaseCase;
+import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class BlockNearestEnemyTest extends AiBaseCase {
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+
+ action = new BlockNearestEnemy();
+ }
+
+ @Test
+ void shouldMoveToEnemyNearTheInvoker() {
+ configureFight(fb -> fb
+ .addSelf(builder -> builder.cell(94).charac(Characteristic.AGILITY, 100))
+ .addEnemy(builder -> builder.cell(124))
+ .addEnemy(builder -> builder.cell(167))
+ );
+
+ DoubleFighter invoc = new DoubleFighter(-10, player.fighter());
+ fight.fighters().joinTurnList(invoc, fight.map().get(152)); // Adjacent to enemy 167
+ invoc.init();
+
+ configureFighterAi(invoc);
+
+ generateAndPerformMove();
+
+ assertEquals(138, fighter.cell().id());
+ assertEquals(2, turn.points().movementPoints());
+ }
+
+ @Test
+ void shouldMoveToNearestEnemyIfInvokerIsInvisible() {
+ configureFight(fb -> fb
+ .addSelf(builder -> builder.cell(152).charac(Characteristic.AGILITY, 100))
+ .addEnemy(builder -> builder.cell(167))
+ .addEnemy(builder -> builder.cell(212))
+ );
+
+ player.fighter().setHidden(player.fighter(), true);
+
+ DoubleFighter invoc = new DoubleFighter(-10, player.fighter());
+ fight.fighters().joinTurnList(invoc, fight.map().get(183));
+ invoc.init();
+
+ configureFighterAi(invoc);
+
+ generateAndPerformMove();
+
+ assertEquals(197, fighter.cell().id());
+ assertEquals(2, turn.points().movementPoints());
+ }
+
+ @Test
+ void shouldMoveToNearestEnemyIfNotInvoked() {
+ configureFight(fb -> fb
+ .addSelf(builder -> builder.cell(152).charac(Characteristic.AGILITY, 100))
+ .addEnemy(builder -> builder.cell(182))
+ .addEnemy(builder -> builder.cell(110))
+ );
+
+ generateAndPerformMove();
+
+ assertEquals(167, fighter.cell().id());
+ assertEquals(2, turn.points().movementPoints());
+ }
+}
diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearFighterTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearFighterTest.java
new file mode 100644
index 000000000..abed82123
--- /dev/null
+++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/MoveNearFighterTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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-2023 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.ai.action;
+
+import fr.quatrevieux.araknemu.game.fight.ai.AI;
+import fr.quatrevieux.araknemu.game.fight.ai.AiBaseCase;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+class MoveNearFighterTest extends AiBaseCase {
+ @Test
+ void generateNotInitialized() {
+ action = new MoveNearFighter<>(AI::enemy);
+ assertFalse(action.generate(Mockito.mock(AI.class), Mockito.mock(AiActionFactory.class)).isPresent());
+ }
+
+ @Test
+ void success() {
+ action = new MoveNearFighter<>(ai -> Optional.of(fight.map().get(222).fighter()));
+ configureFight(fb -> fb
+ .addSelf(builder -> builder.cell(122))
+ .addEnemy(builder -> builder.cell(125))
+ .addEnemy(builder -> builder.cell(126))
+ .addAlly(builder -> builder.cell(222))
+ );
+
+ generateAndPerformMove();
+ assertEquals(165, fighter.cell().id());
+ assertEquals(0, turn.points().movementPoints());
+ }
+
+ @Test
+ void withAllyOnPathShouldBeCircumvented() {
+ action = new MoveNearFighter<>(ai -> Optional.of(fight.map().get(181).fighter()));
+ configureFight(fb -> fb
+ .addSelf(builder -> builder.cell(151))
+ .addAlly(builder -> builder.cell(166))
+ .addEnemy(builder -> builder.cell(181))
+ );
+
+ generateAndPerformMove();
+
+ assertEquals(195, fighter.cell().id());
+ assertEquals(0, turn.points().movementPoints());
+ }
+
+ @Test
+ void whenAllyBlockAccess() {
+ action = new MoveNearFighter<>(ai -> Optional.of(fight.map().get(341).fighter()));
+ configureFight(fb -> fb
+ .addSelf(builder -> builder.cell(211))
+ .addAlly(builder -> builder.cell(284))
+ .addEnemy(builder -> builder.cell(341))
+ );
+
+ generateAndPerformMove();
+
+ assertEquals(256, fighter.cell().id());
+ assertEquals(0, turn.points().movementPoints());
+ }
+
+ // See: https://github.com/Arakne/Araknemu/issues/94
+ @Test
+ void notAccessibleCellShouldTruncateToNearestCell() {
+ action = new MoveNearFighter<>(ai -> Optional.of(fight.map().get(69).fighter()));
+ configureFight(fb -> fb
+ .map(10342)
+ .addSelf(builder -> builder.cell(155))
+ .addEnemy(builder -> builder.cell(69))
+ );
+
+ generateAndPerformMove();
+
+ assertEquals(126, fighter.cell().id());
+ assertEquals(1, turn.points().movementPoints());
+ }
+
+ @Test
+ void noMP() {
+ action = new MoveNearFighter<>(ai -> Optional.of(fight.map().get(125).fighter()));
+ configureFight(fb -> fb
+ .addSelf(builder -> builder.cell(122))
+ .addEnemy(builder -> builder.cell(125))
+ );
+
+ removeAllMP();
+
+ assertDotNotGenerateAction();
+ }
+
+ @Test
+ void onAdjacentCell() {
+ action = new MoveNearFighter<>(ai -> Optional.of(fight.map().get(125).fighter()));
+ configureFight(fb -> fb
+ .addSelf(builder -> builder.cell(110))
+ .addEnemy(builder -> builder.cell(125))
+ );
+
+ assertDotNotGenerateAction();
+ }
+
+ @Test
+ void noAvailableTarget() {
+ action = new MoveNearFighter<>(ai -> Optional.empty());
+ configureFight(fb -> fb
+ .addSelf(builder -> builder.cell(90))
+ .addEnemy(builder -> builder.cell(125))
+ );
+
+ assertDotNotGenerateAction();
+ }
+}
diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilderTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilderTest.java
index 8d9de00bd..c8277a184 100644
--- a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilderTest.java
+++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/action/builder/GeneratorBuilderTest.java
@@ -22,6 +22,7 @@
import fr.quatrevieux.araknemu._test.TestCase;
import fr.quatrevieux.araknemu.game.fight.ai.action.ActionGenerator;
import fr.quatrevieux.araknemu.game.fight.ai.action.Attack;
+import fr.quatrevieux.araknemu.game.fight.ai.action.BlockNearestEnemy;
import fr.quatrevieux.araknemu.game.fight.ai.action.Boost;
import fr.quatrevieux.araknemu.game.fight.ai.action.Debuff;
import fr.quatrevieux.araknemu.game.fight.ai.action.Heal;
@@ -172,6 +173,11 @@ void debuff() {
assertInstanceOf(Debuff.class, builder.debuff(simulator).build());
}
+ @Test
+ void blockNearestEnemy() {
+ assertInstanceOf(BlockNearestEnemy.class, builder.blockNearestEnemy().build());
+ }
+
private void assertActions(ActionGenerator action, Class extends ActionGenerator> ...types) throws NoSuchFieldException, IllegalAccessException {
assertInstanceOf(GeneratorAggregate.class, action);
diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/DoubleAiFactoryTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/DoubleAiFactoryTest.java
new file mode 100644
index 000000000..0e1e8a940
--- /dev/null
+++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/DoubleAiFactoryTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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-2023 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.ai.factory;
+
+import fr.quatrevieux.araknemu.game.fight.Fight;
+import fr.quatrevieux.araknemu.game.fight.FightBaseCase;
+import fr.quatrevieux.araknemu.game.fight.ai.factory.type.Blocking;
+import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DoubleAiFactoryTest extends FightBaseCase {
+ private Fight fight;
+ private DoubleAiFactory factory;
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+
+ fight = createFight();
+ factory = new DoubleAiFactory(new Blocking());
+ }
+
+ @Test
+ void createNotADouble() {
+ assertFalse(factory.create(player.fighter()).isPresent());
+ }
+
+ @Test
+ void createSuccess() {
+ DoubleFighter fighter = new DoubleFighter(-5, player.fighter());
+ fight.nextState();
+ fight.fighters().joinTurnList(fighter, fight.map().get(123));
+
+ assertTrue(factory.create(fighter).isPresent());
+ }
+}
diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/BlockingTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/BlockingTest.java
new file mode 100644
index 000000000..22ee68d62
--- /dev/null
+++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/ai/factory/type/BlockingTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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-2023 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.ai.factory.type;
+
+import fr.quatrevieux.araknemu.data.constant.Characteristic;
+import fr.quatrevieux.araknemu.game.fight.ai.AiBaseCase;
+import fr.quatrevieux.araknemu.game.fight.ai.simulation.Simulator;
+import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter;
+import fr.quatrevieux.araknemu.game.player.spell.SpellBook;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class BlockingTest extends AiBaseCase {
+ @BeforeEach
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ actionFactory = new Blocking();
+ dataSet.pushFunctionalSpells();
+ }
+
+ @Test
+ void shouldMoveNearEnemy() {
+ configureFight(b -> b
+ .addSelf(fb -> fb.cell(210))
+ .addEnemy(fb -> fb.cell(52))
+ );
+
+ assertEquals(11, distance(getEnemy(0)));
+
+ generateAndPerformMove();
+
+ assertEquals(165, fighter.cell().id());
+ assertEquals(8, distance(getEnemy(0)));
+ }
+
+ @Test
+ void shouldMoveNearInvokerEnemy() {
+ configureFight(fb -> fb
+ .addSelf(builder -> builder.cell(94).charac(Characteristic.AGILITY, 100))
+ .addEnemy(builder -> builder.cell(124))
+ .addEnemy(builder -> builder.cell(167))
+ );
+
+ DoubleFighter invoc = new DoubleFighter(-10, player.fighter());
+ fight.fighters().joinTurnList(invoc, fight.map().get(152)); // Adjacent to enemy 167
+ invoc.init();
+
+ configureFighterAi(invoc);
+
+ assertEquals(2, distance(getEnemy(0)));
+
+ generateAndPerformMove();
+
+ assertEquals(138, invoc.cell().id());
+ assertEquals(1, distance(getEnemy(0)));
+ }
+
+ @Test
+ void shouldDoesNothingIfAlreadyAdjacentToEnemy() {
+ configureFight(fb -> fb
+ .addSelf(builder -> builder.cell(94).charac(Characteristic.AGILITY, 100))
+ .addEnemy(builder -> builder.cell(167))
+ );
+
+ DoubleFighter invoc = new DoubleFighter(-10, player.fighter());
+ fight.fighters().joinTurnList(invoc, fight.map().get(152)); // Adjacent to enemy 167
+ invoc.init();
+
+ configureFighterAi(invoc);
+
+ generateAction();
+ assertDotNotGenerateAction();
+ }
+
+ private int distance(Fighter other) {
+ return fighter.cell().coordinate().distance(other.cell());
+ }
+}
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 ff37e896b..c7db89fc9 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
@@ -26,11 +26,13 @@
import fr.quatrevieux.araknemu.game.fight.ai.factory.AiFactory;
import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.Buff;
import fr.quatrevieux.araknemu.game.fight.castable.spell.SpellConstraintsValidator;
+import fr.quatrevieux.araknemu.game.fight.exception.FightException;
import fr.quatrevieux.araknemu.game.fight.fighter.ActiveFighter;
import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
import fr.quatrevieux.araknemu.game.fight.fighter.FighterFactory;
import fr.quatrevieux.araknemu.game.fight.fighter.FighterData;
import fr.quatrevieux.araknemu.game.fight.fighter.PlayableFighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter;
import fr.quatrevieux.araknemu.game.fight.fighter.invocation.InvocationFighter;
import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter;
import fr.quatrevieux.araknemu.game.fight.map.BattlefieldObject;
@@ -55,6 +57,7 @@
import fr.quatrevieux.araknemu.network.game.out.fight.turn.FighterTurnOrder;
import fr.quatrevieux.araknemu.network.game.out.fight.turn.TurnMiddle;
import fr.quatrevieux.araknemu.network.game.out.game.UpdateCells;
+import fr.quatrevieux.araknemu.network.game.out.info.Error;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -1430,6 +1433,30 @@ void staticInvocation() throws SQLException {
assertTrue(fight.map().get(213).hasFighter());
}
+ @Test
+ void doubleInvoc() {
+ castNormal(74, fight.map().get(199)); // Double
+
+ assertTrue(fight.map().get(199).hasFighter());
+ assertInstanceOf(DoubleFighter.class, fight.map().get(199).fighter());
+
+ Fighter invoc = fight.map().get(199).fighter();
+
+ assertEquals(invoc.life().current(), fighter1.life().current());
+ assertEquals(invoc.life().max(), fighter1.life().max());
+ assertTrue(fight.turnList().fighters().contains(invoc));
+
+ requestStack.assertOne(new ActionEffect(180, fighter1, "+" + invoc.sprite()));
+ requestStack.assertOne(ActionEffect.packet(fighter1, new FighterTurnOrder(fight.turnList())));
+
+ invoc.attach(FighterAI.class, null); // Remove AI, to ensure it doesn't play
+
+ passTurns(9); // spell cooldown
+
+ assertThrows(FightException.class, () -> castNormal(74, fight.map().get(200)));
+ requestStack.assertLast(Error.cantCastMaxSummonedCreaturesReached(1));
+ }
+
private List configureFight(Consumer configurator) {
fight.cancel(true);
@@ -1450,8 +1477,9 @@ private List configureFight(Consumer configurator) {
private void passTurns(int number) {
for (; number > 0; --number) {
- fighter1.turn().stop();
- fighter2.turn().stop();
+ for (PlayableFighter fighter : fight.turnList().fighters()) {
+ fighter.turn().stop();
+ }
}
}
diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/invocations/CreateDoubleHandlerTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/invocations/CreateDoubleHandlerTest.java
new file mode 100644
index 000000000..6dcb243ee
--- /dev/null
+++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/castable/effect/handler/invocations/CreateDoubleHandlerTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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-2019 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.castable.effect.handler.invocations;
+
+import fr.quatrevieux.araknemu.data.constant.Characteristic;
+import fr.quatrevieux.araknemu.game.fight.Fight;
+import fr.quatrevieux.araknemu.game.fight.FightBaseCase;
+import fr.quatrevieux.araknemu.game.fight.ai.FighterAI;
+import fr.quatrevieux.araknemu.game.fight.ai.factory.AiFactory;
+import fr.quatrevieux.araknemu.game.fight.castable.FightCastScope;
+import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.FighterData;
+import fr.quatrevieux.araknemu.game.fight.fighter.FighterFactory;
+import fr.quatrevieux.araknemu.game.fight.fighter.invocation.DoubleFighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.invocation.InvocationFighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighter;
+import fr.quatrevieux.araknemu.game.fight.module.AiModule;
+import fr.quatrevieux.araknemu.game.monster.MonsterService;
+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 fr.quatrevieux.araknemu.network.game.out.fight.turn.FighterTurnOrder;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class CreateDoubleHandlerTest extends FightBaseCase {
+ private Fight fight;
+ private PlayerFighter caster;
+ private CreateDoubleHandler handler;
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+
+ fight = createFight();
+ fight.register(new AiModule(container.get(AiFactory.class)));
+ fight.nextState();
+
+ caster = player.fighter();
+
+ handler = new CreateDoubleHandler(container.get(FighterFactory.class), fight);
+
+ requestStack.clear();
+ }
+
+ @Test
+ void handle() {
+ caster.life().alter(caster, -50);
+ requestStack.clear();
+
+ 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(true);
+
+ FightCastScope scope = makeCastScope(caster, spell, effect, fight.map().get(123));
+ handler.handle(scope, scope.effects().get(0));
+
+ Fighter invoc = fight.map().get(123).fighter();
+
+ assertInstanceOf(DoubleFighter.class, invoc);
+ assertContains(invoc, fight.fighters().all());
+ assertContains(invoc, fight.turnList().fighters());
+ assertSame(caster.team(), invoc.team());
+ assertEquals(caster.level(), invoc.level());
+ assertInstanceOf(FighterAI.class, invoc.attachment(FighterAI.class));
+
+ assertEquals(caster.life().current(), invoc.life().current());
+ assertEquals(caster.life().max(), invoc.life().max());
+
+ for (Characteristic characteristic : Characteristic.values()) {
+ assertEquals(caster.characteristics().get(characteristic), invoc.characteristics().get(characteristic));
+ }
+
+ assertEquals(invoc.id(), invoc.sprite().id());
+ assertEquals(invoc.cell().id(), invoc.sprite().cell());
+ assertEquals(invoc.orientation(), invoc.sprite().orientation());
+ assertEquals(caster.sprite().type(), invoc.sprite().type());
+ assertEquals(caster.sprite().gfxId(), invoc.sprite().gfxId());
+ assertEquals(caster.sprite().name(), invoc.sprite().name());
+
+ assertEquals("123;1;0;-1;Bob;1;10^100x100;0;50;0,0,0,0;7b;1c8;315;,,,,;245;6;3;0;0;0;0;0;0;0;0;;", invoc.sprite().toString());
+
+ requestStack.assertAll(
+ new FighterTurnOrder(fight.turnList()),
+ new ActionEffect(180, caster, "+" + invoc.sprite()),
+ new ActionEffect(999, caster, (new FighterTurnOrder(fight.turnList())).toString())
+ );
+ }
+
+ @Test
+ void buff() {
+ 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(true);
+
+ FightCastScope scope = makeCastScope(caster, spell, effect, fight.map().get(123));
+ assertThrows(UnsupportedOperationException.class, () -> handler.buff(scope, scope.effects().get(0)));
+ }
+}
diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/DoubleFighterCharacteristicsTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/DoubleFighterCharacteristicsTest.java
new file mode 100644
index 000000000..599ad4007
--- /dev/null
+++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/DoubleFighterCharacteristicsTest.java
@@ -0,0 +1,94 @@
+/*
+ * This file is part of Araknemu.
+ *
+ * Araknemu is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Araknemu is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Araknemu. If not, see .
+ *
+ * Copyright (c) 2017-2023 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.fighter.invocation;
+
+import fr.quatrevieux.araknemu.data.constant.Characteristic;
+import fr.quatrevieux.araknemu.game.fight.Fight;
+import fr.quatrevieux.araknemu.game.fight.FightBaseCase;
+import fr.quatrevieux.araknemu.game.fight.fighter.event.FighterCharacteristicChanged;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+class DoubleFighterCharacteristicsTest extends FightBaseCase {
+ private DoubleFighterCharacteristics characteristics;
+ private DoubleFighter fighter;
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+
+ Fight fight = createFight();
+
+ fighter = new DoubleFighter(-5, player.fighter());
+ fighter.joinFight(fight, fight.map().get(123));
+
+ characteristics = new DoubleFighterCharacteristics(fighter, player.fighter());
+ }
+
+ @Test
+ void initiative() {
+ assertEquals(0, characteristics.initiative());
+ }
+
+ @Test
+ void discernment() {
+ assertEquals(0, characteristics.discernment());
+ }
+
+ @Test
+ void get() {
+ assertEquals(50, characteristics.get(Characteristic.STRENGTH));
+ assertEquals(6, characteristics.get(Characteristic.ACTION_POINT));
+ assertEquals(3, characteristics.get(Characteristic.MOVEMENT_POINT));
+ }
+
+ @Test
+ void alter() {
+ AtomicReference ref = new AtomicReference<>();
+ fighter.dispatcher().add(FighterCharacteristicChanged.class, ref::set);
+
+ characteristics.alter(Characteristic.STRENGTH, 10);
+ assertEquals(60, characteristics.get(Characteristic.STRENGTH));
+
+ assertEquals(Characteristic.STRENGTH, ref.get().characteristic());
+ assertEquals(10, ref.get().value());
+
+ characteristics.alter(Characteristic.STRENGTH, -10);
+ assertEquals(50, characteristics.get(Characteristic.STRENGTH));
+
+ assertEquals(Characteristic.STRENGTH, ref.get().characteristic());
+ assertEquals(-10, ref.get().value());
+ }
+
+ @Test
+ void initial() {
+ assertSame(player.fighter().characteristics().initial(), characteristics.initial());
+ assertEquals(50, characteristics.initial().get(Characteristic.STRENGTH));
+
+ characteristics.alter(Characteristic.STRENGTH, 10);
+ assertEquals(50, characteristics.initial().get(Characteristic.STRENGTH));
+ }
+}
diff --git a/src/test/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/DoubleFighterTest.java b/src/test/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/DoubleFighterTest.java
new file mode 100644
index 000000000..769edf4d7
--- /dev/null
+++ b/src/test/java/fr/quatrevieux/araknemu/game/fight/fighter/invocation/DoubleFighterTest.java
@@ -0,0 +1,392 @@
+/*
+ * 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-2023 Vincent Quatrevieux
+ */
+
+package fr.quatrevieux.araknemu.game.fight.fighter.invocation;
+
+import fr.arakne.utils.maps.constant.Direction;
+import fr.quatrevieux.araknemu.core.event.Listener;
+import fr.quatrevieux.araknemu.data.constant.Characteristic;
+import fr.quatrevieux.araknemu.game.fight.Fight;
+import fr.quatrevieux.araknemu.game.fight.FightBaseCase;
+import fr.quatrevieux.araknemu.game.fight.castable.effect.buff.BuffList;
+import fr.quatrevieux.araknemu.game.fight.castable.spell.LaunchedSpells;
+import fr.quatrevieux.araknemu.game.fight.exception.FightException;
+import fr.quatrevieux.araknemu.game.fight.fighter.Fighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.States;
+import fr.quatrevieux.araknemu.game.fight.fighter.event.FighterHidden;
+import fr.quatrevieux.araknemu.game.fight.fighter.event.FighterInitialized;
+import fr.quatrevieux.araknemu.game.fight.fighter.event.FighterMoved;
+import fr.quatrevieux.araknemu.game.fight.fighter.event.FighterVisible;
+import fr.quatrevieux.araknemu.game.fight.fighter.monster.MonsterFighter;
+import fr.quatrevieux.araknemu.game.fight.fighter.monster.MonsterFighterSprite;
+import fr.quatrevieux.araknemu.game.fight.fighter.operation.FighterOperation;
+import fr.quatrevieux.araknemu.game.fight.fighter.player.PlayerFighterSprite;
+import fr.quatrevieux.araknemu.game.fight.team.FightTeam;
+import fr.quatrevieux.araknemu.game.fight.turn.FightTurn;
+import fr.quatrevieux.araknemu.game.monster.MonsterService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.sql.SQLException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DoubleFighterTest extends FightBaseCase {
+ private DoubleFighter fighter;
+ private FightTeam team;
+ private Fight fight;
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+
+ dataSet
+ .pushMonsterTemplates()
+ .pushMonsterTemplateInvocations()
+ .pushMonsterSpellsInvocations()
+ .pushMonsterSpells()
+ ;
+
+ fight = createFight();
+ team = fight.team(0);
+
+ fighter = new DoubleFighter(-5, player.fighter());
+ }
+
+ @Test
+ void shouldNotInheritBuffs() {
+ player.fighter().characteristics().alter(Characteristic.STRENGTH, 100);
+
+ fighter = new DoubleFighter(-5, player.fighter());
+ assertEquals(50, fighter.characteristics().get(Characteristic.STRENGTH));
+
+ fighter.characteristics().alter(Characteristic.STRENGTH, 50);
+ assertEquals(100, fighter.characteristics().get(Characteristic.STRENGTH));
+ assertEquals(150, player.fighter().characteristics().get(Characteristic.STRENGTH));
+ }
+
+ @Test
+ void shouldCopyLife() {
+ player.fighter().init();
+ player.fighter().life().alter(player.fighter(), -50);
+ assertEquals(245, player.fighter().life().current());
+ assertEquals(295, player.fighter().life().max());
+
+ fighter = new DoubleFighter(-5, player.fighter());
+ fighter.joinFight(fight, fight.map().get(123));
+ assertEquals(245, fighter.life().current());
+ assertEquals(295, fighter.life().max());
+
+ player.fighter().life().alter(player.fighter(), -100);
+ assertEquals(145, player.fighter().life().current());
+ assertEquals(245, fighter.life().current());
+
+ fighter.life().alter(fighter, -50);
+ assertEquals(145, player.fighter().life().current());
+ assertEquals(195, fighter.life().current());
+ }
+
+ @Test
+ void values() {
+ assertSame(team, fighter.team());
+ assertEquals(-5, fighter.id());
+ assertEquals(Direction.SOUTH_EAST, fighter.orientation());
+ assertFalse(fighter.dead());
+ assertThrows(FightException.class, fighter::weapon);
+ assertInstanceOf(BuffList.class, fighter.buffs());
+ assertInstanceOf(States.class, fighter.states());
+ assertTrue(fighter.ready());
+ assertEquals(player.fighter().level(), fighter.level());
+ assertSame(player.fighter(), fighter.invoker());
+ assertTrue(fighter.invoked());
+ }
+
+ @Test
+ void equals() throws SQLException {
+ assertEquals(fighter, fighter);
+ assertEquals(fighter.hashCode(), fighter.hashCode());
+ assertNotEquals(fighter, makePlayerFighter(gamePlayer()));
+
+ MonsterFighter other = new MonsterFighter(
+ -2,
+ container.get(MonsterService.class).load(36).all().get(0),
+ team
+ );
+
+ assertNotEquals(fighter, other);
+ }
+
+ @Test
+ void attachments() {
+ fighter.attach("key", 42);
+ assertSame(42, fighter.attachment("key"));
+
+ LaunchedSpells launchedSpells = new LaunchedSpells();
+
+ fighter.attach(launchedSpells);
+ assertSame(launchedSpells, fighter.attachment(LaunchedSpells.class));
+ }
+
+ @Test
+ void orientation() {
+ assertEquals(Direction.SOUTH_EAST, fighter.orientation());
+
+ fighter.setOrientation(Direction.NORTH_EAST);
+ assertEquals(Direction.NORTH_EAST, fighter.orientation());
+ }
+
+ @Test
+ void init() throws Exception {
+ Fight fight = createFight();
+
+ AtomicReference ref = new AtomicReference<>();
+ fight.dispatcher().add(FighterInitialized.class, ref::set);
+
+ fighter.joinFight(fight, fight.map().get(123));
+ fighter.init();
+
+ assertSame(fighter, ref.get().fighter());
+ }
+
+ @Test
+ void dead() throws Exception {
+ Fight fight = createFight();
+
+ fighter.joinFight(fight, fight.map().get(123));
+ fighter.init();
+ assertFalse(fighter.dead());
+
+ fighter.life().alter(fighter, -10000);
+
+ assertTrue(fighter.dead());
+ }
+
+ @Test
+ void joinFight() throws Exception {
+ Fight fight = createFight();
+
+ assertFalse(fighter.isOnFight());
+
+ fighter.joinFight(fight, fight.map().get(123));
+
+ assertSame(fight, fighter.fight());
+ assertSame(fight.map().get(123), fighter.cell());
+ assertSame(fighter, fighter.cell().fighter());
+ assertTrue(fighter.isOnFight());
+ }
+
+ @Test
+ void joinFightAlreadyJoinedShouldRaisedException() throws Exception {
+ Fight fight = createFight();
+
+ fighter.joinFight(fight, fight.map().get(123));
+ assertThrows(IllegalStateException.class, () -> fighter.joinFight(fight, fight.map().get(123)));
+ }
+
+ @Test
+ void sprite() throws Exception {
+ Fight fight = createFight();
+ fighter.joinFight(fight, fight.map().get(123));
+
+ assertInstanceOf(PlayerFighterSprite.class, fighter.sprite());
+ assertEquals("123;1;0;-5;Bob;1;10^100x100;0;50;0,0,0,0;7b;1c8;315;,,,,;295;6;3;0;0;0;0;0;0;0;0;;", fighter.sprite().toString());
+
+ fighter.move(fight.map().get(124));
+ assertEquals("124;1;0;-5;Bob;1;10^100x100;0;50;0,0,0,0;7b;1c8;315;,,,,;295;6;3;0;0;0;0;0;0;0;0;;", fighter.sprite().toString());
+ }
+
+ @Test
+ void moveFirstTime() throws Exception {
+ Fight fight = createFight();
+
+ fighter.move(fight.map().get(123));
+
+ assertSame(fight.map().get(123), fighter.cell());
+ assertSame(fighter, fight.map().get(123).fighter());
+ }
+
+ @Test
+ void moveWillLeaveLastCell() throws Exception {
+ Fight fight = createFight();
+
+ fighter.move(fight.map().get(123));
+ fighter.move(fight.map().get(124));
+
+ assertSame(fight.map().get(124), fighter.cell());
+ assertSame(fighter, fight.map().get(124).fighter());
+
+ assertFalse(fight.map().get(123).hasFighter());
+ }
+
+ @Test
+ void moveRemoveCell() throws Exception {
+ Fight fight = createFight();
+
+ fighter.move(fight.map().get(123));
+ fighter.move(null);
+
+ assertThrows(IllegalStateException.class, fighter::cell);
+ assertFalse(fight.map().get(123).hasFighter());
+ }
+
+ @Test
+ void moveShouldDispatchFighterMoved() {
+ fighter.joinFight(fight, fight.map().get(123));
+
+ AtomicReference ref = new AtomicReference<>();
+ fight.dispatcher().add(FighterMoved.class, ref::set);
+
+ fighter.move(fight.map().get(126));
+
+ assertSame(fighter, ref.get().fighter());
+ assertSame(fight.map().get(126), ref.get().cell());
+ }
+
+ @Test
+ void moveShouldNotDispatchFighterMovedWhenNullCellIsGiven() {
+ fighter.joinFight(fight, fight.map().get(123));
+
+ AtomicReference ref = new AtomicReference<>();
+ fight.dispatcher().add(FighterMoved.class, ref::set);
+
+ fighter.move(null);
+
+ assertNull(ref.get());
+ }
+
+ @Test
+ void spells() {
+ assertIterableEquals(fighter.spells(), new ArrayList<>());
+ }
+
+ @Test
+ void dispatcher() {
+ Object event = new Object();
+
+ AtomicReference