package emu.nebula.game.character; import org.bson.Document; import org.bson.types.Binary; import org.bson.types.ObjectId; import dev.morphia.annotations.Entity; import dev.morphia.annotations.Id; import dev.morphia.annotations.Indexed; import dev.morphia.annotations.PreLoad; import emu.nebula.GameConstants; import emu.nebula.Nebula; 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.inventory.ItemParamMap; import emu.nebula.game.player.Player; import emu.nebula.game.player.PlayerChangeInfo; 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.Char; import emu.nebula.proto.Public.CharGemPreset; import emu.nebula.proto.Public.CharGemSlot; import emu.nebula.proto.Public.UI32; import emu.nebula.proto.PublicStarTower.StarTowerChar; import emu.nebula.proto.PublicStarTower.StarTowerCharGem; import emu.nebula.util.Bitset; import it.unimi.dsi.fastutil.ints.IntArrayList; import lombok.Getter; @Getter @Entity(value = "characters", useDiscriminator = false) public class Character implements GameDatabaseObject { @Id private ObjectId uid; @Indexed private int playerUid; private transient CharacterDef data; private transient Player player; private int charId; private int advance; private int level; private int exp; private int skin; private int[] skills; private Bitset talents; private long createTime; @Deprecated // Morphia only! public Character() { } public Character(Player player, int charId) { this(player, GameData.getCharacterDataTable().get(charId)); } public Character(Player player, CharacterDef data) { this.player = player; this.playerUid = player.getUid(); this.charId = data.getId(); this.data = data; this.level = 1; this.skin = data.getDefaultSkinId(); this.skills = new int[] {1, 1, 1, 1, 1}; this.talents = new Bitset(); this.createTime = Nebula.getCurrentTime(); } public void setPlayer(Player player) { this.player = player; } public void setData(CharacterDef data) { if (this.data == null && data.getId() == this.getCharId()) { this.data = data; } } public int getMaxGainableExp() { if (this.getLevel() >= this.getMaxLevel()) { return 0; } int maxLevel = this.getMaxLevel(); int max = 0; for (int i = this.getLevel() + 1; i <= maxLevel; i++) { var data = GameData.getCharacterUpgradeDataTable().get(i); if (data != null) { max += data.getExp(); } } return Math.max(max - this.getExp(), 0); } public int getMaxExp() { if (this.getLevel() >= this.getMaxLevel()) { return 0; } var data = GameData.getCharacterUpgradeDataTable().get(this.level + 1); return data != null ? data.getExp() : 0; } public int getMaxLevel() { return 10 + (this.getAdvance() * 10); } public void addExp(int amount) { // Setup int expRequired = this.getMaxExp(); int oldLevel = this.getLevel(); // Add exp this.exp += amount; // Check for level ups while (this.exp >= expRequired && expRequired > 0) { this.level += 1; this.exp -= expRequired; expRequired = this.getMaxExp(); } // Clamp exp if (this.getLevel() >= this.getMaxLevel()) { this.exp = 0; } // Check if we leveled up if (this.level > oldLevel) { // Trigger quest this.getPlayer().getQuestManager().triggerQuest(QuestCondType.CharacterUpTotal, this.level - oldLevel); } // Save to database this.save(); } // Handlers public PlayerChangeInfo upgrade(ItemParamMap params) { // Calculate exp gained int exp = 0; // Check if item is an exp item for (var entry : params.entries()) { var data = GameData.getCharItemExpDataTable().get(entry.getIntKey()); if (data == null) return null; exp += data.getExpValue() * entry.getIntValue(); } // Clamp exp gain exp = Math.min(this.getMaxGainableExp(), exp); // Calculate gold required params.add(GameConstants.GOLD_ITEM_ID, (int) Math.ceil(exp * 0.15D)); // Verify that the player has the items if (!this.getPlayer().getInventory().verifyItems(params)) { return null; } // Remove items var changes = this.getPlayer().getInventory().removeItems(params, null); // Add exp this.addExp(exp); // Success return changes.setSuccess(true); } public PlayerChangeInfo advance() { // TODO check player level to make sure they can advance this character // Get advance data int advanceId = (this.getData().getAdvanceGroup() * 100) + (this.advance + 1); var data = GameData.getCharacterAdvanceDataTable().get(advanceId); if (data == null) { return null; } // Verify that the player has the items if (!this.getPlayer().getInventory().verifyItems(data.getMaterials())) { return null; } // Remove items var changes = this.getPlayer().getInventory().removeItems(data.getMaterials(), null); // Add advance level this.advance++; // Check if we need to add skin if (this.getAdvance() == this.getData().getAdvanceSkinUnlockLevel()) { // Set advance skin this.skin = this.getData().getAdvanceSkinId(); // Send packets this.getPlayer().addNextPackage( NetMsgId.character_skin_gain_notify, Skin.newInstance().setNew(UI32.newInstance().setValue(this.getSkin())) ); this.getPlayer().addNextPackage( NetMsgId.character_skin_change_notify, SkinChange.newInstance().setCharId(this.getCharId()).setSkinId(this.getSkin()) ); } // Save to database this.save(); // Success return changes.setSuccess(true); } public PlayerChangeInfo upgradeSkill(int index) { // TODO check player level to make sure they can advance this character // Sanity check if (index < 0 || index >= this.getSkills().length) { return null; } // Get advance data int upgradeId = (this.getData().getSkillsUpgradeGroup(index) * 100) + this.getSkills()[index]; var data = GameData.getCharacterSkillUpgradeDataTable().get(upgradeId); if (data == null) { return null; } // Verify that the player has the items if (!this.getPlayer().getInventory().verifyItems(data.getMaterials())) { return null; } // Remove items var changes = this.getPlayer().getInventory().removeItems(data.getMaterials(), null); // Add skill level this.skills[index]++; // Save to database this.save(); // Success return changes.setSuccess(true); } public PlayerChangeInfo unlockTalent(TalentGroupDef talentGroup) { // Get talent item int talentItemId = this.getData().getFragmentsId(); int talentItemCount = this.getPlayer().getInventory().getItemCount(talentItemId); int unlockCount = (int) Math.floor(talentItemCount / 6); // Max unlock count // Sanity check if (unlockCount <= 0) { return null; } // Amount of talents unlocked int amount = 0; var nodes = new IntArrayList(); // Unlock talents for (var talent : talentGroup.getTalents()) { // Skip unlocked talents if (this.getTalents().isSet(talent.getIndex())) { continue; } // Set bit this.getTalents().setBit(talent.getIndex()); // Add nodes nodes.add(talent.getId()); amount++; // Set last talent if we unlocked everything if (talent.getSort() == 10) { this.getTalents().setBit(talentGroup.getMainTalent().getIndex()); nodes.add(talentGroup.getMainTalent().getId()); } // End if (amount >= unlockCount) { break; } } // Skip if we didn't unlock anything if (nodes.size() <= 0) { return null; } // Remove items var changes = getPlayer().getInventory().removeItem(talentItemId, amount * 6); changes.setExtraData(nodes); // Save to database this.save(); // Success return changes.setSuccess(true); } public boolean setSkin(int skinId) { // Sanity check if (this.skin == skinId) { return false; } // Make sure we have the skin if (!getPlayer().getInventory().hasSkin(skinId)) { return false; } // Set skin this.skin = skinId; // Save this.save(); // Success return true; } // Proto public Char toProto() { var proto = Char.newInstance() .setTid(this.getCharId()) .setLevel(this.getLevel()) .setSkin(this.getSkin()) .setAdvance(this.getAdvance()) .setTalentNodes(this.getTalents().toByteArray()) .addAllSkillLvs(this.getSkills()) .setCreateTime(this.getCreateTime()); var gemPresets = proto.getMutableCharGemPresets() .getMutableCharGemPresets(); for (int i = 0; i < 3; i++) { var preset = CharGemPreset.newInstance() .addAllSlotGem(-1, -1, -1); gemPresets.add(preset); } for (int i = 1; i <= 3; i++) { var slot = CharGemSlot.newInstance() .setId(i); proto.addCharGemSlots(slot); } proto.getMutableAffinityQuests(); return proto; } public StarTowerChar toStarTowerProto() { var proto = StarTowerChar.newInstance() .setId(this.getCharId()) .setAdvance(this.getAdvance()) .setLevel(this.getLevel()) .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}); proto.addGems(slot); } return proto; } // Database fix @PreLoad public void onLoad(Document doc) { var talents = doc.get("talents"); if (talents != null && talents.getClass() == Binary.class) { doc.remove("talents"); this.talents = new Bitset(); } } }