diff --git a/src/main/java/fr/quatrevieux/araknemu/data/world/entity/monster/MonsterGroupData.java b/src/main/java/fr/quatrevieux/araknemu/data/world/entity/monster/MonsterGroupData.java index 0af14d757..c23f2ce52 100644 --- a/src/main/java/fr/quatrevieux/araknemu/data/world/entity/monster/MonsterGroupData.java +++ b/src/main/java/fr/quatrevieux/araknemu/data/world/entity/monster/MonsterGroupData.java @@ -36,7 +36,7 @@ public final class MonsterGroupData { private final int id; private final Duration respawnTime; private final @NonNegative int maxSize; - private final @Positive int maxCount; + private final @NonNegative int maxCount; private final List monsters; private final @Nullable String comment; private final Position winFightTeleport; @@ -45,7 +45,7 @@ public final class MonsterGroupData { private final @Positive int totalRate; @SuppressWarnings("cast.unsafe") // @todo remove when nullable monsters list will be denied - public MonsterGroupData(int id, Duration respawnTime, @NonNegative int maxSize, @Positive int maxCount, List monsters, @Nullable String comment, Position winFightTeleport, boolean fixedTeamNumber) { + public MonsterGroupData(int id, Duration respawnTime, @NonNegative int maxSize, @NonNegative int maxCount, List monsters, @Nullable String comment, Position winFightTeleport, boolean fixedTeamNumber) { this.id = id; this.respawnTime = respawnTime; this.maxSize = maxSize; @@ -108,10 +108,11 @@ public List monsters() { * Maximum number of occurrence of the group * * If the value is 1, only one group is spawn, and can respawn only when the previous group has start a fight + * If this value is 0, the group will not spawn automatically * * For dungeon groups, this value should be 1 */ - public @Positive int maxCount() { + public @NonNegative int maxCount() { return maxCount; } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/admin/AdminModule.java b/src/main/java/fr/quatrevieux/araknemu/game/admin/AdminModule.java index aa3ddd151..d0b23f738 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/admin/AdminModule.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/admin/AdminModule.java @@ -52,9 +52,12 @@ import fr.quatrevieux.araknemu.game.admin.executor.argument.HydratorsAggregate; import fr.quatrevieux.araknemu.game.admin.global.GlobalContext; import fr.quatrevieux.araknemu.game.admin.global.Help; +import fr.quatrevieux.araknemu.game.admin.player.AddXp; import fr.quatrevieux.araknemu.game.admin.player.GetItem; +import fr.quatrevieux.araknemu.game.admin.player.LearnSpell; import fr.quatrevieux.araknemu.game.admin.player.PlayerContext; import fr.quatrevieux.araknemu.game.admin.player.PlayerContextResolver; +import fr.quatrevieux.araknemu.game.admin.player.Spawn; import fr.quatrevieux.araknemu.game.admin.player.teleport.CellResolver; import fr.quatrevieux.araknemu.game.admin.player.teleport.Goto; import fr.quatrevieux.araknemu.game.admin.player.teleport.LocationResolver; @@ -74,7 +77,11 @@ import fr.quatrevieux.araknemu.game.exploration.map.GeolocationService; import fr.quatrevieux.araknemu.game.fight.FightService; import fr.quatrevieux.araknemu.game.item.ItemService; +import fr.quatrevieux.araknemu.game.monster.environment.MonsterEnvironmentService; +import fr.quatrevieux.araknemu.game.monster.group.MonsterGroupFactory; import fr.quatrevieux.araknemu.game.player.PlayerService; +import fr.quatrevieux.araknemu.game.player.experience.PlayerExperienceService; +import fr.quatrevieux.araknemu.game.spell.SpellService; import org.apache.logging.log4j.LogManager; import java.nio.file.Paths; @@ -181,6 +188,9 @@ public void configure(PlayerContext context) { new PlayerResolver(container.get(PlayerService.class), container.get(ExplorationMapService.class)), new CellResolver(), })); + add(new AddXp(context.player(), container.get(PlayerExperienceService.class))); + add(new LearnSpell(context.player(), container.get(SpellService.class))); + add(new Spawn(context.player(), container.get(FightService.class), container.get(MonsterEnvironmentService.class), container.get(MonsterGroupFactory.class))); } }), ctx -> container.with(ctx.player()), diff --git a/src/main/java/fr/quatrevieux/araknemu/game/admin/account/Info.java b/src/main/java/fr/quatrevieux/araknemu/game/admin/account/Info.java index da0034477..b4200e0af 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/admin/account/Info.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/admin/account/Info.java @@ -25,6 +25,8 @@ import fr.quatrevieux.araknemu.game.account.GameAccount; import fr.quatrevieux.araknemu.game.admin.AbstractCommand; import fr.quatrevieux.araknemu.game.admin.AdminPerformer; +import fr.quatrevieux.araknemu.game.admin.formatter.Link; +import fr.quatrevieux.araknemu.network.game.GameSession; /** * Info command for account @@ -77,6 +79,15 @@ public void execute(AdminPerformer performer, Void arguments) { performer.error("Logged: No"); } + account.session().map(GameSession::player).ifPresent(player -> { + performer.info( + "Player: {}", + Link.Type.EXECUTE + .create("@" + player.name() + " info") + .text(player.name()) + ); + }); + if (!account.isMaster()) { performer.error("Standard account"); } else { diff --git a/src/main/java/fr/quatrevieux/araknemu/game/admin/player/AddXp.java b/src/main/java/fr/quatrevieux/araknemu/game/admin/player/AddXp.java index 773e38222..94dbdee75 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/admin/player/AddXp.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/admin/player/AddXp.java @@ -23,17 +23,21 @@ import fr.quatrevieux.araknemu.game.admin.AbstractCommand; import fr.quatrevieux.araknemu.game.admin.AdminPerformer; import fr.quatrevieux.araknemu.game.player.GamePlayer; -import org.checkerframework.checker.index.qual.Positive; +import fr.quatrevieux.araknemu.game.player.experience.PlayerExperienceService; +import org.checkerframework.checker.index.qual.NonNegative; import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; /** * Add experience to player */ public final class AddXp extends AbstractCommand { private final GamePlayer player; + private final PlayerExperienceService service; - public AddXp(GamePlayer player) { + public AddXp(GamePlayer player, PlayerExperienceService service) { this.player = player; + this.service = service; } @Override @@ -43,6 +47,7 @@ protected void build(Builder builder) { formatter -> formatter .description("Add experience to player") .example("@John addxp 1000000", "Add 1 million xp to John") + .example("@John addxp --level 150", "Add xp to John to reach level 150") ) .requires(Permission.MANAGE_PLAYER) .arguments(Arguments::new) @@ -56,17 +61,35 @@ public String name() { @Override public void execute(AdminPerformer performer, Arguments arguments) { - player.properties().experience().add(arguments.quantity); + long quantity = arguments.quantity; - performer.success("Add {} xp to {} (level = {})", arguments.quantity, player.name(), player.properties().experience().level()); + if (arguments.level > 0) { + if (arguments.level <= player.properties().experience().level()) { + performer.error("The player level ({}) is already higher than the target level ({})", player.properties().experience().level(), arguments.level); + return; + } + + quantity = Math.max(service.byLevel(arguments.level).experience() - player.properties().experience().current(), 0); + } + + player.properties().experience().add(quantity); + + performer.success("Add {} xp to {} (level = {})", quantity, player.name(), player.properties().experience().level()); } public static final class Arguments { @Argument( - required = true, + required = false, metaVar = "QUANTITY", usage = "The experience quantity to add. Must be an unsigned number." ) - private @Positive long quantity; + private @NonNegative long quantity = 0; + + @Option( + name = "--level", + aliases = {"-l"}, + usage = "The target level. If set, the quantity will be calculated to reach this level. Must be a positive number." + ) + private @NonNegative int level = 0; } } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/admin/player/LearnSpell.java b/src/main/java/fr/quatrevieux/araknemu/game/admin/player/LearnSpell.java new file mode 100644 index 000000000..0c2d7b7d0 --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/admin/player/LearnSpell.java @@ -0,0 +1,89 @@ +/* + * This file is part of Araknemu. + * + * Araknemu is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Araknemu is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Araknemu. If not, see . + * + * Copyright (c) 2017-2024 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.admin.player; + +import fr.quatrevieux.araknemu.common.account.Permission; +import fr.quatrevieux.araknemu.core.dbal.repository.EntityNotFoundException; +import fr.quatrevieux.araknemu.game.admin.AbstractCommand; +import fr.quatrevieux.araknemu.game.admin.AdminPerformer; +import fr.quatrevieux.araknemu.game.admin.exception.AdminException; +import fr.quatrevieux.araknemu.game.player.GamePlayer; +import fr.quatrevieux.araknemu.game.player.spell.SpellBook; +import fr.quatrevieux.araknemu.game.spell.SpellLevels; +import fr.quatrevieux.araknemu.game.spell.SpellService; +import org.kohsuke.args4j.Argument; + +/** + * Learn a spell to a player + */ +public final class LearnSpell extends AbstractCommand { + private final GamePlayer player; + private final SpellService service; + + public LearnSpell(GamePlayer player, SpellService service) { + this.player = player; + this.service = service; + } + + @Override + protected void build(AbstractCommand.Builder builder) { + builder + .help( + formatter -> formatter + .description("Add the given spell to a player") + .example("@John learnspell 366", "John will learn the spell Moon Hammer") + ) + .requires(Permission.MANAGE_PLAYER) + .arguments(Arguments::new) + ; + } + + @Override + public String name() { + return "learnspell"; + } + + @Override + public void execute(AdminPerformer performer, Arguments arguments) throws AdminException { + final SpellLevels toLearn; + + try { + toLearn = service.get(arguments.spellId); + } catch (EntityNotFoundException e) { + performer.error("Spell {} not found", arguments.spellId); + return; + } + + final SpellBook spells = player.properties().spells(); + + if (!spells.canLearn(toLearn)) { + performer.error("Cannot learn spell {} ({})", toLearn.name(), arguments.spellId); + return; + } + + spells.learn(toLearn); + performer.success("The spell {} ({}) has been learned", toLearn.name(), arguments.spellId); + } + + public static final class Arguments { + @Argument(required = true, metaVar = "SPELLID", usage = "The spell ID to learn.") + private int spellId; + } +} diff --git a/src/main/java/fr/quatrevieux/araknemu/game/admin/player/PlayerContext.java b/src/main/java/fr/quatrevieux/araknemu/game/admin/player/PlayerContext.java index ee0c9ff5d..30d3c0edc 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/admin/player/PlayerContext.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/admin/player/PlayerContext.java @@ -48,7 +48,6 @@ protected SimpleContext createContext() { .add(new Info(player)) .add(new SetLife(player)) .add(new AddStats(player)) - .add(new AddXp(player)) .add(new Restriction(player)) .add(new Save(player)) .add(new Message(player)) diff --git a/src/main/java/fr/quatrevieux/araknemu/game/admin/player/Spawn.java b/src/main/java/fr/quatrevieux/araknemu/game/admin/player/Spawn.java new file mode 100644 index 000000000..3004b17fc --- /dev/null +++ b/src/main/java/fr/quatrevieux/araknemu/game/admin/player/Spawn.java @@ -0,0 +1,244 @@ +/* + * This file is part of Araknemu. + * + * Araknemu is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Araknemu is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Araknemu. If not, see . + * + * Copyright (c) 2017-2024 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.admin.player; + +import fr.quatrevieux.araknemu.common.account.Permission; +import fr.quatrevieux.araknemu.data.transformer.Transformer; +import fr.quatrevieux.araknemu.data.value.Position; +import fr.quatrevieux.araknemu.data.world.entity.monster.MonsterGroupData; +import fr.quatrevieux.araknemu.data.world.transformer.MonsterListTransformer; +import fr.quatrevieux.araknemu.game.admin.AbstractCommand; +import fr.quatrevieux.araknemu.game.admin.AdminPerformer; +import fr.quatrevieux.araknemu.game.admin.exception.AdminException; +import fr.quatrevieux.araknemu.game.admin.exception.CommandException; +import fr.quatrevieux.araknemu.game.exploration.map.ExplorationMap; +import fr.quatrevieux.araknemu.game.fight.FightService; +import fr.quatrevieux.araknemu.game.monster.environment.FixedCellSelector; +import fr.quatrevieux.araknemu.game.monster.environment.LivingMonsterGroupPosition; +import fr.quatrevieux.araknemu.game.monster.environment.MonsterEnvironmentService; +import fr.quatrevieux.araknemu.game.monster.environment.RandomCellSelector; +import fr.quatrevieux.araknemu.game.monster.group.MonsterGroup; +import fr.quatrevieux.araknemu.game.monster.group.MonsterGroupFactory; +import fr.quatrevieux.araknemu.game.player.GamePlayer; +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Spawn a monster group on the map + */ +public final class Spawn extends AbstractCommand { + private final GamePlayer player; + private final FightService fightService; + private final MonsterEnvironmentService monsterEnvironmentService; + private final MonsterGroupFactory groupFactory; + private final Transformer> groupParser; + + public Spawn(GamePlayer player, FightService fightService, MonsterEnvironmentService monsterEnvironmentService, MonsterGroupFactory groupFactory) { + this.player = player; + this.fightService = fightService; + this.monsterEnvironmentService = monsterEnvironmentService; + this.groupFactory = groupFactory; + this.groupParser = new MonsterListTransformer(); + } + + @Override + protected void build(AbstractCommand.Builder builder) { + builder + .help( + help -> help + .description("Spawn a monster group on the map") + .example("spawn 52", "Spawn one arakne") + .example("spawn 52x10", "Spawn 10 arakne") + .example("spawn 52|54 --size 8", "Spawn a group of arakne and chafer with at most 8 monsters") + .example("spawn 52|54 --count 4", "Spawn 4 groups of 1 arakne and 1 chafer") + .example("spawn 52|54 --count 4 --move", "Same as above, but monsters will move on the map") + .example("spawn --auto", "Respawn a new group on the map") + .example("spawn --auto --count 4", "Respawn 4 new group on the map") + .example("@John spawn --auto", "Respawn a single group on the map for John") + ) + .requires(Permission.MANAGE_PLAYER) + .arguments(Arguments::new) + ; + } + + @Override + public String name() { + return "spawn"; + } + + @Override + public void execute(AdminPerformer performer, Arguments arguments) throws AdminException { + final ExplorationMap map = player.isExploring() ? player.exploration().map() : null; + + if (map == null) { + error("The player is not on a map"); + return; + } + + if (arguments.group != null && arguments.auto) { + error("You should not specify a group and use --auto option at the same time"); + return; + } + + if (arguments.group != null) { + manual(performer, map, arguments.group, arguments); + } else if (arguments.auto) { + auto(performer, map, arguments); + } else { + error("You should specify a group to spawn or use --auto option"); + } + } + + private void manual(AdminPerformer performer, ExplorationMap map, String groupString, Arguments arguments) throws CommandException { + final List monsters; + + try { + monsters = groupParser.unserialize(groupString); + } catch (Exception e) { + error("Invalid group format : " + e.getMessage()); + return; + } + + final MonsterGroupData data = new MonsterGroupData( + -1, + arguments.respawn, + arguments.size, + arguments.count, + monsters, + "", + new Position(0, 0), + false + ); + + final LivingMonsterGroupPosition position = new LivingMonsterGroupPosition( + groupFactory, + monsterEnvironmentService, + fightService, + data, + arguments.move ? new RandomCellSelector() : new FixedCellSelector(player.position().cell()), + !arguments.move + ); + + try { + // Populate if required to link the map to the group + position.populate(map); + + if (arguments.count == 0) { + position.spawn(); + } + + position.available().forEach(group -> logGroup(performer, group)); + } catch (Exception e) { + error("Cannot spawn the group : " + e.getMessage()); + } + } + + private void auto(AdminPerformer performer, ExplorationMap map, Arguments arguments) throws CommandException { + final Collection groupsOnMap = monsterEnvironmentService.byMap(map.id()); + + if (groupsOnMap.isEmpty()) { + error("The map has no registered groups. Use GROUP argument to define a group to spawn."); + return; + } + + for (LivingMonsterGroupPosition position : groupsOnMap) { + for (int i = 0; i < Math.max(1, arguments.count); ++i) { + logGroup(performer, position.spawn()); + } + } + } + + private void logGroup(AdminPerformer performer, MonsterGroup group) { + performer.success( + "The group with {} has been spawned", + group.monsters().stream() + .map(monster -> monster.name() + " (id: " + monster.id() + ", level: " + monster.level() + ")") + .collect(Collectors.joining(", ")) + ); + } + + // @todo "id" option to spawn an already existing monster group + public static final class Arguments { + @Argument( + metaVar = "GROUP", + usage = "The monster group to spawn.\n" + + "Format:\n" + + "[id 1],[level min 1],[level max 1]x[rate1]|[id 2],[level min 2],[level max 2]x[rate2]\n\n" + + "Monsters are separated by pipe \"|\"\n" + + "Monster level interval are separated by comma \",\"\n" + + "Monster spawn rate is an integer that follow \"x\"\n\n" + + "Levels are not required :\n" + + "- If not set, all available levels are used\n" + + "- If only one is set, the level is constant\n" + + "- If interval is set, only grades into the interval are used\n\n" + + "The spawn rate is not required, and by default, its value is 1" + ) + private @Nullable String group; + + @Option( + name = "--auto", + aliases = "-a", + usage = "If set, the monster group of the map will be spawned. When set, the GROUP argument should not be set." + ) + private boolean auto = false; + + @Option( + name = "--count", + aliases = "-c", + metaVar = "COUNT", + usage = "Number of groups to spawn. If this value is set, monsters group will respawn automatically. By default only one group will spawn, without respawn." + ) + private @NonNegative int count = 0; + + @Option( + name = "--size", + aliases = "-s", + metaVar = "SIZE", + usage = "Maximum number of monsters in the group. By default, all monsters defined in the group will be spawned.", + forbids = {"--auto"} + ) + private @NonNegative int size = 0; + + @Option( + name = "--respawn", + aliases = "-r", + metaVar = "DELAY", + usage = "The respawn delay after a fight. By default, groups will respawn immediately. The option is effective only if the --count option is set.", + depends = {"--count"}, + forbids = {"--auto"} + ) + private Duration respawn = Duration.ZERO; + + @Option( + name = "--move", + aliases = "-m", + usage = "If set, groups will spawn on a random cell, and will move randomly on the map. By default, groups are fixed.", + forbids = {"--auto"} + ) + private boolean move = false; + } +} diff --git a/src/main/java/fr/quatrevieux/araknemu/game/admin/server/Online.java b/src/main/java/fr/quatrevieux/araknemu/game/admin/server/Online.java index 9c504ba4e..c771a9dd9 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/admin/server/Online.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/admin/server/Online.java @@ -161,6 +161,12 @@ public static class Arguments { @Option(name = "--skip", usage = "Skip the first lines.") private int skip = 0; + @Option(name = "--fighting", aliases = {"-f"}, usage = "List only players in a fight.") + private boolean fighting = false; + + @Option(name = "--exploring", aliases = {"-e"}, usage = "List only players in exploration.") + private boolean exploring = false; + @Argument(metaVar = "SEARCH", usage = "Optional. Filter the online player name. Return only players containing the search term into the name.") private @MonotonicNonNull String search = null; @@ -172,6 +178,14 @@ public Stream apply(Stream stream) { stream = stream.filter(player -> player.name().toLowerCase().contains(NullnessUtil.castNonNull(search))); } + if (fighting) { + stream = stream.filter(GamePlayer::isFighting); + } + + if (exploring) { + stream = stream.filter(GamePlayer::isExploring); + } + return stream.skip(skip).limit(limit); } } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/monster/Monster.java b/src/main/java/fr/quatrevieux/araknemu/game/monster/Monster.java index 7eef09a16..5bcf29444 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/monster/Monster.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/monster/Monster.java @@ -56,6 +56,13 @@ public int id() { return template.id(); } + /** + * Get the monster name, for display purpose + */ + public String name() { + return template.name(); + } + /** * The monster sprite */ diff --git a/src/main/java/fr/quatrevieux/araknemu/game/monster/environment/LivingMonsterGroupPosition.java b/src/main/java/fr/quatrevieux/araknemu/game/monster/environment/LivingMonsterGroupPosition.java index 0b0ad1bbd..a29594eef 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/monster/environment/LivingMonsterGroupPosition.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/monster/environment/LivingMonsterGroupPosition.java @@ -81,13 +81,19 @@ public void populate(ExplorationMap map) { * Spawn a new group on the map * * Note: this method will not check the max count of monsters : if called manually, the group count can exceed max count + * + * @return The spawned group */ - public void spawn() { + public MonsterGroup spawn() { if (map == null) { throw new IllegalStateException("Monster group is not on map"); } - map.add(factory.create(data, this)); + final MonsterGroup group = factory.create(data, this); + + map.add(group); + + return group; } /** diff --git a/src/main/java/fr/quatrevieux/araknemu/game/monster/group/generator/FixedMonsterListGenerator.java b/src/main/java/fr/quatrevieux/araknemu/game/monster/group/generator/FixedMonsterListGenerator.java index 06b939459..5046f3e56 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/monster/group/generator/FixedMonsterListGenerator.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/monster/group/generator/FixedMonsterListGenerator.java @@ -23,8 +23,8 @@ import fr.quatrevieux.araknemu.game.monster.Monster; import fr.quatrevieux.araknemu.game.monster.MonsterService; +import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; /** * Generate a fixed monster group @@ -40,9 +40,14 @@ public FixedMonsterListGenerator(MonsterService service) { @Override public List generate(MonsterGroupData data) { - return data.monsters().stream() - .map(monster -> service.load(monster.id()).random(monster.level())) - .collect(Collectors.toList()) - ; + final List monsters = new ArrayList<>(data.monsters().size()); + + for (MonsterGroupData.Monster monster : data.monsters()) { + for (int i = 0; i < monster.rate(); ++i) { + monsters.add(service.load(monster.id()).random(monster.level())); + } + } + + return monsters; } } diff --git a/src/main/java/fr/quatrevieux/araknemu/game/player/experience/PlayerExperienceService.java b/src/main/java/fr/quatrevieux/araknemu/game/player/experience/PlayerExperienceService.java index 8369952ca..a9e5aea2d 100644 --- a/src/main/java/fr/quatrevieux/araknemu/game/player/experience/PlayerExperienceService.java +++ b/src/main/java/fr/quatrevieux/araknemu/game/player/experience/PlayerExperienceService.java @@ -101,7 +101,7 @@ public GamePlayerExperience load(Dispatcher dispatcher, Player player) { * * @param level Level to get */ - PlayerExperience byLevel(int level) { + public PlayerExperience byLevel(int level) { return level <= levels.size() ? levels.get(level - 1) : levels.get(levels.size() - 1) diff --git a/src/test/java/fr/quatrevieux/araknemu/data/world/repository/implementation/local/MonsterGroupDataRepositoryCacheTest.java b/src/test/java/fr/quatrevieux/araknemu/data/world/repository/implementation/local/MonsterGroupDataRepositoryCacheTest.java index c65bdfebf..b9dc33864 100644 --- a/src/test/java/fr/quatrevieux/araknemu/data/world/repository/implementation/local/MonsterGroupDataRepositoryCacheTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/data/world/repository/implementation/local/MonsterGroupDataRepositoryCacheTest.java @@ -85,7 +85,7 @@ void hasCached() { void all() { List groups = repository.all(); - assertCount(3, groups); + assertCount(4, groups); for (MonsterGroupData template : groups) { assertSame( diff --git a/src/test/java/fr/quatrevieux/araknemu/data/world/repository/implementation/sql/SqlMonsterGroupDataRepositoryTest.java b/src/test/java/fr/quatrevieux/araknemu/data/world/repository/implementation/sql/SqlMonsterGroupDataRepositoryTest.java index e5d8b6535..a26591a53 100644 --- a/src/test/java/fr/quatrevieux/araknemu/data/world/repository/implementation/sql/SqlMonsterGroupDataRepositoryTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/data/world/repository/implementation/sql/SqlMonsterGroupDataRepositoryTest.java @@ -116,7 +116,7 @@ void has() { @Test void all() { assertArrayEquals( - new int[] {1, 2, 3}, + new int[] {1, 2, 3, 4}, repository.all().stream().mapToInt(MonsterGroupData::id).toArray() ); } diff --git a/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java b/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java index f4acb3620..feda1a932 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/GameDataSet.java @@ -1078,7 +1078,8 @@ public GameDataSet pushMonsterGroups() throws SQLException, ContainerException { "INSERT INTO `MONSTER_GROUP` (`MONSTER_GROUP_ID`, `MONSTERS`, `MAX_SIZE`, `MAX_COUNT`, `RESPAWN_TIME`, `COMMENT`, `WIN_FIGHT_TELEPORT_MAP_ID`, `WIN_FIGHT_TELEPORT_CELL_ID`, `FIXED_TEAM_NUMBER`) VALUES" + "(1, '31|34,10', 4, 2, 30000, 'larves', 0, 0, 0)," + "(2, '36,3,6', 6, 3, 75000, 'bouftous', 0, 0, 1)," + - "(3, '36', 1, 1, 100, 'reswpan', 10340, 125, 0);" + "(3, '36', 1, 1, 100, 'reswpan', 10340, 125, 0)," + + "(4, '31x2|34x4', 1, 1, 100, 'reswpan', 0, 0, 0);" ); return this; diff --git a/src/test/java/fr/quatrevieux/araknemu/game/admin/AdminModuleTest.java b/src/test/java/fr/quatrevieux/araknemu/game/admin/AdminModuleTest.java index 218a22006..d4c7020a5 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/admin/AdminModuleTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/admin/AdminModuleTest.java @@ -45,10 +45,15 @@ import fr.quatrevieux.araknemu.game.admin.global.Echo; import fr.quatrevieux.araknemu.game.admin.global.GlobalContext; import fr.quatrevieux.araknemu.game.admin.global.Help; +import fr.quatrevieux.araknemu.game.admin.player.AddStats; import fr.quatrevieux.araknemu.game.admin.player.AddXp; import fr.quatrevieux.araknemu.game.admin.player.GetItem; +import fr.quatrevieux.araknemu.game.admin.player.LearnSpell; import fr.quatrevieux.araknemu.game.admin.player.PlayerContext; import fr.quatrevieux.araknemu.game.admin.player.PlayerContextResolver; +import fr.quatrevieux.araknemu.game.admin.player.Restriction; +import fr.quatrevieux.araknemu.game.admin.player.SetLife; +import fr.quatrevieux.araknemu.game.admin.player.Spawn; import fr.quatrevieux.araknemu.game.admin.player.teleport.Goto; import fr.quatrevieux.araknemu.game.admin.server.Banip; import fr.quatrevieux.araknemu.game.admin.server.Kick; @@ -112,7 +117,15 @@ void playerResolver() throws SQLException, ContextException, CommandNotFoundExce assertInstanceOf(GetItem.class, context.command("getitem")); assertInstanceOf(Goto.class, context.command("goto")); assertInstanceOf(AddXp.class, context.command("addxp")); + assertInstanceOf(AddStats.class, context.command("addstats")); + assertInstanceOf(fr.quatrevieux.araknemu.game.admin.player.Info.class, context.command("info")); + assertInstanceOf(fr.quatrevieux.araknemu.game.admin.player.Kick.class, context.command("kick")); assertInstanceOf(fr.quatrevieux.araknemu.game.admin.player.Message.class, context.command("msg")); + assertInstanceOf(Restriction.class, context.command("restriction")); + assertInstanceOf(fr.quatrevieux.araknemu.game.admin.player.Save.class, context.command("save")); + assertInstanceOf(SetLife.class, context.command("setlife")); + assertInstanceOf(LearnSpell.class, context.command("learnspell")); + assertInstanceOf(Spawn.class, context.command("spawn")); } @Test diff --git a/src/test/java/fr/quatrevieux/araknemu/game/admin/CommandTestCase.java b/src/test/java/fr/quatrevieux/araknemu/game/admin/CommandTestCase.java index e7714a69d..d8eada06a 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/admin/CommandTestCase.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/admin/CommandTestCase.java @@ -40,6 +40,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; abstract public class CommandTestCase extends GameBaseCase { protected Command command; @@ -142,6 +143,30 @@ public void assertOutput(String... lines) { ); } + public void assertOutputRegex(String... patterns) { + final String[] logs = performer.logs + .stream() + .map(entry -> entry.message) + .toArray(String[]::new) + ; + + for (int i = 0; i < patterns.length; i++) { + if (i >= logs.length) { + fail("Line " + i + " not found in logs, expected : " + patterns[i]); + continue; + } + + assertTrue( + logs[i].matches(patterns[i]), + "Line " + i + " does not match pattern : " + patterns[i] + "\nActual : " + logs[i] + ); + } + + if (patterns.length < logs.length) { + fail("Too many lines in logs, expected : " + patterns.length + ", actual : " + logs.length); + } + } + public void assertOutputContains(String line) { assertContains( line, diff --git a/src/test/java/fr/quatrevieux/araknemu/game/admin/account/InfoTest.java b/src/test/java/fr/quatrevieux/araknemu/game/admin/account/InfoTest.java index ab94f36b6..4c510e3d8 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/admin/account/InfoTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/admin/account/InfoTest.java @@ -34,6 +34,7 @@ import java.sql.SQLException; import java.util.EnumSet; +import java.util.stream.Collectors; class InfoTest extends CommandTestCase { @Test @@ -72,6 +73,25 @@ void executeLogged() throws ContainerException, AdminException, SQLException { assertOutputContains("Logged: Yes"); } + @Test + void executeLoggedWithPlayer() throws ContainerException, AdminException, SQLException { + GameAccount account = container.get(AccountService.class).load(dataSet.push(new Account(-1, "azerty", "", "uiop"))); + + command = new Info( + account, + container.get(AccountRepository.class) + ); + + GameSession session = (GameSession) container.get(SessionFactory.class).create(new DummyChannel()); + account.attach(session); + makeSimpleGamePlayer(42, session, true); + + execute("info"); + + assertOutputContains("Logged: Yes"); + assertOutputContains("Player: PLAYER_42"); + } + @Test void executeAdmin() throws ContainerException, AdminException, SQLException { GameAccount account = container.get(AccountService.class).load(dataSet.push(new Account(-1, "azerty", "", "uiop", EnumSet.of(Permission.ACCESS, Permission.MANAGE_ACCOUNT), "", ""))); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/admin/player/AddXpTest.java b/src/test/java/fr/quatrevieux/araknemu/game/admin/player/AddXpTest.java index 585b1df35..d80995f30 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/admin/player/AddXpTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/admin/player/AddXpTest.java @@ -22,6 +22,7 @@ import fr.quatrevieux.araknemu.core.di.ContainerException; import fr.quatrevieux.araknemu.game.admin.CommandTestCase; import fr.quatrevieux.araknemu.game.admin.exception.AdminException; +import fr.quatrevieux.araknemu.game.player.experience.PlayerExperienceService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,7 +36,7 @@ class AddXpTest extends CommandTestCase { public void setUp() throws Exception { super.setUp(); - command = new AddXp(gamePlayer(true)); + command = new AddXp(gamePlayer(true), container.get(PlayerExperienceService.class)); } @Test @@ -56,6 +57,42 @@ void executeWithLevelUp() throws ContainerException, SQLException, AdminExceptio assertEquals(6481459, gamePlayer().properties().experience().current()); } + @Test + void executeWithLevel() throws ContainerException, SQLException, AdminException { + execute("addxp", "--level", "69"); + + assertOutput("Add 15573541 xp to Bob (level = 69)"); + + assertEquals(21055000, gamePlayer().properties().experience().current()); + } + + @Test + void executeWithLevelAlias() throws ContainerException, SQLException, AdminException { + execute("addxp", "-l", "69"); + + assertOutput("Add 15573541 xp to Bob (level = 69)"); + + assertEquals(21055000, gamePlayer().properties().experience().current()); + } + + @Test + void executeWithLevelToLow() throws ContainerException, SQLException, AdminException { + execute("addxp", "--level", "20"); + + assertOutput("The player level (50) is already higher than the target level (20)"); + + assertEquals(50, gamePlayer().properties().experience().level()); + } + + @Test + void executeWithLevelSame() throws ContainerException, SQLException, AdminException { + execute("addxp", "--level", "50"); + + assertOutput("The player level (50) is already higher than the target level (50)"); + + assertEquals(50, gamePlayer().properties().experience().level()); + } + @Test void executeWithLongNumber() throws ContainerException, SQLException, AdminException { execute("addxp", "10000000000"); @@ -71,11 +108,13 @@ void help() { "addxp - Add experience to player", "========================================", "SYNOPSIS", - "\taddxp QUANTITY", + "\taddxp [QUANTITY] [--level (-l) N]", "OPTIONS", "\tQUANTITY : The experience quantity to add. Must be an unsigned number.", + "\t--level (-l) : The target level. If set, the quantity will be calculated to reach this level. Must be a positive number.", "EXAMPLES", - "\t@John addxp 1000000 - Add 1 million xp to John", + "\t@John addxp 1000000 - Add 1 million xp to John", + "\t@John addxp --level 150 - Add xp to John to reach level 150", "PERMISSIONS", "\t[ACCESS, MANAGE_PLAYER]" ); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/admin/player/LearnSpellTest.java b/src/test/java/fr/quatrevieux/araknemu/game/admin/player/LearnSpellTest.java new file mode 100644 index 000000000..74778d809 --- /dev/null +++ b/src/test/java/fr/quatrevieux/araknemu/game/admin/player/LearnSpellTest.java @@ -0,0 +1,92 @@ +/* + * This file is part of Araknemu. + * + * Araknemu is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Araknemu is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Araknemu. If not, see . + * + * Copyright (c) 2017-2024 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.admin.player; + +import fr.quatrevieux.araknemu.core.di.ContainerException; +import fr.quatrevieux.araknemu.game.admin.CommandTestCase; +import fr.quatrevieux.araknemu.game.admin.exception.AdminException; +import fr.quatrevieux.araknemu.game.admin.exception.CommandException; +import fr.quatrevieux.araknemu.game.spell.SpellService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.*; + +class LearnSpellTest extends CommandTestCase { + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + command = new LearnSpell(gamePlayer(true), container.get(SpellService.class)); + dataSet.pushFunctionalSpells(); + } + + @Test + void executeSuccess() throws ContainerException, SQLException, AdminException { + execute("learnspell", "109"); + + assertOutput("The spell Bluff (109) has been learned"); + assertTrue(gamePlayer().properties().spells().has(109)); + assertEquals(1, gamePlayer().properties().spells().get(109).level()); + } + + @Test + void executeNotFound() throws ContainerException, SQLException, AdminException { + execute("learnspell", "404"); + + assertOutput("Spell 404 not found"); + assertFalse(gamePlayer().properties().spells().has(404)); + } + + @Test + void executeCannotLearn() throws ContainerException, SQLException, AdminException { + execute("learnspell", "157"); + + assertOutput("Cannot learn spell Epée Céleste (157)"); + assertFalse(gamePlayer().properties().spells().has(157)); + } + + @Test + void executeMissingArgument() throws ContainerException, SQLException, AdminException { + assertThrowsWithMessage( + CommandException.class, "Argument \"SPELLID\" is required", + () -> execute("learnspell") + ); + } + + @Test + void help() { + assertHelp( + "learnspell - Add the given spell to a player", + "========================================", + "SYNOPSIS", + "\tlearnspell SPELLID", + "OPTIONS", + "\tSPELLID : The spell ID to learn.", + "EXAMPLES", + "\t@John learnspell 366 - John will learn the spell Moon Hammer", + "PERMISSIONS", + "\t[ACCESS, MANAGE_PLAYER]" + ); + } +} diff --git a/src/test/java/fr/quatrevieux/araknemu/game/admin/player/PlayerContextTest.java b/src/test/java/fr/quatrevieux/araknemu/game/admin/player/PlayerContextTest.java index 1a3efa16a..5855dee21 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/admin/player/PlayerContextTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/admin/player/PlayerContextTest.java @@ -53,7 +53,6 @@ void commands() throws CommandNotFoundException { assertInstanceOf(Info.class, context.command("info")); assertInstanceOf(SetLife.class, context.command("setlife")); assertInstanceOf(AddStats.class, context.command("addstats")); - assertInstanceOf(AddXp.class, context.command("addxp")); assertInstanceOf(Restriction.class, context.command("restriction")); assertInstanceOf(Message.class, context.command("msg")); assertInstanceOf(Save.class, context.command("save")); @@ -62,7 +61,6 @@ void commands() throws CommandNotFoundException { assertContainsType(Info.class, context.commands()); assertContainsType(SetLife.class, context.commands()); assertContainsType(AddStats.class, context.commands()); - assertContainsType(AddXp.class, context.commands()); assertContainsType(Restriction.class, context.commands()); assertContainsType(Message.class, context.commands()); assertContainsType(Kick.class, context.commands()); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/admin/player/SpawnTest.java b/src/test/java/fr/quatrevieux/araknemu/game/admin/player/SpawnTest.java new file mode 100644 index 000000000..a1e22b120 --- /dev/null +++ b/src/test/java/fr/quatrevieux/araknemu/game/admin/player/SpawnTest.java @@ -0,0 +1,299 @@ +/* + * This file is part of Araknemu. + * + * Araknemu is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Araknemu is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Araknemu. If not, see . + * + * Copyright (c) 2017-2024 Vincent Quatrevieux + */ + +package fr.quatrevieux.araknemu.game.admin.player; + +import fr.quatrevieux.araknemu.core.di.ContainerException; +import fr.quatrevieux.araknemu.data.world.entity.monster.MonsterGroupData; +import fr.quatrevieux.araknemu.data.world.entity.monster.MonsterGroupPosition; +import fr.quatrevieux.araknemu.game.admin.CommandTestCase; +import fr.quatrevieux.araknemu.game.admin.exception.AdminException; +import fr.quatrevieux.araknemu.game.admin.exception.CommandException; +import fr.quatrevieux.araknemu.game.exploration.map.ExplorationMap; +import fr.quatrevieux.araknemu.game.exploration.map.ExplorationMapService; +import fr.quatrevieux.araknemu.game.fight.FightService; +import fr.quatrevieux.araknemu.game.monster.environment.LivingMonsterGroupPosition; +import fr.quatrevieux.araknemu.game.monster.environment.MonsterEnvironmentService; +import fr.quatrevieux.araknemu.game.monster.group.MonsterGroup; +import fr.quatrevieux.araknemu.game.monster.group.MonsterGroupFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.sql.SQLException; +import java.time.Duration; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SpawnTest extends CommandTestCase { + private ExplorationMap map; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + command = new Spawn( + gamePlayer(), + container.get(FightService.class), + container.get(MonsterEnvironmentService.class), + container.get(MonsterGroupFactory.class) + ); + + dataSet + .pushMonsterTemplates() + .pushMonsterSpells() + .pushMonsterGroups() + .pushMonsterGroupPosition(new MonsterGroupPosition(10340, -1, 3)) + ; + + explorationPlayer().leave(); + explorationPlayer().changeMap(container.get(ExplorationMapService.class).load(10340), 123); + map = explorationPlayer().map(); + + groupsOnMap().forEach(map::remove); // Clean map + } + + @Test + void executeSingleMonster() throws ContainerException, SQLException, AdminException { + execute("spawn", "34,7"); + + List groups = groupsOnMap(); + + assertCount(1, groups); + assertCount(1, groups.get(0).monsters()); + assertEquals(34, groups.get(0).monsters().get(0).id()); + assertTrue(groups.get(0).handler().fixed()); + + assertOutput("The group with Larve Verte (id: 34, level: 7) has been spawned"); + } + + @Test + void executeMultipleMonsters() throws ContainerException, SQLException, AdminException { + execute("spawn", "31,2|34,7"); + + List groups = groupsOnMap(); + + assertCount(1, groups); + assertCount(2, groups.get(0).monsters()); + assertEquals(31, groups.get(0).monsters().get(0).id()); + assertEquals(34, groups.get(0).monsters().get(1).id()); + + assertOutput("The group with Larve Bleue (id: 31, level: 2), Larve Verte (id: 34, level: 7) has been spawned"); + } + + @Test + void executeCountOption() throws ContainerException, SQLException, AdminException { + execute("spawn", "31,2", "--count", "4"); + + List groups = groupsOnMap(); + + assertCount(4, groups); + assertEquals(31, groups.get(0).monsters().get(0).id()); + assertEquals(31, groups.get(1).monsters().get(0).id()); + assertEquals(31, groups.get(2).monsters().get(0).id()); + assertEquals(31, groups.get(3).monsters().get(0).id()); + + assertOutput( + "The group with Larve Bleue (id: 31, level: 2) has been spawned", + "The group with Larve Bleue (id: 31, level: 2) has been spawned", + "The group with Larve Bleue (id: 31, level: 2) has been spawned", + "The group with Larve Bleue (id: 31, level: 2) has been spawned" + ); + } + + @Test + void executeSizeOption() throws ContainerException, SQLException, AdminException { + int maxSize = 0; + + for (int i = 0; i < 10; ++i) { + execute("spawn", "31,2", "--size", "8"); + + List groups = groupsOnMap(); + + assertCount(1, groups); + + if (groups.get(0).monsters().size() > maxSize) { + maxSize = groups.get(0).monsters().size(); + } + + map.remove(groups.get(0)); + } + + assertEquals(8, maxSize); + } + + @Test + void executeMoveOption() throws ContainerException, SQLException, AdminException { + execute("spawn", "34,7", "--move"); + + List groups = groupsOnMap(); + + assertCount(1, groups); + assertCount(1, groups.get(0).monsters()); + assertEquals(34, groups.get(0).monsters().get(0).id()); + assertFalse(groups.get(0).handler().fixed()); + + assertOutput("The group with Larve Verte (id: 34, level: 7) has been spawned"); + } + + @Test + void executeRespawnOption() throws ContainerException, SQLException, AdminException, NoSuchFieldException, IllegalAccessException { + execute("spawn", "34,7", "--count", "1", "--respawn", "10m"); + + List groups = groupsOnMap(); + + assertCount(1, groups); + assertCount(1, groups.get(0).monsters()); + assertEquals(34, groups.get(0).monsters().get(0).id()); + + Field field = LivingMonsterGroupPosition.class.getDeclaredField("data"); + field.setAccessible(true); + + MonsterGroupData data = (MonsterGroupData) field.get(groups.get(0).handler()); + assertEquals(Duration.ofMinutes(10), data.respawnTime()); + + assertOutput("The group with Larve Verte (id: 34, level: 7) has been spawned"); + } + + @Test + void autoWithoutGroupOnMap() throws AdminException, SQLException { + explorationPlayer().leave(); + explorationPlayer().changeMap(container.get(ExplorationMapService.class).load(10300), 123); + assertThrowsWithMessage(CommandException.class, "The map has no registered groups. Use GROUP argument to define a group to spawn.", () -> execute("spawn", "--auto")); + } + + @Test + void autoWithSingleGroup() throws AdminException, SQLException { + int count = groupsOnMap().size(); + + execute("spawn", "--auto"); + + List groups = groupsOnMap(); + + assertCount(count + 1, groups); + assertCount(1, groups.get(0).monsters()); + assertEquals(36, groups.get(0).monsters().get(0).id()); + + assertOutputRegex("The group with Bouftou \\(id: 36, level: \\d+\\) has been spawned"); + } + + @Test + void autoWithCount() throws AdminException, SQLException { + int count = groupsOnMap().size(); + + execute("spawn", "--auto", "--count", "5"); + + List groups = groupsOnMap(); + + assertCount(count + 5, groups); + + assertOutputRegex( + "The group with Bouftou \\(id: 36, level: \\d+\\) has been spawned", + "The group with Bouftou \\(id: 36, level: \\d+\\) has been spawned", + "The group with Bouftou \\(id: 36, level: \\d+\\) has been spawned", + "The group with Bouftou \\(id: 36, level: \\d+\\) has been spawned", + "The group with Bouftou \\(id: 36, level: \\d+\\) has been spawned" + ); + } + + @Test + void executeMissingParameter() throws ContainerException, SQLException, AdminException { + assertThrowsWithMessage(CommandException.class, "You should specify a group to spawn or use --auto option", () -> execute("spawn")); + } + + @Test + void notOnMap() throws ContainerException, SQLException, AdminException { + explorationPlayer().leave(); + + assertThrowsWithMessage(CommandException.class, "The player is not on a map", () -> execute("spawn", "36")); + } + + @Test + void cannotUseGroupAndAuto() throws ContainerException, SQLException, AdminException { + assertThrowsWithMessage(CommandException.class, "You should not specify a group and use --auto option at the same time", () -> execute("spawn", "36", "--auto")); + } + + @Test + void autoIncompatibleOptions() throws ContainerException, SQLException, AdminException { + assertThrowsWithMessage(CommandException.class, "option \"--size (-s)\" cannot be used with the option(s) [--auto]", () -> execute("spawn", "--auto", "--size", "5")); + assertThrowsWithMessage(CommandException.class, "option \"--respawn (-r)\" requires the option(s) [--count]", () -> execute("spawn", "--auto", "--respawn", "5s")); + assertThrowsWithMessage(CommandException.class, "option \"--move (-m)\" cannot be used with the option(s) [--auto]", () -> execute("spawn", "--auto", "--move")); + } + + @Test + void invalidGroupString() throws ContainerException, SQLException, AdminException { + assertThrowsWithMessage(CommandException.class, "Invalid group format : For input string: \"invalid\"", () -> execute("spawn", "invalid")); + assertThrowsWithMessage(CommandException.class, "Cannot spawn the group : Monster 404 is not found", () -> execute("spawn", "404")); + assertThrowsWithMessage(CommandException.class, "Cannot spawn the group : Cannot found a free cell on map 10340", () -> execute("spawn", "36", "--count", "1000", "--move")); + } + + @Test + void help() { + assertHelp( + "spawn - Spawn a monster group on the map", + "========================================", + "SYNOPSIS", + "\tspawn [GROUP] [--auto (-a)] [--count (-c) COUNT] [--move (-m)] [--respawn (-r) DELAY] [--size (-s) SIZE]", + "OPTIONS", + "\tGROUP : The monster group to spawn.", + "\t\tFormat:", + "\t\t[id 1],[level min 1],[level max 1]x[rate1]|[id 2],[level min 2],[level max 2]x[rate2]", + "\t\t", + "\t\tMonsters are separated by pipe \"|\"", + "\t\tMonster level interval are separated by comma \",\"", + "\t\tMonster spawn rate is an integer that follow \"x\"", + "\t\t", + "\t\tLevels are not required :", + "\t\t- If not set, all available levels are used", + "\t\t- If only one is set, the level is constant", + "\t\t- If interval is set, only grades into the interval are used", + "\t\t", + "\t\tThe spawn rate is not required, and by default, its value is 1", + "\t--auto (-a) : If set, the monster group of the map will be spawned. When set, the GROUP argument should not be set.", + "\t--count (-c) : Number of groups to spawn. If this value is set, monsters group will respawn automatically. By default only one group will spawn, without respawn.", + "\t--move (-m) : If set, groups will spawn on a random cell, and will move randomly on the map. By default, groups are fixed.", + "\t--respawn (-r) : The respawn delay after a fight. By default, groups will respawn immediately. The option is effective only if the --count option is set.", + "\t--size (-s) : Maximum number of monsters in the group. By default, all monsters defined in the group will be spawned.", + "EXAMPLES", + "\tspawn 52 - Spawn one arakne", + "\tspawn 52x10 - Spawn 10 arakne", + "\tspawn 52|54 --size 8 - Spawn a group of arakne and chafer with at most 8 monsters", + "\tspawn 52|54 --count 4 - Spawn 4 groups of 1 arakne and 1 chafer", + "\tspawn 52|54 --count 4 --move - Same as above, but monsters will move on the map", + "\tspawn --auto - Respawn a new group on the map", + "\tspawn --auto --count 4 - Respawn 4 new group on the map", + "\t@John spawn --auto - Respawn a single group on the map for John", + "PERMISSIONS", + "\t[ACCESS, MANAGE_PLAYER]" + ); + } + + private List groupsOnMap() { + return map.creatures().stream() + .filter(MonsterGroup.class::isInstance) + .map(MonsterGroup.class::cast) + .collect(Collectors.toList()) + ; + } +} diff --git a/src/test/java/fr/quatrevieux/araknemu/game/admin/server/OnlineTest.java b/src/test/java/fr/quatrevieux/araknemu/game/admin/server/OnlineTest.java index 76d0e2405..1bce6082f 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/admin/server/OnlineTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/admin/server/OnlineTest.java @@ -68,6 +68,20 @@ void executeWithExploringPlayer() throws Exception { "There is 1 online players with 1 active sessions", "Bob Feca [-4,3] in exploration [127.0.0.1] - info goto" ); + + execute("online", "--exploring"); + + assertOutput( + "There is 1 online players with 1 active sessions", + "Bob Feca [-4,3] in exploration [127.0.0.1] - info goto" + ); + + execute("online", "--fighting"); + + assertOutput( + "There is 1 online players with 1 active sessions", + "No results found" + ); } @Test @@ -87,6 +101,20 @@ void executeWithFightingPlayer() throws Exception { "There is 1 online players with 1 active sessions", "Bob Feca [-51,10] in combat [127.0.0.1] - info goto" ); + + execute("online", "--fighting"); + + assertOutput( + "There is 1 online players with 1 active sessions", + "Bob Feca [-51,10] in combat [127.0.0.1] - info goto" + ); + + execute("online", "--exploring"); + + assertOutput( + "There is 1 online players with 1 active sessions", + "No results found" + ); } @Test @@ -169,9 +197,11 @@ void help() { "online - List online players", "========================================", "SYNOPSIS", - "\tonline [SEARCH] [--limit N=20] [--skip N]", + "\tonline [SEARCH] [--exploring (-e)] [--fighting (-f)] [--limit N=20] [--skip N]", "OPTIONS", "\tSEARCH : Optional. Filter the online player name. Return only players containing the search term into the name.", + "\t--exploring (-e) : List only players in exploration.", + "\t--fighting (-f) : List only players in a fight.", "\t--limit : Limit the number of returned lines. By default the limit is set to 20.", "\t--skip : Skip the first lines.", "EXAMPLES", diff --git a/src/test/java/fr/quatrevieux/araknemu/game/monster/MonsterTest.java b/src/test/java/fr/quatrevieux/araknemu/game/monster/MonsterTest.java index 2dfc4e045..39a1369d7 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/monster/MonsterTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/monster/MonsterTest.java @@ -58,6 +58,7 @@ void values() { assertEquals(50, monster.life()); assertEquals(35, monster.initiative()); assertEquals("AGGRESSIVE", monster.ai()); + assertEquals("Larve Verte", monster.name()); assertEquals(5, monster.characteristics().get(Characteristic.ACTION_POINT)); assertEquals(3, monster.characteristics().get(Characteristic.MOVEMENT_POINT)); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/monster/environment/LivingMonsterGroupPositionTest.java b/src/test/java/fr/quatrevieux/araknemu/game/monster/environment/LivingMonsterGroupPositionTest.java index d5f5acf0e..991992ccf 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/monster/environment/LivingMonsterGroupPositionTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/monster/environment/LivingMonsterGroupPositionTest.java @@ -107,8 +107,9 @@ void spawn() { monsterGroupPosition.populate(map); assertCount(2, monsterGroupPosition.available()); - monsterGroupPosition.spawn(); + MonsterGroup group = monsterGroupPosition.spawn(); assertCount(3, monsterGroupPosition.available()); + assertContains(group, monsterGroupPosition.available()); } @Test diff --git a/src/test/java/fr/quatrevieux/araknemu/game/monster/environment/MonsterEnvironmentServiceTest.java b/src/test/java/fr/quatrevieux/araknemu/game/monster/environment/MonsterEnvironmentServiceTest.java index cefaf7df2..d09296121 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/monster/environment/MonsterEnvironmentServiceTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/monster/environment/MonsterEnvironmentServiceTest.java @@ -119,7 +119,7 @@ void preload() throws SQLException { service.preload(logger); Mockito.verify(logger).info("Loading monster groups data..."); - Mockito.verify(logger).info("{} monster groups loaded", 3); + Mockito.verify(logger).info("{} monster groups loaded", 4); Mockito.verify(logger).info("Loading monster groups positions..."); Mockito.verify(logger).info("{} Map positions loaded", 2); diff --git a/src/test/java/fr/quatrevieux/araknemu/game/monster/group/generator/FixedMonsterListGeneratorTest.java b/src/test/java/fr/quatrevieux/araknemu/game/monster/group/generator/FixedMonsterListGeneratorTest.java index 664181eff..12c0697b1 100644 --- a/src/test/java/fr/quatrevieux/araknemu/game/monster/group/generator/FixedMonsterListGeneratorTest.java +++ b/src/test/java/fr/quatrevieux/araknemu/game/monster/group/generator/FixedMonsterListGeneratorTest.java @@ -59,4 +59,12 @@ void generate() { assertBetween(2, 6, monsters.get(0).level()); assertEquals(10, monsters.get(1).level()); } + + @Test + void generateShouldUseRateAsMultiplier() { + List monsters = generator.generate(repository.get(4)); + + assertCount(6, monsters); + assertArrayEquals(new int[] {31, 31, 34, 34, 34, 34}, monsters.stream().mapToInt(Monster::id).toArray()); + } }