Skip to content

Commit

Permalink
Merge pull request Arakne#293 from vincent4vx/feature-fight-effect-do…
Browse files Browse the repository at this point in the history
…uble

feat(fight): Handle double effect (Arakne#27)
  • Loading branch information
vincent4vx authored Jun 18, 2023
2 parents d558e0e + cb25345 commit 46a551b
Show file tree
Hide file tree
Showing 37 changed files with 1,780 additions and 76 deletions.
11 changes: 10 additions & 1 deletion src/main/java/fr/quatrevieux/araknemu/game/GameModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
)
);

Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
* 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);
}
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
* 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 <F> the fighter type
*/
public final class BlockNearestEnemy<F extends ActiveFighter> implements ActionGenerator<F> {
private final ActionGenerator<F> moveGenerator;

@SuppressWarnings("methodref.receiver.bound")
public BlockNearestEnemy() {
moveGenerator = new MoveNearFighter<>(this::resolve);
}

@Override
public void initialize(AI<F> ai) {
moveGenerator.initialize(ai);
}

@Override
public Optional<Action> generate(AI<F> ai, AiActionFactory actions) {
return moveGenerator.generate(ai, actions);
}

private Optional<? extends FighterData> resolve(AI<F> ai) {
final FighterData invoker = ai.fighter().invoker();

if (invoker == null || invoker.hidden()) {
return ai.enemy();
}

final CoordinateCell<BattlefieldCell> currentCell = invoker.cell().coordinate();

return ai.helper().enemies().stream()
.filter(fighter -> !fighter.hidden())
.min(Comparator
.<FighterData>comparingInt(f -> currentCell.distance(f.cell()))
.thenComparingInt(f -> f.life().current())
)
;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,73 +19,29 @@

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;

/**
* Try to move near the selected enemy
*/
public final class MoveNearEnemy<F extends ActiveFighter> implements ActionGenerator<F> {
private @MonotonicNonNull Pathfinder<BattlefieldCell> pathfinder;
private @MonotonicNonNull AIHelper helper;
private final ActionGenerator<F> moveGenerator;

public MoveNearEnemy() {
this.moveGenerator = new MoveNearFighter<>(AI::enemy);
}

@Override
public void initialize(AI<F> 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<Action> generate(AI<F> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
* 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<F extends ActiveFighter> implements ActionGenerator<F> {
private @MonotonicNonNull Pathfinder<BattlefieldCell> pathfinder;
private @MonotonicNonNull AIHelper helper;
private final Function<AI<F>, Optional<? extends FighterData>> fighterResolver;

public MoveNearFighter(Function<AI<F>, Optional<? extends FighterData>> fighterResolver) {
this.fighterResolver = fighterResolver;
}

@Override
public void initialize(AI<F> 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<Action> generate(AI<F> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -278,6 +279,17 @@ public final GeneratorBuilder<F> 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<F> blockNearestEnemy() {
return add(new BlockNearestEnemy<>());
}

/**
* Try to teleport near the selected enemy
*
Expand Down
Loading

0 comments on commit 46a551b

Please sign in to comment.