Implement !autobuild command

The `!autobuild` (or `!ab`) command allows you to build a record without needing to add every id. Missing trekkers/discs/potentials/melodies will be auto selected by the server.

Usage:
`!autobuild [character/disc/potential/melody ids...] lv[target record level] s[target record score]`

Examples:
`!autobuild s99999` = Creates a record with a random main trekker with the max score.
`!autobuild 103 lv30` = Creates a record with this trekker at record rank 30.
`!autobuild 155 132 107 s25000` = Creates a record with these trekkers with a record score of about 25000.
`!autobuild 103 510301 510302 510303 510304 lv20` = Creates a record with this trekker and these potentials at record rank 20.

Notes:
- Target level overrides target score. So a command of `!autobuild lv30 s1000` would have a record rank of 30 and NOT a score of 1000.
- If there is no target level or score, the command will default to a target level of 40.
- You can only add trekkers/discs that you own.
This commit is contained in:
Melledy
2025-12-20 04:20:34 -08:00
parent b5fde4433d
commit 53839b48ca
7 changed files with 537 additions and 75 deletions

View File

@@ -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;

View File

@@ -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<GameCharacter>();
// 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<GameDisc>();
// 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<GameCharacter>();
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);
}
}
}

View File

@@ -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,10 +66,30 @@ 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)";
String result = "Created record for " + target.getName();
if (args.getSender() == null) {
result += " (This command may take time to update on the client)";
}
private void parseItem(StarTowerBuildData builder, int id, int count) {
return result;
}
@Getter
public static class StarTowerBuildData {
private Player player;
private StarTowerBuild build;
private List<GameCharacter> characters;
private List<GameDisc> discs;
public StarTowerBuildData(Player player) {
this.player = player;
this.build = new StarTowerBuild(player);
this.characters = new ArrayList<>();
this.discs = new ArrayList<>();
}
public void parseItem(int id, int count) {
// Get item data
var itemData = GameData.getItemDataTable().get(id);
if (itemData == null) {
@@ -82,30 +102,30 @@ public class BuildCommand implements CommandHandler {
// Parse by item id
switch (itemData.getItemSubType()) {
case Char -> {
var character = builder.getPlayer().getCharacters().getCharacterById(id);
var character = this.getPlayer().getCharacters().getCharacterById(id);
if (character == null || !character.getData().isAvailable()) {
break;
}
builder.addCharacter(character);
this.addCharacter(character);
}
case Disc -> {
var disc = builder.getPlayer().getCharacters().getDiscById(id);
var disc = this.getPlayer().getCharacters().getDiscById(id);
if (disc == null || !disc.getData().isAvailable()) {
break;
}
builder.addDisc(disc);
this.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);
this.getBuild().getPotentials().add(id, level);
}
case SubNoteSkill -> {
builder.getBuild().getSubNoteSkills().add(id, count);
this.getBuild().getSubNoteSkills().add(id, count);
}
default -> {
// Ignored
@@ -113,20 +133,6 @@ public class BuildCommand implements CommandHandler {
}
}
@Getter
private static class StarTowerBuildData {
private Player player;
private StarTowerBuild build;
private List<GameCharacter> characters;
private List<GameDisc> discs;
public StarTowerBuildData(Player player) {
this.player = player;
this.build = new StarTowerBuild(player);
this.characters = new ArrayList<>();
this.discs = new ArrayList<>();
}
public void addCharacter(GameCharacter character) {
if (this.characters.contains(character)) {
return;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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);