Implement custom battles using the /spawn command

Example: `/spawn [npc monster id] [battle monster id 1] [battle monster id 2] [battle monster id 3] lv80`
This commit is contained in:
Melledy
2024-05-11 04:47:46 -07:00
parent 24be0f3f3f
commit d87f04f510
12 changed files with 190 additions and 26 deletions

View File

@@ -80,7 +80,7 @@ Server commands can be run in the server console or in-game. There is a dummy us
/refill. Refill your skill points in open world.
/reload. Reloads the server config.
/scene [scene id] [floor id]. Teleports the player to the specified scene.
/spawn [monster/prop id] x[amount] s[stage id]. Spawns a monster or prop near the targeted player.
/spawn [npc monster id/prop id] s[stage id] x[amount] lv[level] r[radius] <battle monster ids...>. Spawns a monster or prop near the targeted player.
/stop. Stops the server
/unstuck @[player id]. Unstucks an offline player if they're in a scene that doesn't load.
/worldlevel [world level]. Sets the targeted player's equilibrium level.

View File

@@ -1,5 +1,8 @@
package emu.lunarcore.command.commands;
import java.util.Comparator;
import java.util.Set;
import emu.lunarcore.LunarCore;
import emu.lunarcore.command.Command;
import emu.lunarcore.command.CommandArgs;
@@ -10,6 +13,9 @@ import emu.lunarcore.data.config.MonsterInfo;
import emu.lunarcore.data.config.PropInfo;
import emu.lunarcore.data.excel.NpcMonsterExcel;
import emu.lunarcore.data.excel.PropExcel;
import emu.lunarcore.data.excel.StageExcel;
import emu.lunarcore.game.battle.BattleStage;
import emu.lunarcore.game.battle.CustomBattleStage;
import emu.lunarcore.game.enums.PropState;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.game.scene.entity.EntityMonster;
@@ -17,8 +23,11 @@ import emu.lunarcore.game.scene.entity.EntityProp;
import emu.lunarcore.util.Position;
import emu.lunarcore.util.Utils;
@Command(label = "spawn", permission = "player.spawn", requireTarget = true, desc = "/spawn [monster/prop id] [stage id] x[amount] lv[level] r[radius]. Spawns a monster or prop near the targeted player.")
@Command(label = "spawn", aliases = {"s"}, permission = "player.spawn", requireTarget = true, desc = "/spawn [npc monster id/prop id] s[stage id] x[amount] lv[level] r[radius] <battle monster ids...>. Spawns a monster or prop near the targeted player.")
public class SpawnCommand implements CommandHandler {
private static final Set<String> SEPARATORS = Set.of("/", "|", "\\");
private int baseNpcMonsterId;
private int baseStageId;
@Override
public void execute(CommandArgs args) {
@@ -29,9 +38,18 @@ public class SpawnCommand implements CommandHandler {
return;
}
// Get id
int id = Utils.parseSafeInt(args.get(0));
int stage = Math.max(Utils.parseSafeInt(args.get(1)), 1);
// Set spawn id
String spawnId = args.get(0);
int id = 0;
if (spawnId.equalsIgnoreCase("monster")) {
id = this.getBaseNpcMonsterId();
} else {
id = Utils.parseSafeInt(spawnId);
}
// Get args
int stageId = args.getStage();
int amount = Math.max(args.getAmount(), 1);
int radius = Math.max(args.getRank(), 5) * 1000;
@@ -44,6 +62,50 @@ public class SpawnCommand implements CommandHandler {
// Spawn monster
NpcMonsterExcel monsterExcel = GameData.getNpcMonsterExcelMap().get(id);
if (monsterExcel != null) {
// Calculate stage
BattleStage stage = null;
if (stageId > 0) {
// Set user specified stage
stage = GameData.getStageExcelMap().get(stageId);
} else if (args.getList().size() <= 1) {
// Get first stage in the excel table
stage = GameData.getStageExcelMap().get(this.getBaseStageId());
} else {
// Build custom stage
var customStage = new CustomBattleStage(this.getBaseStageId());
boolean startNewWave = false;
// Parse extra monster id args
for (int i = 1; i < args.getList().size(); i++) {
String arg = args.get(i);
if (SEPARATORS.contains(arg)) {
// Wave separator
startNewWave = true;
} else {
// Add monster to wave
int monster = Utils.parseSafeInt(arg);
if (GameData.getMonsterExcelMap().containsKey(monster)) {
customStage.addMonster(monster, startNewWave);
}
// Reset
startNewWave = false;
}
}
// Set stage
if (customStage.getMonsterWaves().size() > 0) {
stage = customStage;
}
}
if (stage == null) {
args.sendMessage("Error: No stage or monster waves set");
return;
}
// Get first monster config from floor info that isnt a boss monster
GroupInfo groupInfo = null;
MonsterInfo monsterInfo = null;
@@ -73,7 +135,7 @@ public class SpawnCommand implements CommandHandler {
EntityMonster monster = new EntityMonster(target.getScene(), monsterExcel, groupInfo, monsterInfo);
monster.getPos().set(pos);
monster.setEventId(monsterInfo.getEventID());
monster.setCustomStageId(stage);
monster.setCustomStage(stage);
if (args.getLevel() > 0) {
monster.setCustomLevel(Math.min(args.getLevel(), 100));
@@ -131,4 +193,25 @@ public class SpawnCommand implements CommandHandler {
args.sendMessage("Error: Invalid id");
}
private int getBaseNpcMonsterId() {
if (this.baseNpcMonsterId == 0) {
var excel = GameData.getNpcMonsterExcelMap().values().stream().min(Comparator.comparing(NpcMonsterExcel::getId)).orElseGet(null);
if (excel != null) {
this.baseNpcMonsterId = excel.getId();
}
}
return this.baseNpcMonsterId;
}
private int getBaseStageId() {
if (this.baseStageId == 0) {
var excel = GameData.getStageExcelMap().values().stream().min(Comparator.comparing(StageExcel::getId)).orElseGet(null);
if (excel != null) {
this.baseStageId = excel.getId();
}
}
return this.baseStageId;
}
}

View File

@@ -8,6 +8,7 @@ import lombok.Getter;
@ResourceType(name = {"MonsterConfig.json"})
public class MonsterExcel extends GameResource {
private int MonsterID;
private long MonsterName;
@Override
public int getId() {

View File

@@ -5,6 +5,7 @@ import java.util.List;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.game.battle.BattleStage;
import emu.lunarcore.game.enums.StageType;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
@@ -13,7 +14,7 @@ import lombok.Getter;
@Getter
@ResourceType(name = {"StageConfig.json"})
public class StageExcel extends GameResource {
public class StageExcel extends GameResource implements BattleStage {
private int StageID;
private long StageName;
private StageType StageType;

View File

@@ -7,7 +7,6 @@ import java.util.function.Consumer;
import emu.lunarcore.GameConstants;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.excel.StageExcel;
import emu.lunarcore.game.avatar.GameAvatar;
import emu.lunarcore.game.inventory.GameItem;
import emu.lunarcore.game.player.Player;
@@ -39,7 +38,7 @@ public class Battle {
private final List<GameItem> drops;
private final long timestamp;
private StageExcel stage; // Main battle stage
private BattleStage stage; // Main battle stage
private IntList battleEvents; // TODO maybe turn it into a map?
private Int2ObjectMap<BattleTargetList> battleTargets; // TODO use custom battle target object as value type in case we need to save battles to the db
@@ -67,11 +66,11 @@ public class Battle {
this.timestamp = System.currentTimeMillis();
}
public Battle(Player player, PlayerLineup lineup, StageExcel stage) {
public Battle(Player player, PlayerLineup lineup, BattleStage stage) {
this(player, lineup, stage, true);
}
public Battle(Player player, PlayerLineup lineup, StageExcel stage, boolean loadStage) {
public Battle(Player player, PlayerLineup lineup, BattleStage stage, boolean loadStage) {
this(player, lineup);
this.stage = stage;
@@ -80,11 +79,11 @@ public class Battle {
}
}
public Battle(Player player, PlayerLineup lineup, List<StageExcel> stages) {
public Battle(Player player, PlayerLineup lineup, List<? extends BattleStage> stages) {
this(player, lineup);
this.stage = stages.get(0);
for (StageExcel stage : stages) {
for (var stage : stages) {
this.loadStage(stage);
}
}
@@ -105,8 +104,11 @@ public class Battle {
}
// Get stage
StageExcel stage = GameData.getStageExcelMap().get(npcMonster.getStageId());
if (stage == null) continue;
BattleStage stage = npcMonster.getCustomStage();
if (stage == null) {
stage = GameData.getStageExcelMap().get(npcMonster.getStageId());
if (stage == null) continue;
}
// Set main battle stage if we havent already
if (this.stage == null) {
@@ -118,11 +120,11 @@ public class Battle {
}
}
private void loadStage(StageExcel stage) {
private void loadStage(BattleStage stage) {
this.loadStage(stage, null);
}
private void loadStage(StageExcel stage, EntityMonster npcMonster) {
private void loadStage(BattleStage stage, EntityMonster npcMonster) {
// Build monster waves
for (IntList stageMonsterWave : stage.getMonsterWaves()) {
// Create battle wave

View File

@@ -1,6 +1,5 @@
package emu.lunarcore.game.battle;
import emu.lunarcore.data.excel.StageExcel;
import emu.lunarcore.proto.SceneMonsterOuterClass.SceneMonster;
import emu.lunarcore.proto.SceneMonsterWaveOuterClass.SceneMonsterWave;
import it.unimi.dsi.fastutil.ints.IntArrayList;
@@ -10,13 +9,13 @@ import lombok.Setter;
@Getter
public class BattleMonsterWave {
private final StageExcel stage;
private final BattleStage stage;
private IntList monsters;
@Setter
private int customLevel;
public BattleMonsterWave(StageExcel stage) {
public BattleMonsterWave(BattleStage stage) {
this.stage = stage;
this.monsters = new IntArrayList();
}

View File

@@ -0,0 +1,16 @@
package emu.lunarcore.game.battle;
import java.util.List;
import emu.lunarcore.game.enums.StageType;
import it.unimi.dsi.fastutil.ints.IntList;
public interface BattleStage {
public int getId();
public StageType getStageType();
public List<IntList> getMonsterWaves();
}

View File

@@ -0,0 +1,42 @@
package emu.lunarcore.game.battle;
import java.util.ArrayList;
import java.util.List;
import emu.lunarcore.game.enums.StageType;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
public class CustomBattleStage implements BattleStage {
private int id;
private List<IntList> monsterWaves;
public CustomBattleStage(int id) {
this.id = id;
this.monsterWaves = new ArrayList<>();
}
@Override
public StageType getStageType() {
return StageType.Mainline;
}
public void addMonster(int monsterId, boolean startNewWave) {
if (this.monsterWaves.size() == 0 || startNewWave) {
IntList wave = new IntArrayList();
wave.add(monsterId);
this.monsterWaves.add(wave);
} else {
IntList wave = this.monsterWaves.get(this.monsterWaves.size() - 1);
if (wave.size() < 5) {
wave.add(monsterId);
} else {
this.addMonster(monsterId, true);
}
}
}
}

View File

@@ -69,7 +69,7 @@ public class ChallengeEntityLoader extends SceneEntityLoader {
// Create monster from group monster info
EntityMonster monster = new EntityMonster(scene, npcMonsterExcel, group, monsterInfo);
monster.setEventId(challengeMonsterInfo.getEventId());
monster.setCustomStageId(challengeMonsterInfo.getEventId());
monster.setCustomStage(challengeMonsterInfo.getEventId());
return monster;
}

View File

@@ -58,7 +58,7 @@ public class RogueEntityLoader extends SceneEntityLoader {
// Actually create the monster now
EntityMonster monster = new EntityMonster(scene, npcMonster, group, monsterInfo);
monster.setEventId(rogueMonster.getEventID());
monster.setCustomStageId(rogueMonster.getEventID());
monster.setCustomStage(rogueMonster.getEventID());
return monster;
}

View File

@@ -5,6 +5,7 @@ import emu.lunarcore.data.config.GroupInfo;
import emu.lunarcore.data.config.MonsterInfo;
import emu.lunarcore.data.excel.NpcMonsterExcel;
import emu.lunarcore.game.battle.Battle;
import emu.lunarcore.game.battle.BattleStage;
import emu.lunarcore.game.inventory.ItemParamMap;
import emu.lunarcore.game.scene.Scene;
import emu.lunarcore.game.scene.SceneBuff;
@@ -37,7 +38,7 @@ public class EntityMonster implements GameEntity, Tickable {
@Setter private SceneBuff tempBuff;
private int farmElementId;
@Setter private int customStageId;
private BattleStage customStage;
@Setter private int customLevel;
public EntityMonster(Scene scene, NpcMonsterExcel excel, GroupInfo group, MonsterInfo monsterInfo) {
@@ -59,13 +60,21 @@ public class EntityMonster implements GameEntity, Tickable {
}
public int getStageId() {
if (this.customStageId == 0) {
if (this.customStage == null) {
return (this.getEventId() * 10) + worldLevel;
} else {
return this.customStageId;
return this.customStage.getId();
}
}
public void setCustomStage(BattleStage stage) {
this.customStage = stage;
}
public void setCustomStage(int stageId) {
this.customStage = GameData.getStageExcelMap().get(stageId);
}
public synchronized SceneBuff addBuff(int caster, int buffId, int duration) {
if (this.buffs == null) {
this.buffs = new Int2ObjectOpenHashMap<>();

View File

@@ -114,6 +114,17 @@ public class Handbook {
writer.println(textMap.getOrDefault(excel.getStageName(), "null"));
}
// Dump monsters
writer.println(System.lineSeparator());
writer.println("# Battle Monsters");
list = GameData.getMonsterExcelMap().keySet().intStream().sorted().boxed().toList();
for (int id : list) {
MonsterExcel excel = GameData.getMonsterExcelMap().get(id);
writer.print(excel.getId());
writer.print(" : ");
writer.println(textMap.getOrDefault(excel.getMonsterName(), "null"));
}
// Dump stages
writer.println(System.lineSeparator());
writer.println("# Mazes");