5 Commits

Author SHA1 Message Date
Melledy
dfb93cae4b Implement some tower achievements 2025-12-06 02:01:57 -08:00
Fishia
5707c1c919 feat(tower_defense): implement first clear rewards 2025-12-06 01:21:28 -08:00
Fishia
51f6db9803 fix(tower_defense): level id is key, not value 2025-12-06 01:21:28 -08:00
Fishia
3df873e385 feat: tower defense activity
Bare minimum work done.
2025-12-06 01:21:28 -08:00
HongchengQ
f44262f427 Fix abnormal player online status on duplicate login
- Prevent incorrect player deletion on duplicate login
2025-12-06 00:54:58 -08:00
16 changed files with 364 additions and 38 deletions

View File

@@ -139,6 +139,9 @@ public class GameData {
// Activity
@Getter private static DataTable<ActivityDef> ActivityDataTable = new DataTable<>();
// Tower defense
@Getter private static DataTable<TowerDefenseLevelDef> TowerDefenseLevelDataTable = new DataTable<>();
@Getter private static DataTable<TrialControlDef> TrialControlDataTable = new DataTable<>();
@Getter private static DataTable<TrialGroupDef> TrialGroupDataTable = new DataTable<>();

View File

@@ -0,0 +1,33 @@
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 = "TowerDefenseLevel.json")
public class TowerDefenseLevelDef extends BaseDef {
private int Id;
private int Condition2;
private int Condition3;
private int Item1;
private int Qty1;
private int Item2;
private int Qty2;
private transient ItemParamMap rewards;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
// Parse rewards
this.rewards = new ItemParamMap();
this.rewards.add(this.Item1, this.Qty1);
this.rewards.add(this.Item2, this.Qty2);
}
}

View File

@@ -5,6 +5,7 @@ import java.util.List;
import emu.nebula.GameConstants;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.AchievementDef;
import emu.nebula.game.tower.room.RoomType;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
@@ -34,7 +35,7 @@ public class AchievementHelper {
public static void init() {
// Cache total achievements
for (var condition : AchievementCondition.values()) {
if (condition.name().endsWith("Total")) {
if (condition.name().endsWith("Total") || condition.name().endsWith("Times")) {
incrementalAchievementSet.add(condition.getValue());
}
}
@@ -44,12 +45,15 @@ public class AchievementHelper {
incrementalAchievementSet.add(AchievementCondition.ItemsAdd.getValue());
incrementalAchievementSet.add(AchievementCondition.ItemsDeplete.getValue());
incrementalAchievementSet.add(AchievementCondition.TowerItemsGet.getValue());
incrementalAchievementSet.add(AchievementCondition.TowerEnterRoom.getValue());
// Fix params
fixParams();
}
private static void fixParams() {
// Monolith
// Star Tower TODO
addParam(78, 0, 2);
addParam(79, 0, 4);
addParam(498, 0, 1);
@@ -73,20 +77,44 @@ public class AchievementHelper {
}
// 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);
addParams(393, 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);
addParams(382, 387, 1, 0);
// Star Tower team clear
addParams(95, 98, 1, 0); // Aqua team clear
addParams(99, 102, 2, 0); // Fire team clear
addParams(103, 106, 3, 0); // Earth team clear
addParams(107, 110, 4, 0); // Wind team clear
addParams(111, 114, 5, 0); // Light team clear
addParams(115, 118, 6, 0); // Dark team clear
// Star tower items
addParams(139, 144, GameConstants.TOWER_COIN_ITEM_ID, 0);
addParams(145, 149, 90011, 0);
addParams(150, 154, 90012, 0);
addParams(155, 159, 90013, 0);
addParams(160, 164, 90014, 0);
addParams(165, 169, 90015, 0);
addParams(170, 174, 90016, 0);
addParams(175, 179, 90017, 0);
addParams(180, 184, 90018, 0);
addParams(185, 189, 90019, 0);
addParams(190, 194, 90020, 0);
addParams(195, 199, 90021, 0);
addParams(200, 204, 90022, 0);
addParams(205, 209, 90023, 0);
// Star tower rooms
addParams(210, 216, RoomType.BattleRoom.getValue() + 1, 0);
addParams(217, 223, RoomType.EliteBattleRoom.getValue() + 1, 0);
addParams(224, 230, RoomType.BossRoom.getValue() + 1, 0);
addParams(231, 237, RoomType.FinalBossRoom.getValue() + 1, 0);
addParams(238, 244, RoomType.ShopRoom.getValue() + 1, 0);
addParams(245, 251, RoomType.EventRoom.getValue() + 1, 0);
}
private static void addParam(int achievementId, int param1, int param2) {
@@ -95,4 +123,10 @@ public class AchievementHelper {
data.setParams(param1, param2);
}
private static void addParams(int start, int end, int param1, int param2) {
for (int id = start; id <= end; id++) {
addParam(id, param1, param2);
}
}
}

View File

@@ -138,6 +138,14 @@ public class AchievementManager extends PlayerManager implements GameDatabaseObj
}
}
public synchronized void trigger(AchievementCondition condition, int progress) {
this.trigger(condition.getValue(), progress, 0, 0);
}
public synchronized void trigger(AchievementCondition condition, int progress, int param1, int param2) {
this.trigger(condition.getValue(), progress, param1, param2);
}
public synchronized void trigger(int condition, int progress, int param1, int param2) {
// Sanity check
if (progress <= 0) {

View File

@@ -111,6 +111,7 @@ public class ActivityManager extends PlayerManager implements GameDatabaseObject
GameActivity activity = switch (data.getType()) {
case Trial -> new TrialActivity(this, data);
case TowerDefense -> new TowerDefenseActivity(this, data);
default -> null;
};

View File

@@ -19,6 +19,8 @@ public class ActivityModule extends GameContextModule {
this.activities.add(700103);
this.activities.add(700104);
this.activities.add(700107);
this.activities.add(102001); // Tower defense activity
//this.activities.add(101002);
//this.activities.add(101003);

View File

@@ -0,0 +1,108 @@
package emu.nebula.game.activity.type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
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.inventory.ItemParamMap;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.proto.ActivityDetail.ActivityMsg;
import emu.nebula.proto.Public.ActivityQuest;
import emu.nebula.proto.Public.ActivityTowerDefenseLevel;
import lombok.Getter;
@Getter
@Entity
public class TowerDefenseActivity extends GameActivity {
private Map<Integer, Integer> completedStages;
private Map<Integer, Integer> completedQuests;
@Deprecated // Morphia only
public TowerDefenseActivity() {
}
public TowerDefenseActivity(ActivityManager manager, ActivityDef data) {
super(manager, data);
this.completedStages = new HashMap<Integer, Integer>();
this.completedQuests = new HashMap<Integer, Integer>();
}
public PlayerChangeInfo claimReward(int level) {
// Initialize change info
var change = new PlayerChangeInfo();
// Get rewards
var rewards = GameData.getTowerDefenseLevelDataTable().get(level).getRewards();
// Add rewards
return getPlayer().getInventory().addItems(rewards, change);
}
// 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.getMutableTowerDefense();
// Add completed stages
for (int id : this.completedStages.keySet()) {
// Create proto
var level = ActivityTowerDefenseLevel.newInstance();
// Set proto params
level.setId(id);
level.setStar(this.completedStages.get(id));
// Add to final msg proto
proto.addLevels(level);
}
// Add completed quests
for (int id : this.completedStages.keySet()) {
// Create proto
var quest = ActivityQuest.newInstance();
// Set proto params
quest.setActivityId(this.getId());
quest.setId(id);
quest.setStatus(2); // TODO: properly handle event quests
// Add to final msg proto
proto.addQuests(quest);
}
}
}

View File

@@ -1,6 +1,7 @@
package emu.nebula.game.player;
import java.util.Stack;
import java.util.concurrent.TimeUnit;
import dev.morphia.annotations.AlsoLoad;
import dev.morphia.annotations.Entity;
@@ -185,16 +186,23 @@ public class Player implements GameDatabaseObject {
}
public void setSession(GameSession session) {
if (this.session != null) {
// Sanity check
if (this.session == session) {
return;
}
// Clear player from session
this.session.clearPlayer();
int time = Nebula.getConfig().getServerOptions().sessionTimeout;
long timeout = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(time);
if (this.session == null) {
// Set session
this.session = session;
return;
}
// 1. Sanity check
// 2. Prevent incorrect deletion of players when re-logging into the game
if (this.session == session || this.lastLogin > timeout) {
return;
}
// Clear player from session
this.session.clearPlayer();
// Set session
this.session = session;
}

View File

@@ -10,6 +10,9 @@ import emu.nebula.data.GameData;
import emu.nebula.data.resources.SecondarySkillDef;
import emu.nebula.data.resources.StarTowerDef;
import emu.nebula.data.resources.StarTowerStageDef;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.achievement.AchievementManager;
import emu.nebula.game.character.ElementType;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.character.GameDisc;
import emu.nebula.game.formation.Formation;
@@ -187,6 +190,10 @@ public class StarTowerGame {
return this.manager.getPlayer();
}
public AchievementManager getAchievementManager() {
return this.getPlayer().getAchievementManager();
}
public StarTowerBuild getBuild() {
if (this.build == null) {
this.build = new StarTowerBuild(this);
@@ -199,6 +206,31 @@ public class StarTowerGame {
return this.getData().getDifficulty();
}
/**
* Gets the team element, if the team has 2+ or more elements, then returns null
*/
public ElementType getTeamElement() {
ElementType type = null;
for (int id : this.getCharIds()) {
var character = this.getPlayer().getCharacters().getCharacterById(id);
if (character == null) {
return null;
}
if (type == null) {
type = character.getData().getElementType();
continue;
}
if (type != character.getData().getElementType()) {
return null;
}
}
return type;
}
public GameCharacter getCharByIndex(int index) {
if (index < 0 || index >= this.getCharIds().length) {
return null;
@@ -277,6 +309,9 @@ public class StarTowerGame {
this.room = new StarTowerBaseRoom(this, stage);
}
// Trigger achievement
this.getAchievementManager().trigger(AchievementCondition.TowerEnterRoom, 1, stage.getRoomType() + 1, 0);
// Create cases for the room
this.room.onEnter();
}
@@ -409,6 +444,11 @@ public class StarTowerGame {
// Add to new infos
this.getNewInfos().add(id, count);
// Achievment
if (count > 0) {
this.getAchievementManager().trigger(AchievementCondition.TowerItemsGet, count, id, 0);
}
}
case Res -> {
// Sanity check to make sure we dont remove more than what we have
@@ -425,6 +465,11 @@ public class StarTowerGame {
.setQty(count);
change.add(info);
// Achievment
if (count > 0) {
this.getAchievementManager().trigger(AchievementCondition.TowerItemsGet, count, id, 0);
}
}
default -> {
// Ignored
@@ -775,7 +820,14 @@ public class StarTowerGame {
// Log victory
if (isWin) {
// Add star tower history
this.getManager().getPlayer().getProgress().addStarTowerLog(this.getId());
// Trigger achievement
var elementType = this.getTeamElement();
if (elementType != null) {
this.getAchievementManager().trigger(AchievementCondition.TowerClearSpecificCharacterTypeWithTotal, 1, elementType.getValue(), 0);
}
}
// Complete

View File

@@ -5,6 +5,7 @@ import java.util.Map;
import java.util.stream.Collectors;
import emu.nebula.GameConstants;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.tower.StarTowerShopGoods;
import emu.nebula.proto.PublicStarTower.HawkerCaseData;
@@ -169,6 +170,9 @@ public class StarTowerHawkerCase extends StarTowerBaseCase {
// Set change info
rsp.setChange(change.toProto());
// Achievement
this.getGame().getAchievementManager().trigger(AchievementCondition.TowerSpecificShopReRollTotal, 1);
}
private void buy(int sid, StarTowerInteractResp rsp) {
@@ -205,6 +209,9 @@ public class StarTowerHawkerCase extends StarTowerBaseCase {
// Remove coins
this.getGame().addItem(GameConstants.TOWER_COIN_ITEM_ID, -price, change);
// Achievement
this.getGame().getAchievementManager().trigger(AchievementCondition.TowerSpecificDifficultyShopBuyTimes, 1);
// Set change info
rsp.setChange(change.toProto());
}

View File

@@ -4,6 +4,7 @@ import java.util.Collections;
import emu.nebula.GameConstants;
import emu.nebula.data.resources.StarTowerEventDef;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.proto.PublicStarTower.NPCAffinityInfo;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
@@ -254,6 +255,11 @@ public class StarTowerNpcEventCase extends StarTowerBaseCase {
success.setOptionsResult(completed);
this.completed = completed;
// Achievment
if (completed) {
this.getGame().getAchievementManager().trigger(AchievementCondition.TowerEventTimes, 1);
}
// Complete
return rsp;
}

View File

@@ -3,6 +3,7 @@ package emu.nebula.game.tower.cases;
import java.util.List;
import emu.nebula.GameConstants;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.tower.StarTowerGame;
import emu.nebula.game.tower.StarTowerPotentialInfo;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
@@ -60,20 +61,23 @@ public class StarTowerPotentialCase extends StarTowerBaseCase {
@Override
public StarTowerInteractResp interact(StarTowerInteractReq req, StarTowerInteractResp rsp) {
// Check
// Get select req
var select = req.getMutableSelectReq();
// Handle select option
if (select.hasReRoll()) {
return this.reroll(rsp);
this.reroll(rsp);
} else {
return this.select(select.getIndex(), rsp);
this.select(select.getIndex(), rsp);
}
return rsp;
}
private StarTowerInteractResp reroll(StarTowerInteractResp rsp) {
private void reroll(StarTowerInteractResp rsp) {
// Check if we can reroll
if (!this.canReroll()) {
return rsp;
return;
}
// Check price
@@ -81,7 +85,7 @@ public class StarTowerPotentialCase extends StarTowerBaseCase {
int price = this.getRerollPrice();
if (coin < price) {
return rsp;
return;
}
// Subtract rerolls
@@ -97,7 +101,7 @@ public class StarTowerPotentialCase extends StarTowerBaseCase {
}
if (rerollCase == null) {
return rsp;
return;
}
// Clear reroll count
@@ -114,15 +118,15 @@ public class StarTowerPotentialCase extends StarTowerBaseCase {
rsp.setChange(change.toProto());
// Complete
return rsp;
// Achievement
this.getGame().getAchievementManager().trigger(AchievementCondition.TowerSpecificPotentialReRollTotal, 1);
}
private StarTowerInteractResp select(int index, StarTowerInteractResp rsp) {
private void select(int index, StarTowerInteractResp rsp) {
// Get selected potential
var potential = this.selectId(index);
if (potential == null) {
return rsp;
return;
}
// Add potential
@@ -137,9 +141,6 @@ public class StarTowerPotentialCase extends StarTowerBaseCase {
for (var towerCase : nextCases) {
this.getRoom().addCase(rsp.getMutableCases(), towerCase);
}
// Complete
return rsp;
}
// Proto

View File

@@ -1,6 +1,7 @@
package emu.nebula.game.tower.cases;
import emu.nebula.GameConstants;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractResp;
@@ -69,6 +70,9 @@ public class StarTowerStrengthenMachineCase extends StarTowerBaseCase {
// Increment price
this.increasePrice();
// Achievement
this.getGame().getAchievementManager().trigger(AchievementCondition.TowerSpecificDifficultyStrengthenMachineTotal, 1);
}
// Set success result

View File

@@ -170,7 +170,14 @@ public class HttpServer {
// https://nova-static.stellasora.global/
getApp().get("/meta/serverlist.html", new MetaServerlistHandler(this));
getApp().get("/meta/*.html", new MetaPatchListHandler(this));
/*
fishiatee: Maybe this should be handled better.
For example, if raw meta is detected in say ./web/meta, serve that instead.
Otherwise, detect and serve from custom patchlist definition.
*/
//getApp().get("/meta/*.html", new MetaPatchListHandler(this));
}
private void addGameServerRoutes() {

View File

@@ -0,0 +1,17 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.activity_tower_defense_level_apply_req)
public class HandlerActivityTowerDefenseLevelApplyReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Encode and send
return session.encodeMsg(NetMsgId.activity_tower_defense_level_apply_succeed_ack);
}
}

View File

@@ -0,0 +1,35 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.ActivityTowerDefenseLevelSettle.ActivityTowerDefenseLevelSettleReq;
import emu.nebula.net.HandlerId;
import emu.nebula.Nebula;
import emu.nebula.game.activity.type.TowerDefenseActivity;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.activity_tower_defense_level_settle_req)
public class HandlerActivityTowerDefenseLevelSettleReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request proto
var req = ActivityTowerDefenseLevelSettleReq.parseFrom(message);
// Get activity
var activity = session.getPlayer().getActivityManager().getActivity(TowerDefenseActivity.class, 102001);
// Claim rewards
var change = activity.claimReward((int)req.getLevelId());
// Update completed stages
activity.getCompletedStages().put(req.getLevelId(), req.getStar());
// Save changes
session.getPlayer().save();
// Encode and send
return session.encodeMsg(NetMsgId.activity_tower_defense_level_settle_succeed_ack, change.toProto());
}
}