mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-02-02 06:45:37 +08:00
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:
parent
13c40b53a7
commit
d8c3da8fcd
@ -42,6 +42,7 @@ public class AbilityModifier implements Serializable {
|
|||||||
public String stacking;
|
public String stacking;
|
||||||
|
|
||||||
public AbilityMixinData[] modifierMixins;
|
public AbilityMixinData[] modifierMixins;
|
||||||
|
public AbilityModifierProperty properties;
|
||||||
|
|
||||||
public ElementType elementType;
|
public ElementType elementType;
|
||||||
public DynamicFloat elementDurability = DynamicFloat.ZERO;
|
public DynamicFloat elementDurability = DynamicFloat.ZERO;
|
||||||
@ -328,6 +329,9 @@ public class AbilityModifier implements Serializable {
|
|||||||
|
|
||||||
public int skillID;
|
public int skillID;
|
||||||
public int resistanceListID;
|
public int resistanceListID;
|
||||||
|
public int monsterID;
|
||||||
|
public int summonTag;
|
||||||
|
|
||||||
|
|
||||||
public AbilityModifierAction[] actions;
|
public AbilityModifierAction[] actions;
|
||||||
public AbilityModifierAction[] successActions;
|
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 {
|
public enum State {
|
||||||
LockHP,
|
LockHP,
|
||||||
Invincible,
|
Invincible,
|
||||||
|
@ -8,4 +8,5 @@ import lombok.experimental.FieldDefaults;
|
|||||||
public class ConfigCombat {
|
public class ConfigCombat {
|
||||||
// There are more values that can be added that might be useful in the json
|
// There are more values that can be added that might be useful in the json
|
||||||
ConfigCombatProperty property;
|
ConfigCombatProperty property;
|
||||||
|
ConfigCombatSummon summon;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -71,6 +71,24 @@ public class Ability {
|
|||||||
.filter(action -> action.type == AbilityModifierAction.Type.AvatarSkillStart)
|
.filter(action -> action.type == AbilityModifierAction.Type.AvatarSkillStart)
|
||||||
.map(action -> action.skillID)
|
.map(action -> action.skillID)
|
||||||
.toList());
|
.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) {
|
public static String getAbilityName(AbilityString abString) {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,7 @@ import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo;
|
|||||||
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
|
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
|
||||||
import emu.grasscutter.net.proto.SceneMonsterInfoOuterClass.SceneMonsterInfo;
|
import emu.grasscutter.net.proto.SceneMonsterInfoOuterClass.SceneMonsterInfo;
|
||||||
import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo;
|
import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo;
|
||||||
|
import emu.grasscutter.net.proto.ServantInfoOuterClass.ServantInfo;
|
||||||
import emu.grasscutter.scripts.constants.EventType;
|
import emu.grasscutter.scripts.constants.EventType;
|
||||||
import emu.grasscutter.scripts.data.*;
|
import emu.grasscutter.scripts.data.*;
|
||||||
import emu.grasscutter.server.event.entity.EntityDamageEvent;
|
import emu.grasscutter.server.event.entity.EntityDamageEvent;
|
||||||
@ -49,6 +50,9 @@ public class EntityMonster extends GameEntity {
|
|||||||
@Getter private final Position bornPos;
|
@Getter private final Position bornPos;
|
||||||
@Getter private final int level;
|
@Getter private final int level;
|
||||||
@Getter private EntityWeapon weaponEntity;
|
@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 poseId;
|
||||||
@Getter @Setter private int aiId = -1;
|
@Getter @Setter private int aiId = -1;
|
||||||
|
|
||||||
@ -67,6 +71,9 @@ public class EntityMonster extends GameEntity {
|
|||||||
this.bornPos = this.getPosition().clone();
|
this.bornPos = this.getPosition().clone();
|
||||||
this.level = level;
|
this.level = level;
|
||||||
this.playerOnBattle = new ArrayList<>();
|
this.playerOnBattle = new ArrayList<>();
|
||||||
|
this.summonTagMap = new HashMap<>();
|
||||||
|
this.summonedTag = 0;
|
||||||
|
this.ownerEntityId = 0;
|
||||||
|
|
||||||
if (GameData.getMonsterMappingMap().containsKey(this.getMonsterId())) {
|
if (GameData.getMonsterMappingMap().containsKey(this.getMonsterId())) {
|
||||||
this.configEntityMonster =
|
this.configEntityMonster =
|
||||||
@ -76,6 +83,14 @@ public class EntityMonster extends GameEntity {
|
|||||||
this.configEntityMonster = null;
|
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
|
// Monster weapon
|
||||||
if (getMonsterWeaponId() > 0) {
|
if (getMonsterWeaponId() > 0) {
|
||||||
this.weaponEntity = new EntityWeapon(scene, getMonsterWeaponId());
|
this.weaponEntity = new EntityWeapon(scene, getMonsterWeaponId());
|
||||||
@ -316,6 +331,11 @@ public class EntityMonster extends GameEntity {
|
|||||||
this.getMonsterData().getType().getValue());
|
this.getMonsterData().getType().getValue());
|
||||||
scene.triggerDungeonEvent(
|
scene.triggerDungeonEvent(
|
||||||
DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER, this.getMonsterId());
|
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() {
|
public void recalcStats() {
|
||||||
@ -387,14 +407,21 @@ public class EntityMonster extends GameEntity {
|
|||||||
public SceneEntityInfo toProto() {
|
public SceneEntityInfo toProto() {
|
||||||
var data = this.getMonsterData();
|
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 =
|
var authority =
|
||||||
EntityAuthorityInfo.newBuilder()
|
EntityAuthorityInfo.newBuilder()
|
||||||
.setAbilityInfo(AbilitySyncStateInfo.newBuilder())
|
.setAbilityInfo(AbilitySyncStateInfo.newBuilder())
|
||||||
.setRendererChangedInfo(EntityRendererChangedInfo.newBuilder())
|
.setRendererChangedInfo(EntityRendererChangedInfo.newBuilder())
|
||||||
.setAiInfo(
|
.setAiInfo(aiInfo)
|
||||||
SceneEntityAiInfo.newBuilder()
|
|
||||||
.setIsAiOpen(true)
|
|
||||||
.setBornPos(this.getBornPos().toProto()))
|
|
||||||
.setBornPos(this.getBornPos().toProto())
|
.setBornPos(this.getBornPos().toProto())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -425,7 +452,10 @@ public class EntityMonster extends GameEntity {
|
|||||||
.setAuthorityPeerId(this.getWorld().getHostPeerId())
|
.setAuthorityPeerId(this.getWorld().getHostPeerId())
|
||||||
.setPoseId(this.getPoseId())
|
.setPoseId(this.getPoseId())
|
||||||
.setBlockId(this.getScene().getId())
|
.setBlockId(this.getScene().getId())
|
||||||
|
.setSummonedTag(this.summonedTag)
|
||||||
|
.setOwnerEntityId(this.ownerEntityId)
|
||||||
.setBornType(MonsterBornType.MONSTER_BORN_TYPE_DEFAULT);
|
.setBornType(MonsterBornType.MONSTER_BORN_TYPE_DEFAULT);
|
||||||
|
summonTagMap.forEach((k, v) -> monsterInfo.putSummonTagMap(k, v == null ? 0 : 1));
|
||||||
|
|
||||||
if (this.metaMonster != null) {
|
if (this.metaMonster != null) {
|
||||||
if (this.metaMonster.special_name_id != 0) {
|
if (this.metaMonster.special_name_id != 0) {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package emu.grasscutter.game.entity;
|
package emu.grasscutter.game.entity;
|
||||||
|
|
||||||
|
import emu.grasscutter.Grasscutter;
|
||||||
import emu.grasscutter.data.GameData;
|
import emu.grasscutter.data.GameData;
|
||||||
|
import emu.grasscutter.data.binout.*;
|
||||||
import emu.grasscutter.game.ability.*;
|
import emu.grasscutter.game.ability.*;
|
||||||
import emu.grasscutter.game.player.Player;
|
import emu.grasscutter.game.player.Player;
|
||||||
import emu.grasscutter.game.props.*;
|
import emu.grasscutter.game.props.*;
|
||||||
@ -32,6 +34,8 @@ public abstract class GameEntity {
|
|||||||
@Getter @Setter private int lastMoveReliableSeq;
|
@Getter @Setter private int lastMoveReliableSeq;
|
||||||
|
|
||||||
@Getter @Setter private boolean lockHP;
|
@Getter @Setter private boolean lockHP;
|
||||||
|
private boolean limbo;
|
||||||
|
private float limboHpThreshold;
|
||||||
|
|
||||||
@Setter(AccessLevel.PROTECTED)
|
@Setter(AccessLevel.PROTECTED)
|
||||||
@Getter
|
@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() {
|
protected MotionInfo getMotionInfo() {
|
||||||
return MotionInfo.newBuilder()
|
return MotionInfo.newBuilder()
|
||||||
.setPos(this.getPosition().toProto())
|
.setPos(this.getPosition().toProto())
|
||||||
@ -167,11 +185,26 @@ public abstract class GameEntity {
|
|||||||
return; // If the event is canceled, do not damage the entity.
|
return; // If the event is canceled, do not damage the entity.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float effectiveDamage = 0;
|
||||||
float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
|
float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
|
||||||
if (curHp != Float.POSITIVE_INFINITY && !lockHP || lockHP && curHp <= event.getDamage()) {
|
if (limbo) {
|
||||||
// Add negative HP to the current HP property.
|
float maxHp = getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
|
||||||
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -(event.getDamage()));
|
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.lastAttackType = attackType;
|
||||||
this.checkIfDead();
|
this.checkIfDead();
|
||||||
|
@ -167,7 +167,7 @@ public class World implements Iterable<Player> {
|
|||||||
* @param idType The entity type.
|
* @param idType The entity type.
|
||||||
* @return The next entity ID.
|
* @return The next entity ID.
|
||||||
*/
|
*/
|
||||||
public int getNextEntityId(EntityIdType idType) {
|
public synchronized int getNextEntityId(EntityIdType idType) {
|
||||||
return (idType.getId() << 24) + ++this.nextEntityId;
|
return (idType.getId() << 24) + ++this.nextEntityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user