35 Commits

Author SHA1 Message Date
Melledy
cf63bc0b7e Implement starting musical notes 2025-12-05 23:24:10 -08:00
Melledy
b7bf1fcdeb Implement potential rerolling 2025-12-05 23:01:37 -08:00
Melledy
198d3aac4f Fix story red dot (untested) 2025-12-05 22:34:47 -08:00
Melledy
810427a028 Fix battle pass weekly exp not resetting 2025-12-05 21:05:21 -08:00
Melledy
70c7c849df Implement !battlepass command
Examples:
`!battlepass premium` = Activates elite grant
`!battlepass lv40` = Unlocks the rewards up to level 40
2025-12-05 21:03:09 -08:00
Melledy
0b7f1ae3a2 Fix battle pass red dot when logging in 2025-12-05 20:32:06 -08:00
Melledy
5182e94db7 Fix item count on star tower shop goods 2025-12-05 19:25:20 -08:00
Melledy
426e5bce63 Add a notification when getting the wrong answer in a npc event 2025-12-05 19:11:39 -08:00
HongchengQ
b9c4a174f8 Improve remote command execution response results
- Ensure that commands return specific messages instead of fixed strings after execution
2025-12-05 13:38:48 -08:00
Melledy
6974631601 Implement bonus potential level monolith talents 2025-12-05 02:12:29 -08:00
Melledy
880f0d1d7d Fix max level of potentials in !build being 3 2025-12-05 01:26:12 -08:00
Melledy
86c607c0b3 Implement some star tower npc events 2025-12-04 23:40:24 -08:00
Melledy
3710f0a697 Implement secondary skills in star tower too 2025-12-04 19:32:23 -08:00
Melledy
c19aa5d0a1 Fix disc secondary skills 2025-12-04 16:25:18 -08:00
Melledy
be84e0f406 Remove max level potentials from potential selectors 2025-12-04 00:01:48 -08:00
Melledy
e5cb842fdd Implement monolith shop discounts and improvements 2025-12-03 23:53:18 -08:00
Melledy
15618414a6 Implement monolith shop discounts 2025-12-03 23:15:25 -08:00
Melledy
71de6184b9 Optimize star tower case handling 2025-12-03 22:52:49 -08:00
Melledy
e887d5eb4c Update data versions 2025-12-03 22:45:42 -08:00
Melledy
211e012c42 Update patchlist handler 2025-12-03 22:44:12 -08:00
Melledy
9c87d74ad7 Add /client-code endpoint 2025-12-03 22:34:29 -08:00
Melledy
357d12779b Implement monolith shop refresh 2025-12-03 22:23:41 -08:00
Melledy
c8a7db75aa Implement bonus max potential level 2025-12-03 21:28:47 -08:00
Melledy
ef8846445c Implement monolith enhancement machines 2025-12-03 19:36:47 -08:00
Melledy
7ef7490c37 Support JP and TW clients 2025-12-03 14:08:38 -08:00
Melledy
2c1e1ae2fb Spawn the shop npc at the end of a monolith run 2025-12-03 12:07:05 -08:00
Melledy
e3d34bfa48 Spawn recovery npc after each battle in monolith 2025-12-03 00:20:51 -08:00
Melledy
65250b07bf Don't send door case after every shop purchase 2025-12-02 23:56:57 -08:00
Melledy
b38f4f0957 Fix duplicate potentials in potential selector 2025-12-02 23:50:57 -08:00
Melledy
e4dc85a50f Fix wrong potentials in star tower potential selector 2025-12-02 23:44:45 -08:00
Melledy
893b23b50d Rework star tower 2025-12-02 23:15:31 -08:00
Melledy
33b1cf55d4 Update data version to 60 2025-12-01 21:43:06 -08:00
Melledy
aecea6ab03 Implement !build command for creating records 2025-12-01 13:33:34 -08:00
Melledy
e8e7df7d50 Fix incorrect element type for wind/aqua 2025-12-01 13:21:42 -08:00
Melledy
9188d3b53a Improve command arg handling 2025-12-01 13:16:24 -08:00
53 changed files with 3101 additions and 702 deletions

View File

@@ -29,6 +29,14 @@ For any extra support, questions, or discussions, check out our [Discord](https:
### Not implemented
- Events
### Supported regions
Nebula supports the global PC client by default. If you want to switch regions, you need to change the `region` field in the Nebula config.
Current supported regions (PC): `GLOBAL`, `KR`, `JP`, `TW`
You may need to change the data version when switching regions. The `customDataVersion` field should match the the data version of your client, which is usually the last number of your client's version string (top left of your login screen). Example: 1.0.0.42 = data version 42.
# Running the server and client
### Prerequisites
@@ -49,17 +57,24 @@ For any extra support, questions, or discussions, check out our [Discord](https:
3. Copy and paste the following code into the Fiddlerscript tab of Fiddler Classic. Remember to save the fiddler script after you copy and paste it:
```
import System;
import System.Windows.Forms;
import Fiddler;
import System.Text.RegularExpressions;
class Handlers
{
static var list = [
".yostarplat.com",
".stellasora.global",
".stellasora.kr",
".stellasora.jp",
".stargazer-games.com"
];
static function OnBeforeRequest(oS: Session) {
if (oS.host.EndsWith(".yostarplat.com") || oS.host.EndsWith(".stellasora.global")) {
oS.oRequest.headers.UriScheme = "http";
oS.host = "localhost"; // This can also be replaced with another IP address.
for (var i = 0; i < list.length; i++) {
if (oS.host.EndsWith(list[i])) {
oS.oRequest.headers.UriScheme = "http";
oS.host = "localhost"; // This can also be replaced with another IP address
}
}
}
};
@@ -68,14 +83,6 @@ class Handlers
4. If `autoCreateAccount` is set to true in the config, then you can skip this step. Otherwise, type `/account create [account email]` in the server console to create an account.
5. Login with your account email, the code field is ignored by the server and can be set to anything.
If you are not on the global client, `.stellasora.global` in the fiddlerscript may need to be changed to match the endpoint your client connects to.
### Supported regions
Nebula supports the global client by default. If you want to switch regions, you need to change the `customDataVersion` and `region` fields in the Nebula config. The `customDataVersion` field should match the the data version of your client, which is usually the last number of your client's version string (top left of your login screen). Example: 1.0.0.42 = data version 42.
Current supported regions: `global`, `kr`
### Server commands
Server commands need to be run in the server console OR in the signature edit menu of your profile.

View File

@@ -6,8 +6,23 @@ import emu.nebula.game.inventory.ItemParam;
import emu.nebula.util.WeightedList;
public class GameConstants {
private static final int DATA_VERSION = 54;
private static final String VERSION = "1.2.0";
public static final String VERSION = "1.2.0";
public static int DATA_VERSION = 0;
// Set data versions for each region
static {
RegionConfig.getRegion("global")
.setDataVersion(63);
RegionConfig.getRegion("kr")
.setDataVersion(70);
RegionConfig.getRegion("jp")
.setDataVersion(66);
RegionConfig.getRegion("tw")
.setDataVersion(64);
}
public static final ZoneId UTC_ZONE = ZoneId.of("UTC");
@@ -19,7 +34,6 @@ public class GameConstants {
public static final int GEM_ITEM_ID = 2;
public static final int PREM_GEM_ITEM_ID = 3;
public static final int ENERGY_BUY_ITEM_ID = GEM_ITEM_ID;
public static final int STAR_TOWER_GOLD_ITEM_ID = 11;
public static final int EXP_ITEM_ID = 21;
public static final int MAX_ENERGY = 240;
@@ -37,6 +51,14 @@ public class GameConstants {
public static final int MAX_FRIENDSHIPS = 50;
public static final int MAX_PENDING_FRIENDSHIPS = 30;
public static final int TOWER_COIN_ITEM_ID = 11;
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},
new int[] {200, 150},
@@ -57,7 +79,14 @@ public class GameConstants {
// Helper functions
public static String getGameVersion() {
return VERSION + "." + getDataVersion() + " (" + Nebula.getConfig().getRegion().toUpperCase() + ")";
// Load data version
var region = RegionConfig.getRegion(Nebula.getConfig().getRegion());
// Set data version from region
GameConstants.DATA_VERSION = region.getDataVersion();
// Init game version string
return VERSION + "." + getDataVersion() + " (" + region.getName().toUpperCase() + ")";
}
public static int getDataVersion() {

View File

@@ -43,9 +43,8 @@ public class Nebula {
@Getter private static PluginManager pluginManager;
public static void main(String[] args) {
// Load config + keys first
// Load config first
Nebula.loadConfig();
AeadHelper.loadKeys();
// Start Server
Nebula.getLogger().info("Starting Nebula " + getJarVersion());
@@ -54,6 +53,9 @@ public class Nebula {
boolean generateHandbook = true;
// Load keys
AeadHelper.loadKeys();
// Load plugin manager
Nebula.pluginManager = new PluginManager();

View File

@@ -0,0 +1,42 @@
package emu.nebula;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class RegionConfig {
private String name;
private int dataVersion;
private String serverMetaKey;
private String serverGarbleKey;
private static Object2ObjectMap<String, RegionConfig> REGIONS = new Object2ObjectOpenHashMap<>();
public RegionConfig(String name) {
this.name = name;
this.serverMetaKey = "";
this.serverGarbleKey = "";
}
public RegionConfig setDataVersion(int i) {
this.dataVersion = i;
return this;
}
public RegionConfig setServerMetaKey(String key) {
this.serverMetaKey = key;
return this;
}
public RegionConfig setServerGarbleKey(String key) {
this.serverGarbleKey = key;
return this;
}
public static RegionConfig getRegion(String name) {
String regionName = name.toLowerCase();
return REGIONS.computeIfAbsent(regionName, r -> new RegionConfig(regionName));
}
}

View File

@@ -7,8 +7,8 @@ import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.character.GameDisc;
import emu.nebula.game.player.Player;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.ints.Int2IntLinkedOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import it.unimi.dsi.fastutil.objects.ObjectSet;
import lombok.Getter;
@@ -53,6 +53,9 @@ public class CommandArgs {
} else if (arg.startsWith("lv")) { // Level
this.level = Utils.parseSafeInt(arg.substring(2));
it.remove();
} else if (arg.startsWith("lvl")) { // Level
this.level = Utils.parseSafeInt(arg.substring(3));
it.remove();
} else if (arg.startsWith("a")) { // Advance
this.advance = Utils.parseSafeInt(arg.substring(1));
it.remove();
@@ -76,7 +79,7 @@ public class CommandArgs {
int key = Integer.parseInt(split[0]);
int value = Integer.parseInt(split[1]);
if (this.map == null) this.map = new Int2IntOpenHashMap();
if (this.map == null) this.map = new Int2IntLinkedOpenHashMap();
this.map.put(key, value);
it.remove();

View File

@@ -0,0 +1,61 @@
package emu.nebula.command.commands;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
import emu.nebula.net.NetMsgId;
@Command(label = "battlepass", aliases = {"bp"}, permission = "player.battlepass", desc = "/battlepass [free | premium] lv(level). mMdifies your battle pass")
public class BattlePassCommand implements CommandHandler {
@Override
public String execute(CommandArgs args) {
// Get target
var target = args.getTarget();
var battlepass = target.getBattlePassManager().getBattlePass();
boolean changed = false;
// Check if we are changing premium status
int mode = -1;
for (var arg : args.getList()) {
if (arg.equalsIgnoreCase("free")) {
mode = 0;
} else if (arg.equalsIgnoreCase("premium")) {
mode = 2;
}
}
if (mode >= 0 && battlepass.getMode() != mode) {
battlepass.setMode(mode);
changed = true;
}
// Set level
int level = Math.min(args.getLevel(), 50);
if (level >= 0 && battlepass.getLevel() != level) {
battlepass.setLevel(level);
changed = true;
}
// Check if we have made any changes
if (changed) {
// Save battle pass to the database
battlepass.save();
// Send package to notify the client that the battle pass needs updating
target.addNextPackage(
NetMsgId.battle_pass_info_succeed_ack,
battlepass.toProto()
);
// Success message
return "Changed the battle pass successfully.";
}
// Result message
return "No changes were made to the battle pass.";
}
}

View File

@@ -0,0 +1,179 @@
package emu.nebula.command.commands;
import java.util.ArrayList;
import java.util.List;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
import emu.nebula.data.GameData;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.character.GameDisc;
import emu.nebula.game.player.Player;
import emu.nebula.game.tower.StarTowerBuild;
import emu.nebula.net.NetMsgId;
import emu.nebula.util.Utils;
import lombok.Getter;
@Command(
label = "build",
aliases = {"b", "record", "r"},
permission = "player.build",
requireTarget = true,
desc = "!build [char ids...] [disc ids...] [potential ids...] [melody ids...]"
)
public class BuildCommand implements CommandHandler {
@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;
this.parseItem(builder, id, count);
}
if (args.getMap() != null) {
for (var entry : args.getMap().int2IntEntrySet()) {
int id = entry.getIntKey();
int count = entry.getIntValue();
this.parseItem(builder, id, count);
}
}
// Check if build is valid
if (builder.getCharacters().size() != 3) {
return "Record must have 3 different characters";
}
if (builder.getDiscs().size() < 3 || builder.getDiscs().size() > 6) {
return "Record must have 3-6 different discs";
}
// Create record
var build = builder.toBuild();
// Add to star tower manager
target.getStarTowerManager().getBuilds().put(build.getUid(), build);
// Send package to player
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;
}
// 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
}
}
}
@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;
}
this.characters.add(character);
}
public void addDisc(GameDisc disc) {
if (this.discs.contains(disc)) {
return;
}
this.discs.add(disc);
}
public StarTowerBuild toBuild() {
// Set characters and discs
build.setChars(this.getCharacters());
build.setDiscs(this.getDiscs());
// Clear character potential cache
build.getCharPots().clear();
for (int charId : build.getCharIds()) {
build.getCharPots().put(charId, 0);
}
// Add potentials to character potential cache
var it = build.getPotentials().iterator();
while (it.hasNext()) {
var potential = it.next();
var data = GameData.getPotentialDataTable().get(potential.getIntKey());
if (data == null || !build.getCharPots().containsKey(data.getCharId())) {
it.remove();
continue;
}
build.getCharPots().add(data.getCharId(), 1);
}
// Calculate score
build.calculateScore();
// Return build
return build;
}
}
}

View File

@@ -52,6 +52,8 @@ public class GameData {
@Getter private static DataTable<DiscPromoteDef> DiscPromoteDataTable = new DataTable<>();
@Getter private static DataTable<DiscPromoteLimitDef> DiscPromoteLimitDataTable = new DataTable<>();
@Getter private static DataTable<SecondarySkillDef> SecondarySkillDataTable = new DataTable<>();
// Items
@Getter private static DataTable<ItemDef> ItemDataTable = new DataTable<>();
@Getter private static DataTable<ProductionDef> ProductionDataTable = new DataTable<>();
@@ -114,9 +116,12 @@ public class GameData {
@Getter private static DataTable<StarTowerGrowthNodeDef> StarTowerGrowthNodeDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerFloorExpDef> StarTowerFloorExpDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerTeamExpDef> StarTowerTeamExpDataTable = new DataTable<>();
@Getter private static DataTable<PotentialDef> PotentialDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerEventDef> StarTowerEventDataTable = new DataTable<>();
@Getter private static DataTable<SubNoteSkillPromoteGroupDef> SubNoteSkillPromoteGroupDataTable = new DataTable<>();
@Getter private static DataTable<PotentialDef> PotentialDataTable = new DataTable<>();
@Getter private static DataTable<CharPotentialDef> CharPotentialDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerBookFateCardBundleDef> StarTowerBookFateCardBundleDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerBookFateCardQuestDef> StarTowerBookFateCardQuestDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerBookFateCardDef> StarTowerBookFateCardDataTable = new DataTable<>();

View File

@@ -0,0 +1,22 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "CharPotential.json")
public class CharPotentialDef extends BaseDef {
private int Id;
private int[] MasterSpecificPotentialIds;
private int[] AssistSpecificPotentialIds;
private int[] CommonPotentialIds;
private int[] MasterNormalPotentialIds;
private int[] AssistNormalPotentialIds;
@Override
public int getId() {
return Id;
}
}

View File

@@ -18,6 +18,9 @@ public class DiscDef extends BaseDef {
private int TransformItemId;
private int[] MaxStarTransformItem;
private int[] ReadReward;
private int SecondarySkillGroupId1;
private int SecondarySkillGroupId2;
private int SubNoteSkillGroupId;
private transient ElementType elementType;

View File

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

View File

@@ -2,6 +2,7 @@ package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.tower.StarTowerGame;
import lombok.Getter;
@Getter
@@ -10,11 +11,44 @@ public class PotentialDef extends BaseDef {
private int Id;
private int CharId;
private int Build;
private int BranchType;
private int MaxLevel;
private int[] BuildScore;
private String BriefDesc;
@Override
public int getId() {
return Id;
}
public int getMaxLevel() {
// Check if regular potential
if (this.BranchType == 3) {
return this.BuildScore.length;
}
// Special potential should always have a max level of 1
return this.MaxLevel;
}
public int getMaxLevel(StarTowerGame game) {
// Check if regular potential
if (this.BranchType == 3) {
return this.MaxLevel + game.getModifiers().getBonusMaxPotentialLevel();
}
// Special potential should always have a max level of 1
return this.MaxLevel;
}
public int getBuildScore(int level) {
int index = level - 1;
if (index >= this.BuildScore.length) {
index = this.BuildScore.length - 1;
}
return this.BuildScore[index];
}
}

View File

@@ -0,0 +1,118 @@
package emu.nebula.data.resources;
import java.util.ArrayList;
import java.util.List;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
@Getter
@ResourceType(name = "SecondarySkill.json")
public class SecondarySkillDef extends BaseDef {
private int Id;
private int GroupId;
private int Score;
private String NeedSubNoteSkills;
private transient ItemParamMap reqSubNotes;
@Getter
private static transient Int2ObjectMap<List<SecondarySkillDef>> groups = new Int2ObjectOpenHashMap<>();
@Override
public int getId() {
return Id;
}
public boolean match(ItemParamMap subNotes) {
for (var item : this.reqSubNotes) {
int reqId = item.getIntKey();
int reqCount = item.getIntValue();
int curCount = subNotes.get(reqId);
if (curCount < reqCount) {
return false;
}
}
return true;
}
@Override
public void onLoad() {
// Setup required subnotes
this.reqSubNotes = ItemParamMap.fromJsonString(this.NeedSubNoteSkills);
// Add to group cache
var group = groups.computeIfAbsent(this.GroupId, id -> new ArrayList<>());
group.add(this);
// Clear to save memory
this.NeedSubNoteSkills = null;
}
// Static sub note skill group group
public static List<SecondarySkillDef> getGroup(int id) {
return groups.get(id);
}
public static IntSet calculateSecondarySkills(int[] discIds, ItemParamMap subNotes) {
var secondarySkills = new IntOpenHashSet();
// Get first 3 discs
for (int i = 0; i < 3; i++) {
// Disc id
int discId = discIds[i];
// Get disc data
var data = GameData.getDiscDataTable().get(discId);
if (data == null) continue;
// Add secondary skills
int s1= getSecondarySkill(subNotes, data.getSecondarySkillGroupId1());
if (s1 > 0) {
secondarySkills.add(s1);
}
int s2 = getSecondarySkill(subNotes, data.getSecondarySkillGroupId2());
if (s2 > 0) {
secondarySkills.add(s2);
}
}
return secondarySkills;
}
private static int getSecondarySkill(ItemParamMap subNotes, int groupId) {
// Check group id
if (groupId <= 0) {
return 0;
}
// Get group
var group = SecondarySkillDef.getGroup(groupId);
if (group == null) {
return 0;
}
// Reverse iterator to try and match highest secondary skill first
for (int i = group.size() - 1; i >= 0; i--) {
var data = group.get(i);
if (data.match(subNotes)) {
return data.getId();
}
}
// Failure
return 0;
}
}

View File

@@ -21,8 +21,8 @@ public class StarTowerDef extends BaseDef {
return Id;
}
public int getMaxFloor(int stage) {
int index = stage - 1;
public int getMaxFloor(int stageNum) {
int index = stageNum - 1;
if (index < 0 || index >= this.FloorNum.length) {
return 0;

View File

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

View File

@@ -30,7 +30,7 @@ public class BattlePass implements GameDatabaseObject {
private int uid;
private transient BattlePassManager manager;
private int battlePassId;
private int battlePassId; // Season id
private int mode;
private int level;
private int exp;
@@ -71,6 +71,13 @@ public class BattlePass implements GameDatabaseObject {
return manager.getPlayer();
}
/**
* Sets the mode directly
*/
public synchronized void setMode(int mode) {
this.mode = mode;
}
public boolean isPremium() {
return this.mode > 0;
}
@@ -79,6 +86,14 @@ public class BattlePass implements GameDatabaseObject {
return GameData.getBattlePassRewardDataTable().get((this.getBattlePassId() << 16) + level);
}
/**
* Sets the level directly, use getMaxExp() instead if adding exp.
*/
public synchronized void setLevel(int level) {
this.level = level;
this.exp = 0;
}
public int getMaxExp() {
var data = GameData.getBattlePassLevelDataTable().get(this.getLevel() + 1);
return data != null ? data.getExp() : 0;
@@ -100,6 +115,32 @@ public class BattlePass implements GameDatabaseObject {
}
}
/**
* Returns true if any rewards or quests are claimable
*/
public synchronized boolean hasNew() {
// Check if any quests are complete but unclaimed
for (var quest : getQuests().values()) {
if (quest.isComplete() && !quest.isClaimed()) {
return true;
}
}
// Check if we have any pending rewards
for (int i = 1; i <= this.getLevel(); i++) {
if (!this.getBasicReward().isSet(i)) {
return true;
}
if (this.isPremium() && !this.getPremiumReward().isSet(i)) {
return true;
}
}
// No claimable things
return false;
}
public synchronized void resetDailyQuests(boolean resetWeekly) {
// Reset daily quests
for (var data : GameData.getBattlePassQuestDataTable()) {
@@ -118,6 +159,11 @@ public class BattlePass implements GameDatabaseObject {
this.syncQuest(quest);
}
// Reset weekly limit for exp
if (resetWeekly) {
this.expWeek = 0;
}
// Persist to database
this.save();
}

View File

@@ -12,6 +12,10 @@ public class BattlePassManager extends PlayerManager {
public BattlePassManager(Player player) {
super(player);
}
public boolean hasNew() {
return this.getBattlePass().hasNew();
}
// Database

View File

@@ -7,10 +7,10 @@ import lombok.Getter;
@Getter
public enum ElementType {
INHERIT (0),
WIND (1, 90020),
AQUA (1, 90018),
FIRE (2, 90019),
EARTH (3, 90021),
AQUA (4, 90018),
WIND (4, 90020),
LIGHT (5, 90022),
DARK (6, 90023),
NONE (7);

View File

@@ -821,14 +821,14 @@ public class Player implements GameDatabaseObject {
// Set player states
var state = proto.getMutableState()
.setStorySet(true)
.setStorySet(this.getStoryManager().hasNew())
.setFriend(this.getFriendList().hasPendingRequests());
state.getMutableMail()
.setNew(this.getMailbox().hasNewMail());
state.getMutableBattlePass()
.setState(1);
.setState(this.getBattlePassManager().hasNew() ? 1 : 0);
state.getMutableAchievement()
.setNew(this.getAchievementManager().hasNewAchievements());

View File

@@ -38,6 +38,18 @@ public class StoryManager extends PlayerManager implements GameDatabaseObject {
this.save();
}
public boolean hasNew() {
if (this.getCompletedStories().size() < GameData.getStoryDataTable().size()) {
return true;
}
if (this.getCompletedSets().size() < GameData.getStorySetSectionDataTable().size()) {
return true;
}
return false;
}
public PlayerChangeInfo settle(IntList list) {
// Player change info
@@ -63,6 +75,7 @@ public class StoryManager extends PlayerManager implements GameDatabaseObject {
Nebula.getGameDatabase().addToSet(this, this.getPlayerUid(), "completedStories", id);
}
// Complete
return changes;
}
@@ -90,6 +103,7 @@ public class StoryManager extends PlayerManager implements GameDatabaseObject {
// Save to db
Nebula.getGameDatabase().update(this, this.getPlayerUid(), "completedSets." + chapterId, sectionIndex);
// Complete
return changes;
}
}

View File

@@ -1,12 +1,18 @@
package emu.nebula.game.tower;
import java.util.List;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.SecondarySkillDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.character.GameDisc;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.proto.Public.ItemTpl;
import emu.nebula.proto.PublicStarTower.BuildPotential;
import emu.nebula.proto.PublicStarTower.StarTowerBuildBrief;
@@ -15,6 +21,7 @@ import emu.nebula.proto.PublicStarTower.StarTowerBuildInfo;
import emu.nebula.proto.PublicStarTower.TowerBuildChar;
import emu.nebula.util.Snowflake;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
@Getter
@@ -37,30 +44,29 @@ public class StarTowerBuild implements GameDatabaseObject {
private ItemParamMap potentials;
private ItemParamMap subNoteSkills;
private IntSet secondarySkills;
@Deprecated
public StarTowerBuild() {
}
public StarTowerBuild(StarTowerGame game) {
public StarTowerBuild(Player player) {
this.uid = Snowflake.newUid();
this.playerUid = game.getPlayer().getUid();
this.playerUid = player.getUid();
this.name = "";
this.charPots = new ItemParamMap();
this.potentials = new ItemParamMap();
this.subNoteSkills = new ItemParamMap();
}
public StarTowerBuild(StarTowerGame game) {
// Initialize basic variables
this(game.getPlayer());
// Characters
this.charIds = game.getChars().stream()
.filter(d -> d.getId() > 0)
.mapToInt(d -> d.getId())
.toArray();
// Discs
this.discIds = game.getDiscs().stream()
.filter(d -> d.getId() > 0)
.mapToInt(d -> d.getId())
.toArray();
// Set char/disc ids
this.charIds = game.getCharIds();
this.discIds = game.getDiscIds();
// Add potentials
for (var entry : game.getPotentials()) {
@@ -84,8 +90,23 @@ public class StarTowerBuild implements GameDatabaseObject {
this.getSubNoteSkills().put(entry.getIntKey(), entry.getIntValue());
}
// Set secondary skills
this.secondarySkills = game.getSecondarySkills();
// Caclulate record score and cache it
this.score = this.calculateScore();
this.calculateScore();
}
public void setChars(List<GameCharacter> characters) {
this.charIds = characters.stream()
.mapToInt(c -> c.getCharId())
.toArray();
}
public void setDiscs(List<GameDisc> discs) {
this.discIds = discs.stream()
.mapToInt(d -> d.getDiscId())
.toArray();
}
public void setName(String newName) {
@@ -109,26 +130,38 @@ public class StarTowerBuild implements GameDatabaseObject {
// Score
private int calculateScore() {
// Init score
int score = 0;
public int calculateScore() {
// Clear score
this.score = 0;
// Potentials
// Add score from potentials
for (var potential : this.getPotentials().int2IntEntrySet()) {
var data = GameData.getPotentialDataTable().get(potential.getIntKey());
if (data == null) continue;
int index = potential.getIntValue() - 1;
score += data.getBuildScore()[index];
this.score += data.getBuildScore(potential.getIntValue());
}
// Sub note skills
// Add score from sub note skills
for (var item : this.getSubNoteSkills()) {
score += item.getIntValue() * 15;
this.score += item.getIntValue() * 15;
}
// Check secondary skills
if (this.getSecondarySkills() == null) {
this.secondarySkills = SecondarySkillDef.calculateSecondarySkills(this.getDiscIds(), this.getSubNoteSkills());
}
// Add score from secondary skills
for (int id : this.getSecondarySkills()) {
var data = GameData.getSecondarySkillDataTable().get(id);
if (data == null) continue;
this.score += data.getScore();
}
// Complete
return score;
return this.score;
}
// Proto
@@ -183,6 +216,11 @@ public class StarTowerBuild implements GameDatabaseObject {
proto.addSubNoteSkills(skill);
}
// Secondary skills
for (int id : this.getSecondarySkills()) {
proto.addActiveSecondaryIds(id);
}
return proto;
}

View File

@@ -1,128 +0,0 @@
package emu.nebula.game.tower;
import java.util.HashMap;
import java.util.Map;
import emu.nebula.proto.PublicStarTower.HawkerGoods;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class StarTowerCase {
private int id;
@Setter(AccessLevel.NONE)
private CaseType type;
// Extra data
private int teamLevel;
private int subNoteSkillNum;
private int floorId;
private int roomType;
private int eventId;
private int npcId;
// Selector
private IntList ids;
// Hawker
private Map<Integer, StarTowerShopGoods> goodsList;
public StarTowerCase(CaseType type) {
this.type = type;
}
public void addId(int id) {
if (this.ids == null) {
this.ids = new IntArrayList();
}
this.ids.add(id);
}
public int selectId(int index) {
if (this.getIds() == null) {
return 0;
}
if (index < 0 || index >= this.getIds().size()) {
return 0;
}
return this.getIds().getInt(index);
}
public void addGoods(StarTowerShopGoods goods) {
if (this.goodsList == null) {
this.goodsList = new HashMap<>();
}
this.getGoodsList().put(getGoodsList().size() + 1, goods);
}
// Proto
public StarTowerRoomCase toProto() {
var proto = StarTowerRoomCase.newInstance()
.setId(this.getId());
switch (this.type) {
case Battle -> {
proto.getMutableBattleCase()
.setSubNoteSkillNum(this.getSubNoteSkillNum());
}
case OpenDoor -> {
proto.getMutableDoorCase()
.setFloor(this.getFloorId())
.setType(this.getRoomType());
}
case SyncHP, RecoveryHP -> {
proto.getMutableSyncHPCase();
}
case SelectSpecialPotential -> {
proto.getMutableSelectSpecialPotentialCase()
.setTeamLevel(this.getTeamLevel())
.addAllIds(this.getIds().toIntArray());
}
case PotentialSelect -> {
proto.getMutableSelectPotentialCase();
}
case NpcEvent -> {
proto.getMutableSelectOptionsEventCase()
.setEvtId(this.getEventId())
.setNPCId(this.getNpcId())
.addAllOptions(this.getIds().toIntArray());
}
case Hawker -> {
var hawker = proto.getMutableHawkerCase();
for (var entry : getGoodsList().entrySet()) {
var sid = entry.getKey();
var goods = entry.getValue();
var info = HawkerGoods.newInstance()
.setIdx(goods.getGoodsId())
.setSid(sid)
.setType(goods.getType())
.setGoodsId(102) // ?
.setPrice(goods.getPrice())
.setTag(1);
hawker.addList(info);
}
}
default -> {
}
}
return proto;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -205,7 +205,12 @@ public class StarTowerManager extends PlayerManager {
}
// Create game
this.game = new StarTowerGame(this, data, formation, req);
try {
this.game = new StarTowerGame(this, data, formation, req);
} catch (Exception e) {
Nebula.getLogger().error("Could not create star tower game", e);
return null;
}
// Trigger quest
this.getPlayer().trigger(QuestCondition.TowerEnterFloor, 1);
@@ -322,9 +327,18 @@ public class StarTowerManager extends PlayerManager {
// Database
public void loadFromDatabase() {
// Init builds
this.builds = new Long2ObjectOpenHashMap<>();
// Load builds with the current player's uid
Nebula.getGameDatabase().getObjects(StarTowerBuild.class, "playerUid", getPlayerUid()).forEach(build -> {
// Fix outdated builds
if (build.getSecondarySkills() == null) {
build.calculateScore();
build.save();
}
// Add build
this.builds.put(build.getUid(), build);
});
}

View File

@@ -0,0 +1,181 @@
package emu.nebula.game.tower;
import emu.nebula.GameConstants;
import lombok.Getter;
/**
* Data class to hold various modifiers for star tower.
*/
@Getter
public class StarTowerModifiers {
private StarTowerGame game;
// Strengthen machines
private boolean enableEndStrengthen;
private boolean enableShopStrengthen;
private boolean freeStrengthen;
private int strengthenDiscount;
// Bonus max potential level
private int bonusMaxPotentialLevel;
// Shop
private int shopGoodsCount;
private int shopRerollCount;
private int shopRerollPrice;
private boolean shopDiscountTier1;
private boolean shopDiscountTier2;
private boolean shopDiscountTier3;
// Bonus potential level proc
private double bonusStrengthenChance = 0;
private double bonusPotentialChance = 0;
private int bonusPotentialLevel = 0;
private int potentialRerollCount;
private int potentialRerollDiscount;
public StarTowerModifiers(StarTowerGame game) {
this.game = game;
// Strengthen machines
this.enableEndStrengthen = game.getDifficulty() >= 2 && this.hasGrowthNode(10601);
this.enableShopStrengthen = game.getDifficulty() >= 4 && this.hasGrowthNode(20301);
this.freeStrengthen = this.hasGrowthNode(10801);
// Strengthen discount (Set Meal Agreement)
if (this.hasGrowthNode(30402)) {
this.strengthenDiscount = 60;
} else if (this.hasGrowthNode(30102)) {
this.strengthenDiscount = 30;
}
// Bonus potential max level (Ocean of Souls)
if (this.hasGrowthNode(30301)) {
this.bonusMaxPotentialLevel = 6;
} else if (this.hasGrowthNode(20601)) {
this.bonusMaxPotentialLevel = 4;
}
// Shop extra goods (Monolith Premium)
if (this.hasGrowthNode(20702)) {
this.shopGoodsCount = 8;
} else if (this.hasGrowthNode(20402)) {
this.shopGoodsCount = 6;
} else if (this.hasGrowthNode(10402)) {
this.shopGoodsCount = 4;
} else {
this.shopGoodsCount = 2;
}
if (this.hasGrowthNode(20902)) {
this.shopRerollCount++;
}
if (this.hasGrowthNode(30601)) {
this.shopRerollCount++;
}
if (this.shopRerollCount > 0) {
this.shopRerollPrice = 100;
}
// Shop discount (Member Discount)
this.shopDiscountTier1 = game.getDifficulty() >= 3 && this.hasGrowthNode(20202);
this.shopDiscountTier2 = game.getDifficulty() >= 4 && this.hasGrowthNode(20502);
this.shopDiscountTier3 = game.getDifficulty() >= 5 && this.hasGrowthNode(20802);
// Bonus potential enhancement level procs (Potential Boost)
if (game.getDifficulty() >= 7 && this.hasGrowthNode(30802)) {
this.bonusStrengthenChance = 0.3;
} else if (game.getDifficulty() >= 6 && this.hasGrowthNode(30502)) {
this.bonusStrengthenChance = 0.2;
} else if (game.getDifficulty() >= 6 && this.hasGrowthNode(30202)) {
this.bonusStrengthenChance = 0.1;
}
// Bonus potential levels (Butterflies Inside)
if (game.getDifficulty() >= 7 && this.hasGrowthNode(30901)) {
this.bonusPotentialChance = 0.3;
this.bonusMaxPotentialLevel = 2;
} else if (game.getDifficulty() >= 7 && this.hasGrowthNode(30801)) {
this.bonusPotentialChance = 0.2;
this.bonusMaxPotentialLevel = 1;
} else if (game.getDifficulty() >= 6 && this.hasGrowthNode(30201)) {
this.bonusPotentialChance = 0.1;
this.bonusMaxPotentialLevel = 1;
} else if (game.getDifficulty() >= 5 && this.hasGrowthNode(20801)) {
this.bonusPotentialChance = 0.05;
this.bonusMaxPotentialLevel = 1;
}
// Potential reroll (Cloud Dice)
if (this.hasGrowthNode(20901)) {
this.potentialRerollCount += 1;
}
// Potential reroll price discount (Destiny of Stars)
if (this.hasGrowthNode(30702)) {
this.potentialRerollDiscount = 60;
} else if (this.hasGrowthNode(30401)) {
this.potentialRerollDiscount = 40;
} else if (this.hasGrowthNode(30101)) {
this.potentialRerollDiscount = 30;
}
}
public boolean hasGrowthNode(int nodeId) {
return this.getGame().getManager().hasGrowthNode(nodeId);
}
public int getStartingCoin() {
int coin = 0;
if (this.hasGrowthNode(10103)) {
coin += 50;
} if (this.hasGrowthNode(10403)) {
coin += 100;
} if (this.hasGrowthNode(10702)) {
coin += 200;
}
return coin;
}
public int getStartingSubNotes() {
int subNotes = 0;
if (this.hasGrowthNode(10102)) {
subNotes += 3;
}
return subNotes;
}
public void addStartingItems() {
// Add starting coin directly
int coin = this.getStartingCoin();
if (coin > 0) {
this.getGame().getRes().add(GameConstants.TOWER_COIN_ITEM_ID, coin);
}
// Add starting subnotes
int subNotes = this.getStartingSubNotes();
for (int i = 0; i < subNotes; i++) {
int id = this.getGame().getRandomSubNoteId();
this.getGame().getItems().add(id, 1);
}
}
public void setFreeStrengthen(boolean b) {
this.freeStrengthen = b;
}
public void consumeShopReroll() {
this.shopRerollCount = Math.max(this.shopRerollCount - 1, 0);
}
}

View File

@@ -0,0 +1,26 @@
package emu.nebula.game.tower;
import emu.nebula.proto.PublicStarTower.PotentialInfo;
import lombok.Getter;
@Getter
public class StarTowerPotentialInfo {
private int id;
private int level;
public StarTowerPotentialInfo(int id, int level) {
this.id = id;
this.level = level;
}
// Proto
public PotentialInfo toProto() {
var proto = PotentialInfo.newInstance()
.setTid(this.getId())
.setLevel(this.getLevel());
return proto;
}
}

View File

@@ -7,12 +7,16 @@ import lombok.Getter;
@Entity(useDiscriminator = false)
public class StarTowerShopGoods {
private int type;
private int goodsId;
private int idx; // This is actually the shop goods id
private int goodsId; // Item id
private int price;
private int discount;
private int charPos;
private boolean sold;
public StarTowerShopGoods(int type, int goodsId, int price) {
public StarTowerShopGoods(int type, int idx, int goodsId, int price) {
this.type = type;
this.idx = idx;
this.goodsId = goodsId;
this.price = price;
}
@@ -21,4 +25,40 @@ public class StarTowerShopGoods {
this.sold = true;
}
public void setCharPos(int charPos) {
this.charPos = charPos;
}
public boolean hasDiscount() {
return this.getDiscount() > 0;
}
public void applyDiscount(double percentage) {
this.discount = (int) Math.ceil(this.price * (1.0 - percentage));
}
public int getPrice() {
return this.price - this.discount;
}
public int getDisplayPrice() {
return this.price;
}
public int getCount() {
if (this.getType() == 2) {
return this.getIdx() == 8 ? 15 : 5;
}
return 1;
}
public int getCharId(StarTowerGame game) {
if (this.getCharPos() == 0) {
return 0;
}
int index = this.getCharPos() - 1;
return game.getCharIds()[index];
}
}

View File

@@ -1,4 +1,4 @@
package emu.nebula.game.tower;
package emu.nebula.game.tower.cases;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;

View File

@@ -0,0 +1,57 @@
package emu.nebula.game.tower.cases;
import emu.nebula.game.tower.StarTowerGame;
import emu.nebula.game.tower.StarTowerModifiers;
import emu.nebula.game.tower.room.StarTowerBaseRoom;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractResp;
import lombok.Getter;
/**
* Base class for star tower cases
*/
@Getter
public abstract class StarTowerBaseCase {
private transient StarTowerGame game;
private int id;
public StarTowerBaseCase() {
}
public StarTowerBaseRoom getRoom() {
return this.getGame().getRoom();
}
public StarTowerModifiers getModifiers() {
return this.getGame().getModifiers();
}
public abstract CaseType getType();
public void register(StarTowerBaseRoom room) {
this.game = room.getGame();
this.id = room.getNextCaseId();
this.onRegister();
}
public void onRegister() {
}
public abstract StarTowerInteractResp interact(StarTowerInteractReq req, StarTowerInteractResp rsp);
// Proto
public StarTowerRoomCase toProto() {
var proto = StarTowerRoomCase.newInstance()
.setId(this.getId());
this.encodeProto(proto);
return proto;
}
public abstract void encodeProto(StarTowerRoomCase proto);
}

View File

@@ -0,0 +1,119 @@
package emu.nebula.game.tower.cases;
import emu.nebula.GameConstants;
import emu.nebula.data.GameData;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractResp;
import lombok.Getter;
@Getter
public class StarTowerBattleCase extends StarTowerBaseCase {
private int subNoteSkillNum;
public StarTowerBattleCase() {
this(0);
}
public StarTowerBattleCase(int subNoteSkillNum) {
this.subNoteSkillNum = subNoteSkillNum;
}
@Override
public CaseType getType() {
return CaseType.Battle;
}
@Override
public StarTowerInteractResp interact(StarTowerInteractReq req, StarTowerInteractResp rsp) {
// Parse battle end
var proto = req.getBattleEndReq();
// Init change
var change = new PlayerChangeInfo();
// Handle victory/defeat
if (proto.hasVictory()) {
// Handle leveling up
// Get relevant floor exp data
// fishiatee: THERE'S NO LINQ IN JAVAAAAAAAAAAAAA
var floorExpData = GameData.getStarTowerFloorExpDataTable().stream()
.filter(f -> f.getStarTowerId() == this.getGame().getId())
.findFirst()
.orElseThrow();
int expReward = 0;
// Determine appropriate exp reward
switch (this.getRoom().getType()) {
// Regular battle room
case 0:
expReward = floorExpData.getNormalExp();
break;
// Elite battle room
case 1:
expReward = floorExpData.getEliteExp();
break;
// Non-final boss room
case 2:
expReward = floorExpData.getBossExp();
break;
// Final room
case 3:
expReward = floorExpData.getFinalBossExp();
break;
}
// Level up
this.getGame().addExp(expReward);
this.getGame().addPotentialSelectors(this.getGame().levelUp());
// Add clear time
this.getGame().addBattleTime(proto.getVictory().getTime());
// Handle victory
rsp.getMutableBattleEndResp()
.getMutableVictory()
.setLv(this.getGame().getTeamLevel())
.setBattleTime(this.getGame().getBattleTime());
// Add coin
int coin = this.getRoom().getStage().getInteriorCurrencyQuantity();
this.getGame().addItem(GameConstants.TOWER_COIN_ITEM_ID, coin, change);
// Handle pending potential selectors
var nextCases = this.getGame().handlePendingPotentialSelectors();
for (var towerCase : nextCases) {
this.getGame().addCase(rsp.getMutableCases(), towerCase);
}
// Add sub note skills
this.getGame().addRandomSubNoteSkills(this.getGame().getPendingSubNotes(), change);
// Handle client events for achievements
this.getGame().getPlayer().getAchievementManager().handleClientEvents(proto.getVictory().getEvents());
} else {
// Handle defeat
// TODO
return this.getGame().settle(rsp, false);
}
// Set change
rsp.setChange(change.toProto());
// Return response for the player
return rsp;
}
// Proto
@Override
public void encodeProto(StarTowerRoomCase proto) {
proto.getMutableBattleCase()
.setSubNoteSkillNum(this.getSubNoteSkillNum());
}
}

View File

@@ -0,0 +1,58 @@
package emu.nebula.game.tower.cases;
import emu.nebula.data.resources.StarTowerStageDef;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractResp;
import lombok.Getter;
@Getter
public class StarTowerDoorCase extends StarTowerBaseCase {
private int floorNum;
private int roomType;
public StarTowerDoorCase(int floor, StarTowerStageDef data) {
this.floorNum = floor;
if (data != null) {
this.roomType = data.getRoomType();
}
}
@Override
public CaseType getType() {
return CaseType.OpenDoor;
}
@Override
public StarTowerInteractResp interact(StarTowerInteractReq req, StarTowerInteractResp rsp) {
// Get request
var proto = req.getEnterReq();
// Check if we need to settle on the last floor
if (this.getGame().isOnFinalFloor()) {
return this.getGame().settle(rsp, true);
}
// Enter next room
this.getGame().enterNextRoom();
this.getGame().getRoom().setMapInfo(proto);
// Set room proto
rsp.getMutableEnterResp()
.setRoom(this.getRoom().toProto());
// Done
return rsp;
}
// Proto
@Override
public void encodeProto(StarTowerRoomCase proto) {
proto.getMutableDoorCase()
.setFloor(this.getFloorNum())
.setType(this.getRoomType());
}
}

View File

@@ -0,0 +1,253 @@
package emu.nebula.game.tower.cases;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import emu.nebula.GameConstants;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.tower.StarTowerShopGoods;
import emu.nebula.proto.PublicStarTower.HawkerCaseData;
import emu.nebula.proto.PublicStarTower.HawkerGoods;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractResp;
import emu.nebula.util.Utils;
import lombok.Getter;
@Getter
public class StarTowerHawkerCase extends StarTowerBaseCase {
private Map<Integer, StarTowerShopGoods> goods;
public StarTowerHawkerCase() {
this.goods = new HashMap<>();
}
@Override
public CaseType getType() {
return CaseType.Hawker;
}
@Override
public void onRegister() {
this.initGoods();
}
public void initGoods() {
// Clear goods
this.getGoods().clear();
// Caclulate amount of potentials/sub notes to sell
int total = getModifiers().getShopGoodsCount();
int minPotentials = Math.max(total / 2, 2);
int maxPotentials = Math.max(total - 1, minPotentials);
int potentials = Utils.randomRange(minPotentials, maxPotentials);
int subNotes = total - potentials;
boolean hasCoins = this.getGame().getResCount(GameConstants.TOWER_COIN_ITEM_ID) >= 500;
// Add goods
for (int i = 0; i < potentials; i++) {
// Create potential selector shop item
var goods = new StarTowerShopGoods(1, 1, 102, 200);
// Add character specific potentials
if (Utils.generateRandomDouble() < .2) {
goods.setCharPos(1);
}
// Add to goods map
this.addGoods(goods);
}
for (int i = 0; i < subNotes; i++) {
// Randomize sub note
int id = Utils.randomElement(this.getGame().getSubNoteDropList());
// Create sub note shop item
StarTowerShopGoods goods = null;
if (hasCoins && Utils.randomChance(.25)) {
goods = new StarTowerShopGoods(2, 8, id, 400);
} else {
goods = new StarTowerShopGoods(2, 3, id, 90);
}
// Add to goods map
this.addGoods(goods);
}
// Apply discounts based on star tower growth nodes
if (getModifiers().isShopDiscountTier1()) {
this.applyDiscount(1.0, 2, 0.8);
}
if (getModifiers().isShopDiscountTier2()) {
this.applyDiscount(0.3, 1, 0.5);
}
if (getModifiers().isShopDiscountTier3()) {
this.applyDiscount(1.0, 1, 0.5);
}
}
private void applyDiscount(double chance, int times, double percentage) {
// Check chance
double random = Utils.generateRandomDouble();
if (random > chance) {
return;
}
// Create goods list
var list = this.getGoods().values().stream()
.filter(g -> !g.hasDiscount())
.collect(Collectors.toList());
// Apply discounts
for (int i = 0; i < times; i++) {
// Sanity check
if (list.isEmpty()) {
break;
}
// Get goods and apply discount
var goods = Utils.randomElement(list, true);
goods.applyDiscount(percentage);
}
}
public void addGoods(StarTowerShopGoods goods) {
this.getGoods().put(getGoods().size() + 1, goods);
}
@Override
public StarTowerInteractResp interact(StarTowerInteractReq req, StarTowerInteractResp rsp) {
// Set nil resp
rsp.getMutableNilResp();
// Get hawker req
var hawker = req.getHawkerReq();
if (hawker.hasReRoll()) {
// Refresh shop items
this.refresh(rsp);
} else if (hawker.hasSid()) {
// Buy shop items
this.buy(hawker.getSid(), rsp);
}
// Success
return rsp;
}
private void refresh(StarTowerInteractResp rsp) {
// Check if we can refresh
if (this.getModifiers().getShopRerollCount() <= 0) {
return;
}
// Make sure we have enough currency
int coin = this.getGame().getResCount(GameConstants.TOWER_COIN_ITEM_ID);
int price = this.getModifiers().getShopRerollPrice();
if (coin < price) {
return;
}
// Create new goods
this.initGoods();
// Consume reroll count
this.getGame().getModifiers().consumeShopReroll();
// Set in proto
rsp.getMutableSelectResp()
.setHawkerCase(this.toHawkerCaseProto());
// Remove coins
var change = this.getGame().addItem(GameConstants.TOWER_COIN_ITEM_ID, -price);
// Set change info
rsp.setChange(change.toProto());
}
private void buy(int sid, StarTowerInteractResp rsp) {
// Get goods
var goods = this.getGoods().get(sid);
if (goods == null) {
return;
}
// Make sure we have enough currency
int coin = this.getGame().getResCount(GameConstants.TOWER_COIN_ITEM_ID);
int price = goods.getPrice();
if (coin < price || goods.isSold()) {
return;
}
// Mark goods as sold
goods.markAsSold();
// Create change info
var change = new PlayerChangeInfo();
// Add goods
if (goods.getType() == 1) {
// Potential selector
int charId = goods.getCharId(this.getGame());
this.getGame().addCase(rsp.getMutableCases(), this.getGame().createPotentialSelector(charId));
} else {
// Sub notes
this.getGame().addItem(goods.getGoodsId(), goods.getCount(), change);
}
// Remove coins
this.getGame().addItem(GameConstants.TOWER_COIN_ITEM_ID, -price, change);
// Set change info
rsp.setChange(change.toProto());
}
// Proto
private HawkerCaseData toHawkerCaseProto() {
var hawker = HawkerCaseData.newInstance();
if (this.getModifiers().getShopRerollCount() > 0) {
hawker.setCanReRoll(true);
hawker.setReRollTimes(this.getModifiers().getShopRerollCount());
hawker.setReRollPrice(this.getModifiers().getShopRerollPrice());
}
for (var entry : this.getGoods().entrySet()) {
var sid = entry.getKey();
var goods = entry.getValue();
var info = HawkerGoods.newInstance()
.setSid(sid)
.setType(goods.getType())
.setIdx(goods.getIdx())
.setGoodsId(goods.getGoodsId())
.setPrice(goods.getDisplayPrice())
.setTag(1);
if (goods.hasDiscount()) {
info.setDiscount(goods.getPrice());
}
if (goods.getCharPos() > 0) {
info.setCharPos(goods.getCharPos());
}
hawker.addList(info);
}
return hawker;
}
@Override
public void encodeProto(StarTowerRoomCase proto) {
proto.setHawkerCase(this.toHawkerCaseProto());
}
}

View File

@@ -0,0 +1,324 @@
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;
@Getter
public class StarTowerNpcEventCase extends StarTowerBaseCase {
private int npcId;
private int eventId;
private IntList options;
private boolean completed;
public StarTowerNpcEventCase(int npcId, StarTowerEventDef event) {
this.npcId = npcId;
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
public CaseType getType() {
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);
} else {
success.setOptionsParamId(100140101);
}
}
case 11501, 11502, 11503, 11504, 11505 -> {
if (option == 11503) {
this.addPotentialSelector(rsp);
} else {
success.setOptionsParamId(100140101);
}
}
case 11601, 11602, 11603, 11604, 11605 -> {
if (option == 11603) {
this.addRarePotentialSelector(rsp);
} else {
success.setOptionsParamId(100140101);
}
}
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());
}
}

View File

@@ -0,0 +1,38 @@
package emu.nebula.game.tower.cases;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractResp;
import lombok.Getter;
@Getter
public class StarTowerNpcRecoveryHPCase extends StarTowerBaseCase {
private int effectId;
public StarTowerNpcRecoveryHPCase() {
this(989970); // Restore Hp/Energy by 50%
}
public StarTowerNpcRecoveryHPCase(int effectId) {
this.effectId = effectId;
}
@Override
public CaseType getType() {
return CaseType.NpcRecoveryHP;
}
@Override
public StarTowerInteractResp interact(StarTowerInteractReq req, StarTowerInteractResp rsp) {
return rsp;
}
// Proto
@Override
public void encodeProto(StarTowerRoomCase proto) {
// Set case info
proto.getMutableNpcRecoveryHPCase()
.setEffectId(this.getEffectId());
}
}

View File

@@ -0,0 +1,161 @@
package emu.nebula.game.tower.cases;
import java.util.List;
import emu.nebula.GameConstants;
import emu.nebula.game.tower.StarTowerGame;
import emu.nebula.game.tower.StarTowerPotentialInfo;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractResp;
import lombok.Getter;
@Getter
public class StarTowerPotentialCase extends StarTowerBaseCase {
private int teamLevel;
private int charId;
private int reroll;
private int rerollPrice;
private boolean strengthen;
private List<StarTowerPotentialInfo> potentials;
public StarTowerPotentialCase(StarTowerGame game, boolean strengthen, List<StarTowerPotentialInfo> potentials) {
this(game, 0, potentials);
this.strengthen = strengthen;
}
public StarTowerPotentialCase(StarTowerGame game, int charId, List<StarTowerPotentialInfo> potentials) {
this.teamLevel = game.getTeamLevel();
this.charId = charId;
this.reroll = game.getModifiers().getPotentialRerollCount();
this.rerollPrice = 100 - game.getModifiers().getPotentialRerollDiscount();
this.potentials = potentials;
}
@Override
public CaseType getType() {
return CaseType.PotentialSelect;
}
public boolean isRare() {
return false;
}
public void setReroll(int count) {
this.reroll = count;
}
public boolean canReroll() {
return this.reroll > 0;
}
public StarTowerPotentialInfo selectId(int index) {
if (index < 0 || index >= this.getPotentials().size()) {
return null;
}
return this.getPotentials().get(index);
}
@Override
public StarTowerInteractResp interact(StarTowerInteractReq req, StarTowerInteractResp rsp) {
// Check
var select = req.getMutableSelectReq();
if (select.hasReRoll()) {
return this.reroll(rsp);
} else {
return this.select(select.getIndex(), rsp);
}
}
private StarTowerInteractResp reroll(StarTowerInteractResp rsp) {
// Check if we can reroll
if (!this.canReroll()) {
return rsp;
}
// Check price
int coin = this.getGame().getResCount(GameConstants.TOWER_COIN_ITEM_ID);
int price = this.getRerollPrice();
if (coin < price) {
return rsp;
}
// Subtract rerolls
int newReroll = this.reroll - 1;
// Create reroll case
StarTowerPotentialCase rerollCase = null;
if (this.isStrengthen()) {
rerollCase = this.getGame().createStrengthenSelector();
} else {
rerollCase = this.getGame().createPotentialSelector(this.getCharId(), this.isRare());
}
if (rerollCase == null) {
return rsp;
}
// Clear reroll count
rerollCase.setReroll(newReroll);
// Add reroll case
this.getRoom().addCase(rsp.getMutableCases(), rerollCase);
// Finish subtracting rerolls
this.reroll = newReroll;
// Subtract coins
var change = this.getGame().addItem(GameConstants.TOWER_COIN_ITEM_ID, -price);
rsp.setChange(change.toProto());
// Complete
return rsp;
}
private StarTowerInteractResp select(int index, StarTowerInteractResp rsp) {
// Get selected potential
var potential = this.selectId(index);
if (potential == null) {
return rsp;
}
// Add potential
var change = this.getGame().addItem(potential.getId(), potential.getLevel());
// Set change
rsp.setChange(change.toProto());
// Handle pending potential selectors
var nextCases = this.getGame().handlePendingPotentialSelectors();
for (var towerCase : nextCases) {
this.getRoom().addCase(rsp.getMutableCases(), towerCase);
}
// Complete
return rsp;
}
// Proto
@Override
public void encodeProto(StarTowerRoomCase proto) {
var select = proto.getMutableSelectPotentialCase()
.setTeamLevel(this.getTeamLevel());
for (var potential : this.getPotentials()) {
select.addInfos(potential.toProto());
}
if (this.canReroll()) {
select.setCanReRoll(true);
select.setReRollPrice(this.getRerollPrice());
}
}
}

View File

@@ -0,0 +1,35 @@
package emu.nebula.game.tower.cases;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractResp;
import lombok.Getter;
@Getter
public class StarTowerRecoveryHPCase extends StarTowerBaseCase {
@Override
public CaseType getType() {
return CaseType.RecoveryHP;
}
@Override
public StarTowerInteractResp interact(StarTowerInteractReq req, StarTowerInteractResp rsp) {
// Set nil resp
rsp.getMutableNilResp();
// Add sync hp case
this.getGame().addCase(rsp.getMutableCases(), new StarTowerSyncHPCase());
// Return
return rsp;
}
// Proto
@Override
public void encodeProto(StarTowerRoomCase proto) {
// Set field in the proto
proto.getMutableRecoveryHPCase();
}
}

View File

@@ -0,0 +1,43 @@
package emu.nebula.game.tower.cases;
import java.util.List;
import emu.nebula.game.tower.StarTowerGame;
import emu.nebula.game.tower.StarTowerPotentialInfo;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import lombok.Getter;
@Getter
public class StarTowerSelectSpecialPotentialCase extends StarTowerPotentialCase {
public StarTowerSelectSpecialPotentialCase(StarTowerGame game, int charId, List<StarTowerPotentialInfo> potentials) {
super(game, charId, potentials);
}
@Override
public CaseType getType() {
return CaseType.SelectSpecialPotential;
}
public boolean isRare() {
return true;
}
// Proto
@Override
public void encodeProto(StarTowerRoomCase proto) {
var select = proto.getMutableSelectSpecialPotentialCase()
.setTeamLevel(this.getTeamLevel());
for (var potential : this.getPotentials()) {
select.addIds(potential.getId());
}
if (this.canReroll()) {
select.setCanReRoll(true);
select.setReRollPrice(this.getRerollPrice());
}
}
}

View File

@@ -0,0 +1,92 @@
package emu.nebula.game.tower.cases;
import emu.nebula.GameConstants;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractResp;
import lombok.Getter;
@Getter
public class StarTowerStrengthenMachineCase extends StarTowerBaseCase {
private boolean free;
private int discount;
private int times;
@Override
public void onRegister() {
// Set strengthen price
this.free = this.getModifiers().isFreeStrengthen();
this.discount = this.getModifiers().getStrengthenDiscount();
}
public int getPrice() {
if (this.free) {
return 0;
}
int price = 120 + (this.times * 60) - this.discount;
return Math.max(price, 0);
}
public void increasePrice() {
if (this.free) {
this.free = false;
this.getModifiers().setFreeStrengthen(false);
} else {
this.times++;
}
}
@Override
public CaseType getType() {
return CaseType.StrengthenMachine;
}
@Override
public StarTowerInteractResp interact(StarTowerInteractReq req, StarTowerInteractResp rsp) {
// Init case
StarTowerBaseCase towerCase = null;
// Check coin
int coin = getGame().getResCount(GameConstants.TOWER_COIN_ITEM_ID);
int price = this.getPrice();
if (coin >= price) {
towerCase = getGame().createStrengthenSelector();
}
if (towerCase != null) {
// Add enhancement selector case
this.getRoom().addCase(rsp.getMutableCases(), towerCase);
// Remove coins
var change = this.getGame().addItem(GameConstants.TOWER_COIN_ITEM_ID, -price);
// Set change info
rsp.setChange(change.toProto());
// Increment price
this.increasePrice();
}
// Set success result
rsp.getMutableStrengthenMachineResp()
.setBuySucceed(towerCase != null);
// Complete
return rsp;
}
// Proto
@Override
public void encodeProto(StarTowerRoomCase proto) {
// Set field in the proto
proto.getMutableStrengthenMachineCase()
.setFirstFree(this.isFree())
.setDiscount(this.getDiscount())
.setTimes(this.getTimes());
}
}

View File

@@ -0,0 +1,28 @@
package emu.nebula.game.tower.cases;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractResp;
import lombok.Getter;
@Getter
public class StarTowerSyncHPCase extends StarTowerBaseCase {
@Override
public CaseType getType() {
return CaseType.SyncHP;
}
@Override
public StarTowerInteractResp interact(StarTowerInteractReq req, StarTowerInteractResp rsp) {
return rsp;
}
// Proto
@Override
public void encodeProto(StarTowerRoomCase proto) {
// Set field in the proto
proto.getMutableSyncHPCase();
}
}

View File

@@ -1,10 +1,10 @@
package emu.nebula.game.tower;
package emu.nebula.game.tower.room;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum StarTowerRoomType {
public enum RoomType {
BattleRoom (0),
EliteBattleRoom (1),
BossRoom (2),
@@ -17,19 +17,19 @@ public enum StarTowerRoomType {
@Getter
private final int value;
private final static Int2ObjectMap<StarTowerRoomType> map = new Int2ObjectOpenHashMap<>();
private final static Int2ObjectMap<RoomType> map = new Int2ObjectOpenHashMap<>();
static {
for (StarTowerRoomType type : StarTowerRoomType.values()) {
for (RoomType type : RoomType.values()) {
map.put(type.getValue(), type);
}
}
private StarTowerRoomType(int value) {
private RoomType(int value) {
this.value = value;
}
public static StarTowerRoomType getByValue(int value) {
public static RoomType getByValue(int value) {
return map.get(value);
}
}

View File

@@ -0,0 +1,158 @@
package emu.nebula.game.tower.room;
import emu.nebula.data.resources.StarTowerStageDef;
import emu.nebula.game.tower.StarTowerGame;
import emu.nebula.game.tower.StarTowerModifiers;
import emu.nebula.game.tower.cases.CaseType;
import emu.nebula.game.tower.cases.StarTowerBaseCase;
import emu.nebula.game.tower.cases.StarTowerSyncHPCase;
import emu.nebula.proto.PublicStarTower.InteractEnterReq;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import emu.nebula.proto.PublicStarTower.StarTowerRoomData;
import emu.nebula.proto.StarTowerApply.StarTowerApplyReq;
import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import lombok.Getter;
import us.hebi.quickbuf.RepeatedMessage;
@Getter
public class StarTowerBaseRoom {
// Game
private transient StarTowerGame game;
private transient StarTowerStageDef stage;
// Map info
private int mapId;
private int mapTableId;
private String mapParam;
private int paramId;
// Cases
private int lastCaseId = 0;
private Int2ObjectMap<StarTowerBaseCase> cases;
// Misc
private boolean hasDoor;
public StarTowerBaseRoom(StarTowerGame game, StarTowerStageDef stage) {
this.game = game;
this.stage = stage;
this.cases = new Int2ObjectLinkedOpenHashMap<>();
}
public int getType() {
return stage.getRoomType();
}
public boolean hasDoor() {
return this.hasDoor;
}
public StarTowerModifiers getModifiers() {
return this.getGame().getModifiers();
}
public StarTowerBaseCase createExit() {
return this.getGame().createExit();
}
// Map info
public void setMapInfo(StarTowerApplyReq req) {
this.mapId = req.getMapId();
this.mapTableId = req.getMapTableId();
this.mapParam = req.getMapParam();
this.paramId = req.getParamId();
}
public void setMapInfo(InteractEnterReq req) {
this.mapId = req.getMapId();
this.mapTableId = req.getMapTableId();
this.mapParam = req.getMapParam();
this.paramId = req.getParamId();
}
// Cases
public int getNextCaseId() {
return ++this.lastCaseId;
}
public StarTowerBaseCase getCase(int id) {
return this.getCases().get(id);
}
public StarTowerBaseCase addCase(StarTowerBaseCase towerCase) {
return this.addCase(null, towerCase);
}
public StarTowerBaseCase addCase(RepeatedMessage<StarTowerRoomCase> cases, StarTowerBaseCase towerCase) {
// Sanity check
if (towerCase == null) {
return null;
}
// Set game for tower case
towerCase.register(this);
// Add to cases list
this.getCases().put(towerCase.getId(), towerCase);
// Add case to proto
if (cases != null) {
cases.add(towerCase.toProto());
}
// Check if door case
if (towerCase.getType() == CaseType.OpenDoor) {
this.hasDoor = true;
}
// Complete
return towerCase;
}
// Events
public void onEnter() {
// Create sync hp case
this.addCase(new StarTowerSyncHPCase());
// Create door case
this.createExit();
}
// Proto
public emu.nebula.proto.PublicStarTower.StarTowerRoom toProto() {
var proto = emu.nebula.proto.PublicStarTower.StarTowerRoom.newInstance()
.setData(this.getDataProto());
for (var towerCase : this.getCases().values()) {
proto.addCases(towerCase.toProto());
}
return proto;
}
private StarTowerRoomData getDataProto() {
var proto = StarTowerRoomData.newInstance()
.setFloor(this.getGame().getFloorCount())
.setMapId(this.getMapId())
.setRoomType(this.getType())
.setMapTableId(this.getMapTableId());
if (this.getMapParam() != null && !this.getMapParam().isEmpty()) {
proto.setMapParam(this.getMapParam());
}
if (this.getParamId() != 0) {
proto.setParamId(this.getParamId());
}
return proto;
}
}

View File

@@ -0,0 +1,26 @@
package emu.nebula.game.tower.room;
import emu.nebula.data.resources.StarTowerStageDef;
import emu.nebula.game.tower.StarTowerGame;
import emu.nebula.game.tower.cases.StarTowerBattleCase;
import emu.nebula.game.tower.cases.StarTowerSyncHPCase;
import emu.nebula.util.Utils;
import lombok.Getter;
@Getter
public class StarTowerBattleRoom extends StarTowerBaseRoom {
public StarTowerBattleRoom(StarTowerGame game, StarTowerStageDef stage) {
super(game, stage);
}
@Override
public void onEnter() {
// Create battle case
this.getGame().setPendingSubNotes(Utils.randomRange(1, 3));
this.addCase(new StarTowerBattleCase(this.getGame().getPendingSubNotes()));
// Create sync hp case
this.addCase(new StarTowerSyncHPCase());
}
}

View File

@@ -0,0 +1,71 @@
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;
@Getter
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());
// Create door case
this.createExit();
}
}

View File

@@ -0,0 +1,33 @@
package emu.nebula.game.tower.room;
import emu.nebula.data.resources.StarTowerStageDef;
import emu.nebula.game.tower.StarTowerGame;
import emu.nebula.game.tower.cases.StarTowerHawkerCase;
import emu.nebula.game.tower.cases.StarTowerStrengthenMachineCase;
import emu.nebula.game.tower.cases.StarTowerSyncHPCase;
import lombok.Getter;
@Getter
public class StarTowerHawkerRoom extends StarTowerBaseRoom {
public StarTowerHawkerRoom(StarTowerGame game, StarTowerStageDef stage) {
super(game, stage);
}
@Override
public void onEnter() {
// Create hawker case (shop)
this.addCase(new StarTowerHawkerCase());
// Create strengthen machine
if (this.getModifiers().isEnableShopStrengthen()) {
this.addCase(new StarTowerStrengthenMachineCase());
}
// Create sync hp case
this.addCase(new StarTowerSyncHPCase());
// Create door case
this.createExit();
}
}

View File

@@ -141,6 +141,9 @@ public class HttpServer {
if (this.getType().runGame()) {
this.addGameServerRoutes();
}
// Custom api route(s)
getApp().post("/api/command", new RemoteHandler());
// Exception handler
getApp().exception(Exception.class, (e, c) -> {
@@ -154,8 +157,8 @@ public class HttpServer {
private void addLoginServerRoutes() {
// https://en-sdk-api.yostarplat.com/
getApp().post("/common/config", new CommonConfigHandler(this));
getApp().post("/common/version", new HttpJsonResponse(
"{\"Code\":200,\"Data\":{\"Agreement\":[{\"Version\":\"0.1\",\"Type\":\"user_agreement\",\"Title\":\"用户协议\",\"Content\":\"\",\"Lang\":\"en\"},{\"Version\":\"0.1\",\"Type\":\"privacy_agreement\",\"Title\":\"隐私政策\",\"Content\":\"\",\"Lang\":\"en\"}],\"ErrorCode\":\"4.4\"},\"Msg\":\"OK\"}"));
getApp().post("/common/client-code", new CommonClientCodeHandler());
getApp().post("/common/version", new HttpJsonResponse("{\"Code\":200,\"Data\":{\"Agreement\":[{\"Version\":\"0.1\",\"Type\":\"user_agreement\",\"Title\":\"用户协议\",\"Content\":\"\",\"Lang\":\"en\"},{\"Version\":\"0.1\",\"Type\":\"privacy_agreement\",\"Title\":\"隐私政策\",\"Content\":\"\",\"Lang\":\"en\"}],\"ErrorCode\":\"4.4\"},\"Msg\":\"OK\"}"));
getApp().post("/user/detail", new UserLoginHandler());
getApp().post("/user/set", new UserSetDataHandler());
@@ -163,16 +166,11 @@ public class HttpServer {
getApp().post("/user/quick-login", new UserLoginHandler());
getApp().post("/yostar/get-auth", new GetAuthHandler());
getApp().post("/yostar/send-code", new HttpJsonResponse("{\"Code\":200,\"Data\":{},\"Msg\":\"OK\"}")); // Dummy
// handler
getApp().post("/yostar/send-code", new HttpJsonResponse("{\"Code\":200,\"Data\":{},\"Msg\":\"OK\"}")); // Dummy handler
// https://nova-static.stellasora.global/
getApp().get("/meta/serverlist.html", new MetaServerlistHandler(this));
getApp().get("/meta/win.html", new MetaWinHandler(this));
getApp().post("/api/command", new RemoteHandler());
getApp().get("/meta/*.html", new MetaPatchListHandler(this));
}
private void addGameServerRoutes() {

View File

@@ -5,6 +5,7 @@ import emu.nebula.net.NetMsgId;
import emu.nebula.proto.StarTowerApply.StarTowerApplyReq;
import emu.nebula.proto.StarTowerApply.StarTowerApplyResp;
import emu.nebula.net.HandlerId;
import emu.nebula.GameConstants;
import emu.nebula.game.tower.StarTowerGame;
import emu.nebula.net.GameSession;
@@ -29,6 +30,7 @@ public class HandlerStarTowerApplyReq extends NetHandler {
// Create response
var rsp = StarTowerApplyResp.newInstance()
.setLastId(req.getId())
.setCoinQty(game.getResCount(GameConstants.TOWER_COIN_ITEM_ID))
.setInfo(game.toProto())
.setChange(change.toProto());

View File

@@ -20,7 +20,7 @@ public class HandlerStarTowerGiveUpReq extends NetHandler {
// Build response
var rsp = StarTowerGiveUpResp.newInstance()
.setBuild(game.getBuild().toProto())
.setFloor(game.getFloor());
.setFloor(game.getFloorCount());
rsp.getMutableChange();

File diff suppressed because one or more lines are too long

View File

@@ -12,10 +12,10 @@ import lombok.AccessLevel;
import lombok.Getter;
@Getter(AccessLevel.PRIVATE)
public class MetaWinHandler implements Handler {
public class MetaPatchListHandler implements Handler {
private HttpServer server;
public MetaWinHandler(HttpServer server) {
public MetaPatchListHandler(HttpServer server) {
this.server = server;
}

View File

@@ -40,11 +40,11 @@ public class RemoteHandler implements Handler {
// Check admin key
if (token.equals(adminKey)) {
Nebula.getCommandManager().invoke(null, command);
var commandResult = Nebula.getCommandManager().invoke(null, command);
Nebula.getLogger().warn("\u001B[38;2;252;186;3mRemote Server (Using Admin Key) sent command: /" + command + "\u001B[0m");
ctx.status(200);
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result("{\"Code\":200,\"Data\":{},\"Msg\":\"Command executed\"}");
ctx.result("{\"Code\":200,\"Data\":{},\"Msg\":\"" + commandResult.getMessage() + "\"}");
return;
}
@@ -78,11 +78,11 @@ public class RemoteHandler implements Handler {
Nebula.getLogger().info("Remote Player Request [" + player.getUid() + "]: " + finalCommand);
// Execute as console (null sender) but targeting the player
Nebula.getCommandManager().invoke(null, finalCommand);
var commandResult = Nebula.getCommandManager().invoke(null, finalCommand);
ctx.status(200);
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result("{\"Code\":200,\"Data\":{},\"Msg\":\"Command executed\"}");
ctx.result("{\"Code\":200,\"Data\":{},\"Msg\":\"" + commandResult.getMessage() + "\"}");
return;
}

View File

@@ -19,8 +19,7 @@ import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
import org.bouncycastle.crypto.params.*;
import emu.nebula.Nebula;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import emu.nebula.RegionConfig;
// Official Name: AeadTool
public class AeadHelper {
@@ -34,29 +33,31 @@ public class AeadHelper {
public static byte[] serverGarbleKey = null;
public static byte[] serverMetaKey = null;
private static final Object2ObjectMap<String, String[]> keys = new Object2ObjectOpenHashMap<>();
static {
RegionConfig.getRegion("global")
.setServerMetaKey("ma5Dn2FhC*Xhxy%c")
.setServerGarbleKey("xNdVF^XTa6T3HCUATMQ@sKMLzAw&%L!3");
RegionConfig.getRegion("kr")
.setServerMetaKey("U9cjHuwGDDx&$drn")
.setServerGarbleKey("25hdume9H#*6hHn@d9hSF7tekTwN#JYj");
RegionConfig.getRegion("jp")
.setServerMetaKey("ZnUFA@S9%4KyoryM")
.setServerGarbleKey("yX5Gt64PVvVH6$qwBXaPJC*LZKoK5mYh");
RegionConfig.getRegion("tw")
.setServerMetaKey("owGYVDmfHrxi^4pm")
.setServerGarbleKey("N&mfco452ZH5!nE3s&o5uxB57UGPENVo");
}
public static void loadKeys() {
// Load keys
keys.put("global", new String[] {
"ma5Dn2FhC*Xhxy%c",
"xNdVF^XTa6T3HCUATMQ@sKMLzAw&%L!3"
});
keys.put("kr", new String[] {
"U9cjHuwGDDx&$drn",
"25hdume9H#*6hHn@d9hSF7tekTwN#JYj"
});
// Get key data
var keyData = keys.get(Nebula.getConfig().getRegion().toLowerCase());
if (keyData == null) {
keyData = keys.get("global"); // Default region
}
var region = RegionConfig.getRegion(Nebula.getConfig().getRegion());
// Set keys
serverMetaKey = keyData[0].getBytes(StandardCharsets.US_ASCII);
serverGarbleKey = keyData[1].getBytes(StandardCharsets.US_ASCII);
serverMetaKey = region.getServerMetaKey().getBytes(StandardCharsets.US_ASCII);
serverGarbleKey = region.getServerGarbleKey().getBytes(StandardCharsets.US_ASCII);
}
public static byte[] generateBytes(int size) {

View File

@@ -17,12 +17,13 @@ import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.data.resources.CharacterDef;
import emu.nebula.data.resources.ItemDef;
import emu.nebula.data.resources.PotentialDef;
import emu.nebula.game.inventory.ItemType;
public class Handbook {
public static void generate() {
// Temp vars
Map<String, String> languageKey = null;
List<Integer> list = null;
// Save to file
@@ -41,31 +42,64 @@ public class Handbook {
writer.println(System.lineSeparator());
writer.println("# Characters");
list = GameData.getCharacterDataTable().keySet().intStream().sorted().boxed().toList();
languageKey = loadLanguageKey(CharacterDef.class);
var characterLanguageKey = loadLanguageKey(CharacterDef.class);
for (int id : list) {
CharacterDef data = GameData.getCharacterDataTable().get(id);
writer.print(data.getId());
writer.print(" : ");
writer.print(languageKey.getOrDefault(data.getName(), data.getName()));
writer.print(characterLanguageKey.getOrDefault(data.getName(), data.getName()));
writer.print(" (");
writer.print(data.getElementType().toString());
writer.println(")");
}
// Dump characters
// Dump items
writer.println(System.lineSeparator());
writer.println("# Items");
list = GameData.getItemDataTable().keySet().intStream().sorted().boxed().toList();
languageKey = loadLanguageKey(ItemDef.class);
var itemLanguageKey = loadLanguageKey(ItemDef.class);
for (int id : list) {
ItemDef data = GameData.getItemDataTable().get(id);
writer.print(data.getId());
writer.print(" : ");
writer.print(languageKey.getOrDefault(data.getTitle(), data.getTitle()));
writer.print(itemLanguageKey.getOrDefault(data.getTitle(), data.getTitle()));
writer.print(" [");
writer.print(data.getItemType());
writer.println("]");
writer.print("]");
if (data.getItemType() == ItemType.Disc) {
var discData = GameData.getDiscDataTable().get(id);
if (discData != null) {
writer.print(" (");
writer.print(discData.getElementType().toString());
writer.print(")");
}
}
writer.println("");
}
// Dump potentials
writer.println(System.lineSeparator());
writer.println("# Potentials");
list = GameData.getPotentialDataTable().keySet().intStream().sorted().boxed().toList();
var potentialLanguageKey = loadLanguageKey(PotentialDef.class);
for (int id : list) {
PotentialDef data = GameData.getPotentialDataTable().get(id);
writer.print(data.getId());
writer.print(" : ");
CharacterDef charData = GameData.getCharacterDataTable().get(data.getCharId());
writer.print("[");
writer.print(characterLanguageKey.getOrDefault(charData.getName(), charData.getName()));
writer.print("] ");
ItemDef itemData = GameData.getItemDataTable().get(id);
writer.print(itemLanguageKey.getOrDefault(itemData.getTitle(), itemData.getTitle()));
writer.print(" - ");
writer.print(potentialLanguageKey.getOrDefault(data.getBriefDesc(), data.getBriefDesc()));
writer.println("");
}
} catch (IOException e) {
e.printStackTrace();

View File

@@ -173,18 +173,48 @@ public class Utils {
public static int randomRange(int min, int max) {
return ThreadLocalRandom.current().nextInt(min, max + 1);
}
public static boolean randomChance(double chance) {
if (chance <= 0) {
return false;
}
return ThreadLocalRandom.current().nextDouble() < chance;
}
public static int randomElement(int[] array) {
return array[ThreadLocalRandom.current().nextInt(0, array.length)];
}
public static <T> T randomElement(List<T> list) {
return list.get(ThreadLocalRandom.current().nextInt(0, list.size()));
return randomElement(list, false);
}
public static <T> T randomElement(List<T> list, boolean remove) {
int index = ThreadLocalRandom.current().nextInt(0, list.size());
var object = list.get(index);
if (remove) {
list.remove(index);
}
return object;
}
public static int randomElement(IntList list) {
return list.getInt(ThreadLocalRandom.current().nextInt(0, list.size()));
}
public static int randomElement(IntList list, boolean remove) {
int index = ThreadLocalRandom.current().nextInt(0, list.size());
int object = list.getInt(index);
if (remove) {
list.removeInt(index);
}
return object;
}
/**
* Checks if an integer array contains a value