Basic implementation of character emblems

This commit is contained in:
Melledy
2025-11-16 10:16:07 -08:00
parent f2656903e5
commit 6b699d97ee
15 changed files with 739 additions and 19 deletions

View File

@@ -22,6 +22,10 @@ public class GameConstants {
public static final int MAX_ENERGY = 240;
public static final int ENERGY_REGEN_TIME = 360; // Seconds
public static final int CHARACTER_MAX_GEMS_PER_SLOT = 4;
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_SHOWCASE_IDS = 5;

View File

@@ -28,6 +28,12 @@ public class GameData {
@Getter private static DataTable<TalentGroupDef> TalentGroupDataTable = new DataTable<>();
@Getter private static DataTable<TalentDef> TalentDataTable = new DataTable<>();
@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<>();
@Getter private static DataTable<ChatDef> ChatDataTable = new DataTable<>();
// Discs

View File

@@ -0,0 +1,79 @@
package emu.nebula.data.resources;
import java.util.List;
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.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)
public class CharGemAttrGroupDef extends BaseDef {
private int GroupId;
private int GroupType;
private int Weight;
private String UniqueAttrNumWeight;
private transient WeightedList<Integer> uniqueAttrNum;
private transient List<CharGemAttrTypeDef> attributeTypes;
@Override
public int getId() {
return GroupId;
}
public int getRandomUniqueAttrNum() {
if (this.uniqueAttrNum == null) {
return 0;
}
return this.uniqueAttrNum.next();
}
public CharGemAttrTypeDef getRandomAttributeType(IntList list) {
// Setup blacklist to prevent the same attribute from showing up twice
var blacklist = new IntOpenHashSet();
for (int id : list) {
var value = GameData.getCharGemAttrValueDataTable().get(id);
if (value == null) continue;
int blacklistId = value.getTypeId();
blacklist.add(blacklistId);
}
// Create random generator
var random = new WeightedList<CharGemAttrTypeDef>();
for (var type : this.getAttributeTypes()) {
if (blacklist.contains(type.getId())) {
continue;
}
random.add(100, type);
}
return random.next();
}
@Override
public void onLoad() {
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()) {
this.uniqueAttrNum.add(entry.getValue(), entry.getKey());
}
}
}
}

View File

@@ -0,0 +1,39 @@
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

@@ -0,0 +1,31 @@
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;
@Getter
@ResourceType(name = "CharGemAttrValue.json", loadPriority = LoadPriority.LOW)
public class CharGemAttrValueDef extends BaseDef {
private int Id;
private int TypeId;
private int AttrType;
private int AttrTypeFirstSubtype;
private int AttrTypeSecondSubtype;
private int Rarity;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
var data = GameData.getCharGemAttrTypeDataTable().get(this.TypeId);
if (data != null) {
data.getValues().add(this.getRarity(), this);
}
}
}

View File

@@ -0,0 +1,24 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "CharGem.json")
public class CharGemDef extends BaseDef {
private int Id;
private int GenerateCostTid;
private int RefreshCostTid;
private int Type;
@Override
public int getId() {
return Id;
}
public CharGemSlotControlDef getControlData() {
return GameData.getCharGemSlotControlDataTable().get(this.Type);
}
}

View File

@@ -0,0 +1,78 @@
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 it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
@ResourceType(name = "CharGemSlotControl.json")
public class CharGemSlotControlDef extends BaseDef {
private int Id;
private int Position;
private int MaxAlterNum;
private int UnlockLevel;
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;
@Override
public int getId() {
return Id;
}
public IntList generateAttributes() {
// Generate list of attributes
var list = new IntArrayList();
// Add unique attributes
if (this.UniqueAttrGroupId > 0) {
var group = GameData.getCharGemAttrGroupDataTable().get(this.UniqueAttrGroupId);
int num = group.getRandomUniqueAttrNum();
for (int i = 0; i < num; i++) {
var attributeType = group.getRandomAttributeType(list);
list.add(attributeType.getRandomValue());
}
if (list.size() >= 4) {
return list;
}
}
// Get random attributes
var random = new WeightedList<CharGemAttrGroupDef>();
for (var groupId : this.AttrGroupId) {
var group = GameData.getCharGemAttrGroupDataTable().get(groupId);
if (group == null || group.getWeight() == 0) {
continue;
}
random.add(group.getWeight(), group);
}
// Add up to 4 attributes
while (list.size() < 4) {
var group = random.next();
var attributeType = group.getRandomAttributeType(list);
list.add(attributeType.getRandomValue());
}
// Complete
return list;
}
}

View File

@@ -3,6 +3,7 @@ package emu.nebula.data.resources;
import java.util.List;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import lombok.Getter;
@@ -24,6 +25,8 @@ public class CharacterDef extends BaseDef {
private int FragmentsId;
private int TransformQty;
private int[] GemSlots;
private transient List<ChatDef> chats;
@Override
@@ -39,6 +42,11 @@ public class CharacterDef extends BaseDef {
return this.SkillsUpgradeGroup[index];
}
public CharGemDef getCharGemData(int slotId) {
int id = this.GemSlots[slotId - 1];
return GameData.getCharGemDataTable().get(id);
}
@Override
public void onLoad() {
this.chats = new ObjectArrayList<>();

View File

@@ -1,5 +1,8 @@
package emu.nebula.game.character;
import java.util.ArrayList;
import java.util.List;
import org.bson.Document;
import org.bson.types.Binary;
import org.bson.types.ObjectId;
@@ -7,6 +10,7 @@ import org.bson.types.ObjectId;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import dev.morphia.annotations.PostLoad;
import dev.morphia.annotations.PreLoad;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
@@ -49,9 +53,14 @@ public class Character implements GameDatabaseObject {
private int skin;
private int[] skills;
private Bitset talents;
private CharacterContact contact;
private long createTime;
private int gemPresetIndex;
private List<CharacterGemPreset> gemPresets;
private CharacterGemSlot[] gemSlots;
private CharacterContact contact;
@Deprecated // Morphia only!
public Character() {
@@ -66,12 +75,13 @@ public class Character implements GameDatabaseObject {
this.playerUid = player.getUid();
this.charId = data.getId();
this.data = data;
this.createTime = Nebula.getCurrentTime();
this.level = 1;
this.skin = data.getDefaultSkinId();
this.skills = new int[] {1, 1, 1, 1, 1};
this.talents = new Bitset();
this.createTime = Nebula.getCurrentTime();
this.contact = new CharacterContact(this);
}
@@ -352,6 +362,152 @@ public class Character implements GameDatabaseObject {
return true;
}
// Gems
public boolean hasGemPreset(int index) {
return index >= 0 && index < this.getGemPresets().size();
}
public CharacterGemPreset getCurrentGemPreset() {
return this.getGemPreset(this.getGemPresetIndex());
}
public CharacterGemPreset getGemPreset(int presetIndex) {
while (this.getGemPresetIndex() >= this.getGemPresets().size()) {
this.getGemPresets().add(new CharacterGemPreset(this));
}
return this.getGemPresets().get(presetIndex);
}
public boolean setCurrentGemPreset(int index) {
// Sanity check
if (index < 0 || index >= GameConstants.CHARACTER_MAX_GEM_PRESETS) {
return false;
}
// Set current preset and save to database
this.gemPresetIndex = index;
this.save();
// Success
return true;
}
public CharacterGem getGem(CharacterGemPreset preset, int slotId) {
// Get gem index
int gemIndex = preset.getGemIndex(slotId - 1);
if (gemIndex <= 0) {
return null;
}
// Get gem slot
var slot = this.getGemSlot(slotId);
if (slot == null) {
return null;
}
// Get gem from the slot using preset index
return slot.getGem(gemIndex);
}
public boolean equipGem(int presetIndex, int slotId, int gemIndex) {
// Sanity check
if (presetIndex < 0 || presetIndex >= GameConstants.CHARACTER_MAX_GEM_PRESETS) {
return false;
}
// Get preset
var preset = this.getGemPreset(presetIndex);
// Set gem index in preset
boolean success = preset.setGemIndex(slotId, gemIndex);
// Save if successful
if (success) {
this.save();
}
return success;
}
public boolean hasGemSlot(int slotId) {
// Calculate index from slot id
int index = slotId - 1;
// Sanity check
if (index < 0 || index >= this.getGemSlots().length) {
return false;
}
return this.gemSlots[index] != null;
}
public CharacterGemSlot getGemSlot(int slotId) {
// Calculate index from slot id
int index = slotId - 1;
// Sanity check
if (index < 0 || index >= this.getGemSlots().length) {
return null;
}
// Create gem slot object if it doesnt exist
if (this.gemSlots[index] == null) {
this.gemSlots[index] = new CharacterGemSlot(slotId);
}
return this.gemSlots[index];
}
public synchronized PlayerChangeInfo generateGem(int slotId) {
// Get gem slot
var slot = this.getGemSlot(slotId);
if (slot == null) {
return null;
}
// Skip if slot is full
if (slot.isFull()) {
return null;
}
// Get gem data
var gemData = this.getData().getCharGemData(slotId);
var gemControl = gemData.getControlData();
// Check character level
if (this.getLevel() < gemControl.getUnlockLevel()) {
return null;
}
// Make sure the player has the materials to craft the emblem
if (!getPlayer().getInventory().hasItem(gemData.getGenerateCostTid(), gemControl.getGeneratenCostQty())) {
return null;
}
// Generate attributes and create gem
var attributes = gemControl.generateAttributes();
var gem = new CharacterGem(attributes);
// Add gem to slot
slot.getGems().add(gem);
// Save to database
this.save();
// Consume materials
var change = getPlayer().getInventory().removeItem(gemData.getGenerateCostTid(), gemControl.getGeneratenCostQty());
// Set change info extra info
change.setExtraData(gem);
// Success
return change;
}
// Proto
public Char toProto() {
@@ -363,24 +519,36 @@ public class Character implements GameDatabaseObject {
.setTalentNodes(this.getTalents().toByteArray())
.addAllSkillLvs(this.getSkills())
.setCreateTime(this.getCreateTime());
// Encode gem presets
var gemPresets = proto.getMutableCharGemPresets()
.getMutableCharGemPresets();
.setInUsePresetIndex(this.getGemPresetIndex())
.getMutableCharGemPresets();
for (int i = 0; i < 3; i++) {
var preset = CharGemPreset.newInstance()
.addAllSlotGem(-1, -1, -1);
for (int i = 0; i < GameConstants.CHARACTER_MAX_GEM_PRESETS; i++) {
CharGemPreset info = null;
if (this.hasGemPreset(i)) {
info = getGemPresets().get(i).toProto();
} else {
info = CharGemPreset.newInstance()
.addAllSlotGem(-1, -1, -1);
}
gemPresets.add(preset);
gemPresets.add(info);
}
for (int i = 1; i <= 3; i++) {
var slot = CharGemSlot.newInstance()
.setId(i);
proto.addCharGemSlots(slot);
// Encode gems
for (int i = 1; i <= GameConstants.CHARACTER_MAX_GEM_SLOTS; i++) {
if (this.hasGemSlot(i)) {
var slot = this.getGemSlot(i);
proto.addCharGemSlots(slot.toProto());
} else {
proto.addCharGemSlots(CharGemSlot.newInstance().setId(i));
}
}
// Affinity quests
proto.getMutableAffinityQuests();
return proto;
@@ -394,12 +562,21 @@ public class Character implements GameDatabaseObject {
.setTalentNodes(this.getTalents().toByteArray())
.addAllSkillLvs(this.getSkills());
for (int i = 1; i <= 3; i++) {
var slot = StarTowerCharGem.newInstance()
.setSlotId(i)
.addAllAttributes(new int[] {0, 0, 0, 0});
// Encode gems
var preset = this.getCurrentGemPreset();
for (int i = 1; i <= preset.getLength(); i++) {
var gem = this.getGem(preset, i);
var info = StarTowerCharGem.newInstance()
.setSlotId(i);
proto.addGems(slot);
if (gem != null) {
info.addAllAttributes(gem.getAttributes());
} else {
info.addAllAttributes(new int[] {0, 0, 0, 0});
}
proto.addGems(info);
}
return proto;
@@ -408,11 +585,24 @@ public class Character implements GameDatabaseObject {
// Database fix
@PreLoad
public void onLoad(Document doc) {
public void preLoad(Document doc) {
var talents = doc.get("talents");
if (talents != null && talents.getClass() == Binary.class) {
doc.remove("talents");
this.talents = new Bitset();
}
}
@PostLoad
public void postLoad() {
if (this.gemSlots == null) {
// Create gem slots array if it didn't exist
this.gemSlots = new CharacterGemSlot[GameConstants.CHARACTER_MAX_GEM_SLOTS];
}
if (this.gemPresets == null) {
// Create gem presets list if it didn't exist
this.gemPresets = new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,39 @@
package emu.nebula.game.character;
import dev.morphia.annotations.Entity;
import emu.nebula.proto.Public.CharGem;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
@Entity(useDiscriminator = false)
public class CharacterGem {
private boolean locked;
private int[] attributes;
private int[] alterAttributes;
@Deprecated // Morphia only
public CharacterGem() {
}
public CharacterGem(IntList attributes) {
this.attributes = attributes.toIntArray();
this.alterAttributes = new int[4];
}
public void setLocked(boolean locked) {
this.locked = locked;
}
// Proto
public CharGem toProto() {
var proto = CharGem.newInstance()
.setLock(this.isLocked())
.addAllAttributes(this.getAttributes())
.addAllAlterAttributes(this.getAlterAttributes());
return proto;
}
}

View File

@@ -0,0 +1,58 @@
package emu.nebula.game.character;
import dev.morphia.annotations.Entity;
import emu.nebula.proto.Public.CharGemPreset;
import lombok.Getter;
@Getter
@Entity(useDiscriminator = false)
public class CharacterGemPreset {
private String name;
private int[] gems;
@Deprecated // Morphia only
public CharacterGemPreset() {
}
public CharacterGemPreset(Character character) {
this.gems = new int[] {-1, -1, -1};
}
public int getLength() {
return this.getGems().length;
}
public int getGemIndex(int slotIndex) {
if (slotIndex < 0 || slotIndex >= this.getLength()) {
return -1;
}
return this.getGems()[slotIndex];
}
public boolean setGemIndex(int slotId, int gemIndex) {
int slotIndex = slotId - 1;
if (slotIndex < 0 || slotIndex >= this.getLength()) {
return false;
}
this.getGems()[slotIndex] = gemIndex;
return true;
}
// Proto
public CharGemPreset toProto() {
var proto = CharGemPreset.newInstance()
.addAllSlotGem(this.getGems());
if (this.getName() != null) {
proto.setName(this.getName());
}
return proto;
}
}

View File

@@ -0,0 +1,51 @@
package emu.nebula.game.character;
import java.util.ArrayList;
import java.util.List;
import dev.morphia.annotations.Entity;
import emu.nebula.GameConstants;
import emu.nebula.proto.Public.CharGemSlot;
import lombok.Getter;
@Getter
@Entity(useDiscriminator = false)
public class CharacterGemSlot {
private int id;
private List<CharacterGem> gems;
@Deprecated // Morphia only
public CharacterGemSlot() {
}
public CharacterGemSlot(int id) {
this.id = id;
this.gems = new ArrayList<>();
}
public CharacterGem getGem(int gemId) {
if (gemId < 0 || gemId >= this.getGems().size()) {
return null;
}
return this.getGems().get(gemId);
}
public boolean isFull() {
return getGems().size() >= GameConstants.CHARACTER_MAX_GEMS_PER_SLOT;
}
// Proto
public CharGemSlot toProto() {
var proto = CharGemSlot.newInstance()
.setId(this.getId());
for (var gem : this.getGems()) {
proto.addAlterGems(gem.toProto());
}
return proto;
}
}

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.CharGemEquipGem.CharGemEquipGemReq;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.char_gem_equip_gem_req)
public class HandlerCharGemEquipGemReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = CharGemEquipGemReq.parseFrom(message);
// Get character
var character = session.getPlayer().getCharacters().getCharacterById(req.getCharId());
if (character == null) {
return session.encodeMsg(NetMsgId.char_gem_equip_gem_failed_ack);
}
// Equip gem
boolean success = character.equipGem(req.getPresetId(), req.getSlotId(), req.getGemIndex());
if (success == false) {
return session.encodeMsg(NetMsgId.char_gem_equip_gem_failed_ack);
}
// Encode and send
return session.encodeMsg(NetMsgId.char_gem_equip_gem_succeed_ack);
}
}

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.CharGemGenerate.CharGemGenerateReq;
import emu.nebula.proto.CharGemGenerate.CharGemGenerateResp;
import emu.nebula.net.HandlerId;
import emu.nebula.game.character.CharacterGem;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.char_gem_generate_req)
public class HandlerCharGemGenerateReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = CharGemGenerateReq.parseFrom(message);
// Get character
var character = session.getPlayer().getCharacters().getCharacterById(req.getCharId());
if (character == null) {
return session.encodeMsg(NetMsgId.char_gem_generate_failed_ack);
}
// Generate gem
var change = character.generateGem(req.getSlotId());
if (change == null) {
return session.encodeMsg(NetMsgId.char_gem_generate_failed_ack);
}
var gem = (CharacterGem) change.getExtraData();
// Build response
var rsp = CharGemGenerateResp.newInstance()
.setChangeInfo(change.toProto())
.setCharGem(gem.toProto());
// Encode and send
return session.encodeMsg(NetMsgId.char_gem_generate_succeed_ack, rsp);
}
}

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.CharGemUsePreset.CharGemUsePresetReq;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.char_gem_use_preset_req)
public class HandlerCharGemUsePresetReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = CharGemUsePresetReq.parseFrom(message);
// Get character
var character = session.getPlayer().getCharacters().getCharacterById(req.getCharId());
if (character == null) {
return session.encodeMsg(NetMsgId.char_gem_use_preset_failed_ack);
}
// Use preset
boolean success = character.setCurrentGemPreset(req.getPresetId());
if (success == false) {
return session.encodeMsg(NetMsgId.char_gem_use_preset_failed_ack);
}
// Encode and send
return session.encodeMsg(NetMsgId.char_gem_use_preset_succeed_ack);
}
}