Handle mob summon and limbo state (#2432)

Mob summon: Something like Monster_Apparatus_Perpetual can summon helper mobs. Ensure these helpers actually get summoned and, on their defeat, possibly change the summoner's mob state. Like, temporarily enter weak state.
* Take summon tags from BinOutput/Monster/ConfigMonster_*.json and put them in SceneMonsterInfo
* Handle Summon action in ability modifiers from BinOutput/Ability/Temp/MonsterAbilities/ConfigAbility_Monster_*.json
* On summoner's kill, also kill the summoned mobs

Limbo state: Something like Monster_Invoker_Herald_Water should be invulnerable at a certain HP threshold. Like, shouldn't die when creating their elemental shield. Or, Monster_Apparatus_Perpetual's helper mobs shouldn't die before their summoner.
* Look through ConfigAbility (AbilityData in GC) like Invoker_Herald_Water_StateControl. If any AbilityModifier within specifies state Limbo and properties.Actor_HpThresholdRatio, account for this threshold in GameEntity::damage.
* Don't let the entity die while in limbo. They will be killed by other events.
This commit is contained in:
longfruit
2023-11-16 20:56:37 -08:00
committed by GitHub
parent 13c40b53a7
commit d8c3da8fcd
9 changed files with 199 additions and 8 deletions

View File

@@ -25,6 +25,7 @@ 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.net.proto.ServantInfoOuterClass.ServantInfo;
import emu.grasscutter.scripts.constants.EventType;
import emu.grasscutter.scripts.data.*;
import emu.grasscutter.server.event.entity.EntityDamageEvent;
@@ -49,6 +50,9 @@ public class EntityMonster extends GameEntity {
@Getter private final Position bornPos;
@Getter private final int level;
@Getter private EntityWeapon weaponEntity;
@Getter private Map<Integer, EntityMonster> summonTagMap;
@Getter @Setter private int summonedTag;
@Getter @Setter private int ownerEntityId;
@Getter @Setter private int poseId;
@Getter @Setter private int aiId = -1;
@@ -67,6 +71,9 @@ public class EntityMonster extends GameEntity {
this.bornPos = this.getPosition().clone();
this.level = level;
this.playerOnBattle = new ArrayList<>();
this.summonTagMap = new HashMap<>();
this.summonedTag = 0;
this.ownerEntityId = 0;
if (GameData.getMonsterMappingMap().containsKey(this.getMonsterId())) {
this.configEntityMonster =
@@ -76,6 +83,14 @@ public class EntityMonster extends GameEntity {
this.configEntityMonster = null;
}
if (this.configEntityMonster != null &&
this.configEntityMonster.getCombat() != null &&
this.configEntityMonster.getCombat().getSummon() != null &&
this.configEntityMonster.getCombat().getSummon().getSummonTags() != null) {
this.configEntityMonster.getCombat().getSummon().getSummonTags().forEach(
t -> this.summonTagMap.put(t.getSummonTag(), null));
}
// Monster weapon
if (getMonsterWeaponId() > 0) {
this.weaponEntity = new EntityWeapon(scene, getMonsterWeaponId());
@@ -316,6 +331,11 @@ public class EntityMonster extends GameEntity {
this.getMonsterData().getType().getValue());
scene.triggerDungeonEvent(
DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER, this.getMonsterId());
// If this entity spawned servants, kill those too.
summonTagMap.values().stream()
.filter(Objects::nonNull)
.forEach(entity -> scene.killEntity(entity, killerId));
}
public void recalcStats() {
@@ -387,14 +407,21 @@ public class EntityMonster extends GameEntity {
public SceneEntityInfo toProto() {
var data = this.getMonsterData();
var aiInfo =
SceneEntityAiInfo.newBuilder()
.setIsAiOpen(true)
.setBornPos(this.getBornPos().toProto());
if (ownerEntityId != 0) {
aiInfo.setServantInfo(
ServantInfo.newBuilder()
.setMasterEntityId(ownerEntityId));
}
var authority =
EntityAuthorityInfo.newBuilder()
.setAbilityInfo(AbilitySyncStateInfo.newBuilder())
.setRendererChangedInfo(EntityRendererChangedInfo.newBuilder())
.setAiInfo(
SceneEntityAiInfo.newBuilder()
.setIsAiOpen(true)
.setBornPos(this.getBornPos().toProto()))
.setAiInfo(aiInfo)
.setBornPos(this.getBornPos().toProto())
.build();
@@ -425,7 +452,10 @@ public class EntityMonster extends GameEntity {
.setAuthorityPeerId(this.getWorld().getHostPeerId())
.setPoseId(this.getPoseId())
.setBlockId(this.getScene().getId())
.setSummonedTag(this.summonedTag)
.setOwnerEntityId(this.ownerEntityId)
.setBornType(MonsterBornType.MONSTER_BORN_TYPE_DEFAULT);
summonTagMap.forEach((k, v) -> monsterInfo.putSummonTagMap(k, v == null ? 0 : 1));
if (this.metaMonster != null) {
if (this.metaMonster.special_name_id != 0) {

View File

@@ -1,6 +1,8 @@
package emu.grasscutter.game.entity;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.*;
import emu.grasscutter.game.ability.*;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.*;
@@ -32,6 +34,8 @@ public abstract class GameEntity {
@Getter @Setter private int lastMoveReliableSeq;
@Getter @Setter private boolean lockHP;
private boolean limbo;
private float limboHpThreshold;
@Setter(AccessLevel.PROTECTED)
@Getter
@@ -110,6 +114,20 @@ public abstract class GameEntity {
});
}
protected void setLimbo(float hpThreshold) {
limbo = true;
limboHpThreshold = hpThreshold;
}
public void onAddAbilityModifier(AbilityModifier data) {
// Set limbo state (invulnerability at a certain HP threshold)
// if ability modifier calls for it
if (data.state == AbilityModifier.State.Limbo &&
data.properties != null && data.properties.Actor_HpThresholdRatio > .0f) {
this.setLimbo(data.properties.Actor_HpThresholdRatio);
}
}
protected MotionInfo getMotionInfo() {
return MotionInfo.newBuilder()
.setPos(this.getPosition().toProto())
@@ -167,11 +185,26 @@ public abstract class GameEntity {
return; // If the event is canceled, do not damage the entity.
}
float effectiveDamage = 0;
float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
if (curHp != Float.POSITIVE_INFINITY && !lockHP || lockHP && curHp <= event.getDamage()) {
// Add negative HP to the current HP property.
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -(event.getDamage()));
if (limbo) {
float maxHp = getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
float curRatio = curHp / maxHp;
if (curRatio > limboHpThreshold) {
// OK if this hit takes HP below threshold.
effectiveDamage = event.getDamage();
}
if (effectiveDamage >= curHp && limboHpThreshold > .0f) {
// Don't let entity die while in limbo.
effectiveDamage = curHp - 1;
}
}
else if (curHp != Float.POSITIVE_INFINITY && !lockHP || lockHP && curHp <= event.getDamage()) {
effectiveDamage = event.getDamage();
}
// Add negative HP to the current HP property.
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -effectiveDamage);
this.lastAttackType = attackType;
this.checkIfDead();