package emu.grasscutter.game.entity; import emu.grasscutter.data.GameData; import emu.grasscutter.data.binout.AbilityData; import emu.grasscutter.data.binout.config.ConfigEntityMonster; import emu.grasscutter.data.common.PropGrowCurve; import emu.grasscutter.data.excels.EnvAnimalGatherConfigData; import emu.grasscutter.data.excels.monster.MonsterCurveData; import emu.grasscutter.data.excels.monster.MonsterData; import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.*; import emu.grasscutter.game.quest.enums.QuestContent; import emu.grasscutter.game.world.Position; import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.SceneGroupInstance; import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair; import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo; import emu.grasscutter.net.proto.EntityClientDataOuterClass.EntityClientData; import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo; import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; import emu.grasscutter.net.proto.MonsterBornTypeOuterClass.MonsterBornType; import emu.grasscutter.net.proto.PropPairOuterClass.PropPair; import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType; import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo; import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; import emu.grasscutter.net.proto.SceneMonsterInfoOuterClass.SceneMonsterInfo; import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo; import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.data.SceneMonster; import emu.grasscutter.scripts.data.ScriptArgs; import emu.grasscutter.server.event.entity.EntityDamageEvent; import emu.grasscutter.utils.helpers.ProtoHelper; import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; import lombok.Getter; import lombok.Setter; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Optional; import static emu.grasscutter.scripts.constants.EventType.EVENT_SPECIFIC_MONSTER_HP_CHANGE; public class EntityMonster extends GameEntity { @Getter(onMethod_ = @Override) private final Int2FloatOpenHashMap fightProperties; @Getter(onMethod_ = @Override) private final Position position; @Getter(onMethod_ = @Override) private final Position rotation; @Getter private final MonsterData monsterData; @Getter private final ConfigEntityMonster configEntityMonster; @Getter private final Position bornPos; @Getter private final int level; @Getter private EntityWeapon weaponEntity; @Getter @Setter private int poseId; @Getter @Setter private int aiId = -1; @Getter private List playerOnBattle; @Nullable @Getter @Setter private SceneMonster metaMonster; public EntityMonster(Scene scene, MonsterData monsterData, Position pos, int level) { super(scene); this.id = getWorld().getNextEntityId(EntityIdType.MONSTER); this.monsterData = monsterData; this.fightProperties = new Int2FloatOpenHashMap(); this.position = new Position(pos); this.rotation = new Position(); this.bornPos = getPosition().clone(); this.level = level; this.playerOnBattle = new ArrayList<>(); if(GameData.getMonsterMappingMap().containsKey(getMonsterId())) { this.configEntityMonster = GameData.getMonsterConfigData().get(GameData.getMonsterMappingMap().get(getMonsterId()).getMonsterJson()); } else { this.configEntityMonster = null; } // Monster weapon if (getMonsterWeaponId() > 0) { this.weaponEntity = new EntityWeapon(scene, getMonsterWeaponId()); scene.getWeaponEntities().put(this.weaponEntity.getId(), this.weaponEntity); //this.weaponEntityId = getWorld().getNextEntityId(EntityIdType.WEAPON); } this.recalcStats(); initAbilities(); } private void addConfigAbility(String name){ AbilityData data = GameData.getAbilityData(name); if(data != null) getScene().getWorld().getHost().getAbilityManager().addAbilityToEntity( this, data); } @Override public void initAbilities() { if(configEntityMonster != null) { // Affix abilities var optionalGroup = getScene().getLoadedGroups().stream() .filter(g -> g.id == this.getGroupId()) .findAny(); List affixes = null; if (optionalGroup.isPresent()) { var group = optionalGroup.get(); SceneMonster monster = group.monsters.get(getConfigId()); if(monster != null) affixes = monster.affix; } if (monsterData != null) { if (affixes == null) affixes = monsterData.getAffix(); else affixes.addAll(monsterData.getAffix()); } if(affixes != null) { for(var affixId : affixes) { var affix = GameData.getMonsterAffixDataMap().get(affixId.intValue()); if (!affix.isPreAdd()) continue; //Add the ability for(var name : affix.getAbilityName()) { this.addConfigAbility(name); } } } //TODO: Research if any monster is non humanoid for(var ability : GameData.getConfigGlobalCombat().getDefaultAbilities().getNonHumanoidMoveAbilities()) { this.addConfigAbility(ability); } if (configEntityMonster.getAbilities() != null) for (var configAbilityData : configEntityMonster.getAbilities()) { this.addConfigAbility(configAbilityData.abilityName); } if (optionalGroup.isPresent()) { var group = optionalGroup.get(); SceneMonster monster = group.monsters.get(getConfigId()); if(monster != null && monster.isElite) { addConfigAbility(GameData.getConfigGlobalCombat().getDefaultAbilities().getMonterEliteAbilityName()); } } if (affixes != null) { for (var affixId : affixes) { var affix = GameData.getMonsterAffixDataMap().get(affixId.intValue()); if(affix.isPreAdd()) continue; //Add the ability for(var name : affix.getAbilityName()) { this.addConfigAbility(name); } } } var levelEntityConfig = getScene().getSceneData().getLevelEntityConfig(); var config = GameData.getConfigLevelEntityDataMap().get(levelEntityConfig); if (config == null){ return; } if (config.getMonsterAbilities() != null) { for (var monsterAbility : config.getMonsterAbilities()) { addConfigAbility(monsterAbility.abilityName); } } } } @Override public int getEntityTypeId() { return getMonsterId(); } public int getMonsterWeaponId() { return this.getMonsterData().getWeaponId(); } private int getMonsterId() { return this.getMonsterData().getId(); } @Override public boolean isAlive() { return this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) > 0f; } @Override public void onInteract(Player player, GadgetInteractReq interactReq) { EnvAnimalGatherConfigData gatherData = GameData.getEnvAnimalGatherConfigDataMap().get(this.getMonsterData().getId()); if (gatherData == null) { return; } player.getInventory().addItem(gatherData.getGatherItem(), ActionReason.SubfieldDrop); this.getScene().killEntity(this); } @Override public void onCreate() { // Lua event getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_ANY_MONSTER_LIVE, this.getConfigId())); } @Override public void damage(float amount, int killerId, ElementType attackType) { // Get HP before damage. float hpBeforeDamage = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); // Apply damage. super.damage(amount, killerId, attackType); // Get HP after damage. float hpAfterDamage = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); // Invoke energy drop logic. for (Player player : this.getScene().getPlayers()) { player.getEnergyManager().handleMonsterEnergyDrop(this, hpBeforeDamage, hpAfterDamage); } } @Override public void runLuaCallbacks(EntityDamageEvent event) { super.runLuaCallbacks(event); getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EVENT_SPECIFIC_MONSTER_HP_CHANGE, getConfigId(), monsterData.getId()) .setSourceEntityId(getId()) .setParam3((int) this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) .setEventSource(Integer.toString(getConfigId()))); } @Override public void onDeath(int killerId) { super.onDeath(killerId); // Invoke super class's onDeath() method. var scene = this.getScene(); var challenge = Optional.ofNullable(scene.getChallenge()); var scriptManager = scene.getScriptManager(); Optional.ofNullable(this.getSpawnEntry()).ifPresent(scene.getDeadSpawnedEntities()::add); // first set the challenge data challenge.ifPresent(c -> c.onMonsterDeath(this)); if (scriptManager.isInit() && this.getGroupId() > 0) { Optional.ofNullable(scriptManager.getScriptMonsterSpawnService()).ifPresent(s -> s.onMonsterDead(this)); // prevent spawn monster after success /*if (challenge.map(c -> c.inProgress()).orElse(true)) { scriptManager.callEvent(new ScriptArgs(EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId()).setGroupId(this.getGroupId())); } else if (getScene().getChallenge() == null) { }*/ scriptManager.callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId())); } // Battle Pass trigger scene.getPlayers().forEach(p -> p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_MONSTER_DIE, this.getMonsterId(), 1)); scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_MONSTER_DIE, this.getMonsterId())); scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_KILL_MONSTER, this.getMonsterId())); scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_CLEAR_GROUP_MONSTER, this.getGroupId())); SceneGroupInstance groupInstance = scene.getScriptManager().getGroupInstanceById(this.getGroupId()); if(groupInstance != null && metaMonster != null) groupInstance.getDeadEntities().add(metaMonster.config_id); scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_GROUP_MONSTER, this.getGroupId()); scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_TYPE_MONSTER, this.getMonsterData().getType().getValue()); scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER, this.getMonsterId()); } public void recalcStats() { // Monster data MonsterData data = this.getMonsterData(); // Get hp percent, set to 100% if none float hpPercent = this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) <= 0 ? 1f : this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) / this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); // Clear properties this.getFightProperties().clear(); // Base stats MonsterData.definedFightProperties.forEach(prop -> this.setFightProperty(prop, data.getFightProperty(prop))); // Level curve MonsterCurveData curve = GameData.getMonsterCurveDataMap().get(this.getLevel()); if (curve != null) { for (PropGrowCurve growCurve : data.getPropGrowCurves()) { FightProperty prop = FightProperty.getPropByName(growCurve.getType()); this.setFightProperty(prop, this.getFightProperty(prop) * curve.getMultByProp(growCurve.getGrowCurve())); } } // Set % stats FightProperty.forEachCompoundProperty(c -> this.setFightProperty(c.getResult(), this.getFightProperty(c.getFlat()) + (this.getFightProperty(c.getBase()) * (1f + this.getFightProperty(c.getPercent()))))); // Set current hp this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent); } @Override public SceneEntityInfo toProto() { var data = this.getMonsterData(); var authority = EntityAuthorityInfo.newBuilder() .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) .setRendererChangedInfo(EntityRendererChangedInfo.newBuilder()) .setAiInfo(SceneEntityAiInfo.newBuilder() .setIsAiOpen(true) .setBornPos(this.getBornPos().toProto())) .setBornPos(this.getBornPos().toProto()) .build(); var entityInfo = SceneEntityInfo.newBuilder() .setEntityId(this.getId()) .setEntityType(ProtEntityType.PROT_ENTITY_TYPE_MONSTER) .setMotionInfo(this.getMotionInfo()) .addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder()) .setEntityClientData(EntityClientData.newBuilder()) .setEntityAuthorityInfo(authority) .setLifeState(this.getLifeState().getValue()); this.addAllFightPropsToEntityInfo(entityInfo); entityInfo.addPropList(PropPair.newBuilder() .setType(PlayerProperty.PROP_LEVEL.getId()) .setPropValue(ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, this.getLevel())) .build()); var monsterInfo = SceneMonsterInfo.newBuilder() .setMonsterId(getMonsterId()) .setGroupId(this.getGroupId()) .setConfigId(this.getConfigId()) .addAllAffixList(data.getAffix()) .setAuthorityPeerId(this.getWorld().getHostPeerId()) .setPoseId(this.getPoseId()) .setBlockId(this.getScene().getId()) .setBornType(MonsterBornType.MONSTER_BORN_TYPE_DEFAULT); if (this.metaMonster != null) { if (this.metaMonster.special_name_id != 0) { monsterInfo.setTitleId(this.metaMonster.title_id) .setSpecialNameId(this.metaMonster.special_name_id); } else if (data.getDescribeData() != null) { monsterInfo.setTitleId(data.getDescribeData().getTitleId()) .setSpecialNameId(data.getSpecialNameId()); } } if (this.getMonsterWeaponId() > 0) { SceneWeaponInfo weaponInfo = SceneWeaponInfo.newBuilder() .setEntityId(this.getWeaponEntity() != null ? this.getWeaponEntity().getId() : 0) .setGadgetId(this.getMonsterWeaponId()) .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) .build(); monsterInfo.addWeaponList(weaponInfo); } if (this.aiId != -1) { monsterInfo.setAiConfigId(aiId); } entityInfo.setMonster(monsterInfo); return entityInfo.build(); } }