Skip to content

Commit

Permalink
Handle mob summon and limbo state (#2432)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
longfruit authored Nov 17, 2023
1 parent 13c40b5 commit d8c3da8
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -328,6 +329,9 @@ public enum Type {

public int skillID;
public int resistanceListID;
public int monsterID;
public int summonTag;


public AbilityModifierAction[] actions;
public AbilityModifierAction[] successActions;
Expand Down Expand Up @@ -370,6 +374,11 @@ public enum DropType {
}
}

public static class AbilityModifierProperty implements Serializable {
public float Actor_HpThresholdRatio;
// Add more properties here when GC needs them.
}

public enum State {
LockHP,
Invincible,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
public class ConfigCombat {
// There are more values that can be added that might be useful in the json
ConfigCombatProperty property;
ConfigCombatSummon summon;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
18 changes: 18 additions & 0 deletions src/main/java/emu/grasscutter/game/ability/Ability.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ public Ability(AbilityData data, GameEntity owner, Player playerOwner) {
.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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
38 changes: 34 additions & 4 deletions src/main/java/emu/grasscutter/game/entity/EntityMonster.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
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;
Expand All @@ -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;

Expand All @@ -67,6 +71,9 @@ public EntityMonster(
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 =
Expand All @@ -76,6 +83,14 @@ public EntityMonster(
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());
Expand Down Expand Up @@ -316,6 +331,11 @@ public void onDeath(int killerId) {
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() {
Expand Down Expand Up @@ -387,14 +407,21 @@ public void recalcStats() {
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();

Expand Down Expand Up @@ -425,7 +452,10 @@ public SceneEntityInfo toProto() {
.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) {
Expand Down
39 changes: 36 additions & 3 deletions src/main/java/emu/grasscutter/game/entity/GameEntity.java
Original file line number Diff line number Diff line change
@@ -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.*;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -110,6 +114,20 @@ public void addAllFightPropsToEntityInfo(SceneEntityInfo.Builder entityInfo) {
});
}

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())
Expand Down Expand Up @@ -167,11 +185,26 @@ public void damage(float amount, int killerId, ElementType attackType) {
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();
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/emu/grasscutter/game/world/World.java
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public int getPlayerCount() {
* @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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}

0 comments on commit d8c3da8

Please sign in to comment.