79 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
Melledy
30f565d0d6 Bump version to 1.1.2 2025-11-30 23:49:50 -08:00
Melledy
faa4ea0780 Only show boss blitz leaderboard for the current season 2025-11-30 23:48:24 -08:00
Melledy
354f390c3b Reset score entry if control id doesn't match 2025-11-30 23:42:34 -08:00
Melledy
3171bce3cc Update boss blitz control id 2025-11-30 23:35:04 -08:00
Melledy
56b4d3b66d Fix monolith crash at floor 20 2025-11-30 23:33:05 -08:00
Melledy
585734c2f3 Handle more achievements 2025-11-30 23:16:37 -08:00
Melledy
6f7a92725a Rework how quest/achievement conditions are handled 2025-11-30 17:06:08 -08:00
Melledy
a04f3354f7 Implement trial activities 2025-11-30 15:19:35 -08:00
Melledy
f53bdaba32 Implement achievements properly 2025-11-29 00:29:04 -08:00
Melledy
898e8dd43f Bump version to 1.1.1 2025-11-28 14:33:53 -08:00
Melledy
29db60fd0a Fix commission bonus rewards 2025-11-28 14:29:57 -08:00
Fishia
7577bf87d4 fix(startower): fix level up logic 2025-11-28 07:14:59 -08:00
Melledy
3e47c8ef06 Cleanup + Bump version to 1.1.0 2025-11-27 19:25:40 -08:00
Melledy
a981c6fce4 Save score in cataclysm survivor properly 2025-11-27 12:20:41 -08:00
Melledy
26ea746368 Hardcode emblem attribute values
(They were removed from the data files)
2025-11-27 12:00:08 -08:00
Fishia
4356b194df feat(startower): leveling up 2025-11-27 07:49:00 -08:00
Fishia
bfb22eddff feat: properly handle door cases and currency 2025-11-27 07:49:00 -08:00
Melledy
3f2f58b227 Implement weekly resets for battlepass quests 2025-11-27 01:05:59 -08:00
Melledy
187d715866 Add basic support for other regions 2025-11-26 21:31:09 -08:00
Melledy
7df1be99ef Fix gitignore 2025-11-26 19:48:19 -08:00
RaidenShogun503
6d9e28963d Add Command SetLevel (#8)
* Add Remote Command API (Use KEY)

* Clean up HttpServer by removing unused code

Removed commented-out code

* Add SetLevel Command (need to restart the game for it to take effect)

* Add Proto Request Change Level (Not Work)

* Update setlevel command

---------

Co-authored-by: Melledy <121644117+Melledy@users.noreply.github.com>
2025-11-26 19:45:45 -08:00
Melledy
2acd506245 Improve command system 2025-11-25 23:09:20 -08:00
Melledy
fd8e8925ca Implement heartlink invite 2025-11-25 22:33:25 -08:00
AhAlpha
e529e8e965 Change DATA_VERSION to 54 2025-11-25 22:30:26 -08:00
阁主
3a7d12c7b7 Implement !clean command and f affinity 2025-11-25 19:33:02 -08:00
Furiri
008cd06b32 Add Remote Command API (Use KEY) 2025-11-25 06:24:17 -08:00
Melledy
55ff9b2826 Add completed dummy achievements because the client needs them for random things 2025-11-24 21:38:40 -08:00
Melledy
c78c6a7492 Implement trekker affinity story 2025-11-24 03:07:47 -08:00
Melledy
139b1cf87e Update daily shop reward 2025-11-24 01:39:45 -08:00
Melledy
3059c549d5 Fix cataclysm survivor score calculation 2025-11-21 20:36:35 -08:00
Haru
e8a3576d8b Avoid null pointer 2025-11-21 07:41:09 -08:00
Fishia
a530166347 Add nightly CI 2025-11-21 06:16:23 -08:00
Melledy
0ef1231bf1 Add a config option for configuring session timeout 2025-11-21 05:10:51 -08:00
Melledy
e5fe89ebc9 Implement sending gifts 2025-11-21 04:46:11 -08:00
Melledy
be842e4269 Fix emblem reforging not working when reforging locked attributes 2025-11-21 03:35:28 -08:00
Melledy
eead51d32f Implement starting gold monolith research talents 2025-11-21 02:52:00 -08:00
Melledy
5382877a83 Increase max formations to 6 2025-11-21 02:41:41 -08:00
Melledy
fdd4264b40 Fix showcase not updating when new characters/discs are added 2025-11-21 02:05:36 -08:00
阁主
e25d58d7f5 Fix crash when submitting empty player signature 2025-11-20 23:16:53 -08:00
Melledy
f128b93210 Fix issues with unlocking talents in monolith research 2025-11-20 05:42:44 -08:00
Melledy
6289e3f1b6 Fix buying energy with gems not giving the correct amount 2025-11-20 05:37:21 -08:00
Melledy
480d0d60b3 Fix quick craft not using item choice boxes to create materials 2025-11-20 05:25:39 -08:00
Melledy
e4b7bb74c5 Bump server version to 1.0.1 2025-11-19 06:50:10 -08:00
Melledy
01ed3da162 Fix player registration 2025-11-19 06:25:46 -08:00
165 changed files with 7068 additions and 1064 deletions

33
.github/workflows/gradle.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Nebula Nightly
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582
- name: Build
run: |
chmod +x gradlew
./gradlew jar
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: Nebula-Nightly
path: Nebula.jar

3
.gitignore vendored
View File

@@ -58,12 +58,13 @@ tmp/
.DS_Store
.directory
# Generated/resource/log/plugins folders
# Generated/resource/log/plugins/etc folders
/logs
/plugins
/proto
/resources
/web
/StellaSoraData
# Compiled
/*.jar

View File

@@ -6,7 +6,7 @@ For any extra support, questions, or discussions, check out our [Discord](https:
### Notable features
- Basic profile features
- Character system implemented (except for affinity)
- Character system
- Inventory/Discs working
- Energy system
- Mail system
@@ -15,20 +15,28 @@ For any extra support, questions, or discussions, check out our [Discord](https:
- Battle pass
- Gacha
- Friend system (sending energy not implemented)
- Shop (using only in-game currency)
- Shop (only in-game currency supported)
- Commissions
- Heartlink (missing advanced affinity related features)
- Heartlink
- Achievements
- Monoliths (completeable but many other features missing)
- Bounty Trials
- Menance Arena
- Proving grounds
- Catacylsm Survivor (talents not fully working, score not calculated properly)
- Catacylsm Survivor (talents not fully working)
- Boss Blitz
### Not implemented
- Achievements
- 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,34 +57,43 @@ 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
}
}
}
};
```
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 name, the code field is ignored by the server and can be set to anything.
5. Login with your account email, the code field is ignored by the server and can be set to anything.
### Server commands
Server commands need to be run in the server console OR in the signature edit menu of your profile.
```
!account {create | delete} [email] (reserved player uid) = Creates or deletes an account.
!char [all | {characterId}] lv(level) a(ascension) s(skill level) t(talent level) = Changes the properties of the targeted characters.
!char [all | {characterId}] lv(level) a(ascension) s(skill level) t(talent level) f(affinity level) = Changes the properties of the targeted characters.
!clean [all | {id} ...] [items|resources] = Removes items/resources from the targeted player.
!disc [all | {discId}] lv(level) a(ascension) c(crescendo level) = Changes the properties of the targeted discs.
!give [item id] x[amount] = Gives the targeted player an item through the mail.
!giveall [characters | discs | materials] = Gives the targeted player items.
!level (level) = Sets the player level
!mail = Sends the targeted player a system mail.
!reload = Reloads the server config.
```

View File

@@ -29,7 +29,7 @@ java {
}
}
version = '1.0.0'
version = '1.1.2'
var shouldGenerateProto = System.getenv("GENERATE_PROTO") == "true"
System.out.println(shouldGenerateProto ? "Generating proto files" : "Skipping proto generation")

View File

@@ -17,12 +17,15 @@ public class Config {
public HttpServerConfig httpServer = new HttpServerConfig(80);
public GameServerConfig gameServer = new GameServerConfig(80);
public ServerOptions serverOptions = new ServerOptions();
public ServerRates serverRates = new ServerRates();
public LogOptions logOptions = new LogOptions();
public RemoteCommand remoteCommand = new RemoteCommand();
public int customDataVersion = 0;
public String region = "global";
public String resourceDir = "./resources";
public String webFilesDir = "./web";
public String patchListPath = "./patchlist.json";
@@ -54,35 +57,35 @@ public class Config {
public int bindPort;
public String publicAddress = "127.0.0.1"; // Will return bindAddress if publicAddress is null
public Integer publicPort; // Will return bindPort if publicPort is null
public ServerConfig(int port) {
this.bindPort = port;
}
public String getPublicAddress() {
if (this.publicAddress != null && !this.publicAddress.isEmpty()) {
return this.publicAddress;
}
return this.bindAddress;
}
public int getPublicPort() {
if (this.publicPort != null && this.publicPort != 0) {
return this.publicPort;
}
return this.bindPort;
}
public String getDisplayAddress() {
return (useSSL ? "https" : "http") + "://" + getPublicAddress() + ":" + getPublicPort();
}
}
@Getter
public static class HttpServerConfig extends ServerConfig {
public HttpServerConfig(int port) {
super(port);
}
@@ -90,52 +93,59 @@ public class Config {
@Getter
public static class GameServerConfig extends ServerConfig {
public GameServerConfig(int port) {
super(port);
}
}
@Getter
public static class ServerOptions {
public Set<String> defaultPermissions = Set.of("*");
public boolean autoCreateAccount = true;
public boolean skipIntro = false;
public boolean unlockInstances = true;
public int sessionTimeout = 600; // How long to wait (in seconds) after the last http request from a session
// before removing it from the server
public int dailyResetHour = 0;
public int leaderboardRefreshTime = 60; // Leaderboard refresh time in seconds
public WelcomeMail welcomeMail = new WelcomeMail();
}
@Getter
public static class ServerRates {
public double exp = 1.0;
}
@Getter
public static class LogOptions {
public boolean commands = true;
public boolean packets = false;
}
@Getter
public static class RemoteCommand {
public boolean useRemoteServices = false;
public String serverAdminKey = "HJHASDPIIQWEASDHHAN";
}
@Getter
public static class WelcomeMail {
public String title;
public String sender;
public String content;
public List<ItemParam> attachments;
public WelcomeMail() {
this.title = "Welcome to a Nebula server";
this.sender = "Server";
this.content = "Welcome to Nebula! Please take these items as a starter gift.";
this.attachments = List.of(
new ItemParam(86009, 1),
new ItemParam(86002, 1),
new ItemParam(1, 1_000_000),
new ItemParam(2, 30_000)
);
new ItemParam(86009, 1),
new ItemParam(86002, 1),
new ItemParam(1, 1_000_000),
new ItemParam(2, 30_000));
}
}
}

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 = 51;
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;
@@ -29,7 +43,7 @@ public class GameConstants {
public static final int CHARACTER_MAX_GEM_PRESETS = 3;
public static final int CHARACTER_MAX_GEM_SLOTS = 3;
public static final int MAX_FORMATIONS = 5;
public static final int MAX_FORMATIONS = 6;
public static final int MAX_SHOWCASE_IDS = 5;
public static final int BATTLE_PASS_ID = 1;
@@ -37,6 +51,20 @@ 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},
new int[] {300, 200}
};
// Daily gifts (Custom)
public static final WeightedList<ItemParam> DAILY_GIFTS = new WeightedList<>();
@@ -44,13 +72,21 @@ public class GameConstants {
static {
DAILY_GIFTS.add(1000, new ItemParam(GOLD_ITEM_ID, 8888));
DAILY_GIFTS.add(250, new ItemParam(GOLD_ITEM_ID, 18888));
DAILY_GIFTS.add(250, new ItemParam(33001, 10));
DAILY_GIFTS.add(10, new ItemParam(GEM_ITEM_ID, 50));
}
// Helper functions
public static String getGameVersion() {
return VERSION + "." + getDataVersion();
// 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

@@ -17,6 +17,7 @@ import emu.nebula.game.GameContext;
import emu.nebula.net.PacketHelper;
import emu.nebula.plugin.PluginManager;
import emu.nebula.server.HttpServer;
import emu.nebula.util.AeadHelper;
import emu.nebula.util.Handbook;
import emu.nebula.util.JsonUtils;
import lombok.Getter;
@@ -52,6 +53,9 @@ public class Nebula {
boolean generateHandbook = true;
// Load keys
AeadHelper.loadKeys();
// Load plugin manager
Nebula.pluginManager = new PluginManager();
@@ -107,7 +111,7 @@ public class Nebula {
// Start servers
try {
// Always run http server as it is needed by for dispatch and gateserver
// Always run http server as it is needed by for login and game server
httpServer = new HttpServer(serverType);
httpServer.start();
} catch (Exception exception) {
@@ -233,7 +237,8 @@ public class Nebula {
String input;
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
while ((input = br.readLine()) != null) {
Nebula.getCommandManager().invoke(null, input);
var result = Nebula.getCommandManager().invoke(null, input);
Nebula.getLogger().info(result.getMessage());
}
} catch (Exception e) {
Nebula.getLogger().error("Console error:", e);

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;
@@ -26,6 +26,7 @@ public class CommandArgs {
private int advance = -1;
private int talent = -1;
private int skill = -1;
private int affinity = -1;
private Int2IntMap map;
private ObjectSet<String> flags;
@@ -52,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();
@@ -61,6 +65,9 @@ public class CommandArgs {
} else if (arg.startsWith("s")) { // Skill
this.skill = Utils.parseSafeInt(arg.substring(1));
it.remove();
} else if (arg.startsWith("f")) { // Affinity level
this.affinity = Utils.parseSafeInt(arg.substring(1));
it.remove();
}
} else if (arg.startsWith("-")) { // Flag
if (this.flags == null) this.flags = new ObjectOpenHashSet<>();
@@ -72,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();
@@ -109,18 +116,6 @@ public class CommandArgs {
return this.list.get(index);
}
/**
* Sends a message to the command sender
* @param message
*/
public void sendMessage(String message) {
if (sender != null) {
sender.sendMessage(message);
} else {
Nebula.getLogger().info(message);
}
}
public boolean hasFlag(String flag) {
if (this.flags == null) return false;
return this.flags.contains(flag);
@@ -190,6 +185,16 @@ public class CommandArgs {
hasChanged = true;
}
if (this.getAffinity() >= 0) {
int target = this.getAffinity();
if (target > 50) target = 50;
if (target < 0) target = 0;
if (character.getAffinityLevel() != target) {
character.setAffinityLevel(target);
hasChanged = true;
}
}
return hasChanged;
}

View File

@@ -10,6 +10,6 @@ public interface CommandHandler {
return getData().label();
}
public void execute(CommandArgs args);
public String execute(CommandArgs args);
}

View File

@@ -108,7 +108,7 @@ public class CommandManager {
return sender.getAccount().hasPermission("target." + command.permission());
}
public void invoke(Player sender, String message) {
public CommandResult invoke(Player sender, String message) {
// Parse message into arguments
List<String> args = Arrays.stream(message.split(" ")).collect(Collectors.toCollection(ArrayList::new));
@@ -122,6 +122,9 @@ public class CommandManager {
// Get command handler
CommandHandler handler = this.commands.get(label);
// Create result object
var result = CommandResult.builder();
// Execute command
if (handler != null) {
@@ -131,8 +134,8 @@ public class CommandManager {
// Check if sender has permission to run the command.
if (sender != null && !this.checkPermission(sender, command)) {
// We have a double null check here just in case
sender.sendMessage("You do not have permission to use this command.");
return;
result.message("Error - You do not have permission to use this command");
return result.build();
}
// Build command arguments
@@ -140,14 +143,14 @@ public class CommandManager {
// Check targeted permission
if (sender != cmdArgs.getTarget() && !this.checkTargetPermission(sender, command)) {
cmdArgs.sendMessage("You do not have permission to use this command on another player.");
return;
result.message("Error - You do not have permission to use this command on another player");
return result.build();
}
// Make sure our command has a target
if (command.requireTarget() && cmdArgs.getTarget() == null) {
cmdArgs.sendMessage("Error: Targeted player not found or offline");
return;
result.message("Error - Targeted player not found or offline");
return result.build();
}
// Log
@@ -156,13 +159,20 @@ public class CommandManager {
}
// Run command
handler.execute(cmdArgs);
} else {
if (sender != null) {
sender.sendMessage("Invalid Command!");
} else {
Nebula.getLogger().info("Invalid Command!");
String commandMessage = handler.execute(cmdArgs);
// Parse out last newline
if (commandMessage.endsWith("\n")) {
commandMessage = commandMessage.substring(0, commandMessage.length() - 1);
}
// Set result data
result.command(handler);
result.message(commandMessage);
} else {
result.message("Invalid Command!");
}
return result.build();
}
}

View File

@@ -0,0 +1,19 @@
package emu.nebula.command;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class CommandResult {
private CommandHandler command;
private String message;
public boolean isCommandTypeOf(Class<? extends CommandHandler> handler) {
if (command == null) {
return false;
}
return command.getClass().equals(handler);
}
}

View File

@@ -10,10 +10,9 @@ import emu.nebula.util.Utils;
public class AccountCommand implements CommandHandler {
@Override
public void execute(CommandArgs args) {
public String execute(CommandArgs args) {
if (args.size() < 2) {
args.sendMessage("Invalid amount of args");
return;
return "Invalid amount of args";
}
String command = args.get(0).toLowerCase();
@@ -29,19 +28,22 @@ public class AccountCommand implements CommandHandler {
}
if (AccountHelper.createAccount(username, null, reservedUid) != null) {
args.sendMessage("Account created");
return "Account created";
} else {
args.sendMessage("Account already exists");
return "Account already exists";
}
}
case "delete" -> {
if (AccountHelper.deleteAccount(username)) {
args.sendMessage("Account deleted");
return "Account deleted";
} else {
args.sendMessage("Account doesnt exist");
return "Account doesnt exist";
}
}
}
// Fallback
return "Account sub command not found";
}
}

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

@@ -16,13 +16,13 @@ import emu.nebula.command.CommandHandler;
label = "character",
aliases = {"c", "char"},
permission = "player.character",
requireTarget = true,
desc = "!c [all | {characterId}] lv(level) a(ascension) s(skill level) t(talent level)"
requireTarget = true,
desc = "!c [all | {characterId}] lv(level) a(ascension) s(skill level) t(talent level) f(affinity level)"
)
public class CharacterCommand implements CommandHandler {
@Override
public void execute(CommandArgs args) {
public String execute(CommandArgs args) {
// Init
var player = args.getTarget();
var characters = new HashSet<GameCharacter>();
@@ -51,7 +51,7 @@ public class CharacterCommand implements CommandHandler {
// Sanity check
if (characters.isEmpty()) {
return;
return "Error: No characters selected";
}
// List of modified characters that we send to the client for updates
@@ -72,7 +72,7 @@ public class CharacterCommand implements CommandHandler {
}
if (modified.isEmpty()) {
return;
return "No changes applied";
}
// Encode and send
@@ -83,5 +83,6 @@ public class CharacterCommand implements CommandHandler {
}
player.addNextPackage(NetMsgId.chars_final_notify, proto);
return "Updated " + modified.size() + " character(s)";
}
}

View File

@@ -0,0 +1,129 @@
package emu.nebula.command.commands;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.ItemDef;
import emu.nebula.game.inventory.GameItem;
import emu.nebula.game.inventory.GameResource;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.inventory.ItemType;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.net.NetMsgId;
import emu.nebula.util.Utils;
import java.util.HashSet;
@Command(
label = "clean",
aliases = {"cl", "clear"},
permission = "player.inventory",
requireTarget = true,
desc = "!clean [all | {id} ...] [items|resources]. Removes items/resources from the targeted player."
)
public class CleanCommand implements CommandHandler {
@Override
public String execute(CommandArgs args) {
var player = args.getTarget();
var inv = player.getInventory();
boolean doItems = true;
boolean doResources = true;
boolean all = false;
var ids = new HashSet<Integer>();
for (String arg : args.getList()) {
arg = arg.toLowerCase();
switch (arg) {
case "items" -> {
doResources = false;
continue;
}
case "resources", "res" -> {
doItems = false;
continue;
}
case "all" -> {
all = true;
continue;
}
}
int id = Utils.parseSafeInt(arg);
if (id > 0) {
ids.add(id);
}
}
var change = new PlayerChangeInfo();
var removeMap = new ItemParamMap();
if (all) {
if (doItems) {
for (GameItem item : inv.getItems().values()) {
removeMap.add(item.getItemId(), item.getCount());
}
}
if (doResources) {
for (GameResource res : inv.getResources().values()) {
removeMap.add(res.getResourceId(), res.getCount());
}
}
} else {
for (int id : ids) {
ItemDef data = GameData.getItemDataTable().get(id);
if (data == null) continue;
ItemType type = data.getItemType();
switch (type) {
case Res -> {
if (doResources) {
int count = inv.getResourceCount(id);
if (count > 0) removeMap.add(id, count);
}
}
case Item -> {
if (doItems) {
int count = inv.getItemCount(id);
if (count > 0) removeMap.add(id, count);
}
}
case Disc, Char, CharacterSkin, Title, Honor -> {
}
default -> {
if (doItems) {
int count = inv.getItemCount(id);
if (count > 0) {
removeMap.add(id, count);
break;
}
}
if (doResources) {
int count = inv.getResourceCount(id);
if (count > 0) removeMap.add(id, count);
}
}
}
}
}
if (!removeMap.isEmpty()) {
change = inv.removeItems(removeMap, change);
}
if (change.isEmpty()) {
return "No items/resources removed";
}
player.addNextPackage(NetMsgId.items_change_notify, change.toProto());
return "Inventory cleaned";
}
}

View File

@@ -21,7 +21,7 @@ import emu.nebula.command.CommandHandler;
public class DiscCommand implements CommandHandler {
@Override
public void execute(CommandArgs args) {
public String execute(CommandArgs args) {
// Init
var player = args.getTarget();
var discs = new HashSet<GameDisc>();
@@ -50,7 +50,7 @@ public class DiscCommand implements CommandHandler {
// Sanity check
if (discs.isEmpty()) {
return;
return "No discs selected";
}
// List of modified characters that we send to the client for updates
@@ -71,12 +71,15 @@ public class DiscCommand implements CommandHandler {
}
if (modified.isEmpty()) {
return;
return "No discs changed";
}
// Encode and send
for (var disc : modified) {
player.addNextPackage(NetMsgId.disc_reset_notify, disc.toProto());
}
// Return message
return "Changed " + modified.size() + " discs";
}
}

View File

@@ -31,15 +31,14 @@ public class GiveAllCommand implements CommandHandler {
);
@Override
public void execute(CommandArgs args) {
public String execute(CommandArgs args) {
Player target = args.getTarget();
String type = args.get(0).toLowerCase();
var change = new PlayerChangeInfo();
var message = new StringBuilder();
switch (type) {
default -> args.sendMessage("Error: Invalid type");
case "m", "materials", "mats" -> {
// Create items map
var items = new ItemParamMap();
@@ -66,7 +65,7 @@ public class GiveAllCommand implements CommandHandler {
target.getInventory().addItems(items, change);
// Send message
args.sendMessage("Giving " + target.getName() + " " + items.size() + " items");
message.append("Giving " + target.getName() + " " + items.size() + " items.\n");
}
case "d", "discs" -> {
// Get all discs
@@ -92,7 +91,7 @@ public class GiveAllCommand implements CommandHandler {
}
// Send message
args.sendMessage("Giving " + target.getName() + " all discs");
message.append("Giving " + target.getName() + " all discs.\n");
}
case "c", "characters", "trekkers", "t" -> {
// Get all characters
@@ -118,16 +117,22 @@ public class GiveAllCommand implements CommandHandler {
}
// Send message
args.sendMessage("Giving " + target.getName() + " all characters");
message.append("Giving " + target.getName() + " all characters.\n");
}
default -> {
// Ignored
}
}
if (change.isEmpty()) {
return;
return "No items given to the player";
}
// Encode and send
target.addNextPackage(NetMsgId.items_change_notify, change.toProto());
// Complete
return message.toString();
}
}

View File

@@ -17,7 +17,7 @@ import emu.nebula.command.CommandHandler;
public class GiveCommand implements CommandHandler {
@Override
public void execute(CommandArgs args) {
public String execute(CommandArgs args) {
// Setup mail
var mail = new GameMail("System", "Give Command Result", "");
@@ -31,18 +31,17 @@ public class GiveCommand implements CommandHandler {
var itemData = GameData.getItemDataTable().get(itemId);
if (itemData == null) {
args.sendMessage("Item \"" + arg + "\" does not exist!");
continue;
}
// Add
mail.addAttachment(itemId, amount);
// Log
args.sendMessage("Giving " + args.getTarget().getName() + " " + amount + " of " + itemId);
}
// Add mail
args.getTarget().getMailbox().sendMail(mail);
//
return "Give command success, check your mail";
}
}

View File

@@ -9,8 +9,10 @@ import emu.nebula.command.CommandHandler;
public class HelpCommand implements CommandHandler {
@Override
public void execute(CommandArgs args) {
args.sendMessage("Displaying list of commands:");
public String execute(CommandArgs args) {
var message = new StringBuilder();
message.append("Displaying list of commands:\n");
// Sort command names
var labels = Nebula.getCommandManager().getLabels().keySet().stream().sorted().toList();
@@ -21,9 +23,11 @@ public class HelpCommand implements CommandHandler {
// Only send command description if the sender has permission to use the command
if (Nebula.getCommandManager().checkPermission(args.getSender(), command)) {
args.sendMessage(command.desc());
message.append(command.desc());
message.append("\n");
}
}
return message.toString();
}
}

View File

@@ -17,41 +17,36 @@ import emu.nebula.util.Utils;
desc = "/mail \"subject\" \"body\" [itemId xQty | itemId:qty ...]. Sends the targeted player a system mail."
)
public class MailCommand implements CommandHandler {
private static final String USAGE_TEXT = "Usage: /mail \"subject\" \"body\" [itemId xQty | itemId:qty ...]";
private static final Pattern QUOTED_TEXT = Pattern.compile("\"([^\"]*)\"");
@Override
public void execute(CommandArgs args) {
public String execute(CommandArgs args) {
var target = args.getTarget();
if (target == null) {
args.sendMessage("Error: Targeted player not found or offline");
return;
return "Error - Targeted player not found or offline";
}
String rawInput = args.getRaw() == null ? "" : args.getRaw().trim();
if (rawInput.isEmpty()) {
sendUsage(args);
return;
return USAGE_TEXT;
}
Matcher matcher = QUOTED_TEXT.matcher(rawInput);
if (!matcher.find()) {
sendUsage(args);
return;
return USAGE_TEXT;
}
String subject = matcher.group(1).trim();
if (!matcher.find()) {
args.sendMessage("Mail body must be wrapped in quotes after the subject.");
return;
return "Mail body must be wrapped in quotes after the subject.";
}
String body = matcher.group(1).trim();
int attachmentStartIndex = matcher.end();
if (subject.isEmpty()) {
args.sendMessage("Mail subject cannot be empty.");
return;
return "Mail subject cannot be empty.";
}
if (body.isEmpty()) {
@@ -68,7 +63,7 @@ public class MailCommand implements CommandHandler {
parseAttachments(attachmentSection, mail, args);
target.getMailbox().sendMail(mail);
args.sendMessage("Mail sent to " + target.getName() + " with subject \"" + subject + "\".");
return "Mail sent to " + target.getName() + " with subject \"" + subject + "\".";
}
private void parseAttachments(String attachmentSection, GameMail mail, CommandArgs args) {
@@ -93,7 +88,7 @@ public class MailCommand implements CommandHandler {
if (token.startsWith("x") && token.length() > 1) {
if (pendingItemId == null) {
args.sendMessage("Quantity token '" + token + "' must follow an item id.");
//args.sendMessage("Quantity token '" + token + "' must follow an item id.");
continue;
}
@@ -110,7 +105,7 @@ public class MailCommand implements CommandHandler {
int itemId = Utils.parseSafeInt(token);
if (itemId <= 0) {
args.sendMessage("Invalid item id '" + token + "'.");
//args.sendMessage("Invalid item id '" + token + "'.");
pendingItemId = null;
continue;
}
@@ -119,7 +114,7 @@ public class MailCommand implements CommandHandler {
continue;
}
args.sendMessage("Ignoring attachment token '" + token + "'.");
//args.sendMessage("Ignoring attachment token '" + token + "'.");
}
if (pendingItemId != null) {
@@ -129,7 +124,7 @@ public class MailCommand implements CommandHandler {
private void addAttachment(GameMail mail, CommandArgs args, int itemId, int quantity) {
if (itemId <= 0) {
args.sendMessage("Item id must be positive.");
//args.sendMessage("Item id must be positive.");
return;
}
@@ -145,8 +140,4 @@ public class MailCommand implements CommandHandler {
}
return !token.isEmpty();
}
private void sendUsage(CommandArgs args) {
args.sendMessage("Usage: /mail \"subject\" \"body\" [itemId xQty | itemId:qty ...]");
}
}

View File

@@ -9,14 +9,17 @@ import emu.nebula.command.CommandHandler;
public class ReloadCommand implements CommandHandler {
@Override
public void execute(CommandArgs args) {
public String execute(CommandArgs args) {
// Reload config first
Nebula.loadConfig();
// Reload patch list if the server is running
if (Nebula.getHttpServer() != null) {
Nebula.getHttpServer().loadPatchList();
}
args.sendMessage("Reloaded the server config");
// Result message
return "Reloaded the server config";
}
}

View File

@@ -0,0 +1,32 @@
package emu.nebula.command.commands;
import emu.nebula.Nebula;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
import java.util.Random;
@Command(label = "remote", permission = "player.remote", requireTarget = true, desc = "/remote. Send remote to web remote")
public class RemoteKeyCommand implements CommandHandler {
private final String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
@Override
public String execute(CommandArgs args) {
if (Nebula.getConfig().getRemoteCommand().useRemoteServices) {
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i = 0; i < 8; i++) {
int index = random.nextInt(characters.length());
sb.append(characters.charAt(index));
}
args.getTarget().setRemoteToken(sb.toString());
return "Key Generated: " + sb.toString();
} else {
args.getTarget().setRemoteToken(null);
return "Remote Command Disabled on Server";
}
}
}

View File

@@ -0,0 +1,41 @@
package emu.nebula.command.commands;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
import emu.nebula.data.GameData;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.NotifyGm.GmWorldClass;
import emu.nebula.util.Utils;
@Command(label = "setlevel", aliases = {"level", "l"}, permission = "player.level", requireTarget = true, desc = "/level [level]. Set player level")
public class SetLevelCommand implements CommandHandler {
@Override
public String execute(CommandArgs args) {
// Get target
var target = args.getTarget();
// Parse level
int level = Utils.parseSafeInt(args.get(0));
// Check to make sure world class data exists for this level
var data = GameData.getWorldClassDataTable().get(level);
if (data == null) {
return "The game does not support level " + level;
}
target.setLevel(level);
target.setExp(0);
// Update
target.addNextPackage(
NetMsgId.world_class_number_notify,
GmWorldClass.newInstance()
.setFinalClass(target.getLevel())
.setLastExp(target.getExp())
);
return "Level set to " + level;
}
}

View File

@@ -28,14 +28,23 @@ public class GameData {
@Getter private static DataTable<TalentGroupDef> TalentGroupDataTable = new DataTable<>();
@Getter private static DataTable<TalentDef> TalentDataTable = new DataTable<>();
// Character emblems
@Getter private static DataTable<CharGemDef> CharGemDataTable = new DataTable<>();
@Getter private static DataTable<CharGemSlotControlDef> CharGemSlotControlDataTable = new DataTable<>();
@Getter private static DataTable<CharGemAttrGroupDef> CharGemAttrGroupDataTable = new DataTable<>();
@Getter private static DataTable<CharGemAttrTypeDef> CharGemAttrTypeDataTable = new DataTable<>();
@Getter private static DataTable<CharGemAttrValueDef> CharGemAttrValueDataTable = new DataTable<>();
// Character affinity
@Getter private static DataTable<AffinityLevelDef> AffinityLevelDataTable = new DataTable<>();
@Getter private static DataTable<AffinityGiftDef> AffinityGiftDataTable = new DataTable<>();
@Getter private static DataTable<PlotDef> PlotDataTable = new DataTable<>();
@Getter private static DataTable<ChatDef> ChatDataTable = new DataTable<>();
@Getter private static DataTable<DatingLandmarkDef> DatingLandmarkDataTable = new DataTable<>();
@Getter private static DataTable<DatingLandmarkEventDef> DatingLandmarkEventDataTable = new DataTable<>();
@Getter private static DataTable<DatingCharacterEventDef> DatingCharacterEventDataTable = new DataTable<>();
// Discs
@Getter private static DataTable<DiscDef> DiscDataTable = new DataTable<>();
@Getter private static DataTable<DiscStrengthenDef> DiscStrengthenDataTable = new DataTable<>();
@@ -43,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<>();
@@ -93,6 +104,9 @@ public class GameData {
@Getter private static DataTable<DailyQuestDef> DailyQuestDataTable = new DataTable<>();
@Getter private static DataTable<DailyQuestActiveDef> DailyQuestActiveDataTable = new DataTable<>();
// Achievements
@Getter private static DataTable<AchievementDef> AchievementDataTable = new DataTable<>();
// Tutorial
@Getter private static DataTable<TutorialLevelDef> TutorialLevelDataTable = new DataTable<>();
@@ -100,9 +114,14 @@ public class GameData {
@Getter private static DataTable<StarTowerDef> StarTowerDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerStageDef> StarTowerStageDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerGrowthNodeDef> StarTowerGrowthNodeDataTable = new DataTable<>();
@Getter private static DataTable<PotentialDef> PotentialDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerFloorExpDef> StarTowerFloorExpDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerTeamExpDef> StarTowerTeamExpDataTable = 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<>();
@@ -117,4 +136,10 @@ public class GameData {
// Score boss
@Getter private static DataTable<ScoreBossControlDef> ScoreBossControlDataTable = new DataTable<>();
}
// Activity
@Getter private static DataTable<ActivityDef> ActivityDataTable = new DataTable<>();
@Getter private static DataTable<TrialControlDef> TrialControlDataTable = new DataTable<>();
@Getter private static DataTable<TrialGroupDef> TrialGroupDataTable = new DataTable<>();
}

View File

@@ -1,5 +1,6 @@
package emu.nebula.data;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.util.List;
import java.util.stream.Collectors;
@@ -9,6 +10,7 @@ import org.reflections.Reflections;
import emu.nebula.util.JsonUtils;
import emu.nebula.util.Utils;
import emu.nebula.Nebula;
import emu.nebula.game.achievement.AchievementHelper;
public class ResourceLoader {
private static boolean loaded = false;
@@ -21,6 +23,9 @@ public class ResourceLoader {
// Load
loadResources();
// Add hardcoded achievements params
AchievementHelper.init();
// Done
loaded = true;
Nebula.getLogger().info("Resource loading complete");
@@ -54,9 +59,26 @@ public class ResourceLoader {
int count = 0;
try {
var json = JsonUtils.loadToMap(Nebula.getConfig().resourceDir + "/bin/" + type.name(), String.class, resourceClass);
// Init defs collection
Iterable<?> defs = null;
// Load resource file
if (type.useInternal()) {
// Load from internal resources in jar
try (var in = ResourceLoader.class.getResourceAsStream("/defs/" + type.name()); var reader = new InputStreamReader(in)) {
defs = JsonUtils.loadToList(reader, resourceClass);
} catch (Exception e) {
// Ignored
}
} else {
// Load json from ./resources/bin/ folder
var json = JsonUtils.loadToMap(Nebula.getConfig().resourceDir + "/bin/" + type.name(), String.class, resourceClass);
// Get json values
defs = json.values();
}
for (Object o : json.values()) {
for (Object o : defs) {
BaseDef res = (BaseDef) o;
if (res == null) {

View File

@@ -10,13 +10,15 @@ public @interface ResourceType {
String name();
/**
* Load priority - dictates which order to load this resource, with "highest"
* being loaded first
* Load priority - dictates which order to load this resource, with "highest" being loaded first
*/
LoadPriority loadPriority() default LoadPriority.NORMAL;
Class<?> keyType() default int.class;
/**
* Loads the resource file from inside the jar (./resources/defs/) if set to true
*/
boolean useInternal() default false;
public enum LoadPriority {
HIGHEST(4), HIGH(3), NORMAL(2), LOW(1), LOWEST(0);

View File

@@ -0,0 +1,68 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.achievement.AchievementHelper;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import lombok.Getter;
@Getter
@ResourceType(name = "Achievement.json")
public class AchievementDef extends BaseDef {
private int Id;
private int Type;
private int CompleteCond;
private int AimNumShow;
private int[] Prerequisites;
// Reward
private int Tid1;
private int Qty1;
// Custom params
private transient int param1; // -1 == any, 0 = no param, 1+ = param required
private transient int param2; // -1 == any, 0 = no param, 1+ = param required
@Override
public int getId() {
return Id;
}
public void setParams(int param1, int param2) {
this.param1 = param1;
this.param2 = param2;
}
/**
* Checks if this achievement requires params to match
*/
public boolean hasParam1(int param) {
if (this.param1 < 0) {
return false;
} else if (this.param1 == 0) {
return param != 0;
} else {
return true;
}
}
/**
* Checks if this achievement requires params to match
*/
public boolean hasParam2(int param) {
if (this.param2 < 0) {
return false;
} else if (this.param2 == 0) {
return param != 0;
} else {
return true;
}
}
@Override
public void onLoad() {
// Add to cached achievement list
var list = AchievementHelper.getCache().computeIfAbsent(this.CompleteCond, i -> new ObjectArrayList<>());
list.add(this);
}
}

View File

@@ -0,0 +1,25 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "Activity.json")
public class ActivityDef extends BaseDef {
private int Id;
private int ActivityType;
private transient emu.nebula.game.activity.ActivityType type;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
this.type = emu.nebula.game.activity.ActivityType.getByValue(this.ActivityType);
}
}

View File

@@ -0,0 +1,20 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "AffinityGift.json")
public class AffinityGiftDef extends BaseDef {
private int Id;
private int BaseAffinity;
private int[] Tags;
@Override
public int getId() {
return Id;
}
}

View File

@@ -0,0 +1,19 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "AffinityLevel.json")
public class AffinityLevelDef extends BaseDef {
private int AffinityLevel;
private int NeedExp;
@Override
public int getId() {
return AffinityLevel;
}
}

View File

@@ -1,10 +1,15 @@
package emu.nebula.data.resources;
import java.util.List;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.inventory.ItemRewardList;
import emu.nebula.game.inventory.ItemRewardParam;
import emu.nebula.util.JsonUtils;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
@@ -35,6 +40,8 @@ public class AgentDef extends BaseDef {
private String BonusPreview4;
private transient Int2ObjectMap<AgentDurationDef> durations;
private transient Int2IntOpenHashMap tags;
private transient Int2IntOpenHashMap extraTags;
@Override
public int getId() {
@@ -45,14 +52,83 @@ public class AgentDef extends BaseDef {
this.durations.put(duration.getTime(), duration);
}
public boolean hasTags(List<GameCharacter> characters) {
// Get character tags
var characterTags = new Int2IntOpenHashMap();
for (var character : characters) {
var data = character.getData().getDes();
for (int tag : data.getTag()) {
characterTags.addTo(tag, 1);
}
}
// Validate that we have the tags
for (var entry : this.tags.int2IntEntrySet()) {
int reqTagId = entry.getIntKey();
int reqTagCount = entry.getIntValue();
// Get amount of tags that we have from our characters
int characterTagCount = characterTags.get(reqTagId);
if (reqTagCount > characterTagCount) {
return false;
}
}
return true;
}
public boolean hasExtraTags(List<GameCharacter> characters) {
// Get character tags
var characterTags = new Int2IntOpenHashMap();
for (var character : characters) {
var data = character.getData().getDes();
for (int tag : data.getTag()) {
characterTags.addTo(tag, 1);
}
}
// Validate that we have the tags
for (var entry : this.extraTags.int2IntEntrySet()) {
int reqTagId = entry.getIntKey();
int reqTagCount = entry.getIntValue();
// Get amount of tags that we have from our characters
int characterTagCount = characterTags.get(reqTagId);
if (reqTagCount > characterTagCount) {
return false;
}
}
return true;
}
@Override
public void onLoad() {
// Cache durations
this.durations = new Int2ObjectOpenHashMap<>();
this.addDuration(new AgentDurationDef(this.Time1, this.RewardPreview1, this.BonusPreview1));
this.addDuration(new AgentDurationDef(this.Time2, this.RewardPreview2, this.BonusPreview2));
this.addDuration(new AgentDurationDef(this.Time3, this.RewardPreview3, this.BonusPreview3));
this.addDuration(new AgentDurationDef(this.Time4, this.RewardPreview4, this.BonusPreview4));
// Cache tags
this.tags = new Int2IntOpenHashMap();
this.extraTags = new Int2IntOpenHashMap();
for (int tag : this.Tags) {
this.tags.addTo(tag, 1);
}
for (int tag : this.ExtraTags) {
this.extraTags.addTo(tag, 1);
}
}
@Getter

View File

@@ -1,32 +1,36 @@
package emu.nebula.data.resources;
import java.util.List;
import java.util.Map;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.data.ResourceType.LoadPriority;
import emu.nebula.util.JsonUtils;
import emu.nebula.util.WeightedList;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import lombok.Getter;
@Getter
@ResourceType(name = "CharGemAttrGroup.json", loadPriority = LoadPriority.HIGH)
@ResourceType(name = "CharGemAttrGroups.json", useInternal = true)
public class CharGemAttrGroupDef extends BaseDef {
private int GroupId;
private int GroupType;
private int Id;
private int Weight;
private String UniqueAttrNumWeight;
private IntArrayList AttrTypes;
private Map<Integer, Integer> UniqueAttrNumWeights;
private transient WeightedList<Integer> uniqueAttrNum;
private transient List<CharGemAttrTypeDef> attributeTypes;
private transient List<CharGemAttrTypeData> attributeTypes;
public CharGemAttrGroupDef() {
this.AttrTypes = new IntArrayList();
}
@Override
public int getId() {
return GroupId;
return Id;
}
public int getRandomUniqueAttrNum() {
@@ -37,7 +41,7 @@ public class CharGemAttrGroupDef extends BaseDef {
return this.uniqueAttrNum.next();
}
public CharGemAttrTypeDef getRandomAttributeType(IntList list) {
public CharGemAttrTypeData getRandomAttributeType(IntList list) {
// Setup blacklist to prevent the same attribute from showing up twice
var blacklist = new IntOpenHashSet();
@@ -50,7 +54,7 @@ public class CharGemAttrGroupDef extends BaseDef {
}
// Create random generator
var random = new WeightedList<CharGemAttrTypeDef>();
var random = new WeightedList<CharGemAttrTypeData>();
for (var type : this.getAttributeTypes()) {
if (blacklist.contains(type.getId())) {
@@ -65,15 +69,44 @@ public class CharGemAttrGroupDef extends BaseDef {
@Override
public void onLoad() {
// Init unique attribute weights
this.uniqueAttrNum = new WeightedList<>();
this.attributeTypes = new ObjectArrayList<>();
if (this.UniqueAttrNumWeight != null) {
var json = JsonUtils.decodeMap(this.UniqueAttrNumWeight, Integer.class, Integer.class);
for (var entry : json.entrySet()) {
if (this.UniqueAttrNumWeights != null) {
for (var entry : this.UniqueAttrNumWeights.entrySet()) {
this.uniqueAttrNum.add(entry.getValue(), entry.getKey());
}
}
// Init attribute types
this.attributeTypes = new ObjectArrayList<>();
for (int id : this.getAttrTypes()) {
var type = new CharGemAttrTypeData(id);
this.attributeTypes.add(type);
}
}
@Getter
public static class CharGemAttrTypeData {
private int id;
private WeightedList<CharGemAttrValueDef> values;
public CharGemAttrTypeData(int id) {
this.id = id;
this.values = new WeightedList<>();
}
protected void addValue(CharGemAttrValueDef value) {
this.values.add(value.getRarity(), value);
}
public CharGemAttrValueDef getRandomValueData() {
return this.getValues().next();
}
public int getRandomValue() {
return this.getRandomValueData().getId();
}
}
}

View File

@@ -1,39 +0,0 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.util.WeightedList;
import lombok.Getter;
@Getter
@ResourceType(name = "CharGemAttrType.json")
public class CharGemAttrTypeDef extends BaseDef {
private int Id;
private int GroupId;
private transient WeightedList<CharGemAttrValueDef> values;
@Override
public int getId() {
return Id;
}
public CharGemAttrValueDef getRandomValueData() {
return this.getValues().next();
}
public int getRandomValue() {
return this.getRandomValueData().getId();
}
@Override
public void onLoad() {
this.values = new WeightedList<>();
var data = GameData.getCharGemAttrGroupDataTable().get(this.GroupId);
if (data != null) {
data.getAttributeTypes().add(this);
}
}
}

View File

@@ -12,8 +12,6 @@ public class CharGemAttrValueDef extends BaseDef {
private int Id;
private int TypeId;
private int AttrType;
private int AttrTypeFirstSubtype;
private int AttrTypeSecondSubtype;
private int Rarity;
@Override
@@ -23,9 +21,18 @@ public class CharGemAttrValueDef extends BaseDef {
@Override
public void onLoad() {
var data = GameData.getCharGemAttrTypeDataTable().get(this.TypeId);
if (data != null) {
data.getValues().add(this.getRarity(), this);
// Cache attribute values into attribute group defs
// Honestly a horrible/inefficient way of doing this
for (var data : GameData.getCharGemAttrGroupDataTable()) {
for (var type : data.getAttributeTypes()) {
// Skip if type id doesnt match
if (type.getId() != this.getTypeId()) {
continue;
}
// Add
type.addValue(this);
}
}
}
}

View File

@@ -4,6 +4,7 @@ import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.util.CustomIntArray;
import emu.nebula.util.Utils;
import emu.nebula.util.WeightedList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@@ -19,16 +20,15 @@ public class CharGemSlotControlDef extends BaseDef {
private int GeneratenCostQty;
private int RefreshCostQty;
private int UniqueAttrGroupProb;
private int UniqueAttrGroupId;
private int GuaranteeCount;
private int[] AttrGroupId;
private int LockableNum;
private int LockItemTid;
private int LockItemQty;
// These have to be hardcoded now
private transient int UniqueAttrGroupProb;
private transient int UniqueAttrGroupId;
private transient int[] AttrGroupId;
@Override
public int getId() {
return Id;
@@ -38,9 +38,15 @@ public class CharGemSlotControlDef extends BaseDef {
return this.generateAttributes(new CustomIntArray());
}
// Check if we should add unique attributes based on the probablity
private boolean shouldAddUniqueAttr() {
int random = Utils.randomRange(1, 10000);
return random <= this.UniqueAttrGroupProb;
}
public IntList generateAttributes(CustomIntArray list) {
// Add unique attributes
if (this.UniqueAttrGroupId > 0) {
if (this.UniqueAttrGroupId > 0 && this.shouldAddUniqueAttr()) {
var group = GameData.getCharGemAttrGroupDataTable().get(this.UniqueAttrGroupId);
int num = group.getRandomUniqueAttrNum();
@@ -49,7 +55,7 @@ public class CharGemSlotControlDef extends BaseDef {
list.add(attributeType.getRandomValue());
}
if (list.size() >= this.MaxAlterNum) {
if (list.getValueCount() >= this.MaxAlterNum) {
return list;
}
}
@@ -76,4 +82,25 @@ public class CharGemSlotControlDef extends BaseDef {
// Complete
return list;
}
@Override
public void onLoad() {
// Hard coded values
switch (this.Id) {
case 3:
this.UniqueAttrGroupProb = 3300;
this.UniqueAttrGroupId = 12;
this.AttrGroupId = new int[] {9, 10};
break;
case 2:
this.UniqueAttrGroupProb = 4000;
this.UniqueAttrGroupId = 11;
this.AttrGroupId = new int[] {5, 6, 7, 8};
break;
case 1:
default:
this.AttrGroupId = new int[] {1, 2, 3, 4};
break;
}
}
}

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

@@ -31,6 +31,7 @@ public class CharacterDef extends BaseDef {
private int[] GemSlots;
private transient CharacterDesDef des;
private transient ElementType elementType;
private transient List<ChatDef> chats;
@@ -38,6 +39,10 @@ public class CharacterDef extends BaseDef {
public int getId() {
return Id;
}
protected void setDes(CharacterDesDef des) {
this.des = des;
}
public int getSkillsUpgradeGroup(int index) {
if (index < 0 || index >= this.SkillsUpgradeGroup.length) {

View File

@@ -0,0 +1,60 @@
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 it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import lombok.Getter;
@Getter
@ResourceType(name = "CharacterDes.json", loadPriority = LoadPriority.LOW)
public class CharacterDesDef extends BaseDef {
private int Id;
private int[] Tag;
private IntOpenHashSet PreferTags;
private IntOpenHashSet HateTags;
@Override
public int getId() {
return Id;
}
public boolean isPreferGift(AffinityGiftDef gift) {
if (this.getPreferTags() == null) {
return false;
}
for (int i = 0; i < gift.getTags().length; i++) {
int tag = gift.getTags()[i];
if (this.getPreferTags().contains(tag)) {
return true;
}
}
return false;
}
public boolean isHateGift(AffinityGiftDef gift) {
if (this.getHateTags() == null) {
return false;
}
for (int i = 0; i < gift.getTags().length; i++) {
int tag = gift.getTags()[i];
if (this.getHateTags().contains(tag)) {
return true;
}
}
return false;
}
@Override
public void onLoad() {
var character = GameData.getCharacterDataTable().get(this.getId());
if (character != null) {
character.setDes(this);
}
}
}

View File

@@ -0,0 +1,32 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.data.ResourceType.LoadPriority;
import lombok.Getter;
@Getter
@ResourceType(name = "DatingBranch.json", loadPriority = LoadPriority.LOW)
public class DatingBranchDef extends BaseDef {
private int Id;
private int DatingEventType;
private int[] DatingEventParams;
@Override
public int getId() {
return Id;
}
public int getLandmarkId() {
if (this.DatingEventParams.length <= 0) {
return 0;
}
return this.DatingEventParams[0];
}
@Override
public void onLoad() {
}
}

View File

@@ -0,0 +1,11 @@
package emu.nebula.data.resources;
import emu.nebula.data.ResourceType;
import emu.nebula.data.ResourceType.LoadPriority;
import lombok.Getter;
@Getter
@ResourceType(name = "DatingCharacterEvent.json", loadPriority = LoadPriority.LOW)
public class DatingCharacterEventDef extends DatingLandmarkEventDef {
}

View File

@@ -0,0 +1,61 @@
package emu.nebula.data.resources;
import java.util.ArrayList;
import java.util.List;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.dating.DatingEvent;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter;
@Getter
@ResourceType(name = "DatingLandmark.json")
public class DatingLandmarkDef extends BaseDef {
private int Id;
private transient List<DatingEvent> afterBranches;
private transient Object2ObjectMap<String, List<DatingEvent>> characterEvents;
private transient Object2ObjectMap<String, List<DatingEvent>> landmarkEvents;
@Override
public int getId() {
return Id;
}
public int getRandomAfterBranchId() {
var event = Utils.randomElement(this.afterBranches);
if (event == null) {
return 0;
}
return event.getId();
}
public int getRandomCharacterEventId() {
var list = new ArrayList<DatingEvent>();
for (var events : this.characterEvents.values()) {
list.addAll(events);
}
// Get random event
var event = Utils.randomElement(list);
if (event == null) {
return 0;
}
return event.getId();
}
@Override
public void onLoad() {
this.afterBranches = new ArrayList<>();
this.characterEvents = new Object2ObjectOpenHashMap<>();
this.landmarkEvents = new Object2ObjectOpenHashMap<>();
}
}

View File

@@ -0,0 +1,66 @@
package emu.nebula.data.resources;
import java.util.ArrayList;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.data.ResourceType.LoadPriority;
import emu.nebula.game.dating.DatingEvent;
import lombok.Getter;
@Getter
@ResourceType(name = "DatingLandmarkEvent.json", loadPriority = LoadPriority.LOW)
public class DatingLandmarkEventDef extends BaseDef implements DatingEvent {
private int Id;
private int DatingEventType;
private int Affinity;
private int[] DatingEventParams;
private String Response;
private transient emu.nebula.game.dating.DatingEventType type;
@Override
public int getId() {
return Id;
}
public int getLandmarkId() {
if (this.DatingEventParams.length <= 0) {
return 0;
}
return this.DatingEventParams[0];
}
@Override
public void onLoad() {
// Cache dating event type
this.type = emu.nebula.game.dating.DatingEventType.getByValue(this.getDatingEventType());
// Add to landmark data
var data = GameData.getDatingLandmarkDataTable().get(this.getLandmarkId());
if (data == null) {
return;
}
switch (this.getType()) {
case Landmark -> {
data.getLandmarkEvents()
.computeIfAbsent(this.getResponse(), s -> new ArrayList<>())
.add(this);
}
case Regular -> {
data.getCharacterEvents()
.computeIfAbsent(this.getResponse(), s -> new ArrayList<>())
.add(this);
}
case AfterBranch -> {
data.getAfterBranches().add(this);
}
default -> {
// Ignored
}
}
}
}

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

@@ -0,0 +1,31 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap;
import lombok.Getter;
@Getter
@ResourceType(name = "Plot.json")
public class PlotDef extends BaseDef {
private int Id;
private int Char;
private int UnlockAffinityLevel;
private String Rewards;
private transient ItemParamMap rewards;
@Override
public int getId() {
return this.Id;
}
public ItemParamMap getRewards() {
return this.rewards;
}
@Override
public void onLoad() {
this.rewards = ItemParamMap.fromJsonString(this.Rewards);
}
}

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

@@ -10,6 +10,8 @@ import lombok.Getter;
@ResourceType(name = "StarTower.json")
public class StarTowerDef extends BaseDef {
private int Id;
private int GroupId;
private int Difficulty;
private int[] FloorNum;
private transient int maxFloors;
@@ -19,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

@@ -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 = "StarTowerFloorExp.json")
public class StarTowerFloorExpDef extends BaseDef {
private int Id;
private int StarTowerId;
private int Stage;
private int NormalExp;
private int EliteExp;
private int BossExp;
private int FinalBossExp;
@Override
public int getId() {
return Id;
}
}

View File

@@ -11,6 +11,7 @@ public class StarTowerStageDef extends BaseDef {
private int Stage;
private int Floor;
private int RoomType;
private int InteriorCurrencyQuantity;
@Override
public int getId() {

View File

@@ -0,0 +1,19 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "StarTowerTeamExp.json")
public class StarTowerTeamExpDef extends BaseDef {
private int Id;
private int GroupId;
private int Level;
private int NeedExp;
@Override
public int getId() {
return Id;
}
}

View File

@@ -0,0 +1,18 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import lombok.Getter;
@Getter
@ResourceType(name = "TrialControl.json")
public class TrialControlDef extends BaseDef {
private int Id;
private IntOpenHashSet GroupIds;
@Override
public int getId() {
return Id;
}
}

View File

@@ -0,0 +1,41 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap;
import lombok.Getter;
@Getter
@ResourceType(name = "TrialGroup.json")
public class TrialGroupDef extends BaseDef {
private int Id;
private int RewardId1;
private int Qty1;
private int RewardId2;
private int Qty2;
private int RewardId3;
private int Qty3;
private transient ItemParamMap rewards;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
this.rewards = new ItemParamMap();
if (this.RewardId1 > 0) {
this.rewards.add(this.RewardId1, this.Qty1);
}
if (this.RewardId2 > 0) {
this.rewards.add(this.RewardId2, this.Qty2);
}
if (this.RewardId3 > 0) {
this.rewards.add(this.RewardId3, this.Qty3);
}
}
}

View File

@@ -12,6 +12,18 @@ public class VampireSurvivorDef extends BaseDef {
private int NeedWorldClass;
private int[] FateCardBundle;
private int NormalScore1;
private int EliteScore1;
private int BossScore1;
private int TimeScore1;
private int TimeLimit1;
private int NormalScore2;
private int EliteScore2;
private int BossScore2;
private int TimeScore2;
private int TimeLimit2;
@Override
public int getId() {
return Id;

View File

@@ -26,7 +26,6 @@ import de.bwaldvogel.mongo.backend.h2.H2Backend;
import de.bwaldvogel.mongo.backend.memory.MemoryBackend;
import dev.morphia.*;
import dev.morphia.annotations.Entity;
import dev.morphia.mapping.Mapper;
import dev.morphia.mapping.MapperOptions;
import dev.morphia.query.FindOptions;
import dev.morphia.query.Sort;
@@ -84,7 +83,7 @@ public final class DatabaseManager {
.stream()
.filter(cls -> {
Entity e = cls.getAnnotation(Entity.class);
return e != null && !e.value().equals(Mapper.IGNORED_FIELDNAME);
return e != null;
})
.toList();
@@ -164,12 +163,16 @@ public final class DatabaseManager {
return getDatastore().find(cls).stream();
}
public <T> List<T> getSortedObjects(Class<T> cls, String filter, int limit) {
public <T> List<T> getSortedObjects(Class<T> cls, String filter, int value, String sortBy, int limit) {
var options = new FindOptions()
.sort(Sort.descending(filter))
.sort(Sort.descending(sortBy))
.limit(limit);
return getDatastore().find(cls).iterator(options).toList();
return getDatastore()
.find(cls)
.filter(Filters.eq(filter, value))
.iterator(options)
.toList();
}
public <T> void save(T obj) {

View File

@@ -8,12 +8,13 @@ import java.util.concurrent.TimeUnit;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.game.activity.ActivityModule;
import emu.nebula.game.gacha.GachaModule;
import emu.nebula.game.player.PlayerModule;
import emu.nebula.game.scoreboss.ScoreBossModule;
import emu.nebula.game.tutorial.TutorialModule;
import emu.nebula.net.GameSession;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
@@ -27,6 +28,7 @@ public class GameContext implements Runnable {
private final PlayerModule playerModule;
private final GachaModule gachaModule;
private final TutorialModule tutorialModule;
private final ActivityModule activityModule;
private final ScoreBossModule scoreBossModule;
// Game loop
@@ -34,6 +36,7 @@ public class GameContext implements Runnable {
// Daily
private long epochDays;
private int epochWeeks;
public GameContext() {
this.sessions = new Object2ObjectOpenHashMap<>();
@@ -42,6 +45,7 @@ public class GameContext implements Runnable {
this.playerModule = new PlayerModule(this);
this.gachaModule = new GachaModule(this);
this.tutorialModule = new TutorialModule(this);
this.activityModule = new ActivityModule(this);
this.scoreBossModule = new ScoreBossModule(this);
// Run game loop
@@ -77,7 +81,9 @@ public class GameContext implements Runnable {
// TODO add timeout to config
public synchronized void cleanupInactiveSessions() {
var it = this.getSessions().entrySet().iterator();
long timeout = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(600); // 10 minutes
int time = Nebula.getConfig().getServerOptions().sessionTimeout;
long timeout = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(time);
while (it.hasNext()) {
var session = it.next().getValue();
@@ -101,6 +107,7 @@ public class GameContext implements Runnable {
long lastEpochDays = this.epochDays;
this.epochDays = date.toEpochDay();
this.epochWeeks = Utils.getWeeks(this.epochDays);
// Check if the day was changed
if (this.epochDays > lastEpochDays) {

View File

@@ -0,0 +1,159 @@
package emu.nebula.game.achievement;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum AchievementCondition {
AchievementSpecific (1),
AchievementTotal (2),
BattleTotal (3),
CharacterAcquire (5),
CharacterAcquireQuantityRarityAndAdvancement (6),
CharacterAdvanceTotal (7),
CharacterSkillUpTotal (8),
CharacterSkillWithSpecificUpTotal (9),
CharacterSpecific (10),
CharacterUpLevel (11),
CharacterUpTotal (12),
CharacterWithSpecificAdvance (13),
CharacterWithSpecificAffinity (14),
CharacterWithSpecificUpLevel (15),
CharactersWithSpecificLevelAndQuantity (16),
CharactersWithSpecificNumberLevelAndAttributes (17),
CharactersWithSpecificPlot (18),
CharactersWithSpecificQuantityAdvancementCountAndAttribute (19),
CharactersWithSpecificQuantityAndRarity (20),
CharactersWithSpecificQuantityRarityAndAdvancement (21),
CharactersWithSpecificQuantityRarityAndLevel (22),
ChatTotal (23),
DailyInstanceClearSpecificDifficultyAndTotal (24),
DailyInstanceClearSpecificTypeAndTotal (25),
DailyInstanceClearTotal (26),
DateSpecific (27),
DiscAcquire (28),
DiscAcquireSpecificQuantityAndRarity (29),
DiscAcquireQuantityLevelAndRarity (30),
DiscAcquireQuantityPhaseAndRarity (31),
DiscAcquireQuantityStarAndRarity (32),
DiscLimitBreakTotal (33),
DiscPromoteTotal (34),
DiscStrengthenTotal (35),
DiscWithSpecificQuantityLevelAndRarity (36),
DiscWithSpecificQuantityPhaseAndRarity (37),
DiscWithSpecificQuantityStarAndRarity (38),
GachaCharacterNotSSRTotal (40),
GachaCharacterTenModeSSRTotal (41),
GachaCharacterTotal (42),
GachaTenModeAcquireQuantityAndRarityItems (43),
GachaTotal (44),
GiftGiveTotal (45),
InfinityTowerClearSpecificFloor (46),
InfinityTowerClearTotal (47),
ItemsAdd (48),
ItemsDeplete (49),
ItemsProductTotal (50),
LoginTotal (51),
QuestTravelerDuelChallengeTotal (52),
QuestTourGuideSpecific (53),
QuestTravelerDuelSpecific (54),
QuestWithSpecificType (55),
RegionBossClearSpecificFullStarWithBossIdAndDifficulty (56),
RegionBossClearSpecificLevelWithDifficultyAndTotal (57),
RegionBossClearSpecificTotal (58),
RegionBossClearTotal (59),
SkillsWithSpecificQuantityAndLevel (60),
SkinAcquire (61),
StageClearSpecificStars (62),
StoryClear (63),
TravelerDuelChallengeSpecificBoosLevelWithDifficultyAndTotal (64),
TravelerDuelClearBossTotal (65),
TravelerDuelClearSpecificBossIdAndDifficulty (66),
TravelerDuelChallengeClearSpecificBossLevelAndAffix (67),
TravelerDuelClearSpecificBossLevelWithDifficultyAndTotal (68),
TravelerDuelClearSpecificBossTotal (69),
TravelerDuelChallengeRankUploadTotal (70),
WorldClassSpecific (71),
RegionBossClearSpecificTypeWithTotal (72),
CharactersWithSpecificDatingCount (73),
CharactersDatingTotal (74),
VampireSurvivorScoreTotal (75),
VampireSurvivorSpecificLevelWithSpecificScore (76),
VampireSurvivorPassedSpecificLevel (77),
CharacterParticipateTowerNumber (78),
CharacterAllSkillReachSpecificLevel (79),
TravelerDuelPlayTotal (80),
VampireClearTotal (81),
VampireWithSpecificClearTotal (82),
AgentFinishTotal (83),
AgentWithSpecificFinishTotal (84),
ActivityMiningEnterLayer (86),
ActivityMiningDestroyGrid (87),
BossRushTotalStars (88),
InfinityTowerClearSpecificDifficultyAndTotal (89),
SkillInstanceClearTotal (90),
VampireSurvivorSpecificPassedLevel (91),
WeekBoosClearSpecificDifficultyAndTotal (92),
NpcAffinityWithSpecificLevel (93),
CharacterPassedWithSpecificTowerAndCount (94),
JointDrillScoreTotal (95),
CharGemInstanceClearTotal (104),
DailyShopReceiveShopTotal (105),
AgentApplyTotal (106),
DiscSpecific (114),
ClientReport (200),
TowerBattleTimes (501),
TowerBossChallengeSpecificHighRewardWithTotal (502),
TowerBuildSpecificCharacter (503),
TowerBuildSpecificScoreWithTotal (504),
TowerClearSpecificCharacterTypeWithTotal (505),
TowerClearSpecificGroupIdAndDifficulty (506),
TowerClearSpecificLevelWithDifficultyAndTotal (507),
TowerClearTotal (508),
TowerEnterRoom (509),
TowerEventTimes (511),
TowerFateTimes (512),
TowerItemsGet (513),
TowerSpecificDifficultyShopBuyTimes (514),
TowerGrowthSpecificNote (515),
TowerClearSpecificLevelWithDifficultyAndTotalHistory (516),
TowerBookWithSpecificEvent (517),
TowerBookWithSpecificFateCard (518),
TowerBookWithSpecificPotential (520),
TowerBuildSpecificDifficultyAndScoreWithTotal (521),
TowerSpecificDifficultyStrengthenMachineTotal (522),
TowerSpecificDifficultyKillBossTotal (524),
TowerBookSpecificCharWithPotentialTotal (525),
TowerBuildSpecificCharSpecificScoreWithTotal (526),
TowerGrowthWithSpecificNote (527),
TowerSpecificFateCardReRollTotal (528),
TowerSpecificPotentialReRollTotal (529),
TowerSpecificShopReRollTotal (530),
TowerSpecificNoteActivateTotal (531),
TowerSpecificNoteLevelTotal (532),
TowerSpecificPotentialBonusTotal (533),
TowerSpecificPotentialLuckyTotal (534),
TowerSpecificShopBuyDiscountTotal (535),
TowerSpecificSecondarySkillActivateTotal (536),
TowerSpecificGetExtraNoteLvTotal (537),
TowerSweepTimes (539),
TowerSweepTotal (540);
@Getter
private final int value;
private final static Int2ObjectMap<AchievementCondition> map = new Int2ObjectOpenHashMap<>();
static {
for (AchievementCondition type : AchievementCondition.values()) {
map.put(type.getValue(), type);
}
}
private AchievementCondition(int value) {
this.value = value;
}
public static AchievementCondition getByValue(int value) {
return map.get(value);
}
}

View File

@@ -0,0 +1,98 @@
package emu.nebula.game.achievement;
import java.util.List;
import emu.nebula.GameConstants;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.AchievementDef;
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;
// Because achievements in the data files do not have params, we will hardcode them here
public class AchievementHelper {
// Cache
private static IntSet incrementalAchievementSet = new IntOpenHashSet();
@Getter
private static Int2ObjectMap<List<AchievementDef>> cache = new Int2ObjectOpenHashMap<>();
public static List<AchievementDef> getAchievementsByCondition(int condition) {
return cache.get(condition);
}
//
public static boolean isIncrementalAchievement(int condition) {
return incrementalAchievementSet.contains(condition);
}
// Fix params
public static void init() {
// Cache total achievements
for (var condition : AchievementCondition.values()) {
if (condition.name().endsWith("Total")) {
incrementalAchievementSet.add(condition.getValue());
}
}
incrementalAchievementSet.remove(AchievementCondition.AchievementTotal.getValue());
incrementalAchievementSet.add(AchievementCondition.ItemsAdd.getValue());
incrementalAchievementSet.add(AchievementCondition.ItemsDeplete.getValue());
// Fix params
fixParams();
}
private static void fixParams() {
// Monolith
addParam(78, 0, 2);
addParam(79, 0, 4);
addParam(498, 0, 1);
// Money
addParam(25, GameConstants.GOLD_ITEM_ID, 0);
addParam(26, GameConstants.GOLD_ITEM_ID, 0);
addParam(27, GameConstants.GOLD_ITEM_ID, 0);
addParam(28, GameConstants.GOLD_ITEM_ID, 0);
addParam(29, GameConstants.GOLD_ITEM_ID, 0);
// Ininfite tower
for (int diff = 10, id = 270; diff <= 60; diff += 10) {
addParam(id++, 11000 + diff, 0); // Infinite Arena
addParam(id++, 51000 + diff, 0); // Shake the Floor
addParam(id++, 41000 + diff, 0); // Elegance and Flow
addParam(id++, 71000 + diff, 0); // Upbeat Party
addParam(id++, 31000 + diff, 0); // Thrilling Beat
addParam(id++, 21000 + diff, 0); // Flames and Beats
addParam(id++, 61000 + diff, 0); // Sinister Ritual
}
// Character count
addParam(393, 1, 0);
addParam(394, 1, 0);
addParam(395, 1, 0);
addParam(396, 1, 0);
addParam(397, 1, 0);
addParam(398, 1, 0);
// Disc count
addParam(382, 1, 0);
addParam(383, 1, 0);
addParam(384, 1, 0);
addParam(385, 1, 0);
addParam(386, 1, 0);
addParam(387, 1, 0);
}
private static void addParam(int achievementId, int param1, int param2) {
var data = GameData.getAchievementDataTable().get(achievementId);
if (data == null) return;
data.setParams(param1, param2);
}
}

View File

@@ -0,0 +1,257 @@
package emu.nebula.game.achievement;
import java.util.HashMap;
import java.util.Map;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.AchievementDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.Achievements;
import emu.nebula.proto.Public.Events;
import lombok.Getter;
import lombok.Setter;
import us.hebi.quickbuf.RepeatedInt;
@Getter
@Entity(value = "achievements", useDiscriminator = false)
public class AchievementManager extends PlayerManager implements GameDatabaseObject {
@Id
private int uid;
// Achievement data
private Map<Integer, GameAchievement> achievements;
@Setter
private transient boolean queueSave;
@Deprecated // Morphia only
public AchievementManager() {
}
public AchievementManager(Player player) {
super(player);
this.uid = player.getUid();
this.achievements = new HashMap<>();
this.save();
}
public synchronized int getCompletedAchievementsCount() {
return (int) this.getAchievements().values().stream()
.filter(GameAchievement::isComplete)
.count();
}
/**
* Returns true if there are any unclaimed achievements
*/
public synchronized boolean hasNewAchievements() {
for (var achievement : this.getAchievements().values()) {
if (achievement.isClaimable()) {
return true;
}
}
return false;
}
public synchronized GameAchievement getAchievement(AchievementDef data) {
// Try and get achievement normally
var achievement = this.getAchievements().get(data.getId());
// Create achievement if it doesnt exist
if (achievement == null) {
achievement = new GameAchievement(data);
this.getAchievements().put(achievement.getId(), achievement);
}
return achievement;
}
public synchronized void handleClientEvents(Events events) {
//
boolean hasCompleted = false;
// Parse events
for (var event : events.getList()) {
// Check id
if (event.getId() != AchievementCondition.ClientReport.getValue()) {
continue;
}
// Sanity check
if (event.getData().length() < 2) {
continue;
}
// Get achievement id and progress
int id = event.getData().get(1);
int progress = event.getData().get(0);
if (progress <= 0) {
continue;
}
// Get achievement data
var data = GameData.getAchievementDataTable().get(id);
if (data == null) continue;
// Make sure achivement can be completed by the client
if (data.getCompleteCond() != AchievementCondition.ClientReport.getValue()) {
continue;
}
// Get achievement
var achievement = this.getAchievement(data);
// Update achievement
boolean changed = achievement.trigger(true, progress, 0, 0);
// Only save/update on client if achievement was changed
if (changed) {
// Sync
this.syncAchievement(achievement);
// Set save flag
this.queueSave = true;
// Check if achievement was completed
if (achievement.isComplete()) {
hasCompleted = true;
}
}
}
// Trigger update
if (hasCompleted) {
this.getPlayer().trigger(AchievementCondition.AchievementTotal, this.getCompletedAchievementsCount());
}
}
public synchronized void trigger(int condition, int progress, int param1, int param2) {
// Sanity check
if (progress <= 0) {
return;
}
// Blacklist
if (condition == AchievementCondition.ClientReport.getValue()) {
return;
}
// Get achievements to trigger
var triggerList = AchievementHelper.getAchievementsByCondition(condition);
if (triggerList == null) {
return;
}
// Check what type of achievement condition this is
boolean isTotal = AchievementHelper.isIncrementalAchievement(condition);
boolean hasCompleted = false;
// Parse achievements
for (var data : triggerList) {
// Get achievement
var achievement = this.getAchievement(data);
// Update achievement
boolean changed = achievement.trigger(isTotal, progress, param1, param2);
// Only save/update on client if achievement was changed
if (changed) {
// Sync
this.syncAchievement(achievement);
// Set save flag
this.queueSave = true;
// Check if achievement was completed
if (achievement.isComplete()) {
hasCompleted = true;
}
}
}
// Trigger update
if (hasCompleted) {
this.getPlayer().trigger(AchievementCondition.AchievementTotal, this.getCompletedAchievementsCount());
}
}
/**
* Update this achievement on the player client
*/
private void syncAchievement(GameAchievement achievement) {
if (!getPlayer().hasSession()) {
return;
}
getPlayer().addNextPackage(
NetMsgId.achievement_change_notify,
achievement.toProto()
);
}
public synchronized PlayerChangeInfo recvRewards(RepeatedInt ids) {
// Sanity check
if (ids.length() <= 0) {
return null;
}
// Init variables
var rewards = new ItemParamMap();
// Claim achievements
for (int id : ids) {
// Get achievement
var achievement = this.getAchievements().get(id);
if (achievement == null) continue;
// Check if we can claim this achievement
if (achievement.isClaimable()) {
// Claim
achievement.setClaimed(true);
// Add rewards
rewards.add(achievement.getData().getTid1(), achievement.getData().getQty1());
// Save
this.queueSave = true;
}
}
// Success
return this.getPlayer().getInventory().addItems(rewards);
}
// Proto
public synchronized Achievements toProto() {
var proto = Achievements.newInstance();
for (var achievement : this.getAchievements().values()) {
proto.addList(achievement.toProto());
}
return proto;
}
// Database
@Override
public void save() {
Nebula.getGameDatabase().save(this);
this.queueSave = false;
}
}

View File

@@ -0,0 +1,122 @@
package emu.nebula.game.achievement;
import dev.morphia.annotations.Entity;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.AchievementDef;
import emu.nebula.proto.Public.Achievement;
import emu.nebula.proto.Public.QuestProgress;
import lombok.Getter;
@Getter
@Entity(useDiscriminator = false)
public class GameAchievement {
private int id;
private int curProgress;
private long completed;
private boolean claimed;
private transient AchievementDef data;
@Deprecated // Morphia only
public GameAchievement() {
}
public GameAchievement(AchievementDef data) {
this.id = data.getId();
this.data = data;
}
public AchievementDef getData() {
if (this.data == null) {
this.data = GameData.getAchievementDataTable().get(this.getId());
}
return this.data;
}
public int getMaxProgress() {
return this.getData().getAimNumShow();
}
public boolean isComplete() {
return this.completed > 0;
}
public boolean isClaimable() {
return !this.isClaimed() && this.isComplete();
}
public void setClaimed(boolean claimed) {
this.claimed = claimed;
}
public int getStatus() {
if (this.isClaimed()) {
return 2;
} else if (this.isComplete()) {
return 1;
}
return 0;
}
/**
* Returns true if achievement was updated
*/
public boolean trigger(boolean isTotal, int progress, int param1, int param2) {
// Sanity check
if (this.isComplete()) {
return false;
}
// Check param conditions
var data = this.getData();
if (data == null) return false;
if (data.hasParam1(param1) && data.getParam1() != param1) {
return false;
}
if (data.hasParam2(param2) && data.getParam2() != param2) {
return false;
}
// Set previous progress
int prevProgress = this.curProgress;
// Update progress
if (isTotal) {
this.curProgress += progress;
} else {
this.curProgress = progress;
}
// Check if completed
if (this.getCurProgress() >= this.getMaxProgress()) {
this.curProgress = this.getMaxProgress();
this.completed = Nebula.getCurrentTime();
return true;
}
// Check if progress was changed
return prevProgress != this.curProgress;
}
// Proto
public Achievement toProto() {
var progress = QuestProgress.newInstance()
.setCur(this.getCurProgress())
.setMax(this.getMaxProgress());
var proto = Achievement.newInstance()
.setId(this.getId())
.setCompleted(this.getCompleted())
.setStatus(this.getStatus())
.addProgress(progress);
return proto;
}
}

View File

@@ -0,0 +1,127 @@
package emu.nebula.game.activity;
import java.util.HashMap;
import java.util.Map;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.activity.type.*;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerManager;
import lombok.Getter;
import lombok.Setter;
@Getter
@Entity(value = "activity", useDiscriminator = false)
public class ActivityManager extends PlayerManager implements GameDatabaseObject {
@Id
private int uid;
// Achievement data
private Map<Integer, GameActivity> activities;
@Setter
private transient boolean queueSave;
@Deprecated // Morphia only
public ActivityManager() {
}
public ActivityManager(Player player) {
super(player);
this.uid = player.getUid();
this.activities = new HashMap<>();
}
public <T extends GameActivity> T getActivity(Class<T> activityClass, int id) {
// Get activity first
var activity = this.getActivities().get(id);
if (activity == null) return null;
// Check activity type
if (activityClass.isInstance(activity)) {
return activityClass.cast(activity);
}
// Failure
return null;
}
/**
* This needs to be called after being loaded from the database
*/
public synchronized void init() {
// Check if any activities
var it = this.getActivities().entrySet().iterator();
while (it.hasNext()) {
var entry = it.next();
var activity = entry.getValue();
// Validate the activity to make sure it exists
var data = GameData.getActivityDataTable().get(activity.getId());
if (data == null) {
it.remove();
this.queueSave = true;
}
// Set data
activity.setData(data);
activity.setManager(this);
}
// Load activities
for (var id : Nebula.getGameContext().getActivityModule().getActivities()) {
// Check if we already have this activity
if (this.getActivities().containsKey(id)) {
continue;
}
// Create activity
var activity = this.createActivity(id);
if (activity == null) {
continue;
}
// Add activity
this.getActivities().put(id, activity);
// Set save flag
this.queueSave = true;
}
// Save if any activities were changed
if (this.queueSave) {
this.save();
}
}
private GameActivity createActivity(int id) {
// Get activity data first
var data = GameData.getActivityDataTable().get(id);
if (data == null) {
return null;
}
GameActivity activity = switch (data.getType()) {
case Trial -> new TrialActivity(this, data);
default -> null;
};
return activity;
}
// Database
@Override
public void save() {
Nebula.getGameDatabase().save(this);
this.queueSave = false;
}
}

View File

@@ -0,0 +1,27 @@
package emu.nebula.game.activity;
import emu.nebula.game.GameContext;
import emu.nebula.game.GameContextModule;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
public class ActivityModule extends GameContextModule {
private IntList activities;
public ActivityModule(GameContext context) {
super(context);
this.activities = new IntArrayList();
// Hardcode these activities for now
this.activities.add(700102);
this.activities.add(700103);
this.activities.add(700104);
this.activities.add(700107);
//this.activities.add(101002);
//this.activities.add(101003);
}
}

View File

@@ -0,0 +1,40 @@
package emu.nebula.game.activity;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum ActivityType {
None (0),
PeriodicQuest (1),
LoginReward (2),
Mining (3),
Cookie (4),
TowerDefense (5),
Trial (6),
JointDrill (7),
CG (8),
Levels (9),
Avg (10),
Task (11),
Shop (12),
Advertise (13);
@Getter
private final int value;
private final static Int2ObjectMap<ActivityType> map = new Int2ObjectOpenHashMap<>();
static {
for (ActivityType type : ActivityType.values()) {
map.put(type.getValue(), type);
}
}
private ActivityType(int value) {
this.value = value;
}
public static ActivityType getByValue(int value) {
return map.get(value);
}
}

View File

@@ -0,0 +1,65 @@
package emu.nebula.game.activity;
import dev.morphia.annotations.Entity;
import emu.nebula.Nebula;
import emu.nebula.data.resources.ActivityDef;
import emu.nebula.game.player.Player;
import emu.nebula.proto.ActivityDetail.ActivityMsg;
import emu.nebula.proto.Public.Activity;
import lombok.Getter;
import lombok.Setter;
@Getter
@Entity(useDiscriminator = true)
public abstract class GameActivity {
private int id;
@Setter private transient ActivityManager manager;
@Setter private transient ActivityDef data;
@Deprecated // Morhpia only
public GameActivity() {
}
public GameActivity(ActivityManager manager, ActivityDef data) {
this.id = data.getId();
this.manager = manager;
this.data = data;
}
public Player getPlayer() {
return this.getManager().getPlayer();
}
public void save() {
Nebula.getGameDatabase().update(
this.getManager(),
this.getManager().getPlayerUid(),
"activities." + this.getId(),
this
);
}
// Proto
public Activity toProto() {
var proto = Activity.newInstance()
.setId(this.getId())
.setStartTime(1)
.setEndTime(Integer.MAX_VALUE);
return proto;
}
public ActivityMsg toMsgProto() {
var proto = ActivityMsg.newInstance()
.setId(this.getId());
this.encodeActivityMsg(proto);
return proto;
}
public abstract void encodeActivityMsg(ActivityMsg msg);
}

View File

@@ -0,0 +1,67 @@
package emu.nebula.game.activity.type;
import dev.morphia.annotations.Entity;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.ActivityDef;
import emu.nebula.game.activity.ActivityManager;
import emu.nebula.game.activity.GameActivity;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.proto.ActivityDetail.ActivityMsg;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
@Entity
public class TrialActivity extends GameActivity {
private IntList completed;
@Deprecated // Morphia only
public TrialActivity() {
}
public TrialActivity(ActivityManager manager, ActivityDef data) {
super(manager, data);
this.completed = new IntArrayList();
}
public PlayerChangeInfo claimReward(int groupId) {
// Create change info
var change = new PlayerChangeInfo();
// Make sure we haven't completed this group yet
if (this.getCompleted().contains(groupId)) {
return change;
}
// Get trial control
var control = GameData.getTrialControlDataTable().get(this.getId());
if (control == null) return change;
// Get group
var group = GameData.getTrialGroupDataTable().get(groupId);
if (group == null) return change;
// Set as completed
this.getCompleted().add(groupId);
// Save to database
this.save();
// Add rewards
return getPlayer().getInventory().addItems(group.getRewards(), change);
}
// Proto
@Override
public void encodeActivityMsg(ActivityMsg msg) {
var proto = msg.getMutableTrial();
for (int id : this.getCompleted()) {
proto.addCompletedGroupIds(id);
}
}
}

View File

@@ -8,10 +8,12 @@ import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.data.GameData;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import lombok.Getter;
import us.hebi.quickbuf.RepeatedInt;
@@ -58,13 +60,23 @@ public class AgentManager extends PlayerManager implements GameDatabaseObject {
}
// Make sure we own the characters
var characters = new ArrayList<GameCharacter>();
for (int charId : charIds) {
if (!getPlayer().getCharacters().hasCharacter(charId)) {
var character = getPlayer().getCharacters().getCharacterById(charId);
// Also check if character fits the commission level requirement
if (character == null || character.getLevel() < data.getLevel()) {
return null;
}
characters.add(character);
}
// TODO verify char tags for rewards
// Check char tags
if (!data.hasTags(characters)) {
return null;
}
// Create agent
var agent = new Agent(data, processTime, charIds.toArray());
@@ -73,7 +85,7 @@ public class AgentManager extends PlayerManager implements GameDatabaseObject {
this.getAgents().put(agent.getId(), agent);
// Quest
this.getPlayer().triggerQuest(QuestCondType.AgentApplyTotal, 1);
this.getPlayer().trigger(QuestCondition.AgentApplyTotal, 1);
// Success
return agent;
@@ -139,12 +151,30 @@ public class AgentManager extends PlayerManager implements GameDatabaseObject {
continue;
}
// Create rewards
var rewards = duration.getRewards().generate();
result.setRewards(rewards);
// Check if we had extra tags
var characters = new ArrayList<GameCharacter>();
// Add to inventory
this.getPlayer().getInventory().addItems(rewards, change);
for (int charId : agent.getCharIds()) {
var character = getPlayer().getCharacters().getCharacterById(charId);
if (character == null) continue;
characters.add(character);
}
// Create rewards
result.setRewards(duration.getRewards().generate());
// Add rewards to inventory
this.getPlayer().getInventory().addItems(result.getRewards(), change);
// Add bonus rewards if we meet the requirements
if (data.hasExtraTags(characters)) {
// Get bonus rewards
result.setBonus(duration.getBonus().generate());
// Add rewards to inventory
this.getPlayer().getInventory().addItems(result.getBonus(), change);
}
}
// Set results in change info
@@ -153,8 +183,9 @@ public class AgentManager extends PlayerManager implements GameDatabaseObject {
// Save to database
this.save();
// Quest
this.getPlayer().triggerQuest(QuestCondType.AgentFinishTotal, list.size());
// Quest + Achievements
getPlayer().trigger(QuestCondition.AgentFinishTotal, list.size());
getPlayer().trigger(AchievementCondition.AgentWithSpecificFinishTotal, list.size());
// Success
return change.setSuccess(true);

View File

@@ -16,7 +16,6 @@ import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.quest.GameQuest;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestType;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.BattlePassInfoOuterClass.BattlePassInfo;
@@ -31,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;
@@ -72,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;
}
@@ -80,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;
@@ -101,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()) {
@@ -119,14 +159,19 @@ public class BattlePass implements GameDatabaseObject {
this.syncQuest(quest);
}
// Reset weekly limit for exp
if (resetWeekly) {
this.expWeek = 0;
}
// Persist to database
this.save();
}
public synchronized void trigger(QuestCondType condition, int progress, int param) {
public synchronized void trigger(int condition, int progress, int param1, int param2) {
for (var quest : getQuests().values()) {
// Try to trigger quest
boolean result = quest.trigger(condition, progress, param);
boolean result = quest.trigger(condition, progress, param1, param2);
// Skip if quest progress wasn't changed
if (!result) {

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

@@ -6,6 +6,7 @@ import java.util.Map;
import dev.morphia.annotations.Entity;
import emu.nebula.Nebula;
import emu.nebula.data.resources.ChatDef;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.Contacts;
@@ -125,6 +126,9 @@ public class CharacterContact {
);
}
// Trigger quest/achievement
this.getCharacter().getPlayer().trigger(AchievementCondition.ChatTotal, 1);
// Success
return change.setSuccess(true);
}

View File

@@ -8,19 +8,29 @@ import emu.nebula.data.GameData;
import emu.nebula.data.resources.CharacterDef;
import emu.nebula.data.resources.DiscDef;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.HandbookInfo;
import emu.nebula.util.Bitset;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
import lombok.Setter;
@Getter
public class CharacterStorage extends PlayerManager {
private final Int2ObjectMap<GameCharacter> characters;
private final Int2ObjectMap<GameDisc> discs;
// Flags
@Setter private boolean hasAddedChar;
@Setter private boolean hasAddedDisc;
@Setter private boolean updateCharHandbook;
@Setter private boolean updateDiscHandbook;
public CharacterStorage(Player player) {
super(player);
@@ -65,6 +75,12 @@ public class CharacterStorage extends PlayerManager {
// Add to characters
this.characters.put(character.getCharId(), character);
// Set flags for player to update character skins in their handbook
this.setUpdateCharHandbook(true);
this.setHasAddedChar(true);
// Return character
return character;
}
@@ -72,18 +88,6 @@ public class CharacterStorage extends PlayerManager {
return this.getCharacters().values();
}
public int getNewPhoneMessageCount() {
int count = 0;
for (var character : this.getCharacterCollection()) {
if (character.getContact().hasNew()) {
count++;
}
}
return count;
}
public HandbookInfo getCharacterHandbook() {
var bitset = new Bitset();
@@ -140,6 +144,12 @@ public class CharacterStorage extends PlayerManager {
// Add to discs
this.discs.put(disc.getDiscId(), disc);
// Set flags for player to update discs in their handbook
this.setUpdateDiscHandbook(true);
this.setHasAddedDisc(true);
// Return disc
return disc;
}
@@ -205,6 +215,73 @@ public class CharacterStorage extends PlayerManager {
return change.setExtraData(modifiedDiscs);
}
// Contacts
public int getNewPhoneMessageCount() {
int count = 0;
for (var character : this.getCharacterCollection()) {
if (character.getContact().hasNew()) {
count++;
}
}
return count;
}
//
/**
* Checks if we should add next packages for player
*/
public void checkPlayerState() {
// Check if we need to trigger character achievements
if (this.hasAddedChar) {
this.hasAddedChar = false;
this.getPlayer().trigger(
AchievementCondition.CharactersWithSpecificQuantityAndRarity,
getCharacters().size()
);
this.getPlayer().trigger(
AchievementCondition.CharactersWithSpecificQuantityAndRarity,
(int) getCharacters().values().stream().filter(GameCharacter::isMaster).count(),
1,
0
);
}
// Check if we need to send handbook update to player
if (this.updateCharHandbook) {
this.updateCharHandbook = false;
this.getPlayer().addNextPackage(
NetMsgId.handbook_change_notify,
this.getPlayer().getCharacters().getCharacterHandbook());
}
// Check if we need to trigger disc achievements
if (this.hasAddedChar) {
this.hasAddedChar = false;
this.getPlayer().trigger(
AchievementCondition.DiscAcquireSpecificQuantityAndRarity,
this.getDiscs().size()
);
this.getPlayer().trigger(
AchievementCondition.DiscAcquireSpecificQuantityAndRarity,
(int) getDiscs().values().stream().filter(GameDisc::isMaster).count(),
1,
0
);
}
// Check if we need to send handbook update to player
if (this.updateDiscHandbook) {
this.updateDiscHandbook = false;
this.getPlayer().addNextPackage(
NetMsgId.handbook_change_notify,
this.getPlayer().getCharacters().getDiscHandbook());
}
}
// Database
public void loadFromDatabase() {

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

@@ -18,13 +18,15 @@ import emu.nebula.data.GameData;
import emu.nebula.data.resources.CharacterDef;
import emu.nebula.data.resources.TalentGroupDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Notify.Skin;
import emu.nebula.proto.Notify.SkinChange;
import emu.nebula.proto.Public.AffinityInfo;
import emu.nebula.proto.Public.Char;
import emu.nebula.proto.Public.CharGemPreset;
import emu.nebula.proto.Public.CharGemSlot;
@@ -35,6 +37,7 @@ import emu.nebula.util.Bitset;
import emu.nebula.util.CustomIntArray;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
import us.hebi.quickbuf.RepeatedInt;
@@ -53,9 +56,12 @@ public class GameCharacter implements GameDatabaseObject {
private int advance;
private int level;
private int exp;
private int affinityLevel;
private int affinityExp;
private int skin;
private int[] skills;
private Bitset talents;
private IntSet plots;
private long createTime;
private int gemPresetIndex;
@@ -112,6 +118,10 @@ public class GameCharacter implements GameDatabaseObject {
}
}
public boolean isMaster() {
return this.getData().getGrade() == 1;
}
public void setLevel(int level) {
this.level = level;
}
@@ -119,7 +129,12 @@ public class GameCharacter implements GameDatabaseObject {
public void setAdvance(int advance) {
this.advance = advance;
}
public void setAffinityLevel(int level) {
this.affinityLevel = Math.max(level, 0);
this.affinityExp = 0;
}
public int getMaxGainableExp() {
if (this.getLevel() >= this.getMaxLevel()) {
return 0;
@@ -176,7 +191,7 @@ public class GameCharacter implements GameDatabaseObject {
// Check if we leveled up
if (this.level > oldLevel) {
// Trigger quest
this.getPlayer().triggerQuest(QuestCondType.CharacterUpTotal, this.level - oldLevel);
this.getPlayer().trigger(QuestCondition.CharacterUpTotal, this.level - oldLevel);
}
// Save to database
@@ -245,7 +260,7 @@ public class GameCharacter implements GameDatabaseObject {
// Set advance skin
this.skin = this.getData().getAdvanceSkinId();
// Send packets
// Add next packages
this.getPlayer().addNextPackage(
NetMsgId.character_skin_gain_notify,
Skin.newInstance().setNew(UI32.newInstance().setValue(this.getSkin()))
@@ -254,11 +269,17 @@ public class GameCharacter implements GameDatabaseObject {
NetMsgId.character_skin_change_notify,
SkinChange.newInstance().setCharId(this.getCharId()).setSkinId(this.getSkin())
);
// Set flag for player to update character skins in their handbook
this.getPlayer().getCharacters().setUpdateCharHandbook(true);
}
// Save to database
this.save();
// Trigger quest/achievement
this.getPlayer().trigger(AchievementCondition.CharacterAdvanceTotal, 1);
// Success
return changes.setSuccess(true);
}
@@ -375,6 +396,121 @@ public class GameCharacter implements GameDatabaseObject {
return true;
}
// Affinity
public int getMaxAffinityExp() {
var data = GameData.getAffinityLevelDataTable().get(this.affinityLevel + 1);
return data != null ? data.getNeedExp() : 0;
}
public void addAffinityExp(int amount) {
// Setup
int expRequired = this.getMaxAffinityExp();
// Add exp
this.affinityExp += amount;
// Check for level ups
while (this.affinityExp >= expRequired && expRequired > 0) {
this.affinityLevel += 1;
this.affinityExp -= expRequired;
expRequired = this.getMaxAffinityExp();
}
// Clamp exp
if (expRequired <= 0) {
this.affinityExp = 0;
}
// Save to database
this.save();
}
public PlayerChangeInfo sendGift(ItemParamMap items) {
// Verify that the player has the items
if (!this.getPlayer().getInventory().hasItems(items)) {
return null;
}
// Caluclate amount of exp to gain
var charDescription = this.getData().getDes();
int exp = 0;
int count = 0;
for (var item : items) {
// Get data
var gift = GameData.getAffinityGiftDataTable().get(item.getIntKey());
if (gift == null) {
return null;
}
// Calculate amount
double amount = gift.getBaseAffinity() * item.getIntValue();
if (charDescription.isPreferGift(gift)) {
amount = amount * 1.5D;
} else if (charDescription.isHateGift(gift)) {
amount = amount * 0.8D;
}
// Add
exp += (int) amount;
count += item.getIntValue();
}
// Sanity check
if (exp <= 0) {
return null;
}
// Add affinity exp
this.addAffinityExp(exp);
// Trigger quest/achievement
this.getPlayer().trigger(QuestCondition.GiftGiveTotal, count);
// Remove items
var change = this.getPlayer().getInventory().removeItems(items);
// Success
return change;
}
public PlayerChangeInfo recvPlotReward(int plotId) {
// Create change info
var change = new PlayerChangeInfo();
// Sanity check to prevent players from recving rewards over and over again from the same plot
if (this.getPlots() != null && this.getPlots().contains(plotId)) {
return change;
}
// Get data
var plot = GameData.getPlotDataTable().get(plotId);
// Sanity check to make sure we can complete this plot quest
if (plot == null || plot.getChar() != this.getCharId() || plot.getUnlockAffinityLevel() > this.getAffinityLevel()) {
return change;
}
// Complete plot
if (this.plots == null) {
this.plots = new IntOpenHashSet();
}
this.getPlots().add(plotId);
// Update to database
this.save();
// Add items
this.getPlayer().getInventory().addItems(plot.getRewards(), change);
// Success
return change;
}
// Gems
public boolean hasGemPreset(int index) {
@@ -659,6 +795,8 @@ public class GameCharacter implements GameDatabaseObject {
.setLevel(this.getLevel())
.setSkin(this.getSkin())
.setAdvance(this.getAdvance())
.setAffinityLevel(this.getAffinityLevel())
.setAffinityExp(this.getAffinityExp())
.setTalentNodes(this.getTalents().toByteArray())
.addAllSkillLvs(this.getSkills())
.setCreateTime(this.getCreateTime());
@@ -694,6 +832,12 @@ public class GameCharacter implements GameDatabaseObject {
// Affinity quests
proto.getMutableAffinityQuests();
// Encode plots
if (this.getPlots() != null) {
this.getPlots().forEach(proto::addPlots);
}
// Finish
return proto;
}
@@ -702,6 +846,7 @@ public class GameCharacter implements GameDatabaseObject {
.setId(this.getCharId())
.setAdvance(this.getAdvance())
.setLevel(this.getLevel())
.setAffinityLevel(this.getAffinityLevel())
.setTalentNodes(this.getTalents().toByteArray())
.addAllSkillLvs(this.getSkills());
@@ -725,6 +870,15 @@ public class GameCharacter implements GameDatabaseObject {
return proto;
}
public AffinityInfo getAffinityProto() {
var proto = AffinityInfo.newInstance()
.setCharId(this.getCharId())
.setAffinityLevel(this.getAffinityLevel())
.setAffinityExp(this.getAffinityExp());
return proto;
}
// Database fix
@PreLoad

View File

@@ -11,10 +11,11 @@ import emu.nebula.data.GameData;
import emu.nebula.data.resources.DiscDef;
import emu.nebula.data.resources.SubNoteSkillPromoteGroupDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import emu.nebula.proto.Public.Disc;
import emu.nebula.proto.PublicStarTower.StarTowerDisc;
@@ -70,6 +71,10 @@ public class GameDisc implements GameDatabaseObject {
}
}
public boolean isMaster() {
return GameData.getItemDataTable().get(this.getDiscId()).getRarity() == 1;
}
public void setLevel(int level) {
this.level = level;
}
@@ -145,7 +150,7 @@ public class GameDisc implements GameDatabaseObject {
// Check if we leveled up
if (this.level > oldLevel) {
// Trigger quest
this.getPlayer().triggerQuest(QuestCondType.DiscStrengthenTotal, this.level - oldLevel);
this.getPlayer().trigger(QuestCondition.DiscStrengthenTotal, this.level - oldLevel);
}
// Save to database
@@ -215,6 +220,9 @@ public class GameDisc implements GameDatabaseObject {
// Save to database
this.save();
// Trigger quest/achievement
this.getPlayer().trigger(AchievementCondition.DiscPromoteTotal, 1);
// Success
return change.setSuccess(true);
}

View File

@@ -0,0 +1,7 @@
package emu.nebula.game.dating;
public interface DatingEvent {
public int getId();
}

View File

@@ -0,0 +1,35 @@
package emu.nebula.game.dating;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum DatingEventType {
Start (1),
End (2),
Landmark (3),
Regular (4),
LimitedLandmark (5),
BranchA (6),
BranchB (7),
BeforeBranch (8),
AfterBranch (9);
@Getter
private final int value;
private final static Int2ObjectMap<DatingEventType> map = new Int2ObjectOpenHashMap<>();
static {
for (DatingEventType type : DatingEventType.values()) {
map.put(type.getValue(), type);
}
}
private DatingEventType(int value) {
this.value = value;
}
public static DatingEventType getByValue(int value) {
return map.get(value);
}
}

View File

@@ -0,0 +1,31 @@
package emu.nebula.game.dating;
import emu.nebula.data.resources.DatingLandmarkDef;
import emu.nebula.game.character.GameCharacter;
import lombok.Getter;
@Getter
public class DatingGame {
private GameCharacter character;
private DatingLandmarkDef landmark;
private int[] branchOptionsA;
private int[] branchOptionsB;
public DatingGame(GameCharacter character, DatingLandmarkDef landmark) {
this.character = character;
this.landmark = landmark;
this.branchOptionsA = new int[] {1, 2};
this.branchOptionsB = new int[] {1, 2};
}
public boolean selectDatingBranchA(int optionId) {
// TODO
return true;
}
public boolean selectDatingBranchB(int optionId) {
// TODO
return true;
}
}

View File

@@ -0,0 +1,39 @@
package emu.nebula.game.dating;
import emu.nebula.data.GameData;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.game.quest.QuestCondition;
import lombok.Getter;
@Getter
public class DatingManager extends PlayerManager {
private DatingGame game;
public DatingManager(Player player) {
super(player);
}
public DatingGame selectLandmark(GameCharacter character, int landmarkId) {
// Get landmark data
var data = GameData.getDatingLandmarkDataTable().get(landmarkId);
if (data == null) {
return null;
}
// Set landmark + character
this.game = new DatingGame(character, data);
// Trigger quest/achievement
this.getPlayer().trigger(QuestCondition.CharactersDatingTotal, 1);
// Success
return this.game;
}
public void endDatingGame() {
this.game = null;
}
}

View File

@@ -3,6 +3,7 @@ package emu.nebula.game.gacha;
import emu.nebula.data.GameData;
import emu.nebula.game.GameContext;
import emu.nebula.game.GameContextModule;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.inventory.ItemAcquireMap;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.inventory.ItemType;
@@ -164,6 +165,9 @@ public class GachaModule extends GameContextModule {
var log = new GachaHistoryLog(data.getGachaType(), results);
player.getGachaManager().addGachaHistory(log);
// Trigger achievements
player.trigger(AchievementCondition.GachaTotal, amount);
// Complete
return new GachaResult(info, change, results);
}

View File

@@ -2,6 +2,7 @@ package emu.nebula.game.infinitytower;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.InfinityTowerLevelDef;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.player.PlayerManager;
@@ -75,6 +76,9 @@ public class InfinityTowerManager extends PlayerManager {
// Log in player progress
this.getPlayer().getProgress().addInfinityArenaLog(this.getLevelId());
// Trigger achievement
this.getPlayer().trigger(AchievementCondition.InfinityTowerClearSpecificFloor, 10, this.getLevelId(), 0);
// Success
return change.setSuccess(true);
}

View File

@@ -8,7 +8,7 @@ import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.game.player.PlayerProgress;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import lombok.Getter;
@@ -35,7 +35,7 @@ public class InstanceManager extends PlayerManager {
return this.getPlayer().getProgress();
}
public PlayerChangeInfo settleInstance(InstanceData data, QuestCondType questCondition, Int2IntMap log, String logName, int star) {
public PlayerChangeInfo settleInstance(InstanceData data, QuestCondition questCondition, Int2IntMap log, String logName, int star) {
// Calculate settle data
var settleData = new InstanceSettleData();
@@ -63,8 +63,8 @@ public class InstanceManager extends PlayerManager {
this.getProgress().saveInstanceLog(log, logName, data.getId(), star);
// Quest triggers
this.getPlayer().triggerQuest(questCondition, 1);
this.getPlayer().triggerQuest(QuestCondType.BattleTotal, 1);
this.getPlayer().trigger(questCondition, 1);
this.getPlayer().trigger(QuestCondition.BattleTotal, 1);
}
// Set extra data
@@ -74,7 +74,7 @@ public class InstanceManager extends PlayerManager {
return change.setSuccess(true);
}
public PlayerChangeInfo sweepInstance(InstanceData data, QuestCondType questCondition, Int2IntMap log, int rewardType, int count) {
public PlayerChangeInfo sweepInstance(InstanceData data, QuestCondition questCondition, Int2IntMap log, int rewardType, int count) {
// Sanity check count
if (count <= 0) {
return null;
@@ -131,8 +131,8 @@ public class InstanceManager extends PlayerManager {
change.setExtraData(list);
// Quest triggers
this.getPlayer().triggerQuest(questCondition, count);
this.getPlayer().triggerQuest(QuestCondType.BattleTotal, count);
this.getPlayer().trigger(questCondition, count);
this.getPlayer().trigger(QuestCondition.BattleTotal, count);
// Success
return change.setSuccess(true);

View File

@@ -11,7 +11,7 @@ import emu.nebula.data.resources.MallShopDef;
import emu.nebula.data.resources.ResidentGoodsDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Notify.Skin;
import emu.nebula.proto.Public.Honor;
@@ -20,6 +20,7 @@ import emu.nebula.proto.Public.Res;
import emu.nebula.proto.Public.Title;
import emu.nebula.proto.Public.UI32;
import emu.nebula.util.String2IntMap;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
@@ -152,12 +153,15 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
// Save to database
Nebula.getGameDatabase().addToSet(this, this.getUid(), "extraSkins", id);
// Send packet
// Send packets
this.getPlayer().addNextPackage(
NetMsgId.character_skin_gain_notify,
Skin.newInstance().setNew(UI32.newInstance().setValue(id))
);
// Set flag for player to update character skins in their handbook
this.getPlayer().getCharacters().setUpdateCharHandbook(true);
// Success
return true;
}
@@ -459,11 +463,11 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
}
}
// Trigger quest
// Trigger quest + achievement
if (amount > 0) {
this.getPlayer().triggerQuest(QuestCondType.ItemsAdd, amount, id);
this.getPlayer().trigger(QuestCondition.ItemsAdd, amount, id);
} else {
this.getPlayer().triggerQuest(QuestCondType.ItemsDeplete, Math.abs(amount), id);
this.getPlayer().trigger(QuestCondition.ItemsDeplete, Math.abs(amount), id);
}
//
@@ -631,24 +635,34 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
// Add produced items
this.addItem(data.getProductionId(), data.getProductionPerBatch() * num, change);
// Trigger achievement
this.getPlayer().trigger(AchievementCondition.ItemsProductTotal, num);
// Success
return change.setSuccess(true);
}
public PlayerChangeInfo buyEnergy() {
public PlayerChangeInfo buyEnergy(int count) {
// Validate count
if (count <= 0 || count > 6) {
return null;
}
// Create change info
var change = new PlayerChangeInfo();
// Make sure we have the gems
if (!this.hasItem(GameConstants.ENERGY_BUY_ITEM_ID, 30)) {
int cost = 30 * count;
if (!this.hasItem(GameConstants.ENERGY_BUY_ITEM_ID, cost)) {
return change;
}
// Remove gems
this.removeItem(GameConstants.ENERGY_BUY_ITEM_ID, 30, change);
this.removeItem(GameConstants.ENERGY_BUY_ITEM_ID, cost, change);
// Add energy
this.getPlayer().addEnergy(60, change);
this.getPlayer().addEnergy(60 * count, change);
// Success
return change;
@@ -768,7 +782,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
}
case 3 -> {
// Selected item
int selectCount = data.getUseParams().get(selectId);
int selectCount = data.getUseParams().get(selectId) * count;
if (selectCount <= 0) {
return change;

View File

@@ -2,6 +2,7 @@ package emu.nebula.game.player;
import java.util.Stack;
import dev.morphia.annotations.AlsoLoad;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
@@ -11,9 +12,13 @@ import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.account.Account;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.achievement.AchievementManager;
import emu.nebula.game.activity.ActivityManager;
import emu.nebula.game.agent.AgentManager;
import emu.nebula.game.battlepass.BattlePassManager;
import emu.nebula.game.character.CharacterStorage;
import emu.nebula.game.dating.DatingManager;
import emu.nebula.game.formation.FormationManager;
import emu.nebula.game.friends.FriendList;
import emu.nebula.game.gacha.GachaManager;
@@ -21,7 +26,7 @@ import emu.nebula.game.infinitytower.InfinityTowerManager;
import emu.nebula.game.instance.InstanceManager;
import emu.nebula.game.inventory.Inventory;
import emu.nebula.game.mail.Mailbox;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import emu.nebula.game.quest.QuestManager;
import emu.nebula.game.scoreboss.ScoreBossManager;
import emu.nebula.game.story.StoryManager;
@@ -42,6 +47,7 @@ import emu.nebula.proto.Public.QuestType;
import emu.nebula.proto.Public.Story;
import emu.nebula.proto.Public.WorldClass;
import emu.nebula.proto.Public.WorldClassRewardState;
import emu.nebula.util.Utils;
import emu.nebula.proto.Public.Title;
import lombok.Getter;
@@ -57,6 +63,10 @@ public class Player implements GameDatabaseObject {
private transient Account account;
private transient GameSession session;
@Indexed
@AlsoLoad("playerRemoteToken")
private String remoteToken;
// Details
private String name;
private String signature;
@@ -82,6 +92,7 @@ public class Player implements GameDatabaseObject {
private final transient CharacterStorage characters;
private final transient FriendList friendList;
private final transient BattlePassManager battlePassManager;
private final transient DatingManager datingManager;
private final transient StarTowerManager starTowerManager;
private final transient InstanceManager instanceManager;
private final transient InfinityTowerManager infinityTowerManager;
@@ -96,7 +107,9 @@ public class Player implements GameDatabaseObject {
private transient PlayerProgress progress;
private transient StoryManager storyManager;
private transient QuestManager questManager;
private transient AchievementManager achievementManager;
private transient AgentManager agentManager;
private transient ActivityManager activityManager;
// Extra
private transient Stack<NetMsgPacket> nextPackages;
@@ -108,6 +121,7 @@ public class Player implements GameDatabaseObject {
this.characters = new CharacterStorage(this);
this.friendList = new FriendList(this);
this.battlePassManager = new BattlePassManager(this);
this.datingManager = new DatingManager(this);
this.starTowerManager = new StarTowerManager(this);
this.instanceManager = new InstanceManager(this);
this.infinityTowerManager = new InfinityTowerManager(this);
@@ -193,6 +207,41 @@ public class Player implements GameDatabaseObject {
public boolean hasSession() {
return this.session != null;
}
public void setLevel(int level) {
// Set player world class (level)
this.level = level;
// Save to database
Nebula.getGameDatabase().update(this, this.getUid(), "level", this.level);
// Trigger achievement
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
}
public void setExp(int exp) {
this.exp = exp;
Nebula.getGameDatabase().update(this, this.getUid(), "exp", this.exp);
}
public void setRemoteToken(String token) {
// Skip if tokens are the same
if (this.remoteToken == null) {
if (token == null) {
return;
}
} else if (this.remoteToken != null) {
if (this.remoteToken.equals(token)) {
return;
}
}
// Set remote token
this.remoteToken = token;
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "remoteToken", this.remoteToken);
}
public boolean getGender() {
return this.gender;
@@ -443,16 +492,17 @@ public class Player implements GameDatabaseObject {
// Save to database
Nebula.getGameDatabase().update(
this,
this.getUid(),
"level",
this.getLevel(),
"exp",
this.getExp()
this,
this.getUid(),
"level",
this.getLevel(),
"exp",
this.getExp()
);
// Save level rewards if we changed it
if (oldLevel != this.getLevel()) {
// Update level rewards
this.getQuestManager().saveLevelRewards();
this.addNextPackage(
@@ -460,6 +510,9 @@ public class Player implements GameDatabaseObject {
WorldClassRewardState.newInstance()
.setFlag(getQuestManager().getLevelRewards().toBigEndianByteArray())
);
// Trigger achievement
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
}
// Calculate changes
@@ -512,7 +565,7 @@ public class Player implements GameDatabaseObject {
change = modifyEnergy(-amount, change);
// Trigger quest
this.triggerQuest(QuestCondType.EnergyDeplete, amount);
this.trigger(QuestCondition.EnergyDeplete, amount);
// Complete
return change;
@@ -547,12 +600,6 @@ public class Player implements GameDatabaseObject {
return change;
}
//
public void sendMessage(String string) {
// Empty
}
// Dailies
public void checkResetDailies() {
@@ -561,8 +608,16 @@ public class Player implements GameDatabaseObject {
return;
}
// Check if week has changed (Resets on monday)
// TODO add a config option
int curWeek = Utils.getWeeks(this.getLastEpochDay());
boolean hasWeekChanged = Nebula.getGameContext().getEpochWeeks() > curWeek;
// Reset dailies
this.resetDailies(false);
this.resetDailies(hasWeekChanged);
// Trigger quest/achievement login
this.trigger(QuestCondition.LoginTotal, 1);
// Update last epoch day
this.lastEpochDay = Nebula.getGameContext().getEpochDays();
@@ -575,15 +630,28 @@ public class Player implements GameDatabaseObject {
this.getBattlePassManager().getBattlePass().resetDailyQuests(resetWeekly);
}
// Trigger quests
// Trigger quests + achievements
public void triggerQuest(QuestCondType condition, int progress) {
this.triggerQuest(condition, progress, 0);
public void trigger(int condition, int progress, int param1, int param2) {
this.getQuestManager().trigger(condition, progress, param1, param2);
this.getBattlePassManager().getBattlePass().trigger(condition, progress, param1, param2);
this.getAchievementManager().trigger(condition, progress, param1, param2);
}
public void triggerQuest(QuestCondType condition, int progress, int param) {
this.getQuestManager().trigger(condition, progress, param);
this.getBattlePassManager().getBattlePass().trigger(condition, progress, param);
public void trigger(QuestCondition condition, int progress) {
this.trigger(condition.getValue(), progress, 0, 0);
}
public void trigger(QuestCondition condition, int progress, int param1) {
this.trigger(condition.getValue(), progress, param1, 0);
}
public void trigger(AchievementCondition condition, int progress) {
this.trigger(condition.getValue(), progress, 0, 0);
}
public void trigger(AchievementCondition condition, int progress, int param1, int param2) {
this.trigger(condition.getValue(), progress, param1, param2);
}
// Login
@@ -627,7 +695,9 @@ public class Player implements GameDatabaseObject {
this.gachaManager = this.loadManagerFromDatabase(GachaManager.class);
this.storyManager = this.loadManagerFromDatabase(StoryManager.class);
this.questManager = this.loadManagerFromDatabase(QuestManager.class);
this.achievementManager = this.loadManagerFromDatabase(AchievementManager.class);
this.agentManager = this.loadManagerFromDatabase(AgentManager.class);
this.activityManager = this.loadManagerFromDatabase(ActivityManager.class);
// Database fixes
if (this.showChars == null) {
@@ -635,6 +705,9 @@ public class Player implements GameDatabaseObject {
this.save();
}
// Init activities
this.getActivityManager().init();
// Load complete
this.loaded = true;
}
@@ -643,9 +716,6 @@ public class Player implements GameDatabaseObject {
// See if we need to reset dailies
this.checkResetDailies();
// Trigger quest login
this.triggerQuest(QuestCondType.LoginTotal, 1);
// Update last login time
this.lastLogin = System.currentTimeMillis();
Nebula.getGameDatabase().update(this, this.getUid(), "lastLogin", this.getLastLogin());
@@ -661,6 +731,18 @@ public class Player implements GameDatabaseObject {
this.getNextPackages().add(new NetMsgPacket(msgId, proto));
}
// Misc
/**
* Called AFTER a response is sent to the client
*/
public void afterResponse() {
// Check if we need save achievements
if (this.getAchievementManager().isQueueSave()) {
this.getAchievementManager().save();
}
}
// Proto
public PlayerInfo toProto() {
@@ -739,18 +821,20 @@ 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());
state.getMutableFriendEnergy();
state.getMutableMallPackage();
state.getMutableAchievement();
state.getMutableTravelerDuelQuest()
.setType(QuestType.TravelerDuel);
state.getMutableTravelerDuelChallengeQuest()
@@ -831,6 +915,11 @@ public class Player implements GameDatabaseObject {
agentProto.addInfos(agent.toProto());
}
// Activities
for (var activity : getActivityManager().getActivities().values()) {
proto.addActivities(activity.toProto());
}
// Complete
return proto;
}
@@ -877,4 +966,4 @@ public class Player implements GameDatabaseObject {
return proto;
}
}
}

View File

@@ -135,8 +135,6 @@ public class PlayerModule extends GameContextModule {
// Put in player cache
this.addToCache(player);
System.out.println("created player");
return player;
}

View File

@@ -116,7 +116,7 @@ public class PlayerProgress extends PlayerManager implements GameDatabaseObject
}
// Set
this.starTowerGrowth[index] |= (1 << nodeId);
this.starTowerGrowth[index] |= (1 << (nodeId - 1));
// Success
return true;

View File

@@ -55,19 +55,19 @@ public class GameQuest {
return 0;
}
public boolean trigger(QuestCondType condition, int progress, int param) {
public boolean trigger(int condition, int progress, int param1, int param2) {
// Sanity check
if (this.isComplete()) {
return false;
}
// Skip if not the correct condition
if (this.cond != condition.getValue()) {
if (this.cond != condition) {
return false;
}
// Check quest param
if (this.param != 0 && param != this.param) {
if (this.param != 0 && param1 != this.param) {
return false;
}

View File

@@ -0,0 +1,144 @@
package emu.nebula.game.quest;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum QuestCondition {
BattleTotal (3),
BattlesTotalWithPartner (4),
CharacterAcquireQuantityRarityAndAdvancement (6),
CharacterAdvanceTotal (7),
CharacterSkillUpTotal (8),
CharacterSkillWithSpecificUpTotal (9),
CharacterUpTotal (12),
CharacterWithSpecificAdvance (13),
CharacterWithSpecificUpLevel (15),
CharactersWithSpecificNumberLevelAndAttributes (17),
CharactersWithSpecificQuantityAdvancementCountAndAttribute (19),
CharactersWithSpecificQuantityRarityAndLevel (22),
ChatTotal (23),
DailyInstanceClearSpecificDifficultyAndTotal (24),
DailyInstanceClearSpecificTypeAndTotal (25),
DailyInstanceClearTotal (26),
DiscAcquireQuantityLevelAndRarity (30),
DiscAcquireQuantityPhaseAndRarity (31),
DiscAcquireQuantityStarAndRarity (32),
DiscLimitBreakTotal (33),
DiscPromoteTotal (34),
DiscStrengthenTotal (35),
DiscWithSpecificQuantityLevelAndRarity (36),
DiscWithSpecificQuantityPhaseAndRarity (37),
DiscWithSpecificQuantityStarAndRarity (38),
EnergyDeplete (39),
GachaTotal (44),
GiftGiveTotal (45),
InfinityTowerClearSpecificFloor (46),
InfinityTowerClearTotal (47),
ItemsAdd (48),
ItemsDeplete (49),
ItemsProductTotal (50),
LoginTotal (51),
QuestTravelerDuelChallengeTotal (52),
QuestTourGuideSpecific (53),
QuestTravelerDuelSpecific (54),
QuestWithSpecificType (55),
RegionBossClearSpecificFullStarWithBossIdAndDifficulty (56),
RegionBossClearSpecificLevelWithDifficultyAndTotal (57),
RegionBossClearSpecificTotal (58),
RegionBossClearTotal (59),
SkillsWithSpecificQuantityAndLevel (60),
StageClearSpecificStars (62),
StoryClear (63),
TravelerDuelChallengeSpecificBoosLevelWithDifficultyAndTotal (64),
TravelerDuelClearBossTotal (65),
TravelerDuelClearSpecificBossIdAndDifficulty (66),
TravelerDuelChallengeClearSpecificBossLevelAndAffix (67),
TravelerDuelClearSpecificBossLevelWithDifficultyAndTotal (68),
TravelerDuelClearSpecificBossTotal (69),
TravelerDuelChallengeRankUploadTotal (70),
WorldClassSpecific (71),
RegionBossClearSpecificTypeWithTotal (72),
CharactersWithSpecificDatingCount (73),
CharactersDatingTotal (74),
VampireSurvivorPassedSpecificLevel (77),
CharacterParticipateTowerNumber (78),
CharacterAllSkillReachSpecificLevel (79),
TravelerDuelPlayTotal (80),
VampireClearTotal (81),
VampireWithSpecificClearTotal (82),
AgentFinishTotal (83),
AgentWithSpecificFinishTotal (84),
ActivityMiningEnterLayer (86),
ActivityMiningDestroyGrid (87),
BossRushTotalStars (88),
InfinityTowerClearSpecificDifficultyAndTotal (89),
SkillInstanceClearTotal (90),
VampireSurvivorSpecificPassedLevel (91),
WeekBoosClearSpecificDifficultyAndTotal (92),
NpcAffinityWithSpecificLevel (93),
CharacterPassedWithSpecificTowerAndCount (94),
ActivityCookieLevelAccPackage (96),
ActivityCookieLevelScore (97),
ActivityCookieTypeAccPackage (98),
ActivityCookieTypeAccPackCookie (99),
ActivityCookieTypeAccRhythm (100),
ActivityCookieTypeChallenge (101),
CharGemInstanceClearTotal (104),
DailyShopReceiveShopTotal (105),
AgentApplyTotal (106),
ActivityScore (107),
ActivityTypeAvgReadWithSpecificIdAndLevelId (108),
ActivityTypeLevelPassedWithSpecificIdAndLevelId (109),
ActivityTypeLevel3StarPassedWithSpecificIdAndLevelId (110),
ActivityTypeLevelStarWithSpecificIdAndLevelTypeTotal (111),
ActivityTypeLevelPassedWithSpecificIdAndLevelIdAndSpecificPositionAndCharElem (112),
ActivityTypeLevelPassedSpecificIdTotal (113),
ClientReport (200),
TowerBuildSpecificScoreWithTotal (504),
TowerClearSpecificLevelWithDifficultyAndTotal (507),
TowerEnterTotal (510),
TowerSpecificDifficultyShopBuyTimes (514),
TowerGrowthSpecificNote (515),
TowerClearSpecificLevelWithDifficultyAndTotalHistory (516),
TowerBookWithSpecificEvent (517),
TowerBookWithSpecificFateCard (518),
TowerBookWithSpecificPotential (520),
TowerBuildSpecificDifficultyAndScoreWithTotal (521),
TowerSpecificDifficultyStrengthenMachineTotal (522),
TowerSpecificDifficultyKillBossTotal (524),
TowerBookSpecificCharWithPotentialTotal (525),
TowerBuildSpecificCharSpecificScoreWithTotal (526),
TowerGrowthWithSpecificNote (527),
TowerSpecificFateCardReRollTotal (528),
TowerSpecificPotentialReRollTotal (529),
TowerSpecificShopReRollTotal (530),
TowerSpecificNoteActivateTotal (531),
TowerSpecificNoteLevelTotal (532),
TowerSpecificPotentialBonusTotal (533),
TowerSpecificPotentialLuckyTotal (534),
TowerSpecificShopBuyDiscountTotal (535),
TowerSpecificSecondarySkillActivateTotal (536),
TowerSpecificGetExtraNoteLvTotal (537),
TowerEnterFloor (538),
TowerSweepTimes (539),
TowerSweepTotal (540);
@Getter
private final int value;
private final static Int2ObjectMap<QuestCondition> map = new Int2ObjectOpenHashMap<>();
static {
for (QuestCondition type : QuestCondition.values()) {
map.put(type.getValue(), type);
}
}
private QuestCondition(int value) {
this.value = value;
}
public static QuestCondition getByValue(int value) {
return map.get(value);
}
}

View File

@@ -11,19 +11,20 @@ public class QuestHelper {
@Getter
private static final Int2ObjectMap<QuestParams> battlePassQuestParams = new Int2ObjectOpenHashMap<>();
// Put params here
static {
battlePassQuestParams.put(1001, new QuestParams(QuestCondType.LoginTotal, 1));
battlePassQuestParams.put(1002, new QuestParams(QuestCondType.EnergyDeplete, 160));
battlePassQuestParams.put(1003, new QuestParams(QuestCondType.BattleTotal, 6));
battlePassQuestParams.put(1004, new QuestParams(QuestCondType.QuestWithSpecificType, 5, QuestType.Daily));
battlePassQuestParams.put(2001, new QuestParams(QuestCondType.TowerEnterFloor, 1));
battlePassQuestParams.put(2002, new QuestParams(QuestCondType.WeekBoosClearSpecificDifficultyAndTotal, 3));
battlePassQuestParams.put(2003, new QuestParams(QuestCondType.BattleTotal, 20));
battlePassQuestParams.put(2004, new QuestParams(QuestCondType.LoginTotal, 5));
battlePassQuestParams.put(2005, new QuestParams(QuestCondType.AgentFinishTotal, 3));
battlePassQuestParams.put(2006, new QuestParams(QuestCondType.ItemsDeplete, 100000, 1));
battlePassQuestParams.put(2007, new QuestParams(QuestCondType.GiftGiveTotal, 5));
battlePassQuestParams.put(2008, new QuestParams(QuestCondType.EnergyDeplete, 1200));
battlePassQuestParams.put(1001, new QuestParams(QuestCondition.LoginTotal, 1));
battlePassQuestParams.put(1002, new QuestParams(QuestCondition.EnergyDeplete, 160));
battlePassQuestParams.put(1003, new QuestParams(QuestCondition.BattleTotal, 6));
battlePassQuestParams.put(1004, new QuestParams(QuestCondition.QuestWithSpecificType, 5, QuestType.Daily));
battlePassQuestParams.put(2001, new QuestParams(QuestCondition.TowerEnterFloor, 1));
battlePassQuestParams.put(2002, new QuestParams(QuestCondition.WeekBoosClearSpecificDifficultyAndTotal, 3));
battlePassQuestParams.put(2003, new QuestParams(QuestCondition.BattleTotal, 20));
battlePassQuestParams.put(2004, new QuestParams(QuestCondition.LoginTotal, 5));
battlePassQuestParams.put(2005, new QuestParams(QuestCondition.AgentFinishTotal, 3));
battlePassQuestParams.put(2006, new QuestParams(QuestCondition.ItemsDeplete, 100000, 1));
battlePassQuestParams.put(2007, new QuestParams(QuestCondition.GiftGiveTotal, 5));
battlePassQuestParams.put(2008, new QuestParams(QuestCondition.EnergyDeplete, 1200));
}
@Getter
@@ -40,7 +41,7 @@ public class QuestHelper {
this(cond, new int[] {param});
}
public QuestParams(QuestCondType cond, int... params) {
public QuestParams(QuestCondition cond, int... params) {
this(cond.getValue(), params);
}
}

View File

@@ -114,10 +114,10 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
this.save();
}
public synchronized void trigger(QuestCondType condition, int progress, int param) {
public synchronized void trigger(int condition, int progress, int param1, int param2) {
for (var quest : getQuests().values()) {
// Try to trigger quest
boolean result = quest.trigger(condition, progress, param);
boolean result = quest.trigger(condition, progress, param1, param2);
// Skip if quest progress wasn't changed
if (!result) {
@@ -194,7 +194,7 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
}
// Trigger quest
this.getPlayer().triggerQuest(QuestCondType.QuestWithSpecificType, claimList.size(), QuestType.Daily);
this.getPlayer().trigger(QuestCondition.QuestWithSpecificType, claimList.size(), QuestType.Daily);
// Success
return change.setSuccess(true);
@@ -305,7 +305,7 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
Nebula.getGameDatabase().update(this, this.getUid(), "hasDailyReward", this.hasDailyReward);
// Trigger quest
this.getPlayer().triggerQuest(QuestCondType.DailyShopReceiveShopTotal, 1);
this.getPlayer().trigger(QuestCondition.DailyShopReceiveShopTotal, 1);
// Success
return change.setSuccess(true);

View File

@@ -20,7 +20,7 @@ public class ScoreBossManager extends PlayerManager {
}
public int getControlId() {
return 1;
return Nebula.getGameContext().getScoreBossModule().getControlId();
}
public ScoreBossControlDef getControlData() {
@@ -79,7 +79,7 @@ public class ScoreBossManager extends PlayerManager {
}
// Settle
this.ranking.settle(this.getPlayer(), build, this.getLevelId(), stars, score);
this.ranking.settle(this.getPlayer(), build, getControlId(), getLevelId(), stars, score);
// Save ranking
this.ranking.save();

View File

@@ -21,6 +21,11 @@ public class ScoreBossModule extends GameContextModule {
this.ranking = new ArrayList<>();
}
// TODO calculate from bin data
public int getControlId() {
return 2;
}
private long getRefreshTime() {
return Nebula.getConfig().getServerOptions().leaderboardRefreshTime * 1000;
}
@@ -39,7 +44,7 @@ public class ScoreBossModule extends GameContextModule {
this.ranking.clear();
// Get from database
var list = Nebula.getGameDatabase().getSortedObjects(ScoreBossRankEntry.class, "score", 50);
var list = Nebula.getGameDatabase().getSortedObjects(ScoreBossRankEntry.class, "controlId", this.getControlId(), "score", 50);
for (int i = 0; i < list.size(); i++) {
// Get rank entry and set proto

View File

@@ -60,10 +60,16 @@ public class ScoreBossRankEntry implements GameDatabaseObject {
this.honor = player.getHonor();
}
public void settle(Player player, StarTowerBuild build, int level, int stars, int score) {
public void settle(Player player, StarTowerBuild build, int controlId, int level, int stars, int score) {
// Update player data
this.update(player);
// Reset score entry if control id doesn't match
if (this.controlId != controlId) {
this.controlId = controlId;
this.getTeams().clear();
}
// Set team entry
var team = new ScoreBossTeamEntry(player, build, stars, score);
this.getTeams().put(level, team);

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

@@ -3,11 +3,12 @@ package emu.nebula.game.tower;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.StarTowerGrowthNodeDef;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.game.player.PlayerProgress;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import emu.nebula.proto.StarTowerApply.StarTowerApplyReq;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
@@ -55,7 +56,7 @@ public class StarTowerManager extends PlayerManager {
// Get nodes bits
int nodes = growth[groupIndex];
int test = (1 << data.getNodeId());
int test = (1 << (data.getNodeId() - 1));
// Check if bit is set
return (nodes & test) != 0;
@@ -117,7 +118,7 @@ public class StarTowerManager extends PlayerManager {
this.getProgress().setStarTowerGrowthNode(data.getGroup(), data.getNodeId());
// Remove items
getPlayer().getInventory().removeItem(data.getItemId1(), data.getItemQty1());
getPlayer().getInventory().removeItem(data.getItemId1(), data.getItemQty1(), change);
// Add to unlocked list
unlocked.add(data.getId());
@@ -204,27 +205,53 @@ 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().triggerQuest(QuestCondType.TowerEnterFloor, 1);
this.getPlayer().trigger(QuestCondition.TowerEnterFloor, 1);
// Success
return change.setExtraData(this.game);
}
public StarTowerGame endGame() {
public StarTowerGame endGame(boolean victory) {
// Cache instance
var game = this.game;
if (game != null) {
// Set last build
this.lastBuild = game.getBuild();
// Clear instance
this.game = null;
if (game == null) {
return null;
}
// Set last build
this.lastBuild = game.getBuild();
// Handle victory events
if (victory) {
// Trigger achievements
this.getPlayer().trigger(AchievementCondition.TowerClearTotal, 1);
this.getPlayer().trigger(
AchievementCondition.TowerClearSpecificGroupIdAndDifficulty,
1,
game.getData().getGroupId(),
game.getData().getDifficulty()
);
this.getPlayer().trigger(
AchievementCondition.TowerClearSpecificLevelWithDifficultyAndTotal,
1,
game.getData().getId(),
game.getData().getDifficulty()
);
}
// Clear game instance
this.game = null;
// Return game
return game;
}
@@ -300,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);
}
}

Some files were not shown because too many files have changed in this diff Show More