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; 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; 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 emu.nebula.util.CustomIntArray; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import lombok.Getter; import us.hebi.quickbuf.RepeatedInt; @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; private int gemPresetIndex; private List gemPresets; private CharacterGemSlot[] gemSlots; private CharacterContact contact; @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(); this.contact = new CharacterContact(this); } public void setPlayer(Player player) { this.player = player; } public void setData(CharacterDef data) { // Sanity check if (this.data != null || data.getId() != this.getCharId()) { return; } // Set data this.data = data; // Check contacts if (this.contact == null) { this.contact = new CharacterContact(this); this.save(); } else { this.contact.setCharacter(this); } } 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().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().hasItems(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().hasItems(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().hasItems(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; } // 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 boolean renameGemPreset(int index, String name) { // Sanity check if (index < 0 || index >= GameConstants.CHARACTER_MAX_GEM_PRESETS) { return false; } if (name == null || name.length() > 32) { return false; } // Rename preset var preset = this.getGemPreset(index); preset.setName(name); // Update to database this.save(); // Success return true; } public CharacterGem getGemFromPreset(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 CharacterGem getGemFromSlot(int slotId, int gemIndex) { // Check if gem slot exists if (!this.hasGemSlot(slotId)) { return null; } // Get gem from gem slot var slot = this.getGemSlot(slotId); var gem = slot.getGem(gemIndex); return gem; } 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 boolean lockGem(int slotId, int gemIndex, boolean lock) { // Get gem from slot var gem = this.getGemFromSlot(slotId, gemIndex); if (gem == null) return false; // Lock gem.setLocked(lock); // Save to database this.save(); // Success return true; } 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; } @SuppressWarnings("deprecation") public synchronized PlayerChangeInfo refreshGem(int slotId, int gemIndex, RepeatedInt lockedAttributes) { // Get gem from slot var gem = this.getGemFromSlot(slotId, gemIndex); if (gem == null) 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; } // Get locked attributes if (lockedAttributes.length() > gemControl.getLockableNum()) { return null; } // Calculate the materials we need var materials = new ItemParamMap(); materials.add(gemData.getRefreshCostTid(), gemControl.getRefreshCostQty()); materials.add(gemControl.getLockItemTid(), gemControl.getLockItemQty() * lockedAttributes.length()); // Make sure the player has the materials to craft the emblem if (!getPlayer().getInventory().hasItems(materials)) { return null; } // Create base list of attributes var list = new CustomIntArray(); // Add locked attributes to list if (lockedAttributes.length() != 0) { var locked = new IntOpenHashSet(); lockedAttributes.forEach(locked::add); for (int i = 0; i < gem.getAttributes().length; i++) { int attr = gem.getAttributes()[i]; if (locked.contains(attr)) { list.add(i, attr); } } } // Generate attributes and create gem var attributes = gemControl.generateAttributes(list); gem.setNewAttributes(attributes); // Save to database this.save(); // Consume materials var change = getPlayer().getInventory().removeItems(materials); // Set change info extra info change.setExtraData(gem); // Success return change; } public boolean replaceGemAttributes(int slotId, int gemIndex) { // Get gem from slot var gem = this.getGemFromSlot(slotId, gemIndex); if (gem == null) return false; // Replace attributes with altered ones boolean success = gem.replaceAttributes(); // Save to database if (success) { this.save(); } // Success return success; } // 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()); // Encode gem presets var gemPresets = proto.getMutableCharGemPresets() .setInUsePresetIndex(this.getGemPresetIndex()) .getMutableCharGemPresets(); 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(info); } // 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; } public StarTowerChar toStarTowerProto() { var proto = StarTowerChar.newInstance() .setId(this.getCharId()) .setAdvance(this.getAdvance()) .setLevel(this.getLevel()) .setTalentNodes(this.getTalents().toByteArray()) .addAllSkillLvs(this.getSkills()); // Encode gems var preset = this.getCurrentGemPreset(); for (int i = 1; i <= preset.getLength(); i++) { var gem = this.getGemFromPreset(preset, i); var info = StarTowerCharGem.newInstance() .setSlotId(i); if (gem != null) { info.addAllAttributes(gem.getAttributes()); } else { info.addAllAttributes(new int[] {0, 0, 0, 0}); } proto.addGems(info); } return proto; } // Database fix @PreLoad 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<>(); } } }