diff --git a/src/main/java/emu/nebula/GameConstants.java b/src/main/java/emu/nebula/GameConstants.java index 5a15fe6..2426efa 100644 --- a/src/main/java/emu/nebula/GameConstants.java +++ b/src/main/java/emu/nebula/GameConstants.java @@ -55,6 +55,9 @@ public class GameConstants { public static final int[] TOWER_COMMON_SUB_NOTE_SKILLS = new int[] { 90011, 90012, 90013, 90014, 90015, 90016, 90017 }; + public static final int[] TOWER_EVENTS_IDS = new int[] { + 101, 102, 104, 105, 106, 107, 108, 114, 115, 116, 126, 127, 128 + }; public static int[][] VAMPIRE_SURVIVOR_BONUS_POWER = new int[][] { new int[] {100, 120}, diff --git a/src/main/java/emu/nebula/data/GameData.java b/src/main/java/emu/nebula/data/GameData.java index ba2f29a..dbca835 100644 --- a/src/main/java/emu/nebula/data/GameData.java +++ b/src/main/java/emu/nebula/data/GameData.java @@ -116,6 +116,7 @@ public class GameData { @Getter private static DataTable StarTowerGrowthNodeDataTable = new DataTable<>(); @Getter private static DataTable StarTowerFloorExpDataTable = new DataTable<>(); @Getter private static DataTable StarTowerTeamExpDataTable = new DataTable<>(); + @Getter private static DataTable StarTowerEventDataTable = new DataTable<>(); @Getter private static DataTable SubNoteSkillPromoteGroupDataTable = new DataTable<>(); @Getter private static DataTable PotentialDataTable = new DataTable<>(); diff --git a/src/main/java/emu/nebula/data/resources/EventOptionsDef.java b/src/main/java/emu/nebula/data/resources/EventOptionsDef.java new file mode 100644 index 0000000..53e988b --- /dev/null +++ b/src/main/java/emu/nebula/data/resources/EventOptionsDef.java @@ -0,0 +1,34 @@ +package emu.nebula.data.resources; + +import emu.nebula.data.BaseDef; +import emu.nebula.data.GameData; +import emu.nebula.data.ResourceType; +import emu.nebula.data.ResourceType.LoadPriority; + +import lombok.Getter; + +/** + * We don't need a DataTable for this, since we are only using this class to verify event options for the client + */ +@Getter +@ResourceType(name = "EventOptions.json", loadPriority = LoadPriority.LOW) +public class EventOptionsDef extends BaseDef { + private int Id; + + @Override + public int getId() { + return Id; + } + + @Override + public void onLoad() { + // Get event + var event = GameData.getStarTowerEventDataTable().get(this.Id / 100); + if (event == null) { + return; + } + + // Add to avaliable options + event.getOptionIds().add(this.getId()); + } +} diff --git a/src/main/java/emu/nebula/data/resources/StarTowerEventDef.java b/src/main/java/emu/nebula/data/resources/StarTowerEventDef.java new file mode 100644 index 0000000..4fad857 --- /dev/null +++ b/src/main/java/emu/nebula/data/resources/StarTowerEventDef.java @@ -0,0 +1,35 @@ +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 +@ResourceType(name = "StarTowerEvent.json") +public class StarTowerEventDef extends BaseDef { + private int Id; + private int[] RelatedNPCs; + + private transient IntList optionIds; + + @Override + public int getId() { + return Id; + } + + /** + * Returns a deep copy of our option ids + */ + public IntList getClonedOptionIds() { + var list = new IntArrayList(); + list.addAll(this.getOptionIds()); + return list; + } + + @Override + public void onLoad() { + this.optionIds = new IntArrayList(); + } +} diff --git a/src/main/java/emu/nebula/game/tower/StarTowerGame.java b/src/main/java/emu/nebula/game/tower/StarTowerGame.java index 6d635d0..ca9d96e 100644 --- a/src/main/java/emu/nebula/game/tower/StarTowerGame.java +++ b/src/main/java/emu/nebula/game/tower/StarTowerGame.java @@ -395,6 +395,11 @@ public class StarTowerGame { change.add(info); } case SubNoteSkill -> { + // Sanity check to make sure we dont remove more than what we have + if (count < 0) { + count = Math.max(count, -this.getItems().get(id)); + } + // Add to items this.getItems().add(id, count); @@ -409,6 +414,11 @@ public class StarTowerGame { this.getNewInfos().add(id, count); } case Res -> { + // Sanity check to make sure we dont remove more than what we have + if (count < 0) { + count = Math.max(count, -this.getRes().get(id)); + } + // Add to res this.getRes().add(id, count); @@ -484,10 +494,14 @@ public class StarTowerGame { return this.createPotentialSelector(0); } + public StarTowerBaseCase createPotentialSelector(int charId) { + return this.createPotentialSelector(charId, false); + } + /** * Creates a potential selector for the specified character */ - public StarTowerBaseCase createPotentialSelector(int charId) { + public StarTowerBaseCase createPotentialSelector(int charId, boolean rareOnly) { // Check character id if (charId <= 0) { charId = this.getRandomCharId(); @@ -507,13 +521,21 @@ public class StarTowerGame { if (isMainCharacter) { list.addElements(0, data.getMasterSpecificPotentialIds()); - list.addElements(0, data.getMasterNormalPotentialIds()); + + if (!rareOnly) { + list.addElements(0, data.getMasterNormalPotentialIds()); + } } else { list.addElements(0, data.getAssistSpecificPotentialIds()); - list.addElements(0, data.getAssistNormalPotentialIds()); + + if (!rareOnly) { + list.addElements(0, data.getAssistNormalPotentialIds()); + } } - list.addElements(0, data.getCommonPotentialIds()); + if (!rareOnly) { + list.addElements(0, data.getCommonPotentialIds()); + } // Remove potentials we already have maxed out var potentials = new IntArrayList(); @@ -613,9 +635,13 @@ public class StarTowerGame { this.pendingSubNotes = amount; } + public int getRandomSubNoteId() { + return Utils.randomElement(this.getSubNoteDropList()); + } + private PlayerChangeInfo addRandomSubNoteSkills(PlayerChangeInfo change) { + int id = this.getRandomSubNoteId(); int count = Utils.randomRange(1, 3); - int id = Utils.randomElement(this.getSubNoteDropList()); this.addItem(id, count, change); diff --git a/src/main/java/emu/nebula/game/tower/cases/StarTowerHawkerCase.java b/src/main/java/emu/nebula/game/tower/cases/StarTowerHawkerCase.java index 027bc23..c6b1f9c 100644 --- a/src/main/java/emu/nebula/game/tower/cases/StarTowerHawkerCase.java +++ b/src/main/java/emu/nebula/game/tower/cases/StarTowerHawkerCase.java @@ -72,7 +72,7 @@ public class StarTowerHawkerCase extends StarTowerBaseCase { this.addGoods(goods); } - // Apply discounts based on star tower talents + // Apply discounts based on star tower growth nodes if (getModifiers().isShopDiscountTier1()) { this.applyDiscount(1.0, 2, 0.8); } diff --git a/src/main/java/emu/nebula/game/tower/cases/StarTowerNpcEventCase.java b/src/main/java/emu/nebula/game/tower/cases/StarTowerNpcEventCase.java index 280e29e..4fda627 100644 --- a/src/main/java/emu/nebula/game/tower/cases/StarTowerNpcEventCase.java +++ b/src/main/java/emu/nebula/game/tower/cases/StarTowerNpcEventCase.java @@ -1,8 +1,15 @@ package emu.nebula.game.tower.cases; +import java.util.Collections; + +import emu.nebula.GameConstants; +import emu.nebula.data.resources.StarTowerEventDef; +import emu.nebula.game.player.PlayerChangeInfo; +import emu.nebula.proto.PublicStarTower.NPCAffinityInfo; import emu.nebula.proto.PublicStarTower.StarTowerRoomCase; import emu.nebula.proto.StarTowerInteract.StarTowerInteractReq; import emu.nebula.proto.StarTowerInteract.StarTowerInteractResp; +import emu.nebula.util.Utils; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntList; import lombok.Getter; @@ -12,11 +19,32 @@ public class StarTowerNpcEventCase extends StarTowerBaseCase { private int npcId; private int eventId; private IntList options; + private boolean completed; - public StarTowerNpcEventCase(int npcId, int eventId) { + public StarTowerNpcEventCase(int npcId, StarTowerEventDef event) { this.npcId = npcId; - this.eventId = eventId; + this.eventId = event.getId(); this.options = new IntArrayList(); + + // Add up to 4 random options + var randomOptions = event.getClonedOptionIds(); + int maxOptions = Math.min(randomOptions.size(), 4); + + for (int i = 0; i < maxOptions; i++) { + int optionId = Utils.randomElement(randomOptions, true); + this.options.add(optionId); + } + + // Fix for question type events to always include the answer + if (this.eventId >= 114 && this.eventId <= 116) { + int answerId = (this.eventId * 100) + 3; + if (!this.getOptions().contains(answerId)) { + this.getOptions().set(0, answerId); + } + } + + // Shuffle + Collections.shuffle(this.getOptions()); } @Override @@ -24,18 +52,267 @@ public class StarTowerNpcEventCase extends StarTowerBaseCase { return CaseType.NpcEvent; } + public int getOption(int index) { + if (index < 0 || index >= this.getOptions().size()) { + return 0; + } + + return this.getOptions().getInt(index); + } + @Override public StarTowerInteractResp interact(StarTowerInteractReq req, StarTowerInteractResp rsp) { + // Sanity check to make sure we cant do the event multiple times + if (this.isCompleted()) { + return rsp; + } + + // Get option from selection index + int option = this.getOption(req.getSelectReq().getIndex()); + + // Get select response proto + var selectRsp = rsp.getMutableSelectResp(); + var success = selectRsp.getMutableResp(); + var change = new PlayerChangeInfo(); + + // Completed event flag + boolean completed = true; + + // Handle option id + switch (option) { + case 10101 -> { + if (this.spendCoin(100, change)) { + this.addPotentialSelector(rsp); + } else { + completed = false; + } + } + case 10102 -> { + if (this.spendCoin(120, change)) { + this.addPotentialSelector(rsp); + } else { + completed = false; + } + } + case 10103 -> { + this.addCoin(30, change); + } + case 10201 -> { + if (this.spendCoin(120, change)) { + this.addPotentialSelector(rsp, this.getRandomSupportCharId()); + } else { + completed = false; + } + } + case 10202 -> { + if (this.spendCoin(160, change)) { + this.addPotentialSelector(rsp, this.getMainCharId()); + } else { + completed = false; + } + } + case 10203 -> { + if (this.spendCoin(200, change)) { + this.addRarePotentialSelector(rsp); + } else { + completed = false; + } + } + case 10204 -> { + this.addCoin(30, change); + } + case 10302 -> { + // TODO + if (this.spendSubNotes(5, change)) { + this.addCoin(150, change); + } else { + completed = false; + } + } + case 10303 -> { + this.addCoin(30, change); + } + case 10401 -> { + // TODO + completed = false; + } + case 10402 -> { + if (this.spendCoin(200, change)) { + this.addRarePotentialSelector(rsp); + } else { + completed = false; + } + } + case 10403 -> { + this.addCoin(30, change); + } + case 10501 -> { + if (Utils.randomChance(.5)) { + this.addCoin(200, change); + } else { + this.addCoin(-100, change); + } + } + case 10502 -> { + if (Utils.randomChance(.3)) { + this.addCoin(650, change); + } else { + this.addCoin(-200, change); + } + } + case 10503 -> { + this.addCoin(30, change); + } + case 10601 -> { + if (Utils.randomChance(.5)) { + this.addRarePotentialSelector(rsp); + } + } + case 10602 -> { + this.addPotentialSelector(rsp); + } + case 10603 -> { + this.addCoin(30, change); + } + case 10701, 10702, 10703, 10704, 10705, 10706, 10707 -> { + int subNoteId = (option % 100) + 90010; + this.getGame().addItem(subNoteId, 5, change); + } + case 10708 -> { + int subNoteId = this.getGame().getRandomSubNoteId(); + this.getGame().addItem(subNoteId, 5, change); + } + case 10801, 10802, 10803, 10804, 10805, 10806, 10807 -> { + if (this.spendCoin(140, change)) { + int subNoteId = (option % 100) + 90010; + this.getGame().addItem(subNoteId, 10, change); + } else { + completed = false; + } + } + case 10808 -> { + if (this.spendCoin(90, change)) { + int subNoteId = this.getGame().getRandomSubNoteId(); + this.getGame().addItem(subNoteId, 10, change); + } else { + completed = false; + } + } + case 10809 -> { + this.addCoin(30, change); + } + case 11401, 11402, 11403, 11404, 11405 -> { + if (option == 11403) { + int subNoteId = this.getGame().getRandomSubNoteId(); + this.getGame().addItem(subNoteId, 10, change); + } + } + case 11501, 11502, 11503, 11504, 11505 -> { + if (option == 11503) { + this.addPotentialSelector(rsp); + } + } + case 11601, 11602, 11603, 11604, 11605 -> { + if (option == 11603) { + this.addRarePotentialSelector(rsp); + } + } + case 12601 -> { + this.addPotentialSelector(rsp, this.getRandomSupportCharId()); + } + case 12602 -> { + // Recover 20% hp + } + case 12701 -> { + this.addPotentialSelector(rsp, this.getRandomSupportCharId()); + } + case 12702 -> { + int subNoteId = this.getGame().getRandomSubNoteId(); + this.getGame().addItem(subNoteId, 5, change); + } + case 12801 -> { + this.addRarePotentialSelector(rsp, this.getRandomSupportCharId()); + } + case 12802 -> { + this.addCoin(30, change); + } + default -> { + // Ignored + } + } + + // Set change info + rsp.setChange(change.toProto()); + + // Set success result + success.setOptionsResult(completed); + this.completed = completed; + + // Complete return rsp; } + + // Helper functions + + private boolean spendCoin(int amount, PlayerChangeInfo change) { + int coin = this.getGame().getResCount(GameConstants.TOWER_COIN_ITEM_ID); + + if (coin < amount) { + return false; + } + + this.addCoin(-amount, change); + + return true; + } + + private PlayerChangeInfo addCoin(int amount, PlayerChangeInfo change) { + return this.getGame().addItem(GameConstants.TOWER_COIN_ITEM_ID, amount, change); + } + + private boolean spendSubNotes(int amount, PlayerChangeInfo change) { + // TODO + return false; + } + + private void addPotentialSelector(StarTowerInteractResp rsp) { + this.addPotentialSelector(rsp, 0); + } + + private void addPotentialSelector(StarTowerInteractResp rsp, int charId) { + var selectorCase = this.getGame().createPotentialSelector(charId); + this.getRoom().addCase(rsp.getMutableCases(), selectorCase); + } + + private void addRarePotentialSelector(StarTowerInteractResp rsp) { + this.addRarePotentialSelector(rsp, 0); + } + + private void addRarePotentialSelector(StarTowerInteractResp rsp, int charId) { + var selectorCase = this.getGame().createPotentialSelector(charId, true); + this.getRoom().addCase(rsp.getMutableCases(), selectorCase); + } + + private int getMainCharId() { + return this.getGame().getCharIds()[0]; + } + + private int getRandomSupportCharId() { + return this.getGame().getCharIds()[Utils.randomRange(1, 2)]; + } // Proto @Override public void encodeProto(StarTowerRoomCase proto) { + var info = NPCAffinityInfo.newInstance() + .setNPCId(this.getNpcId()) + .setAffinity(0); + proto.getMutableSelectOptionsEventCase() .setEvtId(this.getEventId()) .setNPCId(this.getNpcId()) + .addInfos(info) .addAllOptions(this.getOptions().toIntArray()); } } diff --git a/src/main/java/emu/nebula/game/tower/room/StarTowerBaseRoom.java b/src/main/java/emu/nebula/game/tower/room/StarTowerBaseRoom.java index b1445a5..1f98ace 100644 --- a/src/main/java/emu/nebula/game/tower/room/StarTowerBaseRoom.java +++ b/src/main/java/emu/nebula/game/tower/room/StarTowerBaseRoom.java @@ -90,6 +90,11 @@ public class StarTowerBaseRoom { } public StarTowerBaseCase addCase(RepeatedMessage cases, StarTowerBaseCase towerCase) { + // Sanity check + if (towerCase == null) { + return null; + } + // Set game for tower case towerCase.register(this); diff --git a/src/main/java/emu/nebula/game/tower/room/StarTowerEventRoom.java b/src/main/java/emu/nebula/game/tower/room/StarTowerEventRoom.java index b92cc56..5ecfa65 100644 --- a/src/main/java/emu/nebula/game/tower/room/StarTowerEventRoom.java +++ b/src/main/java/emu/nebula/game/tower/room/StarTowerEventRoom.java @@ -1,8 +1,17 @@ package emu.nebula.game.tower.room; +import java.util.Arrays; +import java.util.Objects; + +import emu.nebula.GameConstants; +import emu.nebula.data.GameData; +import emu.nebula.data.resources.StarTowerEventDef; import emu.nebula.data.resources.StarTowerStageDef; import emu.nebula.game.tower.StarTowerGame; +import emu.nebula.game.tower.cases.StarTowerBaseCase; +import emu.nebula.game.tower.cases.StarTowerNpcEventCase; import emu.nebula.game.tower.cases.StarTowerSyncHPCase; +import emu.nebula.util.Utils; import lombok.Getter; @@ -12,9 +21,47 @@ public class StarTowerEventRoom extends StarTowerBaseRoom { public StarTowerEventRoom(StarTowerGame game, StarTowerStageDef stage) { super(game, stage); } + + private StarTowerEventDef getRandomEvent() { + /* + var list = GameData.getStarTowerEventDataTable() + .values() + .stream() + .toList(); + */ + + var list = Arrays.stream(GameConstants.TOWER_EVENTS_IDS) + .mapToObj(GameData.getStarTowerEventDataTable()::get) + .filter(Objects::nonNull) + .toList(); + + if (list.isEmpty()) { + return null; + } + + return Utils.randomElement(list); + } + + public StarTowerBaseCase createNpcEvent() { + // Get random event + var event = this.getRandomEvent(); + + if (event == null) { + return null; + } + + // Get random npc + int npcId = Utils.randomElement(event.getRelatedNPCs()); + + // Create case with event + return new StarTowerNpcEventCase(npcId, event); + } @Override public void onEnter() { + // Create npc + this.addCase(this.createNpcEvent()); + // Create sync hp case this.addCase(new StarTowerSyncHPCase()); diff --git a/src/main/java/emu/nebula/util/Utils.java b/src/main/java/emu/nebula/util/Utils.java index f2fed2c..41be82e 100644 --- a/src/main/java/emu/nebula/util/Utils.java +++ b/src/main/java/emu/nebula/util/Utils.java @@ -173,6 +173,10 @@ public class Utils { public static int randomRange(int min, int max) { return ThreadLocalRandom.current().nextInt(min, max + 1); } + + public static boolean randomChance(double chance) { + return ThreadLocalRandom.current().nextDouble() < chance; + } public static int randomElement(int[] array) { return array[ThreadLocalRandom.current().nextInt(0, array.length)];