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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 199 additions and 8 deletions

View File

@ -42,6 +42,7 @@ public class AbilityModifier implements Serializable {
public String stacking;
public AbilityMixinData[] modifierMixins;
public AbilityModifierProperty properties;
public ElementType elementType;
public DynamicFloat elementDurability = DynamicFloat.ZERO;
@ -328,6 +329,9 @@ public class AbilityModifier implements Serializable {
public int skillID;
public int resistanceListID;
public int monsterID;
public int summonTag;
public AbilityModifierAction[] actions;
public AbilityModifierAction[] successActions;
@ -370,6 +374,11 @@ public class AbilityModifier implements Serializable {
}
}
public static class AbilityModifierProperty implements Serializable {
public float Actor_HpThresholdRatio;
// Add more properties here when GC needs them.
}
public enum State {
LockHP,
Invincible,

View File

@ -8,4 +8,5 @@ import lombok.experimental.FieldDefaults;
public class ConfigCombat {
// There are more values that can be added that might be useful in the json
ConfigCombatProperty property;
ConfigCombatSummon summon;
}

View File

@ -0,0 +1,15 @@
package emu.grasscutter.data.binout.config.fields;
import lombok.*;
import lombok.experimental.FieldDefaults;
import java.util.List;
@Data
public class ConfigCombatSummon {
List<SummonTag> summonTags;
@Getter
public final class SummonTag {
int summonTag;
}
}

View File

@ -71,6 +71,24 @@ public class Ability {
.filter(action -> action.type == AbilityModifierAction.Type.AvatarSkillStart)
.map(action -> action.skillID)
.toList());
if (data.onAdded != null) {
processOnAddedAbilityModifiers();
}
}
public void processOnAddedAbilityModifiers() {
for (AbilityModifierAction modifierAction : data.onAdded) {
if (modifierAction.type == null) continue;
if (modifierAction.type == AbilityModifierAction.Type.ApplyModifier) {
if (modifierAction.modifierName == null) continue;
else if (!data.modifiers.containsKey(modifierAction.modifierName)) continue;
var modifierData = data.modifiers.get(modifierAction.modifierName);
owner.onAddAbilityModifier(modifierData);
}
}
}
public static String getAbilityName(AbilityString abString) {

View File

@ -0,0 +1,62 @@
package emu.grasscutter.game.ability.actions;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.game.ability.Ability;
import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.world.*;
import emu.grasscutter.server.packet.send.PacketMonsterSummonTagNotify;
import emu.grasscutter.net.proto.EPKDEHOJFLIOuterClass.EPKDEHOJFLI;
import emu.grasscutter.utils.*;
@AbilityAction(AbilityModifierAction.Type.Summon)
public class ActionSummon extends AbilityActionHandler {
@Override
public synchronized boolean execute(
Ability ability, AbilityModifierAction action, ByteString abilityData, GameEntity target) {
EPKDEHOJFLI summonPosRot = null;
try {
// In game version 4.0, summoned entity's
// position and rotation are packed in EPKDEHOJFLI.
// This is packet AbilityActionSummon and has two fields:
// 4: Vector pos
// 13: Vector rot
summonPosRot = EPKDEHOJFLI.parseFrom(abilityData);
} catch (InvalidProtocolBufferException e) {
Grasscutter.getLogger().error("Failed to parse abilityData: {}", Utils.bytesToHex(abilityData.toByteArray()));
return false;
}
var pos = new Position(summonPosRot.getPos());
var rot = new Position(summonPosRot.getRot());
var monsterId = action.monsterID;
var scene = target.getScene();
var monsterData = GameData.getMonsterDataMap().get(monsterId);
if (monsterData == null) {
Grasscutter.getLogger().error("Failed to find monster by ID {}", monsterId);
return false;
}
if (target instanceof EntityMonster ownerEntity) {
var level = scene.getLevelForMonster(0, ownerEntity.getLevel());
var entity = new EntityMonster(scene, monsterData, pos, rot, level);
ownerEntity.getSummonTagMap().put(action.summonTag, entity);
entity.setSummonedTag(action.summonTag);
entity.setOwnerEntityId(target.getId());
scene.addEntity(entity);
scene.getPlayers().get(0).sendPacket(new PacketMonsterSummonTagNotify(ownerEntity));
Grasscutter.getLogger().trace("Spawned entityId {} monsterId {} pos {} rot {}, target { {} }, action { {} }",
entity.getId(), monsterId, pos, rot, target, action);
return true;
} else {
return false;
}
}
}

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();

View File

@ -167,7 +167,7 @@ public class World implements Iterable<Player> {
* @param idType The entity type.
* @return The next entity ID.
*/
public int getNextEntityId(EntityIdType idType) {
public synchronized int getNextEntityId(EntityIdType idType) {
return (idType.getId() << 24) + ++this.nextEntityId;
}

View File

@ -0,0 +1,23 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.game.entity.EntityMonster;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.MonsterSummonTagNotifyOuterClass.MonsterSummonTagNotify;
import java.util.*;
import static java.util.Map.entry;
public class PacketMonsterSummonTagNotify extends BasePacket {
public PacketMonsterSummonTagNotify(EntityMonster monsterEntity) {
super(PacketOpcodes.MonsterSummonTagNotify);
var proto =
MonsterSummonTagNotify.newBuilder()
.setMonsterEntityId(monsterEntity.getId());
monsterEntity.getSummonTagMap().forEach((k, v) -> proto.putSummonTagMap(k, v == null ? 0 : 1));
this.setData(proto.build());
}
}