Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle mob summon and limbo state #2432

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
}
}
Loading