diff --git a/src/main/java/emu/nebula/GameConstants.java b/src/main/java/emu/nebula/GameConstants.java index 10a0369..c66a008 100644 --- a/src/main/java/emu/nebula/GameConstants.java +++ b/src/main/java/emu/nebula/GameConstants.java @@ -47,6 +47,10 @@ public class GameConstants { public static final int CHARACTER_MAX_GEM_PRESETS = 3; public static final int CHARACTER_MAX_GEM_SLOTS = 3; + public static final int CHARACTER_TAG_VANGUARD = 101; + public static final int CHARACTER_TAG_VERSATILE = 102; + public static final int CHARACTER_TAG_SUPPORT = 103; + public static final int MAX_FORMATIONS = 6; public static final int MAX_SHOWCASE_IDS = 5; diff --git a/src/main/java/emu/nebula/command/commands/AutoBuildCommand.java b/src/main/java/emu/nebula/command/commands/AutoBuildCommand.java new file mode 100644 index 0000000..b6ed357 --- /dev/null +++ b/src/main/java/emu/nebula/command/commands/AutoBuildCommand.java @@ -0,0 +1,443 @@ +package emu.nebula.command.commands; + +import java.util.ArrayList; +import java.util.Collections; + +import emu.nebula.GameConstants; +import emu.nebula.command.Command; +import emu.nebula.command.CommandArgs; +import emu.nebula.command.CommandHandler; +import emu.nebula.command.commands.BuildCommand.StarTowerBuildData; +import emu.nebula.data.GameData; +import emu.nebula.game.character.GameCharacter; +import emu.nebula.game.character.GameDisc; +import emu.nebula.game.inventory.ItemParamMap; +import emu.nebula.net.NetMsgId; +import emu.nebula.util.Utils; + +import it.unimi.dsi.fastutil.ints.IntArrayList; + +@Command( + label = "autobuild", + aliases = {"ab"}, + permission = "player.build", + requireTarget = true, + desc = "!autobuild [character/disc/potential/melody ids...] lv[target record level] s[target record score] = Generates a record for the player with the target score/record level" +) +public class AutoBuildCommand implements CommandHandler { + private static final int[] COMMON_SUB_NOTE_SKILLS = new int[] { + 90011, 90012, 90013, 90014, 90015, 90016, 90017 + }; + + @Override + public String execute(CommandArgs args) { + // Create record + var target = args.getTarget(); + var builder = new StarTowerBuildData(target); + + // Parse items + for (String arg : args.getList()) { + int id = Utils.parseSafeInt(arg); + int count = 1; + + builder.parseItem(id, count); + } + + if (args.getMap() != null) { + for (var entry : args.getMap().int2IntEntrySet()) { + int id = entry.getIntKey(); + int count = entry.getIntValue(); + + builder.parseItem(id, count); + } + } + + // Remove extra characters/discs + while (builder.getCharacters().size() > 3) { + builder.getCharacters().removeLast(); + } + + while (builder.getDiscs().size() > 6) { + builder.getDiscs().removeLast(); + } + + // Add random characters/discs + if (builder.getCharacters().size() < 3) { + int count = 3 - builder.getCharacters().size(); + for (int i = 0; i < count; i++) { + this.pickRandomCharacter(builder); + } + + if (builder.getCharacters().size() < 3) { + return "Error: Not enough trekkers in the record"; + } + } + + if (builder.getDiscs().size() < 6) { + int count = 6 - builder.getDiscs().size(); + for (int i = 0; i < count; i++) { + this.pickRandomDisc(builder); + } + } + + // Get target score + int targetScore = 0; + int targetLevel = 0; + + if (args.getSkill() < 0) { + targetLevel = 40; + } else { + targetScore = args.getSkill(); + } + + if (args.getLevel() > 0) { + // Target level overrides target score. + targetLevel = args.getLevel(); + } + + if (targetLevel > 0) { + var data = GameData.getStarTowerBuildRankDataTable().get(targetLevel); + if (data != null) { + targetScore = data.getMinGrade(); + } + } + + // Pick random potentials and sub notes + this.generate(builder, targetScore); + + // Create record + var build = builder.toBuild(); + + // Add to star tower manager + target.getStarTowerManager().addBuild(build); + + // Send package to player + target.addNextPackage(NetMsgId.st_import_build_notify, build.toProto()); + + // Send result to player + String result = "Generated record for " + target.getName(); + + if (args.getSender() == null) { + result += " (This command may take time to update on the client)"; + } + + return result; + } + + private void pickRandomCharacter(StarTowerBuildData builder) { + // Random list + var list = new ArrayList(); + + // Create list of possible characters to add + var characters = builder.getPlayer().getCharacters().getCharacterCollection() + .stream() + .filter(c -> !builder.getCharacters().contains(c)) + .toList(); + + // Check if record is empty + if (builder.getCharacters().isEmpty()) { + // Get random vanguard trekker + for (var character : characters) { + if (character.getData().getDes().getTag().contains(GameConstants.CHARACTER_TAG_VANGUARD)) { + list.add(character); + } + } + + // Add any trekker if we dont have any vanguard trekkers + if (list.isEmpty()) { + list.addAll(characters); + } + } else { + // Get element of main trekker + var main = builder.getCharacters().get(0); + var element = main.getElementType(); + + // Get trekkers of the same element + for (var character : characters) { + if (character.getData().getElementType() == element) { + list.add(character); + } + } + + // Add any trekker if we dont have any trekkers of the same element + if (list.isEmpty()) { + list.addAll(characters); + } + + // Shuffle list to make it random + Collections.shuffle(list); + + // Add first support trekker we find + for (var character : list) { + if (character.getData().getDes().getTag().contains(GameConstants.CHARACTER_TAG_SUPPORT)) { + builder.getCharacters().add(character); + return; + } + } + + // If we have no support trekkers, then we look for versatile trekkers + for (var character : list) { + if (character.getData().getDes().getTag().contains(GameConstants.CHARACTER_TAG_VERSATILE)) { + builder.getCharacters().add(character); + return; + } + } + } + + // Add random trekker from list + if (list.size() > 0) { + builder.getCharacters().add(Utils.randomElement(list)); + } + } + + private void pickRandomDisc(StarTowerBuildData builder) { + // Get element of main trekker + var main = builder.getCharacters().get(0); + var element = main.getElementType(); + + // Random list + var list = new ArrayList(); + + // Create list of possible discs to add + var discs = builder.getPlayer().getCharacters().getDiscCollection() + .stream() + .filter(d -> !builder.getDiscs().contains(d)) + .toList(); + + // Get discs of the same element + for (var disc : discs) { + if (disc.getData().getElementType() == element) { + list.add(disc); + } + } + + if (list.isEmpty()) { + list.addAll(discs); + } + + // End early if list is still empty + if (list.isEmpty()) { + return; + } + + // Shuffle list to make it random + Collections.shuffle(list); + + // Find random disc + for (var disc : list) { + var item = GameData.getItemDataTable().get(disc.getDiscId()); + if (item.getRarity() == 1) { + builder.getDiscs().add(disc); + return; + } + } + + for (var disc : list) { + var item = GameData.getItemDataTable().get(disc.getDiscId()); + if (item.getRarity() == 2) { + builder.getDiscs().add(disc); + return; + } + } + + // Just add the first disc that we can find + builder.getDiscs().add(list.get(0)); + } + + private void generate(StarTowerBuildData builder, int targetScore) { + // Get possible sub notes + int subNoteScore = (int) (targetScore * .4D); + + // Get possible drops + var drops = new IntArrayList(); + + for (var character : builder.getCharacters()) { + var element = character.getData().getElementType(); + + if (element.getSubNoteSkillItemId() == 0) { + continue; + } + + if (!drops.contains(element.getSubNoteSkillItemId())) { + drops.add(element.getSubNoteSkillItemId()); + } + } + + for (var disc : builder.getDiscs()) { + var element = disc.getData().getElementType(); + + if (element.getSubNoteSkillItemId() == 0) { + continue; + } + + if (!drops.contains(element.getSubNoteSkillItemId())) { + drops.add(element.getSubNoteSkillItemId()); + } + } + + for (int id : COMMON_SUB_NOTE_SKILLS) { + drops.add(id); + } + + // Randomize sub note ids + Collections.shuffle(drops); + + // Distribute sub notes randomly + int amount = (int) Math.ceil(subNoteScore / 15D); + int totalSubNotes = amount; + + // Allocate budget for each sub note + var budget = new ItemParamMap(); + double totalValue = 0; + + for (int subNote : drops) { + int value = Utils.randomRange(1, 10); + budget.add(subNote, value); + totalValue += value; + } + + // Add random sub notes + for (int subNote : drops) { + // Get budgeted value + int value = budget.get(subNote); + int count = (int) Math.ceil((value / totalValue) * totalSubNotes); + + // Get current sub notes + int cur = builder.getBuild().getSubNoteSkills().get(subNote); + int max = Math.max(99 - cur, 0); + + // Clamp + count = Math.min(Math.min(count, amount), max); + amount -= count; + + // Add sub notes + builder.getBuild().getSubNoteSkills().add(subNote, count); + } + + // Add leftover sub notes + if (amount > 0) { + // Randomize again + Collections.shuffle(drops); + + // Add to first sub note that has less than 99 + for (int subNote : drops) { + // End if we have no more sub notes to give + if (amount <= 0) { + break; + } + + // Get current sub notes + int cur = builder.getBuild().getSubNoteSkills().get(subNote); + if (cur >= 99) { + continue; + } + + // Add + int count = Math.min(99 - cur, amount); + amount -= count; + + builder.getBuild().getSubNoteSkills().add(subNote, count); + } + } + + // Calcluate score + builder.toBuild().calculateScore(); + + // Get target potential score + int potentialScore = Math.max(targetScore - builder.getBuild().getScore(), 0); + + // Init weighted list of characters + var characters = new ArrayList(); + + characters.add(builder.getCharacters().get(0)); + characters.add(builder.getCharacters().get(0)); // Main character gets an extra chance to get more potentials + characters.add(builder.getCharacters().get(1)); + characters.add(builder.getCharacters().get(2)); + + Collections.shuffle(characters); + + // Get current amount of special potentials + var specialCounter = new ItemParamMap(); + + for (var entry : builder.getBuild().getPotentials()) { + var potential = GameData.getPotentialDataTable().get(entry.getIntKey()); + if (potential == null) continue; + + if (potential.isSpecial()) { + specialCounter.add(potential.getCharId(), 1); + } + } + + // Cache main trekker + var main = builder.getCharacters().get(0); + + // Get random potentials + while (potentialScore > 0) { + // End + if (potentialScore <= 0 || characters.isEmpty()) { + break; + } + + // Get random character + var character = Utils.randomElement(characters); + + // Get character potential data + var data = GameData.getCharPotentialDataTable().get(character.getCharId()); + + if (data == null) { + break; + } + + // Check if we should give a special potential + int sp = specialCounter.get(character.getCharId()); + boolean special = false; + + if (sp < 2 && potentialScore >= 180) { + special = Utils.randomChance(.25); + } + + if (special) { + specialCounter.add(character.getCharId(), 1); + } + + // Get possible potential list + var list = data.getPotentialList(main == character, special); + + // Remove potentials we already have maxed out + var potentials = new IntArrayList(); + + for (int id : list) { + // Get potential data + var potential = GameData.getPotentialDataTable().get(id); + if (potential == null) continue; + + // Filter out max level ones + int curLevel = builder.getBuild().getPotentials().get(id); + int maxLevel = potential.getMaxLevel(); + + if (curLevel >= maxLevel) { + continue; + } + + // Add + potentials.add(id); + } + + // Remove character if we dont have any possible potentials for it + if (potentials.isEmpty()) { + characters.removeIf(c -> c == character); + continue; + } + + // Get random potential + int id = Utils.randomElement(potentials); + var potential = GameData.getPotentialDataTable().get(id); + + // Add + builder.getBuild().getPotentials().add(id, 1); + + // Decrement score + potentialScore -= potential.getBuildScore(1); + } + } +} diff --git a/src/main/java/emu/nebula/command/commands/BuildCommand.java b/src/main/java/emu/nebula/command/commands/BuildCommand.java index bc4d67a..428bc1a 100644 --- a/src/main/java/emu/nebula/command/commands/BuildCommand.java +++ b/src/main/java/emu/nebula/command/commands/BuildCommand.java @@ -35,7 +35,7 @@ public class BuildCommand implements CommandHandler { int id = Utils.parseSafeInt(arg); int count = 1; - this.parseItem(builder, id, count); + builder.parseItem(id, count); } if (args.getMap() != null) { @@ -43,7 +43,7 @@ public class BuildCommand implements CommandHandler { int id = entry.getIntKey(); int count = entry.getIntValue(); - this.parseItem(builder, id, count); + builder.parseItem(id, count); } } @@ -66,55 +66,17 @@ public class BuildCommand implements CommandHandler { target.addNextPackage(NetMsgId.st_import_build_notify, build.toProto()); // Send result to player - return "Created record for " + target.getName() + " (This command make take time to update on the client)"; - } - - private void parseItem(StarTowerBuildData builder, int id, int count) { - // Get item data - var itemData = GameData.getItemDataTable().get(id); - if (itemData == null) { - return; + String result = "Created record for " + target.getName(); + + if (args.getSender() == null) { + result += " (This command may take time to update on the client)"; } - // Clamp - count = Math.max(count, 1); - - // Parse by item id - switch (itemData.getItemSubType()) { - case Char -> { - var character = builder.getPlayer().getCharacters().getCharacterById(id); - if (character == null || !character.getData().isAvailable()) { - break; - } - - builder.addCharacter(character); - } - case Disc -> { - var disc = builder.getPlayer().getCharacters().getDiscById(id); - if (disc == null || !disc.getData().isAvailable()) { - break; - } - - builder.addDisc(disc); - } - case Potential, SpecificPotential -> { - var potentialData = GameData.getPotentialDataTable().get(id); - if (potentialData == null) break; - - int level = Math.min(count, potentialData.getMaxLevel()); - builder.getBuild().getPotentials().add(id, level); - } - case SubNoteSkill -> { - builder.getBuild().getSubNoteSkills().add(id, count); - } - default -> { - // Ignored - } - } + return result; } @Getter - private static class StarTowerBuildData { + public static class StarTowerBuildData { private Player player; private StarTowerBuild build; private List characters; @@ -127,6 +89,50 @@ public class BuildCommand implements CommandHandler { this.discs = new ArrayList<>(); } + public void parseItem(int id, int count) { + // Get item data + var itemData = GameData.getItemDataTable().get(id); + if (itemData == null) { + return; + } + + // Clamp + count = Math.max(count, 1); + + // Parse by item id + switch (itemData.getItemSubType()) { + case Char -> { + var character = this.getPlayer().getCharacters().getCharacterById(id); + if (character == null || !character.getData().isAvailable()) { + break; + } + + this.addCharacter(character); + } + case Disc -> { + var disc = this.getPlayer().getCharacters().getDiscById(id); + if (disc == null || !disc.getData().isAvailable()) { + break; + } + + this.addDisc(disc); + } + case Potential, SpecificPotential -> { + var potentialData = GameData.getPotentialDataTable().get(id); + if (potentialData == null) break; + + int level = Math.min(count, potentialData.getMaxLevel()); + this.getBuild().getPotentials().add(id, level); + } + case SubNoteSkill -> { + this.getBuild().getSubNoteSkills().add(id, count); + } + default -> { + // Ignored + } + } + } + public void addCharacter(GameCharacter character) { if (this.characters.contains(character)) { return; diff --git a/src/main/java/emu/nebula/data/resources/CharPotentialDef.java b/src/main/java/emu/nebula/data/resources/CharPotentialDef.java index 1d08f4d..8c7eec3 100644 --- a/src/main/java/emu/nebula/data/resources/CharPotentialDef.java +++ b/src/main/java/emu/nebula/data/resources/CharPotentialDef.java @@ -2,6 +2,8 @@ package emu.nebula.data.resources; import emu.nebula.data.BaseDef; import emu.nebula.data.ResourceType; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; import lombok.Getter; @Getter @@ -19,4 +21,31 @@ public class CharPotentialDef extends BaseDef { public int getId() { return Id; } + + public IntList getPotentialList(boolean main, boolean special) { + // Create list + var list = new IntArrayList(); + + // + if (main) { + if (special) { + list.addElements(0, this.getMasterSpecificPotentialIds()); + } else { + list.addElements(0, this.getMasterNormalPotentialIds()); + } + } else { + if (special) { + list.addElements(0, this.getAssistSpecificPotentialIds()); + } else { + list.addElements(0, this.getAssistNormalPotentialIds()); + } + } + + if (!special) { + list.addElements(0, this.getCommonPotentialIds()); + } + + // Complete + return list; + } } diff --git a/src/main/java/emu/nebula/data/resources/CharacterDesDef.java b/src/main/java/emu/nebula/data/resources/CharacterDesDef.java index d4bd807..3dbe090 100644 --- a/src/main/java/emu/nebula/data/resources/CharacterDesDef.java +++ b/src/main/java/emu/nebula/data/resources/CharacterDesDef.java @@ -11,7 +11,7 @@ import lombok.Getter; @ResourceType(name = "CharacterDes.json", loadPriority = LoadPriority.LOW) public class CharacterDesDef extends BaseDef { private int Id; - private int[] Tag; + private IntOpenHashSet Tag; private IntOpenHashSet PreferTags; private IntOpenHashSet HateTags; diff --git a/src/main/java/emu/nebula/data/resources/PotentialDef.java b/src/main/java/emu/nebula/data/resources/PotentialDef.java index 181ebc4..98351f2 100644 --- a/src/main/java/emu/nebula/data/resources/PotentialDef.java +++ b/src/main/java/emu/nebula/data/resources/PotentialDef.java @@ -22,14 +22,14 @@ public class PotentialDef extends BaseDef { return Id; } - public boolean isRare() { + public boolean isSpecial() { return this.BranchType != 3; } public int getMaxLevel() { // Check if regular potential if (this.BranchType == 3) { - return this.BuildScore.length; + return 6; } // Special potential should always have a max level of 1 diff --git a/src/main/java/emu/nebula/game/tower/StarTowerGame.java b/src/main/java/emu/nebula/game/tower/StarTowerGame.java index d87b446..1fdb12c 100644 --- a/src/main/java/emu/nebula/game/tower/StarTowerGame.java +++ b/src/main/java/emu/nebula/game/tower/StarTowerGame.java @@ -421,7 +421,7 @@ public class StarTowerGame { this.getPotentials().put(id, nextLevel); // Add to rare potential count - if (potentialData.isRare()) { + if (potentialData.isSpecial()) { this.getRarePotentialCount().add(potentialData.getCharId(), 1); } @@ -574,46 +574,26 @@ public class StarTowerGame { /** * Creates a potential selector for the specified character */ - public StarTowerPotentialCase createPotentialSelector(int charId, boolean rare) { + public StarTowerPotentialCase createPotentialSelector(int charId, boolean special) { // Check character id if (charId <= 0) { charId = this.getRandomCharId(); } // Make sure character can't have more than 2 rare potentials - if (rare && this.getRarePotentialCount(charId) >= 2) { + if (special && this.getRarePotentialCount(charId) >= 2) { return null; } // Get character potentials var data = GameData.getCharPotentialDataTable().get(charId); + if (data == null) { return null; } // Random potentials list - var list = new IntArrayList(); - - // Add potentials based on character role - boolean isMainCharacter = this.getCharIds()[0] == charId; - - if (isMainCharacter) { - if (rare) { - list.addElements(0, data.getMasterSpecificPotentialIds()); - } else { - list.addElements(0, data.getMasterNormalPotentialIds()); - } - } else { - if (rare) { - list.addElements(0, data.getAssistSpecificPotentialIds()); - } else { - list.addElements(0, data.getAssistNormalPotentialIds()); - } - } - - if (!rare) { - list.addElements(0, data.getCommonPotentialIds()); - } + var list = data.getPotentialList(this.getCharIds()[0] == charId, special); // Remove potentials we already have maxed out var potentials = new IntArrayList(); @@ -674,7 +654,7 @@ public class StarTowerGame { } // Creator potential selector case - if (rare) { + if (special) { return new StarTowerSelectSpecialPotentialCase(this, charId, selector); } else { return new StarTowerPotentialCase(this, charId, selector);