Implement heartlink invite

This commit is contained in:
Melledy
2025-11-25 22:33:25 -08:00
parent e529e8e965
commit fd8e8925ca
17 changed files with 484 additions and 12 deletions

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
@@ -17,7 +17,7 @@ For any extra support, questions, or discussions, check out our [Discord](https:
- Friend system (sending energy not implemented)
- Shop (using only in-game currency)
- Commissions
- Heartlink (missing advanced affinity related features)
- Heartlink
- Monoliths (completeable but many other features missing)
- Bounty Trials
- Menance Arena

View File

@@ -28,18 +28,24 @@ public class GameData {
@Getter private static DataTable<TalentGroupDef> TalentGroupDataTable = new DataTable<>();
@Getter private static DataTable<TalentDef> TalentDataTable = new DataTable<>();
@Getter private static DataTable<AffinityLevelDef> AffinityLevelDataTable = new DataTable<>();
@Getter private static DataTable<AffinityGiftDef> AffinityGiftDataTable = new DataTable<>();
@Getter private static DataTable<PlotDef> PlotDataTable = 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<>();

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

@@ -25,6 +25,7 @@ import emu.nebula.game.quest.QuestCondType;
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;
@@ -861,6 +862,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

@@ -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.QuestCondType;
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
this.getPlayer().triggerQuest(QuestCondType.CharactersDatingTotal, 1);
// Success
return this.game;
}
public void endDatingGame() {
this.game = null;
}
}

View File

@@ -14,6 +14,7 @@ import emu.nebula.game.account.Account;
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;
@@ -83,6 +84,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;
@@ -110,6 +112,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);

View File

@@ -34,12 +34,8 @@ public class HandlerCharAffinityGiftSendReq extends NetHandler {
// Build response
var rsp = CharAffinityGiftSendResp.newInstance()
.setChange(change.toProto());
rsp.getMutableInfo()
.setCharId(character.getCharId())
.setAffinityLevel(character.getAffinityLevel())
.setAffinityExp(character.getAffinityExp());
.setChange(change.toProto())
.setInfo(character.getAffinityProto());
// Encode and send
return session.encodeMsg(NetMsgId.char_affinity_gift_send_succeed_ack, rsp);

View File

@@ -0,0 +1,43 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.CharDatingBranchASelect.CharDatingBranchASelectReq;
import emu.nebula.proto.CharDatingBranchASelect.CharDatingBranchASelectResp;
import emu.nebula.util.Utils;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.char_dating_branch_a_select_req)
public class HandlerCharDatingBranchASelectReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = CharDatingBranchASelectReq.parseFrom(message);
// Get dating game
var game = session.getPlayer().getDatingManager().getGame();
if (game == null) {
return session.encodeMsg(NetMsgId.char_dating_branch_a_select_failed_ack);
}
// Select branch A
game.selectDatingBranchA(req.getOptionId());
// Build response
var rsp = CharDatingBranchASelectResp.newInstance()
.addAllBranchBOptionIds(game.getBranchOptionsB());
// Add random events
for (var events : game.getLandmark().getLandmarkEvents().values()) {
var event = Utils.randomElement(events);
rsp.addLandmarkEventIds(event.getId());
}
// Encode and send
return session.encodeMsg(NetMsgId.char_dating_branch_a_select_succeed_ack, rsp);
}
}

View File

@@ -0,0 +1,37 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.CharDatingBranchBSelect.CharDatingBranchBSelectReq;
import emu.nebula.proto.CharDatingBranchBSelect.CharDatingBranchBSelectResp;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.char_dating_branch_b_select_req)
public class HandlerCharDatingBranchBSelectReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = CharDatingBranchBSelectReq.parseFrom(message);
// Get dating game
var game = session.getPlayer().getDatingManager().getGame();
if (game == null) {
return session.encodeMsg(NetMsgId.char_dating_branch_b_select_failed_ack);
}
// Select branch B
game.selectDatingBranchB(req.getOptionId());
// Build response
var rsp = CharDatingBranchBSelectResp.newInstance()
.setAfterBranchId(game.getLandmark().getRandomAfterBranchId())
.setCharacterEventId(game.getLandmark().getRandomCharacterEventId());
// Encode and send
return session.encodeMsg(NetMsgId.char_dating_branch_b_select_succeed_ack, rsp);
}
}

View File

@@ -0,0 +1,52 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.CharDatingGiftSend.CharDatingGiftSendReq;
import emu.nebula.proto.CharDatingGiftSend.CharDatingGiftSendResp;
import emu.nebula.net.HandlerId;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.char_dating_gift_send_req)
public class HandlerCharDatingGiftSendReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse Request
var req = CharDatingGiftSendReq.parseFrom(message);
// Get dating game
var game = session.getPlayer().getDatingManager().getGame();
if (game == null) {
return session.encodeMsg(NetMsgId.char_dating_gift_send_failed_ack);
}
// Get character
var character = game.getCharacter();
if (character == null || character.getCharId() != req.getCharId()) {
return session.encodeMsg(NetMsgId.char_dating_gift_send_failed_ack);
}
// Parse item templates
var items = ItemParamMap.fromTemplates(req.getItems());
// Send gifts
var change = character.sendGift(items);
if (change == null) {
return session.encodeMsg(NetMsgId.char_affinity_gift_send_failed_ack);
}
// Build response
var rsp = CharDatingGiftSendResp.newInstance()
.setChange(change.toProto())
.setInfo(character.getAffinityProto());
// Encode and send
return session.encodeMsg(NetMsgId.char_dating_gift_send_succeed_ack, rsp);
}
}

View File

@@ -0,0 +1,43 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.CharDatingLandmarkSelect.CharDatingLandmarkSelectReq;
import emu.nebula.proto.CharDatingLandmarkSelect.CharDatingLandmarkSelectResp;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.char_dating_landmark_select_req)
public class HandlerCharDatingLandmarkSelectReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse Request
var req = CharDatingLandmarkSelectReq.parseFrom(message);
// Get character
var character = session.getPlayer().getCharacters().getCharacterById(req.getCharId());
if (character == null) {
return session.encodeMsg(NetMsgId.char_dating_landmark_select_failed_ack);
}
// Set landmark
var game = session.getPlayer().getDatingManager().selectLandmark(character, req.getLandmarkId());
if (game == null) {
return session.encodeMsg(NetMsgId.char_dating_landmark_select_failed_ack);
}
// Build response
var rsp = CharDatingLandmarkSelectResp.newInstance()
.setInfo(character.getAffinityProto())
.addAllBranchAOptionIds(game.getBranchOptionsB());
rsp.getMutableChange();
// Encode and send
return session.encodeMsg(NetMsgId.char_dating_landmark_select_succeed_ack, rsp);
}
}