mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-01-10 17:33:02 +08:00
Continue merging quests
(pt. 1)
Finished last at: `World.java`, line `player.setAvatarsAbilityForScene(newScene);`
This commit is contained in:
parent
c64cc7d5e2
commit
97ee71bcf4
@ -1,24 +1,26 @@
|
||||
package emu.grasscutter.data.excels;
|
||||
|
||||
import emu.grasscutter.data.GameResource;
|
||||
import emu.grasscutter.data.ResourceType;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
|
||||
@ResourceType(
|
||||
name = {"CompoundExcelConfigData.json"},
|
||||
loadPriority = ResourceType.LoadPriority.LOW)
|
||||
@Getter
|
||||
public class CompoundData extends GameResource {
|
||||
@Getter(onMethod_ = @Override)
|
||||
private int id;
|
||||
|
||||
private int groupID;
|
||||
private int rankLevel;
|
||||
private boolean isDefaultUnlocked;
|
||||
private int costTime;
|
||||
private int queueSize;
|
||||
private List<ItemParamData> inputVec;
|
||||
private List<ItemParamData> outputVec;
|
||||
}
|
||||
package emu.grasscutter.data.excels;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import emu.grasscutter.data.GameResource;
|
||||
import emu.grasscutter.data.ResourceType;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
|
||||
@ResourceType(
|
||||
name = {"CompoundExcelConfigData.json"},
|
||||
loadPriority = ResourceType.LoadPriority.LOW)
|
||||
@Getter
|
||||
public class CompoundData extends GameResource {
|
||||
@Getter(onMethod_ = @Override)
|
||||
private int id;
|
||||
|
||||
@SerializedName("groupID")
|
||||
private int groupId;
|
||||
private int rankLevel;
|
||||
private boolean isDefaultUnlocked;
|
||||
private int costTime;
|
||||
private int queueSize;
|
||||
private List<ItemParamData> inputVec;
|
||||
private List<ItemParamData> outputVec;
|
||||
}
|
||||
|
@ -1,178 +1,178 @@
|
||||
package emu.grasscutter.game.dungeons.challenge;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger;
|
||||
import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
|
||||
import emu.grasscutter.game.entity.EntityGadget;
|
||||
import emu.grasscutter.game.entity.EntityMonster;
|
||||
import emu.grasscutter.game.props.WatcherTriggerType;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.scripts.constants.EventType;
|
||||
import emu.grasscutter.scripts.data.SceneGroup;
|
||||
import emu.grasscutter.scripts.data.SceneTrigger;
|
||||
import emu.grasscutter.scripts.data.ScriptArgs;
|
||||
import emu.grasscutter.server.packet.send.PacketDungeonChallengeBeginNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketDungeonChallengeFinishNotify;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class WorldChallenge {
|
||||
private final Scene scene;
|
||||
private final SceneGroup group;
|
||||
private final int challengeId;
|
||||
private final int challengeIndex;
|
||||
private final List<Integer> paramList;
|
||||
private final int timeLimit;
|
||||
private final List<ChallengeTrigger> challengeTriggers;
|
||||
private final int goal;
|
||||
private final AtomicInteger score;
|
||||
private boolean progress;
|
||||
private boolean success;
|
||||
private long startedAt;
|
||||
private int finishedTime;
|
||||
|
||||
public WorldChallenge(
|
||||
Scene scene,
|
||||
SceneGroup group,
|
||||
int challengeId,
|
||||
int challengeIndex,
|
||||
List<Integer> paramList,
|
||||
int timeLimit,
|
||||
int goal,
|
||||
List<ChallengeTrigger> challengeTriggers) {
|
||||
this.scene = scene;
|
||||
this.group = group;
|
||||
this.challengeId = challengeId;
|
||||
this.challengeIndex = challengeIndex;
|
||||
this.paramList = paramList;
|
||||
this.timeLimit = timeLimit;
|
||||
this.challengeTriggers = challengeTriggers;
|
||||
this.goal = goal;
|
||||
this.score = new AtomicInteger(0);
|
||||
}
|
||||
|
||||
public boolean inProgress() {
|
||||
return this.progress;
|
||||
}
|
||||
|
||||
public void onCheckTimeOut() {
|
||||
if (!inProgress()) {
|
||||
return;
|
||||
}
|
||||
if (timeLimit <= 0) {
|
||||
return;
|
||||
}
|
||||
challengeTriggers.forEach(t -> t.onCheckTimeout(this));
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (inProgress()) {
|
||||
Grasscutter.getLogger().info("Could not start a in progress challenge.");
|
||||
return;
|
||||
}
|
||||
this.progress = true;
|
||||
this.startedAt = System.currentTimeMillis();
|
||||
getScene().broadcastPacket(new PacketDungeonChallengeBeginNotify(this));
|
||||
challengeTriggers.forEach(t -> t.onBegin(this));
|
||||
}
|
||||
|
||||
public void done() {
|
||||
if (!this.inProgress()) return;
|
||||
this.finish(true);
|
||||
|
||||
var scene = this.getScene();
|
||||
var dungeonManager = scene.getDungeonManager();
|
||||
if (dungeonManager != null && dungeonManager.getDungeonData() != null) {
|
||||
scene
|
||||
.getPlayers()
|
||||
.forEach(
|
||||
p ->
|
||||
p.getActivityManager()
|
||||
.triggerWatcher(
|
||||
WatcherTriggerType.TRIGGER_FINISH_CHALLENGE,
|
||||
String.valueOf(dungeonManager.getDungeonData().getId()),
|
||||
String.valueOf(this.getGroup().id),
|
||||
String.valueOf(this.getChallengeId())));
|
||||
}
|
||||
|
||||
scene
|
||||
.getScriptManager()
|
||||
.callEvent(
|
||||
// TODO record the time in PARAM2 and used in action
|
||||
new ScriptArgs(this.getGroup().id, EventType.EVENT_CHALLENGE_SUCCESS)
|
||||
.setParam2(finishedTime));
|
||||
this.getScene()
|
||||
.triggerDungeonEvent(
|
||||
DungeonPassConditionType.DUNGEON_COND_FINISH_CHALLENGE,
|
||||
getChallengeId(),
|
||||
getChallengeIndex());
|
||||
|
||||
this.challengeTriggers.forEach(t -> t.onFinish(this));
|
||||
}
|
||||
|
||||
public void fail() {
|
||||
if (!this.inProgress()) return;
|
||||
this.finish(true);
|
||||
|
||||
this.getScene()
|
||||
.getScriptManager()
|
||||
.callEvent(new ScriptArgs(this.getGroup().id, EventType.EVENT_CHALLENGE_FAIL));
|
||||
challengeTriggers.forEach(t -> t.onFinish(this));
|
||||
}
|
||||
|
||||
private void finish(boolean success) {
|
||||
this.progress = false;
|
||||
this.success = success;
|
||||
this.finishedTime = (int) ((System.currentTimeMillis() - this.startedAt) / 1000L);
|
||||
getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this));
|
||||
}
|
||||
|
||||
public int increaseScore() {
|
||||
return score.incrementAndGet();
|
||||
}
|
||||
|
||||
public void onMonsterDeath(EntityMonster monster) {
|
||||
if (!inProgress()) {
|
||||
return;
|
||||
}
|
||||
if (monster.getGroupId() != getGroup().id) {
|
||||
return;
|
||||
}
|
||||
this.challengeTriggers.forEach(t -> t.onMonsterDeath(this, monster));
|
||||
}
|
||||
|
||||
public void onGadgetDeath(EntityGadget gadget) {
|
||||
if (!inProgress()) {
|
||||
return;
|
||||
}
|
||||
if (gadget.getGroupId() != getGroup().id) {
|
||||
return;
|
||||
}
|
||||
this.challengeTriggers.forEach(t -> t.onGadgetDeath(this, gadget));
|
||||
}
|
||||
|
||||
public void onGroupTriggerDeath(SceneTrigger trigger) {
|
||||
if (!this.inProgress()) return;
|
||||
|
||||
var triggerGroup = trigger.getCurrentGroup();
|
||||
if (triggerGroup == null || triggerGroup.id != getGroup().id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.challengeTriggers.forEach(t -> t.onGroupTrigger(this, trigger));
|
||||
}
|
||||
|
||||
public void onGadgetDamage(EntityGadget gadget) {
|
||||
if (!inProgress()) {
|
||||
return;
|
||||
}
|
||||
if (gadget.getGroupId() != getGroup().id) {
|
||||
return;
|
||||
}
|
||||
this.challengeTriggers.forEach(t -> t.onGadgetDamage(this, gadget));
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.dungeons.challenge;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger;
|
||||
import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
|
||||
import emu.grasscutter.game.entity.EntityGadget;
|
||||
import emu.grasscutter.game.entity.EntityMonster;
|
||||
import emu.grasscutter.game.props.WatcherTriggerType;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.scripts.constants.EventType;
|
||||
import emu.grasscutter.scripts.data.SceneGroup;
|
||||
import emu.grasscutter.scripts.data.SceneTrigger;
|
||||
import emu.grasscutter.scripts.data.ScriptArgs;
|
||||
import emu.grasscutter.server.packet.send.PacketDungeonChallengeBeginNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketDungeonChallengeFinishNotify;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class WorldChallenge {
|
||||
private final Scene scene;
|
||||
private final SceneGroup group;
|
||||
private final int challengeId;
|
||||
private final int challengeIndex;
|
||||
private final List<Integer> paramList;
|
||||
private final int timeLimit;
|
||||
private final List<ChallengeTrigger> challengeTriggers;
|
||||
private final int goal;
|
||||
private final AtomicInteger score;
|
||||
private boolean progress;
|
||||
private boolean success;
|
||||
private long startedAt;
|
||||
private int finishedTime;
|
||||
|
||||
public WorldChallenge(
|
||||
Scene scene,
|
||||
SceneGroup group,
|
||||
int challengeId,
|
||||
int challengeIndex,
|
||||
List<Integer> paramList,
|
||||
int timeLimit,
|
||||
int goal,
|
||||
List<ChallengeTrigger> challengeTriggers) {
|
||||
this.scene = scene;
|
||||
this.group = group;
|
||||
this.challengeId = challengeId;
|
||||
this.challengeIndex = challengeIndex;
|
||||
this.paramList = paramList;
|
||||
this.timeLimit = timeLimit;
|
||||
this.challengeTriggers = challengeTriggers;
|
||||
this.goal = goal;
|
||||
this.score = new AtomicInteger(0);
|
||||
}
|
||||
|
||||
public boolean inProgress() {
|
||||
return this.progress;
|
||||
}
|
||||
|
||||
public void onCheckTimeOut() {
|
||||
if (!inProgress()) {
|
||||
return;
|
||||
}
|
||||
if (timeLimit <= 0) {
|
||||
return;
|
||||
}
|
||||
challengeTriggers.forEach(t -> t.onCheckTimeout(this));
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (inProgress()) {
|
||||
Grasscutter.getLogger().info("Could not start a in progress challenge.");
|
||||
return;
|
||||
}
|
||||
this.progress = true;
|
||||
this.startedAt = System.currentTimeMillis();
|
||||
getScene().broadcastPacket(new PacketDungeonChallengeBeginNotify(this));
|
||||
challengeTriggers.forEach(t -> t.onBegin(this));
|
||||
}
|
||||
|
||||
public void done() {
|
||||
if (!this.inProgress()) return;
|
||||
this.finish(true);
|
||||
|
||||
var scene = this.getScene();
|
||||
var dungeonManager = scene.getDungeonManager();
|
||||
if (dungeonManager != null && dungeonManager.getDungeonData() != null) {
|
||||
scene
|
||||
.getPlayers()
|
||||
.forEach(
|
||||
p ->
|
||||
p.getActivityManager()
|
||||
.triggerWatcher(
|
||||
WatcherTriggerType.TRIGGER_FINISH_CHALLENGE,
|
||||
String.valueOf(dungeonManager.getDungeonData().getId()),
|
||||
String.valueOf(this.getGroup().id),
|
||||
String.valueOf(this.getChallengeId())));
|
||||
}
|
||||
|
||||
scene
|
||||
.getScriptManager()
|
||||
.callEvent(
|
||||
// TODO record the time in PARAM2 and used in action
|
||||
new ScriptArgs(this.getGroup().id, EventType.EVENT_CHALLENGE_SUCCESS)
|
||||
.setParam2(finishedTime));
|
||||
this.getScene()
|
||||
.triggerDungeonEvent(
|
||||
DungeonPassConditionType.DUNGEON_COND_FINISH_CHALLENGE,
|
||||
getChallengeId(),
|
||||
getChallengeIndex());
|
||||
|
||||
this.challengeTriggers.forEach(t -> t.onFinish(this));
|
||||
}
|
||||
|
||||
public void fail() {
|
||||
if (!this.inProgress()) return;
|
||||
this.finish(false);
|
||||
|
||||
this.getScene()
|
||||
.getScriptManager()
|
||||
.callEvent(new ScriptArgs(this.getGroup().id, EventType.EVENT_CHALLENGE_FAIL));
|
||||
challengeTriggers.forEach(t -> t.onFinish(this));
|
||||
}
|
||||
|
||||
private void finish(boolean success) {
|
||||
this.progress = false;
|
||||
this.success = success;
|
||||
this.finishedTime = (int) ((this.scene.getSceneTimeSeconds() - this.startedAt));
|
||||
getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this));
|
||||
}
|
||||
|
||||
public int increaseScore() {
|
||||
return score.incrementAndGet();
|
||||
}
|
||||
|
||||
public void onMonsterDeath(EntityMonster monster) {
|
||||
if (!inProgress()) {
|
||||
return;
|
||||
}
|
||||
if (monster.getGroupId() != getGroup().id) {
|
||||
return;
|
||||
}
|
||||
this.challengeTriggers.forEach(t -> t.onMonsterDeath(this, monster));
|
||||
}
|
||||
|
||||
public void onGadgetDeath(EntityGadget gadget) {
|
||||
if (!inProgress()) {
|
||||
return;
|
||||
}
|
||||
if (gadget.getGroupId() != getGroup().id) {
|
||||
return;
|
||||
}
|
||||
this.challengeTriggers.forEach(t -> t.onGadgetDeath(this, gadget));
|
||||
}
|
||||
|
||||
public void onGroupTriggerDeath(SceneTrigger trigger) {
|
||||
if (!this.inProgress()) return;
|
||||
|
||||
var triggerGroup = trigger.getCurrentGroup();
|
||||
if (triggerGroup == null || triggerGroup.id != getGroup().id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.challengeTriggers.forEach(t -> t.onGroupTrigger(this, trigger));
|
||||
}
|
||||
|
||||
public void onGadgetDamage(EntityGadget gadget) {
|
||||
if (!inProgress()) {
|
||||
return;
|
||||
}
|
||||
if (gadget.getGroupId() != getGroup().id) {
|
||||
return;
|
||||
}
|
||||
this.challengeTriggers.forEach(t -> t.onGadgetDamage(this, gadget));
|
||||
}
|
||||
}
|
||||
|
@ -1,370 +1,367 @@
|
||||
package emu.grasscutter.game.entity;
|
||||
|
||||
import emu.grasscutter.GameConstants;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.avatar.AvatarData;
|
||||
import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData;
|
||||
import emu.grasscutter.game.avatar.Avatar;
|
||||
import emu.grasscutter.game.inventory.EquipType;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.EntityIdType;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.game.props.PlayerProperty;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.net.proto.AbilityControlBlockOuterClass.AbilityControlBlock;
|
||||
import emu.grasscutter.net.proto.AbilityEmbryoOuterClass.AbilityEmbryo;
|
||||
import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo;
|
||||
import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair;
|
||||
import emu.grasscutter.net.proto.ChangeEnergyReasonOuterClass.ChangeEnergyReason;
|
||||
import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason;
|
||||
import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo;
|
||||
import emu.grasscutter.net.proto.EntityClientDataOuterClass.EntityClientData;
|
||||
import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo;
|
||||
import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType;
|
||||
import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason;
|
||||
import emu.grasscutter.net.proto.PropPairOuterClass.PropPair;
|
||||
import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType;
|
||||
import emu.grasscutter.net.proto.SceneAvatarInfoOuterClass.SceneAvatarInfo;
|
||||
import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo;
|
||||
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
|
||||
import emu.grasscutter.net.proto.VectorOuterClass.Vector;
|
||||
import emu.grasscutter.server.event.player.PlayerMoveEvent;
|
||||
import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import emu.grasscutter.utils.ProtoHelper;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import it.unimi.dsi.fastutil.ints.Int2FloatMap;
|
||||
import lombok.Getter;
|
||||
import lombok.val;
|
||||
|
||||
public class EntityAvatar extends GameEntity {
|
||||
@Getter private final Avatar avatar;
|
||||
|
||||
@Getter private PlayerDieType killedType;
|
||||
@Getter private int killedBy;
|
||||
|
||||
public EntityAvatar(Avatar avatar) {
|
||||
this(null, avatar);
|
||||
}
|
||||
|
||||
public EntityAvatar(Scene scene, Avatar avatar) {
|
||||
super(scene);
|
||||
|
||||
this.avatar = avatar;
|
||||
this.avatar.setCurrentEnergy();
|
||||
|
||||
if (getScene() != null) {
|
||||
this.id = getScene().getWorld().getNextEntityId(EntityIdType.AVATAR);
|
||||
|
||||
var weapon = getAvatar().getWeapon();
|
||||
if (weapon != null) {
|
||||
weapon.setWeaponEntityId(getScene().getWorld().getNextEntityId(EntityIdType.WEAPON));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEntityTypeId() {
|
||||
return this.getAvatar().getAvatarId();
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return this.avatar.getPlayer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getPosition() {
|
||||
return getPlayer().getPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getRotation() {
|
||||
return getPlayer().getRotation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAlive() {
|
||||
return this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) > 0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Int2FloatMap getFightProperties() {
|
||||
return getAvatar().getFightProperties();
|
||||
}
|
||||
|
||||
public int getWeaponEntityId() {
|
||||
if (getAvatar().getWeapon() != null) {
|
||||
return getAvatar().getWeapon().getWeaponEntityId();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeath(int killerId) {
|
||||
super.onDeath(killerId); // Invoke super class's onDeath() method.
|
||||
|
||||
this.killedType = PlayerDieType.PLAYER_DIE_TYPE_KILL_BY_MONSTER;
|
||||
this.killedBy = killerId;
|
||||
clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_NONE);
|
||||
}
|
||||
|
||||
public void onDeath(PlayerDieType dieType, int killerId) {
|
||||
super.onDeath(killerId); // Invoke super class's onDeath() method.
|
||||
|
||||
this.killedType = dieType;
|
||||
this.killedBy = killerId;
|
||||
clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_NONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float heal(float amount) {
|
||||
// Do not heal character if they are dead
|
||||
if (!this.isAlive()) {
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float healed = super.heal(amount);
|
||||
|
||||
if (healed > 0f) {
|
||||
getScene()
|
||||
.broadcastPacket(
|
||||
new PacketEntityFightPropChangeReasonNotify(
|
||||
this,
|
||||
FightProperty.FIGHT_PROP_CUR_HP,
|
||||
healed,
|
||||
PropChangeReason.PROP_CHANGE_REASON_ABILITY,
|
||||
ChangeHpReason.CHANGE_HP_REASON_ADD_ABILITY));
|
||||
}
|
||||
|
||||
return healed;
|
||||
}
|
||||
|
||||
public void clearEnergy(ChangeEnergyReason reason) {
|
||||
// Fight props.
|
||||
val curEnergyProp = this.getAvatar().getSkillDepot().getElementType().getCurEnergyProp();
|
||||
float curEnergy = this.getFightProperty(curEnergyProp);
|
||||
|
||||
// Set energy to zero.
|
||||
this.avatar.setCurrentEnergy(curEnergyProp, 0);
|
||||
|
||||
// Send packets.
|
||||
this.getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, curEnergyProp));
|
||||
|
||||
if (reason == ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START) {
|
||||
this.getScene()
|
||||
.broadcastPacket(
|
||||
new PacketEntityFightPropChangeReasonNotify(this, curEnergyProp, -curEnergy, reason));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a fixed amount of energy to the current avatar.
|
||||
*
|
||||
* @param amount The amount of energy to add.
|
||||
* @return True if the energy was added, false if the energy was not added.
|
||||
*/
|
||||
public boolean addEnergy(float amount) {
|
||||
var curEnergyProp = this.getAvatar().getSkillDepot().getElementType().getCurEnergyProp();
|
||||
var curEnergy = this.getFightProperty(curEnergyProp);
|
||||
if (curEnergy == amount) return false;
|
||||
|
||||
this.getAvatar().setCurrentEnergy(curEnergyProp, amount);
|
||||
this.getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, curEnergyProp));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void addEnergy(float amount, PropChangeReason reason) {
|
||||
this.addEnergy(amount, reason, false);
|
||||
}
|
||||
|
||||
public void addEnergy(float amount, PropChangeReason reason, boolean isFlat) {
|
||||
// Get current and maximum energy for this avatar.
|
||||
val elementType = this.getAvatar().getSkillDepot().getElementType();
|
||||
val curEnergyProp = elementType.getCurEnergyProp();
|
||||
val maxEnergyProp = elementType.getMaxEnergyProp();
|
||||
|
||||
float curEnergy = this.getFightProperty(curEnergyProp);
|
||||
float maxEnergy = this.getFightProperty(maxEnergyProp);
|
||||
|
||||
// Scale amount by energy recharge, if the amount is not flat.
|
||||
if (!isFlat) {
|
||||
amount *= this.getFightProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY);
|
||||
}
|
||||
|
||||
// Determine the new energy value.
|
||||
float newEnergy = Math.min(curEnergy + amount, maxEnergy);
|
||||
|
||||
// Set energy and notify.
|
||||
if (newEnergy != curEnergy) {
|
||||
this.avatar.setCurrentEnergy(curEnergyProp, newEnergy);
|
||||
|
||||
this.getScene()
|
||||
.broadcastPacket(new PacketAvatarFightPropUpdateNotify(this.getAvatar(), curEnergyProp));
|
||||
this.getScene()
|
||||
.broadcastPacket(
|
||||
new PacketEntityFightPropChangeReasonNotify(this, curEnergyProp, newEnergy, reason));
|
||||
}
|
||||
}
|
||||
|
||||
public SceneAvatarInfo getSceneAvatarInfo() {
|
||||
val avatar = this.getAvatar();
|
||||
val player = this.getPlayer();
|
||||
SceneAvatarInfo.Builder avatarInfo =
|
||||
SceneAvatarInfo.newBuilder()
|
||||
.setUid(player.getUid())
|
||||
.setAvatarId(avatar.getAvatarId())
|
||||
.setGuid(avatar.getGuid())
|
||||
.setPeerId(player.getPeerId())
|
||||
.addAllTalentIdList(avatar.getTalentIdList())
|
||||
.setCoreProudSkillLevel(avatar.getCoreProudSkillLevel())
|
||||
.putAllSkillLevelMap(avatar.getSkillLevelMap())
|
||||
.setSkillDepotId(avatar.getSkillDepotId())
|
||||
.addAllInherentProudSkillList(avatar.getProudSkillList())
|
||||
.putAllProudSkillExtraLevelMap(avatar.getProudSkillBonusMap())
|
||||
.addAllTeamResonanceList(player.getTeamManager().getTeamResonances())
|
||||
.setWearingFlycloakId(avatar.getFlyCloak())
|
||||
.setCostumeId(avatar.getCostume())
|
||||
.setBornTime(avatar.getBornTime());
|
||||
|
||||
for (GameItem item : avatar.getEquips().values()) {
|
||||
if (item.getItemData().getEquipType() == EquipType.EQUIP_WEAPON) {
|
||||
avatarInfo.setWeapon(item.createSceneWeaponInfo());
|
||||
} else {
|
||||
avatarInfo.addReliquaryList(item.createSceneReliquaryInfo());
|
||||
}
|
||||
avatarInfo.addEquipIdList(item.getItemId());
|
||||
}
|
||||
|
||||
return avatarInfo.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SceneEntityInfo toProto() {
|
||||
EntityAuthorityInfo authority =
|
||||
EntityAuthorityInfo.newBuilder()
|
||||
.setAbilityInfo(AbilitySyncStateInfo.newBuilder())
|
||||
.setRendererChangedInfo(EntityRendererChangedInfo.newBuilder())
|
||||
.setAiInfo(
|
||||
SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(Vector.newBuilder()))
|
||||
.setBornPos(Vector.newBuilder())
|
||||
.build();
|
||||
|
||||
SceneEntityInfo.Builder entityInfo =
|
||||
SceneEntityInfo.newBuilder()
|
||||
.setEntityId(getId())
|
||||
.setEntityType(ProtEntityType.PROT_ENTITY_TYPE_AVATAR)
|
||||
.addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder())
|
||||
.setEntityClientData(EntityClientData.newBuilder())
|
||||
.setEntityAuthorityInfo(authority)
|
||||
.setLastMoveSceneTimeMs(this.getLastMoveSceneTimeMs())
|
||||
.setLastMoveReliableSeq(this.getLastMoveReliableSeq())
|
||||
.setLifeState(this.getLifeState().getValue());
|
||||
|
||||
if (this.getScene() != null) {
|
||||
entityInfo.setMotionInfo(this.getMotionInfo());
|
||||
}
|
||||
|
||||
this.addAllFightPropsToEntityInfo(entityInfo);
|
||||
|
||||
PropPair pair =
|
||||
PropPair.newBuilder()
|
||||
.setType(PlayerProperty.PROP_LEVEL.getId())
|
||||
.setPropValue(
|
||||
ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, getAvatar().getLevel()))
|
||||
.build();
|
||||
entityInfo.addPropList(pair);
|
||||
|
||||
entityInfo.setAvatar(this.getSceneAvatarInfo());
|
||||
|
||||
return entityInfo.build();
|
||||
}
|
||||
|
||||
public AbilityControlBlock getAbilityControlBlock() {
|
||||
AvatarData data = this.getAvatar().getAvatarData();
|
||||
AbilityControlBlock.Builder abilityControlBlock = AbilityControlBlock.newBuilder();
|
||||
int embryoId = 0;
|
||||
|
||||
// Add avatar abilities
|
||||
if (data.getAbilities() != null) {
|
||||
for (int id : data.getAbilities()) {
|
||||
AbilityEmbryo emb =
|
||||
AbilityEmbryo.newBuilder()
|
||||
.setAbilityId(++embryoId)
|
||||
.setAbilityNameHash(id)
|
||||
.setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME)
|
||||
.build();
|
||||
abilityControlBlock.addAbilityEmbryoList(emb);
|
||||
}
|
||||
}
|
||||
// Add default abilities
|
||||
for (int id : GameConstants.DEFAULT_ABILITY_HASHES) {
|
||||
AbilityEmbryo emb =
|
||||
AbilityEmbryo.newBuilder()
|
||||
.setAbilityId(++embryoId)
|
||||
.setAbilityNameHash(id)
|
||||
.setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME)
|
||||
.build();
|
||||
abilityControlBlock.addAbilityEmbryoList(emb);
|
||||
}
|
||||
// Add team resonances
|
||||
for (int id : this.getPlayer().getTeamManager().getTeamResonancesConfig()) {
|
||||
AbilityEmbryo emb =
|
||||
AbilityEmbryo.newBuilder()
|
||||
.setAbilityId(++embryoId)
|
||||
.setAbilityNameHash(id)
|
||||
.setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME)
|
||||
.build();
|
||||
abilityControlBlock.addAbilityEmbryoList(emb);
|
||||
}
|
||||
// Add skill depot abilities
|
||||
AvatarSkillDepotData skillDepot =
|
||||
GameData.getAvatarSkillDepotDataMap().get(this.getAvatar().getSkillDepotId());
|
||||
if (skillDepot != null && skillDepot.getAbilities() != null) {
|
||||
for (int id : skillDepot.getAbilities()) {
|
||||
AbilityEmbryo emb =
|
||||
AbilityEmbryo.newBuilder()
|
||||
.setAbilityId(++embryoId)
|
||||
.setAbilityNameHash(id)
|
||||
.setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME)
|
||||
.build();
|
||||
abilityControlBlock.addAbilityEmbryoList(emb);
|
||||
}
|
||||
}
|
||||
// Add equip abilities
|
||||
if (this.getAvatar().getExtraAbilityEmbryos().size() > 0) {
|
||||
for (String skill : this.getAvatar().getExtraAbilityEmbryos()) {
|
||||
AbilityEmbryo emb =
|
||||
AbilityEmbryo.newBuilder()
|
||||
.setAbilityId(++embryoId)
|
||||
.setAbilityNameHash(Utils.abilityHash(skill))
|
||||
.setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME)
|
||||
.build();
|
||||
abilityControlBlock.addAbilityEmbryoList(emb);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
return abilityControlBlock.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this entity to a new position. Additionally invoke player move event.
|
||||
*
|
||||
* @param newPosition The new position.
|
||||
* @param rotation The new rotation.
|
||||
*/
|
||||
@Override
|
||||
public void move(Position newPosition, Position rotation) {
|
||||
// Invoke player move event.
|
||||
PlayerMoveEvent event =
|
||||
new PlayerMoveEvent(
|
||||
this.getPlayer(), PlayerMoveEvent.MoveType.PLAYER, this.getPosition(), newPosition);
|
||||
event.call();
|
||||
|
||||
// Set position and rotation.
|
||||
super.move(event.getDestination(), rotation);
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.entity;
|
||||
|
||||
import emu.grasscutter.GameConstants;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.avatar.AvatarData;
|
||||
import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData;
|
||||
import emu.grasscutter.game.avatar.Avatar;
|
||||
import emu.grasscutter.game.inventory.EquipType;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.EntityIdType;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.game.props.PlayerProperty;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.net.proto.AbilityControlBlockOuterClass.AbilityControlBlock;
|
||||
import emu.grasscutter.net.proto.AbilityEmbryoOuterClass.AbilityEmbryo;
|
||||
import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo;
|
||||
import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair;
|
||||
import emu.grasscutter.net.proto.ChangeEnergyReasonOuterClass.ChangeEnergyReason;
|
||||
import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason;
|
||||
import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo;
|
||||
import emu.grasscutter.net.proto.EntityClientDataOuterClass.EntityClientData;
|
||||
import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo;
|
||||
import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType;
|
||||
import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason;
|
||||
import emu.grasscutter.net.proto.PropPairOuterClass.PropPair;
|
||||
import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType;
|
||||
import emu.grasscutter.net.proto.SceneAvatarInfoOuterClass.SceneAvatarInfo;
|
||||
import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo;
|
||||
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
|
||||
import emu.grasscutter.net.proto.VectorOuterClass.Vector;
|
||||
import emu.grasscutter.server.event.player.PlayerMoveEvent;
|
||||
import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import emu.grasscutter.utils.ProtoHelper;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import it.unimi.dsi.fastutil.ints.Int2FloatMap;
|
||||
import lombok.Getter;
|
||||
import lombok.val;
|
||||
|
||||
public class EntityAvatar extends GameEntity {
|
||||
@Getter private final Avatar avatar;
|
||||
|
||||
@Getter private PlayerDieType killedType;
|
||||
@Getter private int killedBy;
|
||||
|
||||
public EntityAvatar(Avatar avatar) {
|
||||
this(null, avatar);
|
||||
}
|
||||
|
||||
public EntityAvatar(Scene scene, Avatar avatar) {
|
||||
super(scene);
|
||||
|
||||
this.avatar = avatar;
|
||||
this.avatar.setCurrentEnergy();
|
||||
if (scene != null) this.id = getScene().getWorld().getNextEntityId(EntityIdType.AVATAR);
|
||||
|
||||
GameItem weapon = this.getAvatar().getWeapon();
|
||||
if (weapon != null) {
|
||||
weapon.setWeaponEntityId(getScene().getWorld().getNextEntityId(EntityIdType.WEAPON));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEntityTypeId() {
|
||||
return this.getAvatar().getAvatarId();
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return this.avatar.getPlayer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getPosition() {
|
||||
return getPlayer().getPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getRotation() {
|
||||
return getPlayer().getRotation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAlive() {
|
||||
return this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) > 0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Int2FloatMap getFightProperties() {
|
||||
return getAvatar().getFightProperties();
|
||||
}
|
||||
|
||||
public int getWeaponEntityId() {
|
||||
if (getAvatar().getWeapon() != null) {
|
||||
return getAvatar().getWeapon().getWeaponEntityId();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeath(int killerId) {
|
||||
super.onDeath(killerId); // Invoke super class's onDeath() method.
|
||||
|
||||
this.killedType = PlayerDieType.PLAYER_DIE_TYPE_KILL_BY_MONSTER;
|
||||
this.killedBy = killerId;
|
||||
clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_NONE);
|
||||
}
|
||||
|
||||
public void onDeath(PlayerDieType dieType, int killerId) {
|
||||
super.onDeath(killerId); // Invoke super class's onDeath() method.
|
||||
|
||||
this.killedType = dieType;
|
||||
this.killedBy = killerId;
|
||||
clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_NONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float heal(float amount) {
|
||||
// Do not heal character if they are dead
|
||||
if (!this.isAlive()) {
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float healed = super.heal(amount);
|
||||
|
||||
if (healed > 0f) {
|
||||
getScene()
|
||||
.broadcastPacket(
|
||||
new PacketEntityFightPropChangeReasonNotify(
|
||||
this,
|
||||
FightProperty.FIGHT_PROP_CUR_HP,
|
||||
healed,
|
||||
PropChangeReason.PROP_CHANGE_REASON_ABILITY,
|
||||
ChangeHpReason.CHANGE_HP_REASON_ADD_ABILITY));
|
||||
}
|
||||
|
||||
return healed;
|
||||
}
|
||||
|
||||
public void clearEnergy(ChangeEnergyReason reason) {
|
||||
// Fight props.
|
||||
val curEnergyProp = this.getAvatar().getSkillDepot().getElementType().getCurEnergyProp();
|
||||
float curEnergy = this.getFightProperty(curEnergyProp);
|
||||
|
||||
// Set energy to zero.
|
||||
this.avatar.setCurrentEnergy(curEnergyProp, 0);
|
||||
|
||||
// Send packets.
|
||||
this.getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, curEnergyProp));
|
||||
|
||||
if (reason == ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START) {
|
||||
this.getScene()
|
||||
.broadcastPacket(
|
||||
new PacketEntityFightPropChangeReasonNotify(this, curEnergyProp, -curEnergy, reason));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a fixed amount of energy to the current avatar.
|
||||
*
|
||||
* @param amount The amount of energy to add.
|
||||
* @return True if the energy was added, false if the energy was not added.
|
||||
*/
|
||||
public boolean addEnergy(float amount) {
|
||||
var curEnergyProp = this.getAvatar().getSkillDepot().getElementType().getCurEnergyProp();
|
||||
var curEnergy = this.getFightProperty(curEnergyProp);
|
||||
if (curEnergy == amount) return false;
|
||||
|
||||
this.getAvatar().setCurrentEnergy(curEnergyProp, amount);
|
||||
this.getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, curEnergyProp));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void addEnergy(float amount, PropChangeReason reason) {
|
||||
this.addEnergy(amount, reason, false);
|
||||
}
|
||||
|
||||
public void addEnergy(float amount, PropChangeReason reason, boolean isFlat) {
|
||||
// Get current and maximum energy for this avatar.
|
||||
val elementType = this.getAvatar().getSkillDepot().getElementType();
|
||||
val curEnergyProp = elementType.getCurEnergyProp();
|
||||
val maxEnergyProp = elementType.getMaxEnergyProp();
|
||||
|
||||
float curEnergy = this.getFightProperty(curEnergyProp);
|
||||
float maxEnergy = this.getFightProperty(maxEnergyProp);
|
||||
|
||||
// Scale amount by energy recharge, if the amount is not flat.
|
||||
if (!isFlat) {
|
||||
amount *= this.getFightProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY);
|
||||
}
|
||||
|
||||
// Determine the new energy value.
|
||||
float newEnergy = Math.min(curEnergy + amount, maxEnergy);
|
||||
|
||||
// Set energy and notify.
|
||||
if (newEnergy != curEnergy) {
|
||||
this.avatar.setCurrentEnergy(curEnergyProp, newEnergy);
|
||||
|
||||
this.getScene()
|
||||
.broadcastPacket(new PacketAvatarFightPropUpdateNotify(this.getAvatar(), curEnergyProp));
|
||||
this.getScene()
|
||||
.broadcastPacket(
|
||||
new PacketEntityFightPropChangeReasonNotify(this, curEnergyProp, newEnergy, reason));
|
||||
}
|
||||
}
|
||||
|
||||
public SceneAvatarInfo getSceneAvatarInfo() {
|
||||
val avatar = this.getAvatar();
|
||||
val player = this.getPlayer();
|
||||
SceneAvatarInfo.Builder avatarInfo =
|
||||
SceneAvatarInfo.newBuilder()
|
||||
.setUid(player.getUid())
|
||||
.setAvatarId(avatar.getAvatarId())
|
||||
.setGuid(avatar.getGuid())
|
||||
.setPeerId(player.getPeerId())
|
||||
.addAllTalentIdList(avatar.getTalentIdList())
|
||||
.setCoreProudSkillLevel(avatar.getCoreProudSkillLevel())
|
||||
.putAllSkillLevelMap(avatar.getSkillLevelMap())
|
||||
.setSkillDepotId(avatar.getSkillDepotId())
|
||||
.addAllInherentProudSkillList(avatar.getProudSkillList())
|
||||
.putAllProudSkillExtraLevelMap(avatar.getProudSkillBonusMap())
|
||||
.addAllTeamResonanceList(player.getTeamManager().getTeamResonances())
|
||||
.setWearingFlycloakId(avatar.getFlyCloak())
|
||||
.setCostumeId(avatar.getCostume())
|
||||
.setBornTime(avatar.getBornTime());
|
||||
|
||||
for (GameItem item : avatar.getEquips().values()) {
|
||||
if (item.getItemData().getEquipType() == EquipType.EQUIP_WEAPON) {
|
||||
avatarInfo.setWeapon(item.createSceneWeaponInfo());
|
||||
} else {
|
||||
avatarInfo.addReliquaryList(item.createSceneReliquaryInfo());
|
||||
}
|
||||
avatarInfo.addEquipIdList(item.getItemId());
|
||||
}
|
||||
|
||||
return avatarInfo.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SceneEntityInfo toProto() {
|
||||
EntityAuthorityInfo authority =
|
||||
EntityAuthorityInfo.newBuilder()
|
||||
.setAbilityInfo(AbilitySyncStateInfo.newBuilder())
|
||||
.setRendererChangedInfo(EntityRendererChangedInfo.newBuilder())
|
||||
.setAiInfo(
|
||||
SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(Vector.newBuilder()))
|
||||
.setBornPos(Vector.newBuilder())
|
||||
.build();
|
||||
|
||||
SceneEntityInfo.Builder entityInfo =
|
||||
SceneEntityInfo.newBuilder()
|
||||
.setEntityId(getId())
|
||||
.setEntityType(ProtEntityType.PROT_ENTITY_TYPE_AVATAR)
|
||||
.addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder())
|
||||
.setEntityClientData(EntityClientData.newBuilder())
|
||||
.setEntityAuthorityInfo(authority)
|
||||
.setLastMoveSceneTimeMs(this.getLastMoveSceneTimeMs())
|
||||
.setLastMoveReliableSeq(this.getLastMoveReliableSeq())
|
||||
.setLifeState(this.getLifeState().getValue());
|
||||
|
||||
if (this.getScene() != null) {
|
||||
entityInfo.setMotionInfo(this.getMotionInfo());
|
||||
}
|
||||
|
||||
this.addAllFightPropsToEntityInfo(entityInfo);
|
||||
|
||||
PropPair pair =
|
||||
PropPair.newBuilder()
|
||||
.setType(PlayerProperty.PROP_LEVEL.getId())
|
||||
.setPropValue(
|
||||
ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, getAvatar().getLevel()))
|
||||
.build();
|
||||
entityInfo.addPropList(pair);
|
||||
|
||||
entityInfo.setAvatar(this.getSceneAvatarInfo());
|
||||
|
||||
return entityInfo.build();
|
||||
}
|
||||
|
||||
public AbilityControlBlock getAbilityControlBlock() {
|
||||
AvatarData data = this.getAvatar().getAvatarData();
|
||||
AbilityControlBlock.Builder abilityControlBlock = AbilityControlBlock.newBuilder();
|
||||
int embryoId = 0;
|
||||
|
||||
// Add avatar abilities
|
||||
if (data.getAbilities() != null) {
|
||||
for (int id : data.getAbilities()) {
|
||||
AbilityEmbryo emb =
|
||||
AbilityEmbryo.newBuilder()
|
||||
.setAbilityId(++embryoId)
|
||||
.setAbilityNameHash(id)
|
||||
.setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME)
|
||||
.build();
|
||||
abilityControlBlock.addAbilityEmbryoList(emb);
|
||||
}
|
||||
}
|
||||
// Add default abilities
|
||||
for (int id : GameConstants.DEFAULT_ABILITY_HASHES) {
|
||||
AbilityEmbryo emb =
|
||||
AbilityEmbryo.newBuilder()
|
||||
.setAbilityId(++embryoId)
|
||||
.setAbilityNameHash(id)
|
||||
.setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME)
|
||||
.build();
|
||||
abilityControlBlock.addAbilityEmbryoList(emb);
|
||||
}
|
||||
// Add team resonances
|
||||
for (int id : this.getPlayer().getTeamManager().getTeamResonancesConfig()) {
|
||||
AbilityEmbryo emb =
|
||||
AbilityEmbryo.newBuilder()
|
||||
.setAbilityId(++embryoId)
|
||||
.setAbilityNameHash(id)
|
||||
.setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME)
|
||||
.build();
|
||||
abilityControlBlock.addAbilityEmbryoList(emb);
|
||||
}
|
||||
// Add skill depot abilities
|
||||
AvatarSkillDepotData skillDepot =
|
||||
GameData.getAvatarSkillDepotDataMap().get(this.getAvatar().getSkillDepotId());
|
||||
if (skillDepot != null && skillDepot.getAbilities() != null) {
|
||||
for (int id : skillDepot.getAbilities()) {
|
||||
AbilityEmbryo emb =
|
||||
AbilityEmbryo.newBuilder()
|
||||
.setAbilityId(++embryoId)
|
||||
.setAbilityNameHash(id)
|
||||
.setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME)
|
||||
.build();
|
||||
abilityControlBlock.addAbilityEmbryoList(emb);
|
||||
}
|
||||
}
|
||||
// Add equip abilities
|
||||
if (this.getAvatar().getExtraAbilityEmbryos().size() > 0) {
|
||||
for (String skill : this.getAvatar().getExtraAbilityEmbryos()) {
|
||||
AbilityEmbryo emb =
|
||||
AbilityEmbryo.newBuilder()
|
||||
.setAbilityId(++embryoId)
|
||||
.setAbilityNameHash(Utils.abilityHash(skill))
|
||||
.setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME)
|
||||
.build();
|
||||
abilityControlBlock.addAbilityEmbryoList(emb);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
return abilityControlBlock.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this entity to a new position. Additionally invoke player move event.
|
||||
*
|
||||
* @param newPosition The new position.
|
||||
* @param rotation The new rotation.
|
||||
*/
|
||||
@Override
|
||||
public void move(Position newPosition, Position rotation) {
|
||||
// Invoke player move event.
|
||||
PlayerMoveEvent event =
|
||||
new PlayerMoveEvent(
|
||||
this.getPlayer(), PlayerMoveEvent.MoveType.PLAYER, this.getPosition(), newPosition);
|
||||
event.call();
|
||||
|
||||
// Set position and rotation.
|
||||
super.move(event.getDestination(), rotation);
|
||||
}
|
||||
}
|
||||
|
@ -1,63 +1,91 @@
|
||||
package emu.grasscutter.game.entity;
|
||||
|
||||
import emu.grasscutter.data.binout.config.ConfigEntityGadget;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import lombok.Getter;
|
||||
|
||||
public abstract class EntityBaseGadget extends GameEntity {
|
||||
@Getter(onMethod_ = @Override)
|
||||
protected final Position position;
|
||||
|
||||
@Getter(onMethod_ = @Override)
|
||||
protected final Position rotation;
|
||||
|
||||
public EntityBaseGadget(Scene scene) {
|
||||
this(scene, null, null);
|
||||
}
|
||||
|
||||
public EntityBaseGadget(Scene scene, Position position, Position rotation) {
|
||||
super(scene);
|
||||
this.position = position != null ? position.clone() : new Position();
|
||||
this.rotation = rotation != null ? rotation.clone() : new Position();
|
||||
}
|
||||
|
||||
public abstract int getGadgetId();
|
||||
|
||||
@Override
|
||||
public int getEntityTypeId() {
|
||||
return this.getGadgetId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeath(int killerId) {
|
||||
super.onDeath(killerId); // Invoke super class's onDeath() method.
|
||||
}
|
||||
|
||||
protected void fillFightProps(ConfigEntityGadget configGadget) {
|
||||
if (configGadget == null || configGadget.getCombat() == null) {
|
||||
return;
|
||||
}
|
||||
var combatData = configGadget.getCombat();
|
||||
var combatProperties = combatData.getProperty();
|
||||
|
||||
var targetHp = combatProperties.getHP();
|
||||
setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, targetHp);
|
||||
setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, targetHp);
|
||||
if (combatProperties.isInvincible()) {
|
||||
targetHp = Float.POSITIVE_INFINITY;
|
||||
}
|
||||
setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, targetHp);
|
||||
|
||||
var atk = combatProperties.getAttack();
|
||||
setFightProperty(FightProperty.FIGHT_PROP_BASE_ATTACK, atk);
|
||||
setFightProperty(FightProperty.FIGHT_PROP_CUR_ATTACK, atk);
|
||||
|
||||
var def = combatProperties.getDefence();
|
||||
setFightProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE, def);
|
||||
setFightProperty(FightProperty.FIGHT_PROP_CUR_DEFENSE, def);
|
||||
|
||||
setLockHP(combatProperties.isLockHP());
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.entity;
|
||||
|
||||
import emu.grasscutter.data.binout.config.ConfigEntityGadget;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.game.quest.enums.QuestContent;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.scripts.data.ScriptArgs;
|
||||
import emu.grasscutter.server.event.entity.EntityDamageEvent;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import lombok.Getter;
|
||||
|
||||
import static emu.grasscutter.scripts.constants.EventType.EVENT_SPECIFIC_GADGET_HP_CHANGE;
|
||||
|
||||
public abstract class EntityBaseGadget extends GameEntity {
|
||||
@Getter(onMethod_ = @Override)
|
||||
protected final Position position;
|
||||
|
||||
@Getter(onMethod_ = @Override)
|
||||
protected final Position rotation;
|
||||
|
||||
public EntityBaseGadget(Scene scene) {
|
||||
this(scene, null, null);
|
||||
}
|
||||
|
||||
public EntityBaseGadget(Scene scene, Position position, Position rotation) {
|
||||
super(scene);
|
||||
this.position = position != null ? position.clone() : new Position();
|
||||
this.rotation = rotation != null ? rotation.clone() : new Position();
|
||||
}
|
||||
|
||||
public abstract int getGadgetId();
|
||||
|
||||
@Override
|
||||
public int getEntityTypeId() {
|
||||
return this.getGadgetId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeath(int killerId) {
|
||||
super.onDeath(killerId); // Invoke super class's onDeath() method.
|
||||
|
||||
getScene()
|
||||
.getPlayers()
|
||||
.forEach(
|
||||
p ->
|
||||
p.getQuestManager()
|
||||
.queueEvent(QuestContent.QUEST_CONTENT_DESTROY_GADGET, this.getGadgetId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runLuaCallbacks(EntityDamageEvent event) {
|
||||
super.runLuaCallbacks(event);
|
||||
getScene()
|
||||
.getScriptManager()
|
||||
.callEvent(
|
||||
new ScriptArgs(
|
||||
this.getGroupId(),
|
||||
EVENT_SPECIFIC_GADGET_HP_CHANGE,
|
||||
getConfigId(),
|
||||
getGadgetId())
|
||||
.setSourceEntityId(getId())
|
||||
.setParam3((int) this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP))
|
||||
.setEventSource(Integer.toString(getConfigId())));
|
||||
}
|
||||
|
||||
protected void fillFightProps(ConfigEntityGadget configGadget) {
|
||||
if (configGadget == null || configGadget.getCombat() == null) {
|
||||
return;
|
||||
}
|
||||
var combatData = configGadget.getCombat();
|
||||
var combatProperties = combatData.getProperty();
|
||||
|
||||
var targetHp = combatProperties.getHP();
|
||||
setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, targetHp);
|
||||
setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, targetHp);
|
||||
if (combatProperties.isInvincible()) {
|
||||
targetHp = Float.POSITIVE_INFINITY;
|
||||
}
|
||||
setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, targetHp);
|
||||
|
||||
var atk = combatProperties.getAttack();
|
||||
setFightProperty(FightProperty.FIGHT_PROP_BASE_ATTACK, atk);
|
||||
setFightProperty(FightProperty.FIGHT_PROP_CUR_ATTACK, atk);
|
||||
|
||||
var def = combatProperties.getDefence();
|
||||
setFightProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE, def);
|
||||
setFightProperty(FightProperty.FIGHT_PROP_CUR_DEFENSE, def);
|
||||
|
||||
setLockHP(combatProperties.isLockHP());
|
||||
}
|
||||
}
|
||||
|
@ -1,96 +1,96 @@
|
||||
package emu.grasscutter.game.entity;
|
||||
|
||||
import emu.grasscutter.game.props.EntityIdType;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass;
|
||||
import emu.grasscutter.scripts.data.SceneRegion;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import it.unimi.dsi.fastutil.ints.Int2FloatMap;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class EntityRegion extends GameEntity {
|
||||
private final Position position;
|
||||
private final Set<Integer> entities; // Ids of entities inside this region
|
||||
private final SceneRegion metaRegion;
|
||||
private boolean hasNewEntities;
|
||||
private boolean entityLeave;
|
||||
|
||||
public EntityRegion(Scene scene, SceneRegion region) {
|
||||
super(scene);
|
||||
|
||||
this.id = getScene().getWorld().getNextEntityId(EntityIdType.REGION);
|
||||
this.setGroupId(region.group.id);
|
||||
this.setBlockId(region.group.block_id);
|
||||
this.setConfigId(region.config_id);
|
||||
this.position = region.pos.clone();
|
||||
this.entities = ConcurrentHashMap.newKeySet();
|
||||
this.metaRegion = region;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEntityTypeId() {
|
||||
return this.metaRegion.config_id;
|
||||
}
|
||||
|
||||
public void addEntity(GameEntity entity) {
|
||||
if (this.getEntities().contains(entity.getId())) {
|
||||
return;
|
||||
}
|
||||
this.getEntities().add(entity.getId());
|
||||
this.hasNewEntities = true;
|
||||
}
|
||||
|
||||
public boolean hasNewEntities() {
|
||||
return hasNewEntities;
|
||||
}
|
||||
|
||||
public void resetNewEntities() {
|
||||
hasNewEntities = false;
|
||||
}
|
||||
|
||||
public void removeEntity(int entityId) {
|
||||
this.getEntities().remove(entityId);
|
||||
this.entityLeave = true;
|
||||
}
|
||||
|
||||
public void removeEntity(GameEntity entity) {
|
||||
this.getEntities().remove(entity.getId());
|
||||
this.entityLeave = true;
|
||||
}
|
||||
|
||||
public boolean entityLeave() {
|
||||
return this.entityLeave;
|
||||
}
|
||||
|
||||
public void resetEntityLeave() {
|
||||
this.entityLeave = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Int2FloatMap getFightProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getRotation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SceneEntityInfoOuterClass.SceneEntityInfo toProto() {
|
||||
/** The Region Entity would not be sent to client. */
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getFirstEntityId() {
|
||||
return entities.stream().findFirst().orElse(0);
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.entity;
|
||||
|
||||
import emu.grasscutter.game.props.EntityIdType;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass;
|
||||
import emu.grasscutter.scripts.data.SceneRegion;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import it.unimi.dsi.fastutil.ints.Int2FloatMap;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class EntityRegion extends GameEntity {
|
||||
private final Position position;
|
||||
private final Set<Integer> entities; // Ids of entities inside this region
|
||||
private final SceneRegion metaRegion;
|
||||
private boolean hasNewEntities;
|
||||
private boolean entityLeave;
|
||||
|
||||
public EntityRegion(Scene scene, SceneRegion region) {
|
||||
super(scene);
|
||||
|
||||
this.id = getScene().getWorld().getNextEntityId(EntityIdType.REGION);
|
||||
this.setGroupId(region.group.id);
|
||||
this.setBlockId(region.group.block_id);
|
||||
this.setConfigId(region.config_id);
|
||||
this.position = region.pos.clone();
|
||||
this.entities = ConcurrentHashMap.newKeySet();
|
||||
this.metaRegion = region;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEntityTypeId() {
|
||||
return this.metaRegion.config_id;
|
||||
}
|
||||
|
||||
public void addEntity(GameEntity entity) {
|
||||
if (this.getEntities().contains(entity.getId())) {
|
||||
return;
|
||||
}
|
||||
this.getEntities().add(entity.getId());
|
||||
this.hasNewEntities = true;
|
||||
}
|
||||
|
||||
public boolean hasNewEntities() {
|
||||
return hasNewEntities;
|
||||
}
|
||||
|
||||
public void resetNewEntities() {
|
||||
hasNewEntities = false;
|
||||
}
|
||||
|
||||
public void removeEntity(int entityId) {
|
||||
this.getEntities().remove(entityId);
|
||||
this.entityLeave = true;
|
||||
}
|
||||
|
||||
public void removeEntity(GameEntity entity) {
|
||||
this.getEntities().remove(entity.getId());
|
||||
this.entityLeave = true;
|
||||
}
|
||||
|
||||
public boolean entityLeave() {
|
||||
return this.entityLeave;
|
||||
}
|
||||
|
||||
public void resetEntityLeave() {
|
||||
this.entityLeave = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Int2FloatMap getFightProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getRotation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SceneEntityInfoOuterClass.SceneEntityInfo toProto() {
|
||||
/** The Region Entity would not be sent to client. */
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getFirstEntityId() {
|
||||
return entities.stream().findFirst().orElse(0);
|
||||
}
|
||||
}
|
||||
|
@ -1,264 +1,271 @@
|
||||
package emu.grasscutter.game.entity;
|
||||
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.ElementType;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.game.props.LifeState;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.game.world.SpawnDataEntry;
|
||||
import emu.grasscutter.game.world.World;
|
||||
import emu.grasscutter.net.proto.FightPropPairOuterClass.FightPropPair;
|
||||
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
|
||||
import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo;
|
||||
import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState;
|
||||
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
|
||||
import emu.grasscutter.net.proto.VectorOuterClass.Vector;
|
||||
import emu.grasscutter.scripts.data.controller.EntityController;
|
||||
import emu.grasscutter.server.event.entity.EntityDamageEvent;
|
||||
import emu.grasscutter.server.event.entity.EntityDeathEvent;
|
||||
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import it.unimi.dsi.fastutil.ints.Int2FloatMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2FloatMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public abstract class GameEntity {
|
||||
@Getter private final Scene scene;
|
||||
@Getter protected int id;
|
||||
@Getter @Setter private SpawnDataEntry spawnEntry;
|
||||
|
||||
@Getter @Setter private int blockId;
|
||||
@Getter @Setter private int configId;
|
||||
@Getter @Setter private int groupId;
|
||||
|
||||
@Getter @Setter private MotionState motionState;
|
||||
@Getter @Setter private int lastMoveSceneTimeMs;
|
||||
@Getter @Setter private int lastMoveReliableSeq;
|
||||
|
||||
@Getter @Setter private boolean lockHP;
|
||||
|
||||
// Lua controller for specific actions
|
||||
@Getter @Setter private EntityController entityController;
|
||||
@Getter private ElementType lastAttackType = ElementType.None;
|
||||
|
||||
// Abilities
|
||||
private Object2FloatMap<String> metaOverrideMap;
|
||||
private Int2ObjectMap<String> metaModifiers;
|
||||
|
||||
public GameEntity(Scene scene) {
|
||||
this.scene = scene;
|
||||
this.motionState = MotionState.MOTION_STATE_NONE;
|
||||
}
|
||||
|
||||
public int getEntityType() {
|
||||
return this.getId() >> 24;
|
||||
}
|
||||
|
||||
public abstract int getEntityTypeId();
|
||||
|
||||
public World getWorld() {
|
||||
return this.getScene().getWorld();
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public LifeState getLifeState() {
|
||||
return this.isAlive() ? LifeState.LIFE_ALIVE : LifeState.LIFE_DEAD;
|
||||
}
|
||||
|
||||
public Object2FloatMap<String> getMetaOverrideMap() {
|
||||
if (this.metaOverrideMap == null) {
|
||||
this.metaOverrideMap = new Object2FloatOpenHashMap<>();
|
||||
}
|
||||
return this.metaOverrideMap;
|
||||
}
|
||||
|
||||
public Int2ObjectMap<String> getMetaModifiers() {
|
||||
if (this.metaModifiers == null) {
|
||||
this.metaModifiers = new Int2ObjectOpenHashMap<>();
|
||||
}
|
||||
return this.metaModifiers;
|
||||
}
|
||||
|
||||
public abstract Int2FloatMap getFightProperties();
|
||||
|
||||
public abstract Position getPosition();
|
||||
|
||||
public abstract Position getRotation();
|
||||
|
||||
public void setFightProperty(FightProperty prop, float value) {
|
||||
this.getFightProperties().put(prop.getId(), value);
|
||||
}
|
||||
|
||||
public void setFightProperty(int id, float value) {
|
||||
this.getFightProperties().put(id, value);
|
||||
}
|
||||
|
||||
public void addFightProperty(FightProperty prop, float value) {
|
||||
this.getFightProperties().put(prop.getId(), this.getFightProperty(prop) + value);
|
||||
}
|
||||
|
||||
public float getFightProperty(FightProperty prop) {
|
||||
return this.getFightProperties().getOrDefault(prop.getId(), 0f);
|
||||
}
|
||||
|
||||
public boolean hasFightProperty(FightProperty prop) {
|
||||
return this.getFightProperties().containsKey(prop.getId());
|
||||
}
|
||||
|
||||
public void addAllFightPropsToEntityInfo(SceneEntityInfo.Builder entityInfo) {
|
||||
this.getFightProperties()
|
||||
.forEach(
|
||||
(key, value) -> {
|
||||
if (key == 0) return;
|
||||
entityInfo.addFightPropList(
|
||||
FightPropPair.newBuilder().setPropType(key).setPropValue(value).build());
|
||||
});
|
||||
}
|
||||
|
||||
protected MotionInfo getMotionInfo() {
|
||||
return MotionInfo.newBuilder()
|
||||
.setPos(this.getPosition().toProto())
|
||||
.setRot(this.getRotation().toProto())
|
||||
.setSpeed(Vector.newBuilder())
|
||||
.setState(this.getMotionState())
|
||||
.build();
|
||||
}
|
||||
|
||||
public float heal(float amount) {
|
||||
if (this.getFightProperties() == null) {
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float curHp = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
|
||||
float maxHp = this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
|
||||
|
||||
if (curHp >= maxHp) {
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float healed = Math.min(maxHp - curHp, amount);
|
||||
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, healed);
|
||||
|
||||
this.getScene()
|
||||
.broadcastPacket(
|
||||
new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP));
|
||||
|
||||
return healed;
|
||||
}
|
||||
|
||||
public void damage(float amount) {
|
||||
this.damage(amount, 0, ElementType.None);
|
||||
}
|
||||
|
||||
public void damage(float amount, int killerId, ElementType attackType) {
|
||||
// Check if the entity has properties.
|
||||
if (this.getFightProperties() == null || !hasFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoke entity damage event.
|
||||
EntityDamageEvent event =
|
||||
new EntityDamageEvent(this, amount, attackType, this.getScene().getEntityById(killerId));
|
||||
event.call();
|
||||
if (event.isCanceled()) {
|
||||
return; // If the event is canceled, do not damage the entity.
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
// Check if dead
|
||||
boolean isDead = false;
|
||||
if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
|
||||
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
|
||||
isDead = true;
|
||||
}
|
||||
|
||||
// Packets
|
||||
this.getScene()
|
||||
.broadcastPacket(
|
||||
new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP));
|
||||
|
||||
// Check if dead.
|
||||
if (isDead) {
|
||||
this.getScene().killEntity(this, killerId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the Lua callbacks for {@link EntityDamageEvent}.
|
||||
*
|
||||
* @param event The damage event.
|
||||
*/
|
||||
public void runLuaCallbacks(EntityDamageEvent event) {
|
||||
if (entityController != null) {
|
||||
entityController.onBeHurt(this, event.getAttackElementType(), true); // todo is host handling
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this entity to a new position.
|
||||
*
|
||||
* @param position The new position.
|
||||
* @param rotation The new rotation.
|
||||
*/
|
||||
public void move(Position position, Position rotation) {
|
||||
// Set the position and rotation.
|
||||
this.getPosition().set(position);
|
||||
this.getRotation().set(rotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a player interacts with this entity
|
||||
*
|
||||
* @param player Player that is interacting with this entity
|
||||
* @param interactReq Interact request protobuf data
|
||||
*/
|
||||
public void onInteract(Player player, GadgetInteractReq interactReq) {}
|
||||
|
||||
/** Called when this entity is added to the world */
|
||||
public void onCreate() {}
|
||||
|
||||
public void onRemoved() {}
|
||||
|
||||
public void onTick(int sceneTime) {
|
||||
if (entityController != null) {
|
||||
entityController.onTimer(this, sceneTime);
|
||||
}
|
||||
}
|
||||
|
||||
public int onClientExecuteRequest(int param1, int param2, int param3) {
|
||||
if (entityController != null) {
|
||||
return entityController.onClientExecuteRequest(this, param1, param2, param3);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this entity dies
|
||||
*
|
||||
* @param killerId Entity id of the entity that killed this entity
|
||||
*/
|
||||
public void onDeath(int killerId) {
|
||||
// Invoke entity death event.
|
||||
EntityDeathEvent event = new EntityDeathEvent(this, killerId);
|
||||
event.call();
|
||||
|
||||
// Run Lua callbacks.
|
||||
if (entityController != null) {
|
||||
entityController.onDie(this, getLastAttackType());
|
||||
}
|
||||
}
|
||||
|
||||
public abstract SceneEntityInfo toProto();
|
||||
}
|
||||
package emu.grasscutter.game.entity;
|
||||
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.ElementType;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.game.props.LifeState;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.game.world.SpawnDataEntry;
|
||||
import emu.grasscutter.game.world.World;
|
||||
import emu.grasscutter.net.proto.FightPropPairOuterClass.FightPropPair;
|
||||
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
|
||||
import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo;
|
||||
import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState;
|
||||
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
|
||||
import emu.grasscutter.net.proto.VectorOuterClass.Vector;
|
||||
import emu.grasscutter.scripts.data.controller.EntityController;
|
||||
import emu.grasscutter.server.event.entity.EntityDamageEvent;
|
||||
import emu.grasscutter.server.event.entity.EntityDeathEvent;
|
||||
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import it.unimi.dsi.fastutil.ints.Int2FloatMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2FloatMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public abstract class GameEntity {
|
||||
@Getter private final Scene scene;
|
||||
@Getter protected int id;
|
||||
@Getter @Setter private SpawnDataEntry spawnEntry;
|
||||
|
||||
@Getter @Setter private int blockId;
|
||||
@Getter @Setter private int configId;
|
||||
@Getter @Setter private int groupId;
|
||||
|
||||
@Getter @Setter private MotionState motionState;
|
||||
@Getter @Setter private int lastMoveSceneTimeMs;
|
||||
@Getter @Setter private int lastMoveReliableSeq;
|
||||
|
||||
@Getter @Setter private boolean lockHP;
|
||||
|
||||
// Lua controller for specific actions
|
||||
@Getter @Setter private EntityController entityController;
|
||||
@Getter private ElementType lastAttackType = ElementType.None;
|
||||
|
||||
// Abilities
|
||||
private Object2FloatMap<String> metaOverrideMap;
|
||||
private Int2ObjectMap<String> metaModifiers;
|
||||
|
||||
public GameEntity(Scene scene) {
|
||||
this.scene = scene;
|
||||
this.motionState = MotionState.MOTION_STATE_NONE;
|
||||
}
|
||||
|
||||
public int getEntityType() {
|
||||
return this.getId() >> 24;
|
||||
}
|
||||
|
||||
public abstract int getEntityTypeId();
|
||||
|
||||
public World getWorld() {
|
||||
return this.getScene().getWorld();
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public LifeState getLifeState() {
|
||||
return this.isAlive() ? LifeState.LIFE_ALIVE : LifeState.LIFE_DEAD;
|
||||
}
|
||||
|
||||
public Object2FloatMap<String> getMetaOverrideMap() {
|
||||
if (this.metaOverrideMap == null) {
|
||||
this.metaOverrideMap = new Object2FloatOpenHashMap<>();
|
||||
}
|
||||
return this.metaOverrideMap;
|
||||
}
|
||||
|
||||
public Int2ObjectMap<String> getMetaModifiers() {
|
||||
if (this.metaModifiers == null) {
|
||||
this.metaModifiers = new Int2ObjectOpenHashMap<>();
|
||||
}
|
||||
return this.metaModifiers;
|
||||
}
|
||||
|
||||
public abstract Int2FloatMap getFightProperties();
|
||||
|
||||
public abstract Position getPosition();
|
||||
|
||||
public abstract Position getRotation();
|
||||
|
||||
public void setFightProperty(FightProperty prop, float value) {
|
||||
this.getFightProperties().put(prop.getId(), value);
|
||||
}
|
||||
|
||||
public void setFightProperty(int id, float value) {
|
||||
this.getFightProperties().put(id, value);
|
||||
}
|
||||
|
||||
public void addFightProperty(FightProperty prop, float value) {
|
||||
this.getFightProperties().put(prop.getId(), this.getFightProperty(prop) + value);
|
||||
}
|
||||
|
||||
public float getFightProperty(FightProperty prop) {
|
||||
return this.getFightProperties().getOrDefault(prop.getId(), 0f);
|
||||
}
|
||||
|
||||
public boolean hasFightProperty(FightProperty prop) {
|
||||
return this.getFightProperties().containsKey(prop.getId());
|
||||
}
|
||||
|
||||
public void addAllFightPropsToEntityInfo(SceneEntityInfo.Builder entityInfo) {
|
||||
this.getFightProperties()
|
||||
.forEach(
|
||||
(key, value) -> {
|
||||
if (key == 0) return;
|
||||
entityInfo.addFightPropList(
|
||||
FightPropPair.newBuilder().setPropType(key).setPropValue(value).build());
|
||||
});
|
||||
}
|
||||
|
||||
protected MotionInfo getMotionInfo() {
|
||||
return MotionInfo.newBuilder()
|
||||
.setPos(this.getPosition().toProto())
|
||||
.setRot(this.getRotation().toProto())
|
||||
.setSpeed(Vector.newBuilder())
|
||||
.setState(this.getMotionState())
|
||||
.build();
|
||||
}
|
||||
|
||||
public float heal(float amount) {
|
||||
if (this.getFightProperties() == null) {
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float curHp = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
|
||||
float maxHp = this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
|
||||
|
||||
if (curHp >= maxHp) {
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float healed = Math.min(maxHp - curHp, amount);
|
||||
this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, healed);
|
||||
|
||||
this.getScene()
|
||||
.broadcastPacket(
|
||||
new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP));
|
||||
|
||||
return healed;
|
||||
}
|
||||
|
||||
public void damage(float amount) {
|
||||
this.damage(amount, 0, ElementType.None);
|
||||
}
|
||||
|
||||
public void damage(float amount, ElementType attackType) {
|
||||
this.damage(amount, 0, attackType);
|
||||
}
|
||||
|
||||
public void damage(float amount, int killerId, ElementType attackType) {
|
||||
// Check if the entity has properties.
|
||||
if (this.getFightProperties() == null || !hasFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoke entity damage event.
|
||||
EntityDamageEvent event =
|
||||
new EntityDamageEvent(this, amount, attackType, this.getScene().getEntityById(killerId));
|
||||
event.call();
|
||||
if (event.isCanceled()) {
|
||||
return; // If the event is canceled, do not damage the entity.
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
this.lastAttackType = attackType;
|
||||
|
||||
// Check if dead
|
||||
boolean isDead = false;
|
||||
if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
|
||||
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
|
||||
isDead = true;
|
||||
}
|
||||
this.runLuaCallbacks(event);
|
||||
|
||||
// Packets
|
||||
this.getScene()
|
||||
.broadcastPacket(
|
||||
new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP));
|
||||
|
||||
// Check if dead.
|
||||
if (isDead) {
|
||||
this.getScene().killEntity(this, killerId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the Lua callbacks for {@link EntityDamageEvent}.
|
||||
*
|
||||
* @param event The damage event.
|
||||
*/
|
||||
public void runLuaCallbacks(EntityDamageEvent event) {
|
||||
if (entityController != null) {
|
||||
entityController.onBeHurt(this, event.getAttackElementType(), true); // todo is host handling
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this entity to a new position.
|
||||
*
|
||||
* @param position The new position.
|
||||
* @param rotation The new rotation.
|
||||
*/
|
||||
public void move(Position position, Position rotation) {
|
||||
// Set the position and rotation.
|
||||
this.getPosition().set(position);
|
||||
this.getRotation().set(rotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a player interacts with this entity
|
||||
*
|
||||
* @param player Player that is interacting with this entity
|
||||
* @param interactReq Interact request protobuf data
|
||||
*/
|
||||
public void onInteract(Player player, GadgetInteractReq interactReq) {}
|
||||
|
||||
/** Called when this entity is added to the world */
|
||||
public void onCreate() {}
|
||||
|
||||
public void onRemoved() {}
|
||||
|
||||
public void onTick(int sceneTime) {
|
||||
if (entityController != null) {
|
||||
entityController.onTimer(this, sceneTime);
|
||||
}
|
||||
}
|
||||
|
||||
public int onClientExecuteRequest(int param1, int param2, int param3) {
|
||||
if (entityController != null) {
|
||||
return entityController.onClientExecuteRequest(this, param1, param2, param3);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this entity dies
|
||||
*
|
||||
* @param killerId Entity id of the entity that killed this entity
|
||||
*/
|
||||
public void onDeath(int killerId) {
|
||||
// Invoke entity death event.
|
||||
EntityDeathEvent event = new EntityDeathEvent(this, killerId);
|
||||
event.call();
|
||||
|
||||
// Run Lua callbacks.
|
||||
if (entityController != null) {
|
||||
entityController.onDie(this, getLastAttackType());
|
||||
}
|
||||
}
|
||||
|
||||
public abstract SceneEntityInfo toProto();
|
||||
}
|
||||
|
@ -1,86 +1,98 @@
|
||||
package emu.grasscutter.game.entity.gadget;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.ItemData;
|
||||
import emu.grasscutter.game.entity.EntityGadget;
|
||||
import emu.grasscutter.game.entity.EntityItem;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
|
||||
import emu.grasscutter.net.proto.GatherGadgetInfoOuterClass.GatherGadgetInfo;
|
||||
import emu.grasscutter.net.proto.InteractTypeOuterClass.InteractType;
|
||||
import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo;
|
||||
import emu.grasscutter.server.packet.send.PacketGadgetInteractRsp;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
|
||||
public class GadgetGatherObject extends GadgetContent {
|
||||
private int itemId;
|
||||
private boolean isForbidGuest;
|
||||
|
||||
public GadgetGatherObject(EntityGadget gadget) {
|
||||
super(gadget);
|
||||
|
||||
if (gadget.getSpawnEntry() != null) {
|
||||
this.itemId = gadget.getSpawnEntry().getGatherItemId();
|
||||
}
|
||||
}
|
||||
|
||||
public int getItemId() {
|
||||
return this.itemId;
|
||||
}
|
||||
|
||||
public boolean isForbidGuest() {
|
||||
return isForbidGuest;
|
||||
}
|
||||
|
||||
public boolean onInteract(Player player, GadgetInteractReq req) {
|
||||
// Sanity check
|
||||
ItemData itemData = GameData.getItemDataMap().get(getItemId());
|
||||
if (itemData == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GameItem item = new GameItem(itemData, 1);
|
||||
player.getInventory().addItem(item, ActionReason.Gather);
|
||||
|
||||
getGadget()
|
||||
.getScene()
|
||||
.broadcastPacket(
|
||||
new PacketGadgetInteractRsp(getGadget(), InteractType.INTERACT_TYPE_GATHER));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) {
|
||||
GatherGadgetInfo gatherGadgetInfo =
|
||||
GatherGadgetInfo.newBuilder()
|
||||
.setItemId(this.getItemId())
|
||||
.setIsForbidGuest(this.isForbidGuest())
|
||||
.build();
|
||||
|
||||
gadgetInfo.setGatherGadget(gatherGadgetInfo);
|
||||
}
|
||||
|
||||
public void dropItems(Player player) {
|
||||
Scene scene = getGadget().getScene();
|
||||
int times = Utils.randomRange(1, 2);
|
||||
|
||||
for (int i = 0; i < times; i++) {
|
||||
EntityItem item =
|
||||
new EntityItem(
|
||||
scene,
|
||||
player,
|
||||
GameData.getItemDataMap().get(itemId),
|
||||
getGadget().getPosition().nearby2d(1f).addY(2f),
|
||||
1,
|
||||
true);
|
||||
|
||||
scene.addEntity(item);
|
||||
}
|
||||
|
||||
scene.killEntity(this.getGadget(), player.getTeamManager().getCurrentAvatarEntity().getId());
|
||||
// Todo: add record
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.entity.gadget;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.GatherData;
|
||||
import emu.grasscutter.data.excels.ItemData;
|
||||
import emu.grasscutter.game.entity.EntityGadget;
|
||||
import emu.grasscutter.game.entity.EntityItem;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
|
||||
import emu.grasscutter.net.proto.GatherGadgetInfoOuterClass.GatherGadgetInfo;
|
||||
import emu.grasscutter.net.proto.InteractTypeOuterClass.InteractType;
|
||||
import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo;
|
||||
import emu.grasscutter.server.packet.send.PacketGadgetInteractRsp;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
|
||||
public final class GadgetGatherObject extends GadgetContent {
|
||||
private int itemId;
|
||||
private boolean isForbidGuest;
|
||||
|
||||
public GadgetGatherObject(EntityGadget gadget) {
|
||||
super(gadget);
|
||||
|
||||
// overwrites the default spawn handling
|
||||
if (gadget.getSpawnEntry() != null) {
|
||||
this.itemId = gadget.getSpawnEntry().getGatherItemId();
|
||||
return;
|
||||
}
|
||||
|
||||
GatherData gatherData = GameData.getGatherDataMap().get(gadget.getPointType());
|
||||
if (gatherData != null) {
|
||||
this.itemId = gatherData.getItemId();
|
||||
this.isForbidGuest = gatherData.isForbidGuest();
|
||||
} else {
|
||||
Grasscutter.getLogger().error("invalid gather object: {}", gadget.getConfigId());
|
||||
}
|
||||
}
|
||||
|
||||
public int getItemId() {
|
||||
return this.itemId;
|
||||
}
|
||||
|
||||
public boolean isForbidGuest() {
|
||||
return isForbidGuest;
|
||||
}
|
||||
|
||||
public boolean onInteract(Player player, GadgetInteractReq req) {
|
||||
// Sanity check
|
||||
ItemData itemData = GameData.getItemDataMap().get(getItemId());
|
||||
if (itemData == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GameItem item = new GameItem(itemData, 1);
|
||||
player.getInventory().addItem(item, ActionReason.Gather);
|
||||
|
||||
getGadget()
|
||||
.getScene()
|
||||
.broadcastPacket(
|
||||
new PacketGadgetInteractRsp(getGadget(), InteractType.INTERACT_TYPE_GATHER));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) {
|
||||
GatherGadgetInfo gatherGadgetInfo =
|
||||
GatherGadgetInfo.newBuilder()
|
||||
.setItemId(this.getItemId())
|
||||
.setIsForbidGuest(this.isForbidGuest())
|
||||
.build();
|
||||
|
||||
gadgetInfo.setGatherGadget(gatherGadgetInfo);
|
||||
}
|
||||
|
||||
public void dropItems(Player player) {
|
||||
Scene scene = getGadget().getScene();
|
||||
int times = Utils.randomRange(1, 2);
|
||||
|
||||
for (int i = 0; i < times; i++) {
|
||||
EntityItem item =
|
||||
new EntityItem(
|
||||
scene,
|
||||
player,
|
||||
GameData.getItemDataMap().get(itemId),
|
||||
getGadget().getPosition().nearby2d(1f).addY(2f),
|
||||
1,
|
||||
true);
|
||||
|
||||
scene.addEntity(item);
|
||||
}
|
||||
|
||||
scene.killEntity(this.getGadget(), player.getTeamManager().getCurrentAvatarEntity().getId());
|
||||
// Todo: add record
|
||||
}
|
||||
}
|
||||
|
@ -1,83 +1,65 @@
|
||||
package emu.grasscutter.game.entity.gadget;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.GatherData;
|
||||
import emu.grasscutter.game.entity.EntityGadget;
|
||||
import emu.grasscutter.game.entity.EntityItem;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
|
||||
import emu.grasscutter.net.proto.GatherGadgetInfoOuterClass.GatherGadgetInfo;
|
||||
import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
|
||||
public class GadgetGatherPoint extends GadgetContent {
|
||||
private final int itemId;
|
||||
private boolean isForbidGuest;
|
||||
|
||||
public GadgetGatherPoint(EntityGadget gadget) {
|
||||
super(gadget);
|
||||
|
||||
if (gadget.getSpawnEntry() != null) {
|
||||
this.itemId = gadget.getSpawnEntry().getGatherItemId();
|
||||
} else {
|
||||
GatherData gatherData = GameData.getGatherDataMap().get(gadget.getPointType());
|
||||
this.itemId = gatherData.getItemId();
|
||||
this.isForbidGuest = gatherData.isForbidGuest();
|
||||
}
|
||||
}
|
||||
|
||||
public int getItemId() {
|
||||
return this.itemId;
|
||||
}
|
||||
|
||||
public boolean isForbidGuest() {
|
||||
return isForbidGuest;
|
||||
}
|
||||
|
||||
public boolean onInteract(Player player, GadgetInteractReq req) {
|
||||
GameItem item = new GameItem(getItemId(), 1);
|
||||
|
||||
player.getInventory().addItem(item, ActionReason.Gather);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) {
|
||||
GatherGadgetInfo gatherGadgetInfo =
|
||||
GatherGadgetInfo.newBuilder()
|
||||
.setItemId(this.getItemId())
|
||||
.setIsForbidGuest(this.isForbidGuest())
|
||||
.build();
|
||||
|
||||
gadgetInfo.setGatherGadget(gatherGadgetInfo);
|
||||
}
|
||||
|
||||
public void dropItems(Player player) {
|
||||
Scene scene = getGadget().getScene();
|
||||
int times = Utils.randomRange(1, 2);
|
||||
|
||||
for (int i = 0; i < times; i++) {
|
||||
EntityItem item =
|
||||
new EntityItem(
|
||||
scene,
|
||||
player,
|
||||
GameData.getItemDataMap().get(itemId),
|
||||
getGadget()
|
||||
.getPosition()
|
||||
.clone()
|
||||
.addY(2f)
|
||||
.addX(Utils.randomFloatRange(-1f, 1f))
|
||||
.addZ(Utils.randomFloatRange(-1f, 1f)),
|
||||
1,
|
||||
true);
|
||||
|
||||
scene.addEntity(item);
|
||||
}
|
||||
|
||||
scene.killEntity(this.getGadget(), player.getTeamManager().getCurrentAvatarEntity().getId());
|
||||
// Todo: add record
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.entity.gadget;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.GatherData;
|
||||
import emu.grasscutter.game.entity.EntityGadget;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
|
||||
import emu.grasscutter.net.proto.GatherGadgetInfoOuterClass.GatherGadgetInfo;
|
||||
import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo;
|
||||
|
||||
/** Spawner for the gather objects */
|
||||
public final class GadgetGatherPoint extends GadgetContent {
|
||||
private final GatherData gatherData;
|
||||
private final EntityGadget gatherObjectChild;
|
||||
|
||||
public GadgetGatherPoint(EntityGadget gadget) {
|
||||
super(gadget);
|
||||
|
||||
this.gatherData = GameData.getGatherDataMap().get(gadget.getPointType());
|
||||
|
||||
var scene = gadget.getScene();
|
||||
gatherObjectChild = new EntityGadget(scene, gatherData.getGadgetId(), gadget.getBornPos());
|
||||
|
||||
gatherObjectChild.setBlockId(gadget.getBlockId());
|
||||
gatherObjectChild.setConfigId(gadget.getConfigId());
|
||||
gatherObjectChild.setGroupId(gadget.getGroupId());
|
||||
gatherObjectChild.getRotation().set(gadget.getRotation());
|
||||
gatherObjectChild.setState(gadget.getState());
|
||||
gatherObjectChild.setPointType(gadget.getPointType());
|
||||
gatherObjectChild.setMetaGadget(gadget.getMetaGadget());
|
||||
gatherObjectChild.buildContent();
|
||||
|
||||
gadget.getChildren().add(gatherObjectChild);
|
||||
scene.addEntity(gatherObjectChild);
|
||||
}
|
||||
|
||||
public int getItemId() {
|
||||
return this.gatherData.getItemId();
|
||||
}
|
||||
|
||||
public boolean isForbidGuest() {
|
||||
return this.gatherData.isForbidGuest();
|
||||
}
|
||||
|
||||
public boolean onInteract(Player player, GadgetInteractReq req) {
|
||||
GameItem item = new GameItem(getItemId(), 1);
|
||||
|
||||
player.getInventory().addItem(item, ActionReason.Gather);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) {
|
||||
// todo does official use this for the spawners?
|
||||
GatherGadgetInfo gatherGadgetInfo =
|
||||
GatherGadgetInfo.newBuilder()
|
||||
.setItemId(this.getItemId())
|
||||
.setIsForbidGuest(this.isForbidGuest())
|
||||
.build();
|
||||
|
||||
gadgetInfo.setGatherGadget(gatherGadgetInfo);
|
||||
}
|
||||
}
|
||||
|
@ -1,65 +1,68 @@
|
||||
package emu.grasscutter.game.entity.gadget;
|
||||
|
||||
import emu.grasscutter.game.entity.EntityGadget;
|
||||
import emu.grasscutter.game.entity.gadget.worktop.WorktopWorktopOptionHandler;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
|
||||
import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo;
|
||||
import emu.grasscutter.net.proto.SelectWorktopOptionReqOuterClass.SelectWorktopOptionReq;
|
||||
import emu.grasscutter.net.proto.WorktopInfoOuterClass.WorktopInfo;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class GadgetWorktop extends GadgetContent {
|
||||
private IntSet worktopOptions;
|
||||
private WorktopWorktopOptionHandler handler;
|
||||
|
||||
public GadgetWorktop(EntityGadget gadget) {
|
||||
super(gadget);
|
||||
}
|
||||
|
||||
public IntSet getWorktopOptions() {
|
||||
return worktopOptions;
|
||||
}
|
||||
|
||||
public void addWorktopOptions(int[] options) {
|
||||
if (this.worktopOptions == null) {
|
||||
this.worktopOptions = new IntOpenHashSet();
|
||||
}
|
||||
Arrays.stream(options).forEach(this.worktopOptions::add);
|
||||
}
|
||||
|
||||
public void removeWorktopOption(int option) {
|
||||
if (this.worktopOptions == null) {
|
||||
return;
|
||||
}
|
||||
this.worktopOptions.remove(option);
|
||||
}
|
||||
|
||||
public boolean onInteract(Player player, GadgetInteractReq req) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) {
|
||||
if (this.worktopOptions == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
WorktopInfo worktop =
|
||||
WorktopInfo.newBuilder().addAllOptionList(this.getWorktopOptions()).build();
|
||||
|
||||
gadgetInfo.setWorktop(worktop);
|
||||
}
|
||||
|
||||
public void setOnSelectWorktopOptionEvent(WorktopWorktopOptionHandler handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public boolean onSelectWorktopOption(SelectWorktopOptionReq req) {
|
||||
if (this.handler != null) {
|
||||
this.handler.onSelectWorktopOption(this, req.getOptionId());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.entity.gadget;
|
||||
|
||||
import emu.grasscutter.game.entity.EntityGadget;
|
||||
import emu.grasscutter.game.entity.gadget.worktop.WorktopWorktopOptionHandler;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
|
||||
import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo;
|
||||
import emu.grasscutter.net.proto.SelectWorktopOptionReqOuterClass.SelectWorktopOptionReq;
|
||||
import emu.grasscutter.net.proto.WorktopInfoOuterClass.WorktopInfo;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class GadgetWorktop extends GadgetContent {
|
||||
private IntSet worktopOptions;
|
||||
private WorktopWorktopOptionHandler handler;
|
||||
|
||||
public GadgetWorktop(EntityGadget gadget) {
|
||||
super(gadget);
|
||||
}
|
||||
|
||||
public IntSet getWorktopOptions() {
|
||||
if (this.worktopOptions == null) {
|
||||
this.worktopOptions = new IntOpenHashSet();
|
||||
}
|
||||
return worktopOptions;
|
||||
}
|
||||
|
||||
public void addWorktopOptions(int[] options) {
|
||||
if (this.worktopOptions == null) {
|
||||
this.worktopOptions = new IntOpenHashSet();
|
||||
}
|
||||
Arrays.stream(options).forEach(this.worktopOptions::add);
|
||||
}
|
||||
|
||||
public void removeWorktopOption(int option) {
|
||||
if (this.worktopOptions == null) {
|
||||
return;
|
||||
}
|
||||
this.worktopOptions.remove(option);
|
||||
}
|
||||
|
||||
public boolean onInteract(Player player, GadgetInteractReq req) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) {
|
||||
if (this.worktopOptions == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
WorktopInfo worktop =
|
||||
WorktopInfo.newBuilder().addAllOptionList(this.getWorktopOptions()).build();
|
||||
|
||||
gadgetInfo.setWorktop(worktop);
|
||||
}
|
||||
|
||||
public void setOnSelectWorktopOptionEvent(WorktopWorktopOptionHandler handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public boolean onSelectWorktopOption(SelectWorktopOptionReq req) {
|
||||
if (this.handler != null) {
|
||||
this.handler.onSelectWorktopOption(this, req.getOptionId());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1,61 +1,67 @@
|
||||
package emu.grasscutter.game.entity.gadget.chest;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.game.entity.gadget.GadgetChest;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.server.packet.send.PacketGadgetAutoPickDropInfoNotify;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BossChestInteractHandler implements ChestInteractHandler {
|
||||
@Override
|
||||
public boolean isTwoStep() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInteract(GadgetChest chest, Player player) {
|
||||
return this.onInteract(chest, player, false);
|
||||
}
|
||||
|
||||
public boolean onInteract(GadgetChest chest, Player player, boolean useCondensedResin) {
|
||||
var blossomRewards =
|
||||
player
|
||||
.getScene()
|
||||
.getBlossomManager()
|
||||
.onReward(player, chest.getGadget(), useCondensedResin);
|
||||
if (blossomRewards != null) {
|
||||
player.getInventory().addItems(blossomRewards, ActionReason.OpenWorldBossChest);
|
||||
player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(blossomRewards));
|
||||
return true;
|
||||
}
|
||||
|
||||
var worldDataManager = chest.getGadget().getScene().getWorld().getServer().getWorldDataSystem();
|
||||
var monster =
|
||||
chest
|
||||
.getGadget()
|
||||
.getMetaGadget()
|
||||
.group
|
||||
.monsters
|
||||
.get(chest.getGadget().getMetaGadget().boss_chest.monster_config_id);
|
||||
var reward = worldDataManager.getRewardByBossId(monster.monster_id);
|
||||
|
||||
if (reward == null) {
|
||||
Grasscutter.getLogger()
|
||||
.warn("Could not found the reward of boss monster {}", monster.monster_id);
|
||||
return false;
|
||||
}
|
||||
List<GameItem> rewards = new ArrayList<>();
|
||||
for (ItemParamData param : reward.getPreviewItems()) {
|
||||
rewards.add(new GameItem(param.getId(), Math.max(param.getCount(), 1)));
|
||||
}
|
||||
|
||||
player.getInventory().addItems(rewards, ActionReason.OpenWorldBossChest);
|
||||
player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(rewards));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.entity.gadget.chest;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.game.entity.gadget.GadgetChest;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.server.packet.send.PacketGadgetAutoPickDropInfoNotify;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BossChestInteractHandler implements ChestInteractHandler {
|
||||
@Override
|
||||
public boolean isTwoStep() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInteract(GadgetChest chest, Player player) {
|
||||
return this.onInteract(chest, player, false);
|
||||
}
|
||||
|
||||
public boolean onInteract(GadgetChest chest, Player player, boolean useCondensedResin) {
|
||||
var blossomRewards =
|
||||
player
|
||||
.getScene()
|
||||
.getBlossomManager()
|
||||
.onReward(player, chest.getGadget(), useCondensedResin);
|
||||
if (blossomRewards != null) {
|
||||
player.getInventory().addItems(blossomRewards, ActionReason.OpenWorldBossChest);
|
||||
player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(blossomRewards));
|
||||
return true;
|
||||
}
|
||||
|
||||
var worldDataManager = chest.getGadget().getScene().getWorld().getServer().getWorldDataSystem();
|
||||
var monster =
|
||||
chest
|
||||
.getGadget()
|
||||
.getMetaGadget()
|
||||
.group
|
||||
.monsters
|
||||
.get(chest.getGadget().getMetaGadget().boss_chest.monster_config_id);
|
||||
var reward = worldDataManager.getRewardByBossId(monster.monster_id);
|
||||
|
||||
if (reward == null) {
|
||||
var dungeonManager = player.getScene().getDungeonManager();
|
||||
|
||||
if (dungeonManager != null) {
|
||||
return dungeonManager.getStatueDrops(
|
||||
player, useCondensedResin, chest.getGadget().getGroupId());
|
||||
}
|
||||
Grasscutter.getLogger()
|
||||
.warn("Could not found the reward of boss monster {}", monster.monster_id);
|
||||
return false;
|
||||
}
|
||||
List<GameItem> rewards = new ArrayList<>();
|
||||
for (ItemParamData param : reward.getPreviewItems()) {
|
||||
rewards.add(new GameItem(param.getId(), Math.max(param.getCount(), 1)));
|
||||
}
|
||||
|
||||
player.getInventory().addItems(rewards, ActionReason.OpenWorldBossChest);
|
||||
player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(rewards));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +1,45 @@
|
||||
package emu.grasscutter.game.inventory;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum EquipType {
|
||||
EQUIP_NONE(0),
|
||||
EQUIP_BRACER(1),
|
||||
EQUIP_NECKLACE(2),
|
||||
EQUIP_SHOES(3),
|
||||
EQUIP_RING(4),
|
||||
EQUIP_DRESS(5),
|
||||
EQUIP_WEAPON(6);
|
||||
|
||||
private static final Int2ObjectMap<EquipType> map = new Int2ObjectOpenHashMap<>();
|
||||
private static final Map<String, EquipType> stringMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
Stream.of(values())
|
||||
.forEach(
|
||||
e -> {
|
||||
map.put(e.getValue(), e);
|
||||
stringMap.put(e.name(), e);
|
||||
});
|
||||
}
|
||||
|
||||
private final int value;
|
||||
|
||||
EquipType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static EquipType getTypeByValue(int value) {
|
||||
return map.getOrDefault(value, EQUIP_NONE);
|
||||
}
|
||||
|
||||
public static EquipType getTypeByName(String name) {
|
||||
return stringMap.getOrDefault(name, EQUIP_NONE);
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.inventory;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum EquipType {
|
||||
EQUIP_NONE(0),
|
||||
EQUIP_BRACER(1),
|
||||
EQUIP_NECKLACE(2),
|
||||
EQUIP_SHOES(3),
|
||||
EQUIP_RING(4),
|
||||
EQUIP_DRESS(5),
|
||||
EQUIP_WEAPON(6);
|
||||
|
||||
private static final Int2ObjectMap<EquipType> map = new Int2ObjectOpenHashMap<>();
|
||||
private static final Map<String, EquipType> stringMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
Stream.of(values())
|
||||
.forEach(
|
||||
e -> {
|
||||
map.put(e.getValue(), e);
|
||||
stringMap.put(e.name(), e);
|
||||
});
|
||||
}
|
||||
|
||||
@Getter private final int value;
|
||||
|
||||
EquipType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static EquipType getTypeByValue(int value) {
|
||||
return map.getOrDefault(value, EQUIP_NONE);
|
||||
}
|
||||
|
||||
public static EquipType getTypeByName(String name) {
|
||||
return stringMap.getOrDefault(name, EQUIP_NONE);
|
||||
}
|
||||
}
|
||||
|
@ -1,363 +1,371 @@
|
||||
package emu.grasscutter.game.inventory;
|
||||
|
||||
import dev.morphia.annotations.*;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.GameDepot;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.data.excels.ItemData;
|
||||
import emu.grasscutter.data.excels.reliquary.ReliquaryAffixData;
|
||||
import emu.grasscutter.data.excels.reliquary.ReliquaryMainPropData;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo;
|
||||
import emu.grasscutter.net.proto.EquipOuterClass.Equip;
|
||||
import emu.grasscutter.net.proto.FurnitureOuterClass.Furniture;
|
||||
import emu.grasscutter.net.proto.ItemHintOuterClass.ItemHint;
|
||||
import emu.grasscutter.net.proto.ItemOuterClass.Item;
|
||||
import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam;
|
||||
import emu.grasscutter.net.proto.MaterialOuterClass.Material;
|
||||
import emu.grasscutter.net.proto.ReliquaryOuterClass.Reliquary;
|
||||
import emu.grasscutter.net.proto.SceneReliquaryInfoOuterClass.SceneReliquaryInfo;
|
||||
import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo;
|
||||
import emu.grasscutter.net.proto.WeaponOuterClass.Weapon;
|
||||
import emu.grasscutter.utils.WeightedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
@Entity(value = "items", useDiscriminator = false)
|
||||
public class GameItem {
|
||||
@Id private ObjectId id;
|
||||
@Indexed private int ownerId;
|
||||
@Getter @Setter private int itemId;
|
||||
@Getter @Setter private int count;
|
||||
|
||||
@Transient @Getter private long guid; // Player unique id
|
||||
@Transient @Getter @Setter private ItemData itemData;
|
||||
|
||||
// Equips
|
||||
@Getter @Setter private int level;
|
||||
@Getter @Setter private int exp;
|
||||
@Getter @Setter private int totalExp;
|
||||
@Getter @Setter private int promoteLevel;
|
||||
@Getter @Setter private boolean locked;
|
||||
|
||||
// Weapon
|
||||
@Getter private List<Integer> affixes;
|
||||
@Getter @Setter private int refinement = 0;
|
||||
|
||||
// Relic
|
||||
@Getter @Setter private int mainPropId;
|
||||
@Getter private List<Integer> appendPropIdList;
|
||||
|
||||
@Getter @Setter private int equipCharacter;
|
||||
@Transient @Getter @Setter private int weaponEntityId;
|
||||
|
||||
public GameItem() {
|
||||
// Morphia only
|
||||
}
|
||||
|
||||
public GameItem(int itemId) {
|
||||
this(GameData.getItemDataMap().get(itemId));
|
||||
}
|
||||
|
||||
public GameItem(int itemId, int count) {
|
||||
this(GameData.getItemDataMap().get(itemId), count);
|
||||
}
|
||||
|
||||
public GameItem(ItemParamData itemParamData) {
|
||||
this(itemParamData.getId(), itemParamData.getCount());
|
||||
}
|
||||
|
||||
public GameItem(ItemData data) {
|
||||
this(data, 1);
|
||||
}
|
||||
|
||||
public GameItem(ItemData data, int count) {
|
||||
this.itemId = data.getId();
|
||||
this.itemData = data;
|
||||
|
||||
switch (data.getItemType()) {
|
||||
case ITEM_VIRTUAL:
|
||||
this.count = count;
|
||||
break;
|
||||
case ITEM_WEAPON:
|
||||
this.count = 1;
|
||||
this.level = Math.max(this.count, 1); // ??????????????????
|
||||
this.affixes = new ArrayList<>(2);
|
||||
if (data.getSkillAffix() != null) {
|
||||
for (int skillAffix : data.getSkillAffix()) {
|
||||
if (skillAffix > 0) {
|
||||
this.affixes.add(skillAffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ITEM_RELIQUARY:
|
||||
this.count = 1;
|
||||
this.level = 1;
|
||||
this.appendPropIdList = new ArrayList<>();
|
||||
// Create main property
|
||||
ReliquaryMainPropData mainPropData =
|
||||
GameDepot.getRandomRelicMainProp(data.getMainPropDepotId());
|
||||
if (mainPropData != null) {
|
||||
this.mainPropId = mainPropData.getId();
|
||||
}
|
||||
// Create extra stats
|
||||
this.addAppendProps(data.getAppendPropNum());
|
||||
break;
|
||||
default:
|
||||
this.count = Math.min(count, data.getStackLimit());
|
||||
}
|
||||
}
|
||||
|
||||
public static int getMinPromoteLevel(int level) {
|
||||
if (level > 80) {
|
||||
return 6;
|
||||
} else if (level > 70) {
|
||||
return 5;
|
||||
} else if (level > 60) {
|
||||
return 4;
|
||||
} else if (level > 50) {
|
||||
return 3;
|
||||
} else if (level > 40) {
|
||||
return 2;
|
||||
} else if (level > 20) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getOwnerId() {
|
||||
return ownerId;
|
||||
}
|
||||
|
||||
public void setOwner(Player player) {
|
||||
this.ownerId = player.getUid();
|
||||
this.guid = player.getNextGameGuid();
|
||||
}
|
||||
|
||||
public ObjectId getObjectId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ItemType getItemType() {
|
||||
return this.itemData.getItemType();
|
||||
}
|
||||
|
||||
public int getEquipSlot() {
|
||||
return this.getItemData().getEquipType().getValue();
|
||||
}
|
||||
|
||||
public boolean isEquipped() {
|
||||
return this.getEquipCharacter() > 0;
|
||||
}
|
||||
|
||||
public boolean isDestroyable() {
|
||||
return !this.isLocked() && !this.isEquipped();
|
||||
}
|
||||
|
||||
public void addAppendProp() {
|
||||
if (this.appendPropIdList == null) {
|
||||
this.appendPropIdList = new ArrayList<>();
|
||||
}
|
||||
|
||||
if (this.appendPropIdList.size() < 4) {
|
||||
this.addNewAppendProp();
|
||||
} else {
|
||||
this.upgradeRandomAppendProp();
|
||||
}
|
||||
}
|
||||
|
||||
public void addAppendProps(int quantity) {
|
||||
int num = Math.max(quantity, 0);
|
||||
for (int i = 0; i < num; i++) {
|
||||
this.addAppendProp();
|
||||
}
|
||||
}
|
||||
|
||||
private Set<FightProperty> getAppendFightProperties() {
|
||||
Set<FightProperty> props = new HashSet<>();
|
||||
// Previously this would check no more than the first four affixes, however custom artifacts may
|
||||
// not respect this order.
|
||||
for (int appendPropId : this.appendPropIdList) {
|
||||
ReliquaryAffixData affixData = GameData.getReliquaryAffixDataMap().get(appendPropId);
|
||||
if (affixData != null) {
|
||||
props.add(affixData.getFightProp());
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
private void addNewAppendProp() {
|
||||
List<ReliquaryAffixData> affixList =
|
||||
GameDepot.getRelicAffixList(this.itemData.getAppendPropDepotId());
|
||||
|
||||
if (affixList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build blacklist - Dont add same stat as main/sub stat
|
||||
Set<FightProperty> blacklist = this.getAppendFightProperties();
|
||||
ReliquaryMainPropData mainPropData =
|
||||
GameData.getReliquaryMainPropDataMap().get(this.mainPropId);
|
||||
if (mainPropData != null) {
|
||||
blacklist.add(mainPropData.getFightProp());
|
||||
}
|
||||
|
||||
// Build random list
|
||||
WeightedList<ReliquaryAffixData> randomList = new WeightedList<>();
|
||||
for (ReliquaryAffixData affix : affixList) {
|
||||
if (!blacklist.contains(affix.getFightProp())) {
|
||||
randomList.add(affix.getWeight(), affix);
|
||||
}
|
||||
}
|
||||
|
||||
if (randomList.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add random stat
|
||||
ReliquaryAffixData affixData = randomList.next();
|
||||
this.appendPropIdList.add(affixData.getId());
|
||||
}
|
||||
|
||||
private void upgradeRandomAppendProp() {
|
||||
List<ReliquaryAffixData> affixList =
|
||||
GameDepot.getRelicAffixList(this.itemData.getAppendPropDepotId());
|
||||
|
||||
if (affixList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build whitelist
|
||||
Set<FightProperty> whitelist = this.getAppendFightProperties();
|
||||
|
||||
// Build random list
|
||||
WeightedList<ReliquaryAffixData> randomList = new WeightedList<>();
|
||||
for (ReliquaryAffixData affix : affixList) {
|
||||
if (whitelist.contains(affix.getFightProp())) {
|
||||
randomList.add(affix.getUpgradeWeight(), affix);
|
||||
}
|
||||
}
|
||||
|
||||
// Add random stat
|
||||
ReliquaryAffixData affixData = randomList.next();
|
||||
this.appendPropIdList.add(affixData.getId());
|
||||
}
|
||||
|
||||
@PostLoad
|
||||
public void onLoad() {
|
||||
if (this.itemData == null) {
|
||||
this.itemData = GameData.getItemDataMap().get(getItemId());
|
||||
}
|
||||
}
|
||||
|
||||
public void save() {
|
||||
if (this.count > 0 && this.ownerId > 0) {
|
||||
DatabaseHelper.saveItem(this);
|
||||
} else if (this.getObjectId() != null) {
|
||||
DatabaseHelper.deleteItem(this);
|
||||
}
|
||||
}
|
||||
|
||||
public SceneWeaponInfo createSceneWeaponInfo() {
|
||||
SceneWeaponInfo.Builder weaponInfo =
|
||||
SceneWeaponInfo.newBuilder()
|
||||
.setEntityId(this.getWeaponEntityId())
|
||||
.setItemId(this.getItemId())
|
||||
.setGuid(this.getGuid())
|
||||
.setLevel(this.getLevel())
|
||||
.setGadgetId(this.getItemData().getGadgetId())
|
||||
.setAbilityInfo(AbilitySyncStateInfo.newBuilder().setIsInited(getAffixes().size() > 0));
|
||||
|
||||
if (this.getAffixes() != null && this.getAffixes().size() > 0) {
|
||||
for (int affix : this.getAffixes()) {
|
||||
weaponInfo.putAffixMap(affix, this.getRefinement());
|
||||
}
|
||||
}
|
||||
|
||||
return weaponInfo.build();
|
||||
}
|
||||
|
||||
public SceneReliquaryInfo createSceneReliquaryInfo() {
|
||||
SceneReliquaryInfo relicInfo =
|
||||
SceneReliquaryInfo.newBuilder()
|
||||
.setItemId(this.getItemId())
|
||||
.setGuid(this.getGuid())
|
||||
.setLevel(this.getLevel())
|
||||
.build();
|
||||
|
||||
return relicInfo;
|
||||
}
|
||||
|
||||
public Weapon toWeaponProto() {
|
||||
Weapon.Builder weapon =
|
||||
Weapon.newBuilder()
|
||||
.setLevel(this.getLevel())
|
||||
.setExp(this.getExp())
|
||||
.setPromoteLevel(this.getPromoteLevel());
|
||||
|
||||
if (this.getAffixes() != null && this.getAffixes().size() > 0) {
|
||||
for (int affix : this.getAffixes()) {
|
||||
weapon.putAffixMap(affix, this.getRefinement());
|
||||
}
|
||||
}
|
||||
|
||||
return weapon.build();
|
||||
}
|
||||
|
||||
public Reliquary toReliquaryProto() {
|
||||
Reliquary.Builder relic =
|
||||
Reliquary.newBuilder()
|
||||
.setLevel(this.getLevel())
|
||||
.setExp(this.getExp())
|
||||
.setPromoteLevel(this.getPromoteLevel())
|
||||
.setMainPropId(this.getMainPropId())
|
||||
.addAllAppendPropIdList(this.getAppendPropIdList());
|
||||
|
||||
return relic.build();
|
||||
}
|
||||
|
||||
public Item toProto() {
|
||||
Item.Builder proto = Item.newBuilder().setGuid(this.getGuid()).setItemId(this.getItemId());
|
||||
|
||||
switch (getItemType()) {
|
||||
case ITEM_WEAPON:
|
||||
Weapon weapon = this.toWeaponProto();
|
||||
proto.setEquip(Equip.newBuilder().setWeapon(weapon).setIsLocked(this.isLocked()).build());
|
||||
break;
|
||||
case ITEM_RELIQUARY:
|
||||
Reliquary relic = this.toReliquaryProto();
|
||||
proto.setEquip(Equip.newBuilder().setReliquary(relic).setIsLocked(this.isLocked()).build());
|
||||
break;
|
||||
case ITEM_FURNITURE:
|
||||
Furniture furniture = Furniture.newBuilder().setCount(getCount()).build();
|
||||
proto.setFurniture(furniture);
|
||||
break;
|
||||
default:
|
||||
Material material = Material.newBuilder().setCount(getCount()).build();
|
||||
proto.setMaterial(material);
|
||||
break;
|
||||
}
|
||||
|
||||
return proto.build();
|
||||
}
|
||||
|
||||
public ItemHint toItemHintProto() {
|
||||
return ItemHint.newBuilder()
|
||||
.setItemId(getItemId())
|
||||
.setCount(getCount())
|
||||
.setIsNew(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
public ItemParam toItemParam() {
|
||||
return ItemParam.newBuilder().setItemId(this.getItemId()).setCount(this.getCount()).build();
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.inventory;
|
||||
|
||||
import dev.morphia.annotations.*;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.GameDepot;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.data.excels.ItemData;
|
||||
import emu.grasscutter.data.excels.reliquary.ReliquaryAffixData;
|
||||
import emu.grasscutter.data.excels.reliquary.ReliquaryMainPropData;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo;
|
||||
import emu.grasscutter.net.proto.EquipOuterClass.Equip;
|
||||
import emu.grasscutter.net.proto.FurnitureOuterClass.Furniture;
|
||||
import emu.grasscutter.net.proto.ItemHintOuterClass.ItemHint;
|
||||
import emu.grasscutter.net.proto.ItemOuterClass.Item;
|
||||
import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam;
|
||||
import emu.grasscutter.net.proto.MaterialOuterClass.Material;
|
||||
import emu.grasscutter.net.proto.ReliquaryOuterClass.Reliquary;
|
||||
import emu.grasscutter.net.proto.SceneReliquaryInfoOuterClass.SceneReliquaryInfo;
|
||||
import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo;
|
||||
import emu.grasscutter.net.proto.WeaponOuterClass.Weapon;
|
||||
import emu.grasscutter.utils.WeightedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
@Entity(value = "items", useDiscriminator = false)
|
||||
public class GameItem {
|
||||
@Id private ObjectId id;
|
||||
@Indexed private int ownerId;
|
||||
@Getter @Setter private int itemId;
|
||||
@Getter @Setter private int count;
|
||||
|
||||
@Transient @Getter private long guid; // Player unique id
|
||||
@Transient @Getter @Setter private ItemData itemData;
|
||||
|
||||
// Equips
|
||||
@Getter @Setter private int level;
|
||||
@Getter @Setter private int exp;
|
||||
@Getter @Setter private int totalExp;
|
||||
@Getter @Setter private int promoteLevel;
|
||||
@Getter @Setter private boolean locked;
|
||||
|
||||
// Weapon
|
||||
@Getter private List<Integer> affixes;
|
||||
@Getter @Setter private int refinement = 0;
|
||||
|
||||
// Relic
|
||||
@Getter @Setter private int mainPropId;
|
||||
@Getter private List<Integer> appendPropIdList;
|
||||
|
||||
@Getter @Setter private int equipCharacter;
|
||||
@Transient @Getter @Setter private int weaponEntityId;
|
||||
@Transient @Getter private boolean newItem = false;
|
||||
|
||||
public GameItem() {
|
||||
// Morphia only
|
||||
}
|
||||
|
||||
public GameItem(int itemId) {
|
||||
this(GameData.getItemDataMap().get(itemId));
|
||||
}
|
||||
|
||||
public GameItem(int itemId, int count) {
|
||||
this(GameData.getItemDataMap().get(itemId), count);
|
||||
}
|
||||
|
||||
public GameItem(ItemParamData itemParamData) {
|
||||
this(itemParamData.getId(), itemParamData.getCount());
|
||||
}
|
||||
|
||||
public GameItem(ItemData data) {
|
||||
this(data, 1);
|
||||
}
|
||||
|
||||
public GameItem(ItemData data, int count) {
|
||||
this.itemId = data.getId();
|
||||
this.itemData = data;
|
||||
|
||||
switch (data.getItemType()) {
|
||||
case ITEM_VIRTUAL:
|
||||
this.count = count;
|
||||
break;
|
||||
case ITEM_WEAPON:
|
||||
this.count = 1;
|
||||
this.level = Math.max(this.count, 1); // ??????????????????
|
||||
this.affixes = new ArrayList<>(2);
|
||||
if (data.getSkillAffix() != null) {
|
||||
for (int skillAffix : data.getSkillAffix()) {
|
||||
if (skillAffix > 0) {
|
||||
this.affixes.add(skillAffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ITEM_RELIQUARY:
|
||||
this.count = 1;
|
||||
this.level = 1;
|
||||
this.appendPropIdList = new ArrayList<>();
|
||||
// Create main property
|
||||
ReliquaryMainPropData mainPropData =
|
||||
GameDepot.getRandomRelicMainProp(data.getMainPropDepotId());
|
||||
if (mainPropData != null) {
|
||||
this.mainPropId = mainPropData.getId();
|
||||
}
|
||||
// Create extra stats
|
||||
this.addAppendProps(data.getAppendPropNum());
|
||||
break;
|
||||
default:
|
||||
this.count = Math.min(count, data.getStackLimit());
|
||||
}
|
||||
}
|
||||
|
||||
public int getOwnerId() {
|
||||
return ownerId;
|
||||
}
|
||||
|
||||
public void setOwner(Player player) {
|
||||
this.ownerId = player.getUid();
|
||||
this.guid = player.getNextGameGuid();
|
||||
}
|
||||
|
||||
public void checkIsNew(Inventory inventory) {
|
||||
// display notification when player obtain new item
|
||||
if (inventory.getItemByGuid(this.itemId) == null) {
|
||||
this.newItem = true;
|
||||
}
|
||||
}
|
||||
|
||||
public ObjectId getObjectId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ItemType getItemType() {
|
||||
return this.itemData.getItemType();
|
||||
}
|
||||
|
||||
public static int getMinPromoteLevel(int level) {
|
||||
if (level > 80) {
|
||||
return 6;
|
||||
} else if (level > 70) {
|
||||
return 5;
|
||||
} else if (level > 60) {
|
||||
return 4;
|
||||
} else if (level > 50) {
|
||||
return 3;
|
||||
} else if (level > 40) {
|
||||
return 2;
|
||||
} else if (level > 20) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getEquipSlot() {
|
||||
return this.getItemData().getEquipType().getValue();
|
||||
}
|
||||
|
||||
public boolean isEquipped() {
|
||||
return this.getEquipCharacter() > 0;
|
||||
}
|
||||
|
||||
public boolean isDestroyable() {
|
||||
return !this.isLocked() && !this.isEquipped();
|
||||
}
|
||||
|
||||
public void addAppendProp() {
|
||||
if (this.appendPropIdList == null) {
|
||||
this.appendPropIdList = new ArrayList<>();
|
||||
}
|
||||
|
||||
if (this.appendPropIdList.size() < 4) {
|
||||
this.addNewAppendProp();
|
||||
} else {
|
||||
this.upgradeRandomAppendProp();
|
||||
}
|
||||
}
|
||||
|
||||
public void addAppendProps(int quantity) {
|
||||
int num = Math.max(quantity, 0);
|
||||
for (int i = 0; i < num; i++) {
|
||||
this.addAppendProp();
|
||||
}
|
||||
}
|
||||
|
||||
private Set<FightProperty> getAppendFightProperties() {
|
||||
Set<FightProperty> props = new HashSet<>();
|
||||
// Previously this would check no more than the first four affixes, however custom artifacts may
|
||||
// not respect this order.
|
||||
for (int appendPropId : this.appendPropIdList) {
|
||||
ReliquaryAffixData affixData = GameData.getReliquaryAffixDataMap().get(appendPropId);
|
||||
if (affixData != null) {
|
||||
props.add(affixData.getFightProp());
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
private void addNewAppendProp() {
|
||||
List<ReliquaryAffixData> affixList =
|
||||
GameDepot.getRelicAffixList(this.itemData.getAppendPropDepotId());
|
||||
|
||||
if (affixList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build blacklist - Dont add same stat as main/sub stat
|
||||
Set<FightProperty> blacklist = this.getAppendFightProperties();
|
||||
ReliquaryMainPropData mainPropData =
|
||||
GameData.getReliquaryMainPropDataMap().get(this.mainPropId);
|
||||
if (mainPropData != null) {
|
||||
blacklist.add(mainPropData.getFightProp());
|
||||
}
|
||||
|
||||
// Build random list
|
||||
WeightedList<ReliquaryAffixData> randomList = new WeightedList<>();
|
||||
for (ReliquaryAffixData affix : affixList) {
|
||||
if (!blacklist.contains(affix.getFightProp())) {
|
||||
randomList.add(affix.getWeight(), affix);
|
||||
}
|
||||
}
|
||||
|
||||
if (randomList.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add random stat
|
||||
ReliquaryAffixData affixData = randomList.next();
|
||||
this.appendPropIdList.add(affixData.getId());
|
||||
}
|
||||
|
||||
private void upgradeRandomAppendProp() {
|
||||
List<ReliquaryAffixData> affixList =
|
||||
GameDepot.getRelicAffixList(this.itemData.getAppendPropDepotId());
|
||||
|
||||
if (affixList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build whitelist
|
||||
Set<FightProperty> whitelist = this.getAppendFightProperties();
|
||||
|
||||
// Build random list
|
||||
WeightedList<ReliquaryAffixData> randomList = new WeightedList<>();
|
||||
for (ReliquaryAffixData affix : affixList) {
|
||||
if (whitelist.contains(affix.getFightProp())) {
|
||||
randomList.add(affix.getUpgradeWeight(), affix);
|
||||
}
|
||||
}
|
||||
|
||||
// Add random stat
|
||||
ReliquaryAffixData affixData = randomList.next();
|
||||
this.appendPropIdList.add(affixData.getId());
|
||||
}
|
||||
|
||||
@PostLoad
|
||||
public void onLoad() {
|
||||
if (this.itemData == null) {
|
||||
this.itemData = GameData.getItemDataMap().get(getItemId());
|
||||
}
|
||||
}
|
||||
|
||||
public void save() {
|
||||
if (this.count > 0 && this.ownerId > 0) {
|
||||
DatabaseHelper.saveItem(this);
|
||||
} else if (this.getObjectId() != null) {
|
||||
DatabaseHelper.deleteItem(this);
|
||||
}
|
||||
}
|
||||
|
||||
public SceneWeaponInfo createSceneWeaponInfo() {
|
||||
SceneWeaponInfo.Builder weaponInfo =
|
||||
SceneWeaponInfo.newBuilder()
|
||||
.setEntityId(this.getWeaponEntityId())
|
||||
.setItemId(this.getItemId())
|
||||
.setGuid(this.getGuid())
|
||||
.setLevel(this.getLevel())
|
||||
.setGadgetId(this.getItemData().getGadgetId())
|
||||
.setAbilityInfo(AbilitySyncStateInfo.newBuilder().setIsInited(getAffixes().size() > 0));
|
||||
|
||||
if (this.getAffixes() != null && this.getAffixes().size() > 0) {
|
||||
for (int affix : this.getAffixes()) {
|
||||
weaponInfo.putAffixMap(affix, this.getRefinement());
|
||||
}
|
||||
}
|
||||
|
||||
return weaponInfo.build();
|
||||
}
|
||||
|
||||
public SceneReliquaryInfo createSceneReliquaryInfo() {
|
||||
SceneReliquaryInfo relicInfo =
|
||||
SceneReliquaryInfo.newBuilder()
|
||||
.setItemId(this.getItemId())
|
||||
.setGuid(this.getGuid())
|
||||
.setLevel(this.getLevel())
|
||||
.build();
|
||||
|
||||
return relicInfo;
|
||||
}
|
||||
|
||||
public Weapon toWeaponProto() {
|
||||
Weapon.Builder weapon =
|
||||
Weapon.newBuilder()
|
||||
.setLevel(this.getLevel())
|
||||
.setExp(this.getExp())
|
||||
.setPromoteLevel(this.getPromoteLevel());
|
||||
|
||||
if (this.getAffixes() != null && this.getAffixes().size() > 0) {
|
||||
for (int affix : this.getAffixes()) {
|
||||
weapon.putAffixMap(affix, this.getRefinement());
|
||||
}
|
||||
}
|
||||
|
||||
return weapon.build();
|
||||
}
|
||||
|
||||
public Reliquary toReliquaryProto() {
|
||||
Reliquary.Builder relic =
|
||||
Reliquary.newBuilder()
|
||||
.setLevel(this.getLevel())
|
||||
.setExp(this.getExp())
|
||||
.setPromoteLevel(this.getPromoteLevel())
|
||||
.setMainPropId(this.getMainPropId())
|
||||
.addAllAppendPropIdList(this.getAppendPropIdList());
|
||||
|
||||
return relic.build();
|
||||
}
|
||||
|
||||
public Item toProto() {
|
||||
Item.Builder proto = Item.newBuilder().setGuid(this.getGuid()).setItemId(this.getItemId());
|
||||
|
||||
switch (getItemType()) {
|
||||
case ITEM_WEAPON:
|
||||
Weapon weapon = this.toWeaponProto();
|
||||
proto.setEquip(Equip.newBuilder().setWeapon(weapon).setIsLocked(this.isLocked()).build());
|
||||
break;
|
||||
case ITEM_RELIQUARY:
|
||||
Reliquary relic = this.toReliquaryProto();
|
||||
proto.setEquip(Equip.newBuilder().setReliquary(relic).setIsLocked(this.isLocked()).build());
|
||||
break;
|
||||
case ITEM_FURNITURE:
|
||||
Furniture furniture = Furniture.newBuilder().setCount(getCount()).build();
|
||||
proto.setFurniture(furniture);
|
||||
break;
|
||||
default:
|
||||
Material material = Material.newBuilder().setCount(getCount()).build();
|
||||
proto.setMaterial(material);
|
||||
break;
|
||||
}
|
||||
|
||||
return proto.build();
|
||||
}
|
||||
|
||||
public ItemHint toItemHintProto() {
|
||||
return ItemHint.newBuilder()
|
||||
.setItemId(getItemId())
|
||||
.setCount(getCount())
|
||||
.setIsNew(this.isNewItem())
|
||||
.build();
|
||||
}
|
||||
|
||||
public ItemParam toItemParam() {
|
||||
return ItemParam.newBuilder().setItemId(this.getItemId()).setCount(this.getCount()).build();
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,47 +1,45 @@
|
||||
package emu.grasscutter.game.inventory;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum ItemQuality {
|
||||
QUALITY_NONE(0),
|
||||
QUALITY_WHITE(1),
|
||||
QUALITY_GREEN(2),
|
||||
QUALITY_BLUE(3),
|
||||
QUALITY_PURPLE(4),
|
||||
QUALITY_ORANGE(5),
|
||||
QUALITY_ORANGE_SP(105);
|
||||
|
||||
private static final Int2ObjectMap<ItemQuality> map = new Int2ObjectOpenHashMap<>();
|
||||
private static final Map<String, ItemQuality> stringMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
Stream.of(values())
|
||||
.forEach(
|
||||
e -> {
|
||||
map.put(e.getValue(), e);
|
||||
stringMap.put(e.name(), e);
|
||||
});
|
||||
}
|
||||
|
||||
private final int value;
|
||||
|
||||
ItemQuality(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static ItemQuality getTypeByValue(int value) {
|
||||
return map.getOrDefault(value, QUALITY_NONE);
|
||||
}
|
||||
|
||||
public static ItemQuality getTypeByName(String name) {
|
||||
return stringMap.getOrDefault(name, QUALITY_NONE);
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.inventory;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum ItemQuality {
|
||||
QUALITY_NONE(0),
|
||||
QUALITY_WHITE(1),
|
||||
QUALITY_GREEN(2),
|
||||
QUALITY_BLUE(3),
|
||||
QUALITY_PURPLE(4),
|
||||
QUALITY_ORANGE(5),
|
||||
QUALITY_ORANGE_SP(105);
|
||||
|
||||
private static final Int2ObjectMap<ItemQuality> map = new Int2ObjectOpenHashMap<>();
|
||||
private static final Map<String, ItemQuality> stringMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
Stream.of(values())
|
||||
.forEach(
|
||||
e -> {
|
||||
map.put(e.getValue(), e);
|
||||
stringMap.put(e.name(), e);
|
||||
});
|
||||
}
|
||||
|
||||
@Getter private final int value;
|
||||
|
||||
ItemQuality(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static ItemQuality getTypeByValue(int value) {
|
||||
return map.getOrDefault(value, QUALITY_NONE);
|
||||
}
|
||||
|
||||
public static ItemQuality getTypeByName(String name) {
|
||||
return stringMap.getOrDefault(name, QUALITY_NONE);
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +1,45 @@
|
||||
package emu.grasscutter.game.inventory;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum ItemType {
|
||||
ITEM_NONE(0),
|
||||
ITEM_VIRTUAL(1),
|
||||
ITEM_MATERIAL(2),
|
||||
ITEM_RELIQUARY(3),
|
||||
ITEM_WEAPON(4),
|
||||
ITEM_DISPLAY(5),
|
||||
ITEM_FURNITURE(6);
|
||||
|
||||
private static final Int2ObjectMap<ItemType> map = new Int2ObjectOpenHashMap<>();
|
||||
private static final Map<String, ItemType> stringMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
Stream.of(values())
|
||||
.forEach(
|
||||
e -> {
|
||||
map.put(e.getValue(), e);
|
||||
stringMap.put(e.name(), e);
|
||||
});
|
||||
}
|
||||
|
||||
private final int value;
|
||||
|
||||
ItemType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static ItemType getTypeByValue(int value) {
|
||||
return map.getOrDefault(value, ITEM_NONE);
|
||||
}
|
||||
|
||||
public static ItemType getTypeByName(String name) {
|
||||
return stringMap.getOrDefault(name, ITEM_NONE);
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.inventory;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum ItemType {
|
||||
ITEM_NONE(0),
|
||||
ITEM_VIRTUAL(1),
|
||||
ITEM_MATERIAL(2),
|
||||
ITEM_RELIQUARY(3),
|
||||
ITEM_WEAPON(4),
|
||||
ITEM_DISPLAY(5),
|
||||
ITEM_FURNITURE(6);
|
||||
|
||||
private static final Int2ObjectMap<ItemType> map = new Int2ObjectOpenHashMap<>();
|
||||
private static final Map<String, ItemType> stringMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
Stream.of(values())
|
||||
.forEach(
|
||||
e -> {
|
||||
map.put(e.getValue(), e);
|
||||
stringMap.put(e.name(), e);
|
||||
});
|
||||
}
|
||||
|
||||
@Getter private final int value;
|
||||
|
||||
ItemType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static ItemType getTypeByValue(int value) {
|
||||
return map.getOrDefault(value, ITEM_NONE);
|
||||
}
|
||||
|
||||
public static ItemType getTypeByName(String name) {
|
||||
return stringMap.getOrDefault(name, ITEM_NONE);
|
||||
}
|
||||
}
|
||||
|
@ -1,81 +1,79 @@
|
||||
package emu.grasscutter.game.inventory;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum MaterialType {
|
||||
MATERIAL_NONE(0),
|
||||
MATERIAL_FOOD(1),
|
||||
MATERIAL_QUEST(2),
|
||||
MATERIAL_EXCHANGE(4),
|
||||
MATERIAL_CONSUME(5),
|
||||
MATERIAL_EXP_FRUIT(6),
|
||||
MATERIAL_AVATAR(7),
|
||||
MATERIAL_ADSORBATE(8),
|
||||
MATERIAL_CRICKET(9),
|
||||
MATERIAL_ELEM_CRYSTAL(10),
|
||||
MATERIAL_WEAPON_EXP_STONE(11),
|
||||
MATERIAL_CHEST(12),
|
||||
MATERIAL_RELIQUARY_MATERIAL(13),
|
||||
MATERIAL_AVATAR_MATERIAL(14),
|
||||
MATERIAL_NOTICE_ADD_HP(15),
|
||||
MATERIAL_SEA_LAMP(16),
|
||||
MATERIAL_SELECTABLE_CHEST(17),
|
||||
MATERIAL_FLYCLOAK(18),
|
||||
MATERIAL_NAMECARD(19),
|
||||
MATERIAL_TALENT(20),
|
||||
MATERIAL_WIDGET(21),
|
||||
MATERIAL_CHEST_BATCH_USE(22),
|
||||
MATERIAL_FAKE_ABSORBATE(23),
|
||||
MATERIAL_CONSUME_BATCH_USE(24),
|
||||
MATERIAL_WOOD(25),
|
||||
MATERIAL_FURNITURE_FORMULA(27),
|
||||
MATERIAL_CHANNELLER_SLAB_BUFF(28),
|
||||
MATERIAL_FURNITURE_SUITE_FORMULA(29),
|
||||
MATERIAL_COSTUME(30),
|
||||
MATERIAL_HOME_SEED(31),
|
||||
MATERIAL_FISH_BAIT(32),
|
||||
MATERIAL_FISH_ROD(33),
|
||||
MATERIAL_SUMO_BUFF(34),
|
||||
MATERIAL_FIREWORKS(35),
|
||||
MATERIAL_BGM(36),
|
||||
MATERIAL_SPICE_FOOD(37),
|
||||
MATERIAL_ACTIVITY_ROBOT(38),
|
||||
MATERIAL_ACTIVITY_GEAR(39),
|
||||
MATERIAL_ACTIVITY_JIGSAW(40),
|
||||
MATERIAL_ARANARA(41),
|
||||
MATERIAL_DESHRET_MANUAL(46);
|
||||
|
||||
private static final Int2ObjectMap<MaterialType> map = new Int2ObjectOpenHashMap<>();
|
||||
private static final Map<String, MaterialType> stringMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
Stream.of(values())
|
||||
.forEach(
|
||||
e -> {
|
||||
map.put(e.getValue(), e);
|
||||
stringMap.put(e.name(), e);
|
||||
});
|
||||
}
|
||||
|
||||
private final int value;
|
||||
|
||||
MaterialType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static MaterialType getTypeByValue(int value) {
|
||||
return map.getOrDefault(value, MATERIAL_NONE);
|
||||
}
|
||||
|
||||
public static MaterialType getTypeByName(String name) {
|
||||
return stringMap.getOrDefault(name, MATERIAL_NONE);
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.inventory;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum MaterialType {
|
||||
MATERIAL_NONE(0),
|
||||
MATERIAL_FOOD(1),
|
||||
MATERIAL_QUEST(2),
|
||||
MATERIAL_EXCHANGE(4),
|
||||
MATERIAL_CONSUME(5),
|
||||
MATERIAL_EXP_FRUIT(6),
|
||||
MATERIAL_AVATAR(7),
|
||||
MATERIAL_ADSORBATE(8),
|
||||
MATERIAL_CRICKET(9),
|
||||
MATERIAL_ELEM_CRYSTAL(10),
|
||||
MATERIAL_WEAPON_EXP_STONE(11),
|
||||
MATERIAL_CHEST(12),
|
||||
MATERIAL_RELIQUARY_MATERIAL(13),
|
||||
MATERIAL_AVATAR_MATERIAL(14),
|
||||
MATERIAL_NOTICE_ADD_HP(15),
|
||||
MATERIAL_SEA_LAMP(16),
|
||||
MATERIAL_SELECTABLE_CHEST(17),
|
||||
MATERIAL_FLYCLOAK(18),
|
||||
MATERIAL_NAMECARD(19),
|
||||
MATERIAL_TALENT(20),
|
||||
MATERIAL_WIDGET(21),
|
||||
MATERIAL_CHEST_BATCH_USE(22),
|
||||
MATERIAL_FAKE_ABSORBATE(23),
|
||||
MATERIAL_CONSUME_BATCH_USE(24),
|
||||
MATERIAL_WOOD(25),
|
||||
MATERIAL_FURNITURE_FORMULA(27),
|
||||
MATERIAL_CHANNELLER_SLAB_BUFF(28),
|
||||
MATERIAL_FURNITURE_SUITE_FORMULA(29),
|
||||
MATERIAL_COSTUME(30),
|
||||
MATERIAL_HOME_SEED(31),
|
||||
MATERIAL_FISH_BAIT(32),
|
||||
MATERIAL_FISH_ROD(33),
|
||||
MATERIAL_SUMO_BUFF(34),
|
||||
MATERIAL_FIREWORKS(35),
|
||||
MATERIAL_BGM(36),
|
||||
MATERIAL_SPICE_FOOD(37),
|
||||
MATERIAL_ACTIVITY_ROBOT(38),
|
||||
MATERIAL_ACTIVITY_GEAR(39),
|
||||
MATERIAL_ACTIVITY_JIGSAW(40),
|
||||
MATERIAL_ARANARA(41),
|
||||
MATERIAL_DESHRET_MANUAL(46);
|
||||
|
||||
private static final Int2ObjectMap<MaterialType> map = new Int2ObjectOpenHashMap<>();
|
||||
private static final Map<String, MaterialType> stringMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
Stream.of(values())
|
||||
.forEach(
|
||||
e -> {
|
||||
map.put(e.getValue(), e);
|
||||
stringMap.put(e.name(), e);
|
||||
});
|
||||
}
|
||||
|
||||
@Getter private final int value;
|
||||
|
||||
MaterialType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static MaterialType getTypeByValue(int value) {
|
||||
return map.getOrDefault(value, MATERIAL_NONE);
|
||||
}
|
||||
|
||||
public static MaterialType getTypeByName(String name) {
|
||||
return stringMap.getOrDefault(name, MATERIAL_NONE);
|
||||
}
|
||||
}
|
||||
|
@ -1,243 +1,244 @@
|
||||
package emu.grasscutter.game.managers.blossom;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.GameDepot;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.data.excels.RewardPreviewData;
|
||||
import emu.grasscutter.game.entity.EntityGadget;
|
||||
import emu.grasscutter.game.entity.GameEntity;
|
||||
import emu.grasscutter.game.entity.gadget.GadgetWorktop;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.game.world.SpawnDataEntry;
|
||||
import emu.grasscutter.game.world.SpawnDataEntry.SpawnGroupEntry;
|
||||
import emu.grasscutter.net.proto.BlossomBriefInfoOuterClass;
|
||||
import emu.grasscutter.net.proto.VisionTypeOuterClass;
|
||||
import emu.grasscutter.server.packet.send.PacketBlossomBriefInfoNotify;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.ints.IntList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BlossomManager {
|
||||
private final Scene scene;
|
||||
private final List<BlossomActivity> blossomActivities = new ArrayList<>();
|
||||
private final List<BlossomActivity> activeChests = new ArrayList<>();
|
||||
private final List<EntityGadget> createdEntity = new ArrayList<>();
|
||||
private final List<SpawnDataEntry> blossomConsumed = new ArrayList<>();
|
||||
|
||||
public BlossomManager(Scene scene) {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
private static Integer getPreviewReward(BlossomType type, int worldLevel) {
|
||||
// TODO: blossoms should be based on their city
|
||||
if (type == null) {
|
||||
Grasscutter.getLogger().error("Illegal blossom type {}", type);
|
||||
return null;
|
||||
}
|
||||
|
||||
int blossomChestId = type.getBlossomChestId();
|
||||
var dataMap = GameData.getBlossomRefreshExcelConfigDataMap();
|
||||
for (var data : dataMap.values()) {
|
||||
if (blossomChestId == data.getBlossomChestId()) {
|
||||
var dropVecList = data.getDropVec();
|
||||
if (worldLevel > dropVecList.length) {
|
||||
Grasscutter.getLogger().error("Illegal world level {}", worldLevel);
|
||||
return null;
|
||||
}
|
||||
return dropVecList[worldLevel].getPreviewReward();
|
||||
}
|
||||
}
|
||||
Grasscutter.getLogger().error("Cannot find blossom type {}", type);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static RewardPreviewData getRewardList(BlossomType type, int worldLevel) {
|
||||
Integer previewReward = getPreviewReward(type, worldLevel);
|
||||
if (previewReward == null) return null;
|
||||
return GameData.getRewardPreviewDataMap().get((int) previewReward);
|
||||
}
|
||||
|
||||
public static IntList getRandomMonstersID(int difficulty, int count) {
|
||||
IntList result = new IntArrayList();
|
||||
List<Integer> monsters =
|
||||
GameDepot.getBlossomConfig().getMonsterIdsPerDifficulty().get(difficulty);
|
||||
for (int i = 0; i < count; i++) {
|
||||
result.add((int) monsters.get(Utils.randomRange(0, monsters.size() - 1)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void onTick() {
|
||||
synchronized (blossomActivities) {
|
||||
var it = blossomActivities.iterator();
|
||||
while (it.hasNext()) {
|
||||
var active = it.next();
|
||||
active.onTick();
|
||||
if (active.getPass()) {
|
||||
EntityGadget chest = active.getChest();
|
||||
scene.addEntity(chest);
|
||||
scene.setChallenge(null);
|
||||
activeChests.add(active);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void recycleGadgetEntity(List<GameEntity> entities) {
|
||||
for (var entity : entities) {
|
||||
if (entity instanceof EntityGadget gadget) {
|
||||
createdEntity.remove(gadget);
|
||||
}
|
||||
}
|
||||
notifyIcon();
|
||||
}
|
||||
|
||||
public void initBlossom(EntityGadget gadget) {
|
||||
if (createdEntity.contains(gadget)) {
|
||||
return;
|
||||
}
|
||||
if (blossomConsumed.contains(gadget.getSpawnEntry())) {
|
||||
return;
|
||||
}
|
||||
var id = gadget.getGadgetId();
|
||||
if (BlossomType.valueOf(id) == null) {
|
||||
return;
|
||||
}
|
||||
gadget.buildContent();
|
||||
gadget.setState(204);
|
||||
int worldLevel = getWorldLevel();
|
||||
GadgetWorktop gadgetWorktop = ((GadgetWorktop) gadget.getContent());
|
||||
gadgetWorktop.addWorktopOptions(new int[] {187});
|
||||
gadgetWorktop.setOnSelectWorktopOptionEvent(
|
||||
(GadgetWorktop context, int option) -> {
|
||||
BlossomActivity activity;
|
||||
EntityGadget entityGadget = context.getGadget();
|
||||
synchronized (blossomActivities) {
|
||||
for (BlossomActivity i : this.blossomActivities) {
|
||||
if (i.getGadget() == entityGadget) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int volume = 0;
|
||||
IntList monsters = new IntArrayList();
|
||||
while (true) {
|
||||
var remain = GameDepot.getBlossomConfig().getMonsterFightingVolume() - volume;
|
||||
if (remain <= 0) {
|
||||
break;
|
||||
}
|
||||
var rand = Utils.randomRange(1, 100);
|
||||
if (rand > 85 && remain >= 50) { // 15% ,generate strong monster
|
||||
monsters.addAll(getRandomMonstersID(2, 1));
|
||||
volume += 50;
|
||||
} else if (rand > 50 && remain >= 20) { // 35% ,generate normal monster
|
||||
monsters.addAll(getRandomMonstersID(1, 1));
|
||||
volume += 20;
|
||||
} else { // 50% ,generate weak monster
|
||||
monsters.addAll(getRandomMonstersID(0, 1));
|
||||
volume += 10;
|
||||
}
|
||||
}
|
||||
|
||||
Grasscutter.getLogger().info("Blossom Monsters:" + monsters);
|
||||
|
||||
activity = new BlossomActivity(entityGadget, monsters, -1, worldLevel);
|
||||
blossomActivities.add(activity);
|
||||
}
|
||||
entityGadget.updateState(201);
|
||||
scene.setChallenge(activity.getChallenge());
|
||||
scene.removeEntity(entityGadget, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE);
|
||||
activity.start();
|
||||
return true;
|
||||
});
|
||||
createdEntity.add(gadget);
|
||||
notifyIcon();
|
||||
}
|
||||
|
||||
public void notifyIcon() {
|
||||
final int wl = getWorldLevel();
|
||||
final int worldLevel = (wl < 0) ? 0 : ((wl > 8) ? 8 : wl);
|
||||
final var worldLevelData = GameData.getWorldLevelDataMap().get(worldLevel);
|
||||
final int monsterLevel = (worldLevelData != null) ? worldLevelData.getMonsterLevel() : 1;
|
||||
List<BlossomBriefInfoOuterClass.BlossomBriefInfo> blossoms = new ArrayList<>();
|
||||
GameDepot.getSpawnLists()
|
||||
.forEach(
|
||||
(gridBlockId, spawnDataEntryList) -> {
|
||||
int sceneId = gridBlockId.getSceneId();
|
||||
spawnDataEntryList.stream()
|
||||
.map(SpawnDataEntry::getGroup)
|
||||
.map(SpawnGroupEntry::getSpawns)
|
||||
.flatMap(List::stream)
|
||||
.filter(spawn -> !blossomConsumed.contains(spawn))
|
||||
.filter(spawn -> BlossomType.valueOf(spawn.getGadgetId()) != null)
|
||||
.forEach(
|
||||
spawn -> {
|
||||
var type = BlossomType.valueOf(spawn.getGadgetId());
|
||||
int previewReward = getPreviewReward(type, worldLevel);
|
||||
blossoms.add(
|
||||
BlossomBriefInfoOuterClass.BlossomBriefInfo.newBuilder()
|
||||
.setSceneId(sceneId)
|
||||
.setPos(spawn.getPos().toProto())
|
||||
.setResin(20)
|
||||
.setMonsterLevel(monsterLevel)
|
||||
.setRewardId(previewReward)
|
||||
.setCircleCampId(type.getCircleCampId())
|
||||
.setRefreshId(
|
||||
type.getBlossomChestId()) // TODO: replace when using actual
|
||||
// leylines
|
||||
.build());
|
||||
});
|
||||
});
|
||||
scene.broadcastPacket(new PacketBlossomBriefInfoNotify(blossoms));
|
||||
}
|
||||
|
||||
public int getWorldLevel() {
|
||||
return scene.getWorld().getWorldLevel();
|
||||
}
|
||||
|
||||
public List<GameItem> onReward(Player player, EntityGadget chest, boolean useCondensedResin) {
|
||||
var resinManager = player.getResinManager();
|
||||
synchronized (activeChests) {
|
||||
var it = activeChests.iterator();
|
||||
while (it.hasNext()) {
|
||||
var activeChest = it.next();
|
||||
if (activeChest.getChest() == chest) {
|
||||
boolean pay =
|
||||
useCondensedResin ? resinManager.useCondensedResin(1) : resinManager.useResin(20);
|
||||
if (pay) {
|
||||
int worldLevel = getWorldLevel();
|
||||
List<GameItem> items = new ArrayList<>();
|
||||
var gadget = activeChest.getGadget();
|
||||
var type = BlossomType.valueOf(gadget.getGadgetId());
|
||||
RewardPreviewData blossomRewards = getRewardList(type, worldLevel);
|
||||
if (blossomRewards == null) {
|
||||
Grasscutter.getLogger()
|
||||
.error("Blossom could not support world level : " + worldLevel);
|
||||
return null;
|
||||
}
|
||||
var rewards = blossomRewards.getPreviewItems();
|
||||
for (ItemParamData blossomReward : rewards) {
|
||||
int rewardCount = blossomReward.getCount();
|
||||
if (useCondensedResin) {
|
||||
rewardCount += blossomReward.getCount(); // Double!
|
||||
}
|
||||
items.add(new GameItem(blossomReward.getItemId(), rewardCount));
|
||||
}
|
||||
it.remove();
|
||||
recycleGadgetEntity(List.of(gadget));
|
||||
blossomConsumed.add(gadget.getSpawnEntry());
|
||||
return items;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.managers.blossom;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.GameDepot;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.data.excels.RewardPreviewData;
|
||||
import emu.grasscutter.game.entity.EntityGadget;
|
||||
import emu.grasscutter.game.entity.GameEntity;
|
||||
import emu.grasscutter.game.entity.gadget.GadgetWorktop;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.game.world.SpawnDataEntry;
|
||||
import emu.grasscutter.game.world.SpawnDataEntry.SpawnGroupEntry;
|
||||
import emu.grasscutter.net.proto.BlossomBriefInfoOuterClass;
|
||||
import emu.grasscutter.net.proto.VisionTypeOuterClass;
|
||||
import emu.grasscutter.server.packet.send.PacketBlossomBriefInfoNotify;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.ints.IntList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BlossomManager {
|
||||
private final Scene scene;
|
||||
private final List<BlossomActivity> blossomActivities = new ArrayList<>();
|
||||
private final List<BlossomActivity> activeChests = new ArrayList<>();
|
||||
private final List<EntityGadget> createdEntity = new ArrayList<>();
|
||||
|
||||
private final List<SpawnDataEntry> blossomConsumed = new ArrayList<>();
|
||||
|
||||
public BlossomManager(Scene scene) {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
public void onTick() {
|
||||
synchronized (blossomActivities) {
|
||||
var it = blossomActivities.iterator();
|
||||
while (it.hasNext()) {
|
||||
var active = it.next();
|
||||
active.onTick();
|
||||
if (active.getPass()) {
|
||||
EntityGadget chest = active.getChest();
|
||||
scene.addEntity(chest);
|
||||
scene.setChallenge(null);
|
||||
activeChests.add(active);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void recycleGadgetEntity(List<GameEntity> entities) {
|
||||
for (var entity : entities) {
|
||||
if (entity instanceof EntityGadget gadget) {
|
||||
createdEntity.remove(gadget);
|
||||
}
|
||||
}
|
||||
notifyIcon();
|
||||
}
|
||||
|
||||
public void initBlossom(EntityGadget gadget) {
|
||||
if (createdEntity.contains(gadget)) {
|
||||
return;
|
||||
}
|
||||
if (blossomConsumed.contains(gadget.getSpawnEntry())) {
|
||||
return;
|
||||
}
|
||||
var id = gadget.getGadgetId();
|
||||
if (BlossomType.valueOf(id) == null) {
|
||||
return;
|
||||
}
|
||||
gadget.buildContent();
|
||||
gadget.setState(204);
|
||||
int worldLevel = getWorldLevel();
|
||||
GadgetWorktop gadgetWorktop = ((GadgetWorktop) gadget.getContent());
|
||||
gadgetWorktop.addWorktopOptions(new int[] {187});
|
||||
gadgetWorktop.setOnSelectWorktopOptionEvent(
|
||||
(GadgetWorktop context, int option) -> {
|
||||
BlossomActivity activity;
|
||||
EntityGadget entityGadget = context.getGadget();
|
||||
synchronized (blossomActivities) {
|
||||
for (BlossomActivity i : this.blossomActivities) {
|
||||
if (i.getGadget() == entityGadget) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int volume = 0;
|
||||
IntList monsters = new IntArrayList();
|
||||
while (true) {
|
||||
var remain = GameDepot.getBlossomConfig().getMonsterFightingVolume() - volume;
|
||||
if (remain <= 0) {
|
||||
break;
|
||||
}
|
||||
var rand = Utils.randomRange(1, 100);
|
||||
if (rand > 85 && remain >= 50) { // 15% ,generate strong monster
|
||||
monsters.addAll(getRandomMonstersID(2, 1));
|
||||
volume += 50;
|
||||
} else if (rand > 50 && remain >= 20) { // 35% ,generate normal monster
|
||||
monsters.addAll(getRandomMonstersID(1, 1));
|
||||
volume += 20;
|
||||
} else { // 50% ,generate weak monster
|
||||
monsters.addAll(getRandomMonstersID(0, 1));
|
||||
volume += 10;
|
||||
}
|
||||
}
|
||||
|
||||
Grasscutter.getLogger().info("Blossom Monsters:" + monsters);
|
||||
|
||||
activity = new BlossomActivity(entityGadget, monsters, -1, worldLevel);
|
||||
blossomActivities.add(activity);
|
||||
}
|
||||
entityGadget.updateState(201);
|
||||
scene.setChallenge(activity.getChallenge());
|
||||
scene.removeEntity(entityGadget, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE);
|
||||
activity.start();
|
||||
return true;
|
||||
});
|
||||
createdEntity.add(gadget);
|
||||
notifyIcon();
|
||||
}
|
||||
|
||||
public void notifyIcon() {
|
||||
final int wl = getWorldLevel();
|
||||
final int worldLevel = (wl < 0) ? 0 : ((wl > 8) ? 8 : wl);
|
||||
final var worldLevelData = GameData.getWorldLevelDataMap().get(worldLevel);
|
||||
final int monsterLevel = (worldLevelData != null) ? worldLevelData.getMonsterLevel() : 1;
|
||||
List<BlossomBriefInfoOuterClass.BlossomBriefInfo> blossoms = new ArrayList<>();
|
||||
GameDepot.getSpawnLists()
|
||||
.forEach(
|
||||
(gridBlockId, spawnDataEntryList) -> {
|
||||
int sceneId = gridBlockId.getSceneId();
|
||||
spawnDataEntryList.stream()
|
||||
.map(SpawnDataEntry::getGroup)
|
||||
.map(SpawnGroupEntry::getSpawns)
|
||||
.flatMap(List::stream)
|
||||
.filter(spawn -> !blossomConsumed.contains(spawn))
|
||||
.filter(spawn -> BlossomType.valueOf(spawn.getGadgetId()) != null)
|
||||
.forEach(
|
||||
spawn -> {
|
||||
var type = BlossomType.valueOf(spawn.getGadgetId());
|
||||
int previewReward = getPreviewReward(type, worldLevel);
|
||||
blossoms.add(
|
||||
BlossomBriefInfoOuterClass.BlossomBriefInfo.newBuilder()
|
||||
.setSceneId(sceneId)
|
||||
.setPos(spawn.getPos().toProto())
|
||||
.setResin(20)
|
||||
.setMonsterLevel(monsterLevel)
|
||||
.setRewardId(previewReward)
|
||||
.setCircleCampId(type.getCircleCampId())
|
||||
.setRefreshId(
|
||||
type.getBlossomChestId()) // TODO: replace when using actual
|
||||
// leylines
|
||||
.build());
|
||||
});
|
||||
});
|
||||
scene.broadcastPacket(new PacketBlossomBriefInfoNotify(blossoms));
|
||||
}
|
||||
|
||||
public int getWorldLevel() {
|
||||
return scene.getWorld().getWorldLevel();
|
||||
}
|
||||
|
||||
private static Integer getPreviewReward(BlossomType type, int worldLevel) {
|
||||
// TODO: blossoms should be based on their city
|
||||
if (type == null) {
|
||||
Grasscutter.getLogger().error("Illegal blossom type {}", type);
|
||||
return null;
|
||||
}
|
||||
|
||||
int blossomChestId = type.getBlossomChestId();
|
||||
var dataMap = GameData.getBlossomRefreshExcelConfigDataMap();
|
||||
for (var data : dataMap.values()) {
|
||||
if (blossomChestId == data.getBlossomChestId()) {
|
||||
var dropVecList = data.getDropVec();
|
||||
if (worldLevel > dropVecList.length) {
|
||||
Grasscutter.getLogger().error("Illegal world level {}", worldLevel);
|
||||
return null;
|
||||
}
|
||||
return dropVecList[worldLevel].getPreviewReward();
|
||||
}
|
||||
}
|
||||
Grasscutter.getLogger().error("Cannot find blossom type {}", type);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static RewardPreviewData getRewardList(BlossomType type, int worldLevel) {
|
||||
Integer previewReward = getPreviewReward(type, worldLevel);
|
||||
if (previewReward == null) return null;
|
||||
return GameData.getRewardPreviewDataMap().get((int) previewReward);
|
||||
}
|
||||
|
||||
public List<GameItem> onReward(Player player, EntityGadget chest, boolean useCondensedResin) {
|
||||
var resinManager = player.getResinManager();
|
||||
synchronized (activeChests) {
|
||||
var it = activeChests.iterator();
|
||||
while (it.hasNext()) {
|
||||
var activeChest = it.next();
|
||||
if (activeChest.getChest() == chest) {
|
||||
boolean pay =
|
||||
useCondensedResin ? resinManager.useCondensedResin(1) : resinManager.useResin(20);
|
||||
if (pay) {
|
||||
int worldLevel = getWorldLevel();
|
||||
List<GameItem> items = new ArrayList<>();
|
||||
var gadget = activeChest.getGadget();
|
||||
var type = BlossomType.valueOf(gadget.getGadgetId());
|
||||
RewardPreviewData blossomRewards = getRewardList(type, worldLevel);
|
||||
if (blossomRewards == null) {
|
||||
Grasscutter.getLogger()
|
||||
.error("Blossom could not support world level : " + worldLevel);
|
||||
return null;
|
||||
}
|
||||
var rewards = blossomRewards.getPreviewItems();
|
||||
for (ItemParamData blossomReward : rewards) {
|
||||
int rewardCount = blossomReward.getCount();
|
||||
if (useCondensedResin) {
|
||||
rewardCount += blossomReward.getCount(); // Double!
|
||||
}
|
||||
items.add(new GameItem(blossomReward.getItemId(), rewardCount));
|
||||
}
|
||||
it.remove();
|
||||
recycleGadgetEntity(List.of(gadget));
|
||||
blossomConsumed.add(gadget.getSpawnEntry());
|
||||
return items;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static IntList getRandomMonstersID(int difficulty, int count) {
|
||||
IntList result = new IntArrayList();
|
||||
List<Integer> monsters =
|
||||
GameDepot.getBlossomConfig().getMonsterIdsPerDifficulty().get(difficulty);
|
||||
for (int i = 0; i < count; i++) {
|
||||
result.add((int) monsters.get(Utils.randomRange(0, monsters.size() - 1)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -1,163 +1,163 @@
|
||||
package emu.grasscutter.game.managers.cooking;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.data.excels.CompoundData;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.player.BasePlayerManager;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.net.proto.CompoundQueueDataOuterClass.CompoundQueueData;
|
||||
import emu.grasscutter.net.proto.GetCompoundDataReqOuterClass.GetCompoundDataReq;
|
||||
import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam;
|
||||
import emu.grasscutter.net.proto.PlayerCompoundMaterialReqOuterClass.PlayerCompoundMaterialReq;
|
||||
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
|
||||
import emu.grasscutter.net.proto.TakeCompoundOutputReqOuterClass.TakeCompoundOutputReq;
|
||||
import emu.grasscutter.server.packet.send.PackageTakeCompoundOutputRsp;
|
||||
import emu.grasscutter.server.packet.send.PacketCompoundDataNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketGetCompoundDataRsp;
|
||||
import emu.grasscutter.server.packet.send.PacketPlayerCompoundMaterialRsp;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import java.util.*;
|
||||
|
||||
public class CookingCompoundManager extends BasePlayerManager {
|
||||
private static Set<Integer> defaultUnlockedCompounds;
|
||||
private static Map<Integer, Set<Integer>> compoundGroups;
|
||||
// TODO:bind it to player
|
||||
private static Set<Integer> unlocked;
|
||||
|
||||
public CookingCompoundManager(Player player) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
defaultUnlockedCompounds = new HashSet<>();
|
||||
compoundGroups = new HashMap<>();
|
||||
GameData.getCompoundDataMap()
|
||||
.forEach(
|
||||
(id, compound) -> {
|
||||
if (compound.isDefaultUnlocked()) {
|
||||
defaultUnlockedCompounds.add(id);
|
||||
}
|
||||
compoundGroups.computeIfAbsent(compound.getGroupID(), gid -> new HashSet<>()).add(id);
|
||||
});
|
||||
// TODO:Because we haven't implemented fishing feature,unlock all compounds related to
|
||||
// fish.Besides,it should be bound to player rather than manager.
|
||||
unlocked = new HashSet<>(defaultUnlockedCompounds);
|
||||
if (compoundGroups.containsKey(3)) // Avoid NPE from Resources error
|
||||
unlocked.addAll(compoundGroups.get(3));
|
||||
}
|
||||
|
||||
private synchronized List<CompoundQueueData> getCompoundQueueData() {
|
||||
List<CompoundQueueData> compoundQueueData =
|
||||
new ArrayList<>(player.getActiveCookCompounds().size());
|
||||
int currentTime = Utils.getCurrentSeconds();
|
||||
for (var item : player.getActiveCookCompounds().values()) {
|
||||
var data =
|
||||
CompoundQueueData.newBuilder()
|
||||
.setCompoundId(item.getCompoundId())
|
||||
.setOutputCount(item.getOutputCount(currentTime))
|
||||
.setOutputTime(item.getOutputTime(currentTime))
|
||||
.setWaitCount(item.getWaitCount(currentTime))
|
||||
.build();
|
||||
compoundQueueData.add(data);
|
||||
}
|
||||
return compoundQueueData;
|
||||
}
|
||||
|
||||
public synchronized void handleGetCompoundDataReq(GetCompoundDataReq req) {
|
||||
player.sendPacket(new PacketGetCompoundDataRsp(unlocked, getCompoundQueueData()));
|
||||
}
|
||||
|
||||
public synchronized void handlePlayerCompoundMaterialReq(PlayerCompoundMaterialReq req) {
|
||||
int id = req.getCompoundId(), count = req.getCount();
|
||||
CompoundData compound = GameData.getCompoundDataMap().get(id);
|
||||
var activeCompounds = player.getActiveCookCompounds();
|
||||
|
||||
// check whether the compound is available
|
||||
// TODO:add other compounds,see my pr for detail
|
||||
if (!unlocked.contains(id)) {
|
||||
player.sendPacket(new PacketPlayerCompoundMaterialRsp(Retcode.RET_FAIL_VALUE));
|
||||
return;
|
||||
}
|
||||
// check whether the queue is full
|
||||
if (activeCompounds.containsKey(id)
|
||||
&& activeCompounds.get(id).getTotalCount() + count > compound.getQueueSize()) {
|
||||
player.sendPacket(new PacketPlayerCompoundMaterialRsp(Retcode.RET_COMPOUND_QUEUE_FULL_VALUE));
|
||||
return;
|
||||
}
|
||||
// try to consume raw materials
|
||||
if (!player.getInventory().payItems(compound.getInputVec(), count)) {
|
||||
// TODO:I'm not sure whether retcode is correct.
|
||||
player.sendPacket(
|
||||
new PacketPlayerCompoundMaterialRsp(Retcode.RET_ITEM_COUNT_NOT_ENOUGH_VALUE));
|
||||
return;
|
||||
}
|
||||
ActiveCookCompoundData c;
|
||||
int currentTime = Utils.getCurrentSeconds();
|
||||
if (activeCompounds.containsKey(id)) {
|
||||
c = activeCompounds.get(id);
|
||||
c.addCompound(count, currentTime);
|
||||
} else {
|
||||
c = new ActiveCookCompoundData(id, compound.getCostTime(), count, currentTime);
|
||||
activeCompounds.put(id, c);
|
||||
}
|
||||
var data =
|
||||
CompoundQueueData.newBuilder()
|
||||
.setCompoundId(id)
|
||||
.setOutputCount(c.getOutputCount(currentTime))
|
||||
.setOutputTime(c.getOutputTime(currentTime))
|
||||
.setWaitCount(c.getWaitCount(currentTime))
|
||||
.build();
|
||||
player.sendPacket(new PacketPlayerCompoundMaterialRsp(data));
|
||||
}
|
||||
|
||||
public synchronized void handleTakeCompoundOutputReq(TakeCompoundOutputReq req) {
|
||||
// Client won't set compound_id and will set group_id instead.
|
||||
int groupId = req.getCompoundGroupId();
|
||||
var activeCompounds = player.getActiveCookCompounds();
|
||||
int now = Utils.getCurrentSeconds();
|
||||
// check available queues
|
||||
boolean success = false;
|
||||
Map<Integer, GameItem> allRewards = new HashMap<>();
|
||||
for (int id : compoundGroups.get(groupId)) {
|
||||
if (!activeCompounds.containsKey(id)) continue;
|
||||
int quantity = activeCompounds.get(id).takeCompound(now);
|
||||
if (activeCompounds.get(id).getTotalCount() == 0) activeCompounds.remove(id);
|
||||
if (quantity == 0) continue;
|
||||
List<ItemParamData> rewards = GameData.getCompoundDataMap().get(id).getOutputVec();
|
||||
for (var i : rewards) {
|
||||
if (i.getId() == 0) continue;
|
||||
if (allRewards.containsKey(i.getId())) {
|
||||
GameItem item = allRewards.get(i.getId());
|
||||
item.setCount(item.getCount() + i.getCount() * quantity);
|
||||
} else {
|
||||
allRewards.put(i.getId(), new GameItem(i.getId(), i.getCount() * quantity));
|
||||
}
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
// give player the rewards
|
||||
if (success) {
|
||||
player.getInventory().addItems(allRewards.values(), ActionReason.Compound);
|
||||
player.sendPacket(
|
||||
new PackageTakeCompoundOutputRsp(
|
||||
allRewards.values().stream()
|
||||
.map(
|
||||
i ->
|
||||
ItemParam.newBuilder()
|
||||
.setItemId(i.getItemId())
|
||||
.setCount(i.getCount())
|
||||
.build())
|
||||
.toList(),
|
||||
Retcode.RET_SUCC_VALUE));
|
||||
} else {
|
||||
player.sendPacket(
|
||||
new PackageTakeCompoundOutputRsp(null, Retcode.RET_COMPOUND_NOT_FINISH_VALUE));
|
||||
}
|
||||
}
|
||||
|
||||
public void onPlayerLogin() {
|
||||
player.sendPacket(new PacketCompoundDataNotify(unlocked, getCompoundQueueData()));
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.managers.cooking;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.data.excels.CompoundData;
|
||||
import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.player.BasePlayerManager;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.net.proto.CompoundQueueDataOuterClass.CompoundQueueData;
|
||||
import emu.grasscutter.net.proto.GetCompoundDataReqOuterClass.GetCompoundDataReq;
|
||||
import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam;
|
||||
import emu.grasscutter.net.proto.PlayerCompoundMaterialReqOuterClass.PlayerCompoundMaterialReq;
|
||||
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
|
||||
import emu.grasscutter.net.proto.TakeCompoundOutputReqOuterClass.TakeCompoundOutputReq;
|
||||
import emu.grasscutter.server.packet.send.PackageTakeCompoundOutputRsp;
|
||||
import emu.grasscutter.server.packet.send.PacketCompoundDataNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketGetCompoundDataRsp;
|
||||
import emu.grasscutter.server.packet.send.PacketPlayerCompoundMaterialRsp;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import java.util.*;
|
||||
|
||||
public class CookingCompoundManager extends BasePlayerManager {
|
||||
private static Set<Integer> defaultUnlockedCompounds;
|
||||
private static Map<Integer, Set<Integer>> compoundGroups;
|
||||
// TODO:bind it to player
|
||||
private static Set<Integer> unlocked;
|
||||
|
||||
public CookingCompoundManager(Player player) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
defaultUnlockedCompounds = new HashSet<>();
|
||||
compoundGroups = new HashMap<>();
|
||||
GameData.getCompoundDataMap()
|
||||
.forEach(
|
||||
(id, compound) -> {
|
||||
if (compound.isDefaultUnlocked()) {
|
||||
defaultUnlockedCompounds.add(id);
|
||||
}
|
||||
compoundGroups.computeIfAbsent(compound.getGroupId(), gid -> new HashSet<>()).add(id);
|
||||
});
|
||||
// TODO:Because we haven't implemented fishing feature,unlock all compounds related to
|
||||
// fish.Besides,it should be bound to player rather than manager.
|
||||
unlocked = new HashSet<>(defaultUnlockedCompounds);
|
||||
if (compoundGroups.containsKey(3)) // Avoid NPE from Resources error
|
||||
unlocked.addAll(compoundGroups.get(3));
|
||||
}
|
||||
|
||||
private synchronized List<CompoundQueueData> getCompoundQueueData() {
|
||||
List<CompoundQueueData> compoundQueueData =
|
||||
new ArrayList<>(player.getActiveCookCompounds().size());
|
||||
int currentTime = Utils.getCurrentSeconds();
|
||||
for (var item : player.getActiveCookCompounds().values()) {
|
||||
var data =
|
||||
CompoundQueueData.newBuilder()
|
||||
.setCompoundId(item.getCompoundId())
|
||||
.setOutputCount(item.getOutputCount(currentTime))
|
||||
.setOutputTime(item.getOutputTime(currentTime))
|
||||
.setWaitCount(item.getWaitCount(currentTime))
|
||||
.build();
|
||||
compoundQueueData.add(data);
|
||||
}
|
||||
return compoundQueueData;
|
||||
}
|
||||
|
||||
public synchronized void handleGetCompoundDataReq(GetCompoundDataReq req) {
|
||||
player.sendPacket(new PacketGetCompoundDataRsp(unlocked, getCompoundQueueData()));
|
||||
}
|
||||
|
||||
public synchronized void handlePlayerCompoundMaterialReq(PlayerCompoundMaterialReq req) {
|
||||
int id = req.getCompoundId(), count = req.getCount();
|
||||
CompoundData compound = GameData.getCompoundDataMap().get(id);
|
||||
var activeCompounds = player.getActiveCookCompounds();
|
||||
|
||||
// check whether the compound is available
|
||||
// TODO:add other compounds,see my pr for detail
|
||||
if (!unlocked.contains(id)) {
|
||||
player.sendPacket(new PacketPlayerCompoundMaterialRsp(Retcode.RET_FAIL_VALUE));
|
||||
return;
|
||||
}
|
||||
// check whether the queue is full
|
||||
if (activeCompounds.containsKey(id)
|
||||
&& activeCompounds.get(id).getTotalCount() + count > compound.getQueueSize()) {
|
||||
player.sendPacket(new PacketPlayerCompoundMaterialRsp(Retcode.RET_COMPOUND_QUEUE_FULL_VALUE));
|
||||
return;
|
||||
}
|
||||
// try to consume raw materials
|
||||
if (!player.getInventory().payItems(compound.getInputVec(), count)) {
|
||||
// TODO:I'm not sure whether retcode is correct.
|
||||
player.sendPacket(
|
||||
new PacketPlayerCompoundMaterialRsp(Retcode.RET_ITEM_COUNT_NOT_ENOUGH_VALUE));
|
||||
return;
|
||||
}
|
||||
ActiveCookCompoundData c;
|
||||
int currentTime = Utils.getCurrentSeconds();
|
||||
if (activeCompounds.containsKey(id)) {
|
||||
c = activeCompounds.get(id);
|
||||
c.addCompound(count, currentTime);
|
||||
} else {
|
||||
c = new ActiveCookCompoundData(id, compound.getCostTime(), count, currentTime);
|
||||
activeCompounds.put(id, c);
|
||||
}
|
||||
var data =
|
||||
CompoundQueueData.newBuilder()
|
||||
.setCompoundId(id)
|
||||
.setOutputCount(c.getOutputCount(currentTime))
|
||||
.setOutputTime(c.getOutputTime(currentTime))
|
||||
.setWaitCount(c.getWaitCount(currentTime))
|
||||
.build();
|
||||
player.sendPacket(new PacketPlayerCompoundMaterialRsp(data));
|
||||
}
|
||||
|
||||
public synchronized void handleTakeCompoundOutputReq(TakeCompoundOutputReq req) {
|
||||
// Client won't set compound_id and will set group_id instead.
|
||||
int groupId = req.getCompoundGroupId();
|
||||
var activeCompounds = player.getActiveCookCompounds();
|
||||
int now = Utils.getCurrentSeconds();
|
||||
// check available queues
|
||||
boolean success = false;
|
||||
Map<Integer, GameItem> allRewards = new HashMap<>();
|
||||
for (int id : compoundGroups.get(groupId)) {
|
||||
if (!activeCompounds.containsKey(id)) continue;
|
||||
int quantity = activeCompounds.get(id).takeCompound(now);
|
||||
if (activeCompounds.get(id).getTotalCount() == 0) activeCompounds.remove(id);
|
||||
if (quantity == 0) continue;
|
||||
List<ItemParamData> rewards = GameData.getCompoundDataMap().get(id).getOutputVec();
|
||||
for (var i : rewards) {
|
||||
if (i.getId() == 0) continue;
|
||||
if (allRewards.containsKey(i.getId())) {
|
||||
GameItem item = allRewards.get(i.getId());
|
||||
item.setCount(item.getCount() + i.getCount() * quantity);
|
||||
} else {
|
||||
allRewards.put(i.getId(), new GameItem(i.getId(), i.getCount() * quantity));
|
||||
}
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
// give player the rewards
|
||||
if (success) {
|
||||
player.getInventory().addItems(allRewards.values(), ActionReason.Compound);
|
||||
player.sendPacket(
|
||||
new PackageTakeCompoundOutputRsp(
|
||||
allRewards.values().stream()
|
||||
.map(
|
||||
i ->
|
||||
ItemParam.newBuilder()
|
||||
.setItemId(i.getItemId())
|
||||
.setCount(i.getCount())
|
||||
.build())
|
||||
.toList(),
|
||||
Retcode.RET_SUCC_VALUE));
|
||||
} else {
|
||||
player.sendPacket(
|
||||
new PackageTakeCompoundOutputRsp(null, Retcode.RET_COMPOUND_NOT_FINISH_VALUE));
|
||||
}
|
||||
}
|
||||
|
||||
public void onPlayerLogin() {
|
||||
player.sendPacket(new PacketCompoundDataNotify(unlocked, getCompoundQueueData()));
|
||||
}
|
||||
}
|
||||
|
@ -1,420 +1,418 @@
|
||||
package emu.grasscutter.game.managers.energy;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.DataLoader;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.ItemData;
|
||||
import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData;
|
||||
import emu.grasscutter.data.excels.monster.MonsterData.HpDrops;
|
||||
import emu.grasscutter.game.avatar.Avatar;
|
||||
import emu.grasscutter.game.entity.*;
|
||||
import emu.grasscutter.game.player.BasePlayerManager;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.ElementType;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.game.props.MonsterType;
|
||||
import emu.grasscutter.game.props.WeaponType;
|
||||
import emu.grasscutter.net.proto.AbilityActionGenerateElemBallOuterClass.AbilityActionGenerateElemBall;
|
||||
import emu.grasscutter.net.proto.AbilityIdentifierOuterClass.AbilityIdentifier;
|
||||
import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry;
|
||||
import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult;
|
||||
import emu.grasscutter.net.proto.ChangeEnergyReasonOuterClass.ChangeEnergyReason;
|
||||
import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo;
|
||||
import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import lombok.Getter;
|
||||
|
||||
public class EnergyManager extends BasePlayerManager {
|
||||
private static final Int2ObjectMap<List<EnergyDropInfo>> energyDropData =
|
||||
new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2ObjectMap<List<SkillParticleGenerationInfo>>
|
||||
skillParticleGenerationData = new Int2ObjectOpenHashMap<>();
|
||||
private final Object2IntMap<EntityAvatar> avatarNormalProbabilities;
|
||||
@Getter private boolean energyUsage; // Should energy usage be enabled for this player?
|
||||
|
||||
public EnergyManager(Player player) {
|
||||
super(player);
|
||||
this.avatarNormalProbabilities = new Object2IntOpenHashMap<>();
|
||||
this.energyUsage = GAME_OPTIONS.energyUsage;
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
// Read the data we need for monster energy drops.
|
||||
try {
|
||||
DataLoader.loadList("EnergyDrop.json", EnergyDropEntry.class)
|
||||
.forEach(
|
||||
entry -> {
|
||||
energyDropData.put(entry.getDropId(), entry.getDropList());
|
||||
});
|
||||
|
||||
Grasscutter.getLogger().debug("Energy drop data successfully loaded.");
|
||||
} catch (Exception ex) {
|
||||
Grasscutter.getLogger().error("Unable to load energy drop data.", ex);
|
||||
}
|
||||
|
||||
// Read the data for particle generation from skills
|
||||
try {
|
||||
DataLoader.loadList("SkillParticleGeneration.json", SkillParticleGenerationEntry.class)
|
||||
.forEach(
|
||||
entry -> {
|
||||
skillParticleGenerationData.put(entry.getAvatarId(), entry.getAmountList());
|
||||
});
|
||||
|
||||
Grasscutter.getLogger().debug("Skill particle generation data successfully loaded.");
|
||||
} catch (Exception ex) {
|
||||
Grasscutter.getLogger().error("Unable to load skill particle generation data data.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/** Particle creation for elemental skills. */
|
||||
private int getBallCountForAvatar(int avatarId) {
|
||||
// We default to two particles.
|
||||
int count = 2;
|
||||
|
||||
// If we don't have any data for this avatar, stop.
|
||||
if (!skillParticleGenerationData.containsKey(avatarId)) {
|
||||
Grasscutter.getLogger().warn("No particle generation data for avatarId {} found.", avatarId);
|
||||
}
|
||||
// If we do have data, roll for how many particles we should generate.
|
||||
else {
|
||||
int roll = ThreadLocalRandom.current().nextInt(0, 100);
|
||||
int percentageStack = 0;
|
||||
for (SkillParticleGenerationInfo info : skillParticleGenerationData.get(avatarId)) {
|
||||
int chance = info.getChance();
|
||||
percentageStack += chance;
|
||||
if (roll < percentageStack) {
|
||||
count = info.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Done.
|
||||
return count;
|
||||
}
|
||||
|
||||
private int getBallIdForElement(ElementType element) {
|
||||
// If we have no element, we default to an element-less particle.
|
||||
if (element == null) {
|
||||
return 2024;
|
||||
}
|
||||
|
||||
// Otherwise, we determine the particle's ID based on the element.
|
||||
return switch (element) {
|
||||
case Fire -> 2017;
|
||||
case Water -> 2018;
|
||||
case Grass -> 2019;
|
||||
case Electric -> 2020;
|
||||
case Wind -> 2021;
|
||||
case Ice -> 2022;
|
||||
case Rock -> 2023;
|
||||
default -> 2024;
|
||||
};
|
||||
}
|
||||
|
||||
public void handleGenerateElemBall(AbilityInvokeEntry invoke)
|
||||
throws InvalidProtocolBufferException {
|
||||
// ToDo:
|
||||
// This is also called when a weapon like Favonius Warbow etc. creates energy through its
|
||||
// passive.
|
||||
// We are not handling this correctly at the moment.
|
||||
|
||||
// Get action info.
|
||||
AbilityActionGenerateElemBall action =
|
||||
AbilityActionGenerateElemBall.parseFrom(invoke.getAbilityData());
|
||||
if (action == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Default to an elementless particle.
|
||||
int itemId = 2024;
|
||||
|
||||
// Generate 2 particles by default.
|
||||
int amount = 2;
|
||||
|
||||
// Try to get the casting avatar from the player's party.
|
||||
Optional<EntityAvatar> avatarEntity =
|
||||
this.getCastingAvatarEntityForEnergy(invoke.getEntityId());
|
||||
|
||||
// Bug: invokes twice sometimes, Ayato, Keqing
|
||||
// ToDo: deal with press, hold difference. deal with charge(Beidou, Yunjin)
|
||||
if (avatarEntity.isPresent()) {
|
||||
Avatar avatar = avatarEntity.get().getAvatar();
|
||||
|
||||
if (avatar != null) {
|
||||
int avatarId = avatar.getAvatarId();
|
||||
AvatarSkillDepotData skillDepotData = avatar.getSkillDepot();
|
||||
|
||||
// Determine how many particles we need to create for this avatar.
|
||||
amount = this.getBallCountForAvatar(avatarId);
|
||||
|
||||
// Determine the avatar's element, and based on that the ID of the
|
||||
// particles we have to generate.
|
||||
if (skillDepotData != null) {
|
||||
ElementType element = skillDepotData.getElementType();
|
||||
itemId = this.getBallIdForElement(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the particles.
|
||||
var pos = new Position(action.getPos());
|
||||
for (int i = 0; i < amount; i++) {
|
||||
this.generateElemBall(itemId, pos, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Energy generation for NAs/CAs.
|
||||
*
|
||||
* @param avatar The avatar.
|
||||
*/
|
||||
private void generateEnergyForNormalAndCharged(EntityAvatar avatar) {
|
||||
// This logic is based on the descriptions given in
|
||||
// https://genshin-impact.fandom.com/wiki/Energy#Energy_Generated_by_Normal_Attacks
|
||||
// https://library.keqingmains.com/combat-mechanics/energy#auto-attacking
|
||||
// Those descriptions are lacking in some information, so this implementation most likely
|
||||
// does not fully replicate the behavior of the official server. Open questions:
|
||||
// - Does the probability for a character reset after some time?
|
||||
// - Does the probability for a character reset when switching them out?
|
||||
// - Does this really count every individual hit separately?
|
||||
|
||||
// Get the avatar's weapon type.
|
||||
WeaponType weaponType = avatar.getAvatar().getAvatarData().getWeaponType();
|
||||
|
||||
// Check if we already have probability data for this avatar. If not, insert it.
|
||||
if (!this.avatarNormalProbabilities.containsKey(avatar)) {
|
||||
this.avatarNormalProbabilities.put(avatar, weaponType.getEnergyGainInitialProbability());
|
||||
}
|
||||
|
||||
// Roll for energy.
|
||||
int currentProbability = this.avatarNormalProbabilities.getInt(avatar);
|
||||
int roll = ThreadLocalRandom.current().nextInt(0, 100);
|
||||
|
||||
// If the player wins the roll, we increase the avatar's energy and reset the probability.
|
||||
if (roll < currentProbability) {
|
||||
avatar.addEnergy(1.0f, PropChangeReason.PROP_CHANGE_REASON_ABILITY, true);
|
||||
this.avatarNormalProbabilities.put(avatar, weaponType.getEnergyGainInitialProbability());
|
||||
}
|
||||
// Otherwise, we increase the probability for the next hit.
|
||||
else {
|
||||
this.avatarNormalProbabilities.put(
|
||||
avatar, currentProbability + weaponType.getEnergyGainIncreaseProbability());
|
||||
}
|
||||
}
|
||||
|
||||
public void handleAttackHit(EvtBeingHitInfo hitInfo) {
|
||||
// Get the attack result.
|
||||
AttackResult attackRes = hitInfo.getAttackResult();
|
||||
|
||||
// Make sure the attack was performed by the currently active avatar. If not, we ignore the hit.
|
||||
Optional<EntityAvatar> attackerEntity =
|
||||
this.getCastingAvatarEntityForEnergy(attackRes.getAttackerId());
|
||||
if (attackerEntity.isEmpty()
|
||||
|| this.player.getTeamManager().getCurrentAvatarEntity().getId()
|
||||
!= attackerEntity.get().getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the target is an actual enemy.
|
||||
GameEntity targetEntity = this.player.getScene().getEntityById(attackRes.getDefenseId());
|
||||
if (!(targetEntity instanceof EntityMonster targetMonster)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MonsterType targetType = targetMonster.getMonsterData().getType();
|
||||
if (targetType != MonsterType.MONSTER_ORDINARY && targetType != MonsterType.MONSTER_BOSS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the ability that caused this hit.
|
||||
AbilityIdentifier ability = attackRes.getAbilityIdentifier();
|
||||
|
||||
// Make sure there is no actual "ability" associated with the hit. For now, this is how we
|
||||
// identify normal and charged attacks. Note that this is not completely accurate:
|
||||
// - Many character's charged attacks have an ability associated with them. This means that,
|
||||
// for now, we don't identify charged attacks reliably.
|
||||
// - There might also be some cases where we incorrectly identify something as a normal or
|
||||
// charged attack that is not (Diluc's E?).
|
||||
// - Catalyst normal attacks have an ability, so we don't handle those for now.
|
||||
// ToDo: Fix all of that.
|
||||
if (ability != AbilityIdentifier.getDefaultInstance()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the energy generation.
|
||||
this.generateEnergyForNormalAndCharged(attackerEntity.get());
|
||||
}
|
||||
|
||||
/*
|
||||
* Energy logic related to using skills.
|
||||
*/
|
||||
|
||||
private void handleBurstCast(Avatar avatar, int skillId) {
|
||||
// Don't do anything if energy usage is disabled.
|
||||
if (!GAME_OPTIONS.energyUsage || !this.energyUsage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the cast skill was a burst, consume energy.
|
||||
if (avatar.getSkillDepot() != null && skillId == avatar.getSkillDepot().getEnergySkill()) {
|
||||
avatar.getAsEntity().clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START);
|
||||
}
|
||||
}
|
||||
|
||||
public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) {
|
||||
// Determine the entity that has cast the skill. Cancel if we can't find that avatar.
|
||||
Optional<EntityAvatar> caster =
|
||||
this.player.getTeamManager().getActiveTeam().stream()
|
||||
.filter(character -> character.getId() == casterId)
|
||||
.findFirst();
|
||||
|
||||
if (caster.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Avatar avatar = caster.get().getAvatar();
|
||||
|
||||
// Handle elemental burst.
|
||||
this.handleBurstCast(avatar, skillId);
|
||||
}
|
||||
|
||||
/*
|
||||
* Monster energy drops.
|
||||
*/
|
||||
|
||||
private void generateElemBallDrops(EntityMonster monster, int dropId) {
|
||||
// Generate all drops specified for the given drop id.
|
||||
if (!energyDropData.containsKey(dropId)) {
|
||||
Grasscutter.getLogger().warn("No drop data for dropId {} found.", dropId);
|
||||
return;
|
||||
}
|
||||
|
||||
for (EnergyDropInfo info : energyDropData.get(dropId)) {
|
||||
this.generateElemBall(info.getBallId(), monster.getPosition(), info.getCount());
|
||||
}
|
||||
}
|
||||
|
||||
public void handleMonsterEnergyDrop(
|
||||
EntityMonster monster, float hpBeforeDamage, float hpAfterDamage) {
|
||||
// Make sure this is actually a monster.
|
||||
// Note that some wildlife also has that type, like boars or birds.
|
||||
MonsterType type = monster.getMonsterData().getType();
|
||||
if (type != MonsterType.MONSTER_ORDINARY && type != MonsterType.MONSTER_BOSS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the HP thresholds for before and after the damage was taken.
|
||||
float maxHp = monster.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
|
||||
float thresholdBefore = hpBeforeDamage / maxHp;
|
||||
float thresholdAfter = hpAfterDamage / maxHp;
|
||||
|
||||
// Determine the thresholds the monster has passed, and generate drops based on that.
|
||||
for (HpDrops drop : monster.getMonsterData().getHpDrops()) {
|
||||
if (drop.getDropId() == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float threshold = drop.getHpPercent() / 100.0f;
|
||||
if (threshold < thresholdBefore && threshold >= thresholdAfter) {
|
||||
this.generateElemBallDrops(monster, drop.getDropId());
|
||||
}
|
||||
}
|
||||
|
||||
// Handle kill drops.
|
||||
if (hpAfterDamage <= 0 && monster.getMonsterData().getKillDropId() != 0) {
|
||||
this.generateElemBallDrops(monster, monster.getMonsterData().getKillDropId());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Utilities.
|
||||
*/
|
||||
|
||||
private void generateElemBall(int ballId, Position position, int count) {
|
||||
// Generate a particle/orb with the specified parameters.
|
||||
ItemData itemData = GameData.getItemDataMap().get(ballId);
|
||||
if (itemData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
EntityItem energyBall =
|
||||
new EntityItem(this.getPlayer().getScene(), this.getPlayer(), itemData, position, count);
|
||||
this.getPlayer().getScene().addEntity(energyBall);
|
||||
}
|
||||
|
||||
private Optional<EntityAvatar> getCastingAvatarEntityForEnergy(int invokeEntityId) {
|
||||
// To determine the avatar that has cast the skill that caused the energy particle to be
|
||||
// generated,
|
||||
// we have to look at the entity that has invoked the ability. This can either be that avatar
|
||||
// directly,
|
||||
// or it can be an `EntityClientGadget`, owned (some way up the owner hierarchy) by the avatar
|
||||
// that cast the skill.
|
||||
|
||||
// Try to get the invoking entity from the scene.
|
||||
GameEntity entity = this.player.getScene().getEntityById(invokeEntityId);
|
||||
|
||||
// Determine the ID of the entity that originally cast this skill. If the scene entity is null,
|
||||
// or not an `EntityClientGadget`, we assume that we are directly looking at the casting avatar
|
||||
// (the null case will happen if the avatar was switched out between casting the skill and the
|
||||
// particle being generated). If the scene entity is an `EntityClientGadget`, we need to find
|
||||
// the
|
||||
// ID of the original owner of that gadget.
|
||||
int avatarEntityId =
|
||||
(!(entity instanceof EntityClientGadget))
|
||||
? invokeEntityId
|
||||
: ((EntityClientGadget) entity).getOriginalOwnerEntityId();
|
||||
|
||||
// Finally, find the avatar entity in the player's team.
|
||||
return this.player.getTeamManager().getActiveTeam().stream()
|
||||
.filter(character -> character.getId() == avatarEntityId)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refills the energy of the active avatar.
|
||||
*
|
||||
* @return True if the energy was refilled, false otherwise.
|
||||
*/
|
||||
public boolean refillActiveEnergy() {
|
||||
var activeEntity = this.player.getTeamManager().getCurrentAvatarEntity();
|
||||
return activeEntity.addEnergy(
|
||||
activeEntity.getAvatar().getSkillDepot().getEnergySkillData().getCostElemVal());
|
||||
}
|
||||
|
||||
/**
|
||||
* Refills the energy of the entire team.
|
||||
*
|
||||
* @param changeReason The reason for the energy change.
|
||||
* @param isFlat Whether the energy should be added as a flat value.
|
||||
*/
|
||||
public void refillTeamEnergy(PropChangeReason changeReason, boolean isFlat) {
|
||||
for (var entityAvatar : this.player.getTeamManager().getActiveTeam()) {
|
||||
// giving the exact amount read off the AvatarSkillData.json
|
||||
entityAvatar.addEnergy(
|
||||
entityAvatar.getAvatar().getSkillDepot().getEnergySkillData().getCostElemVal(),
|
||||
changeReason,
|
||||
isFlat);
|
||||
}
|
||||
}
|
||||
|
||||
public void setEnergyUsage(boolean energyUsage) {
|
||||
this.energyUsage = energyUsage;
|
||||
if (!energyUsage) { // Refill team energy if usage is disabled
|
||||
for (EntityAvatar entityAvatar : this.player.getTeamManager().getActiveTeam()) {
|
||||
entityAvatar.addEnergy(1000, PropChangeReason.PROP_CHANGE_REASON_GM, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.managers.energy;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.DataLoader;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.ItemData;
|
||||
import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData;
|
||||
import emu.grasscutter.data.excels.monster.MonsterData.HpDrops;
|
||||
import emu.grasscutter.game.avatar.Avatar;
|
||||
import emu.grasscutter.game.entity.*;
|
||||
import emu.grasscutter.game.player.BasePlayerManager;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.ElementType;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.game.props.MonsterType;
|
||||
import emu.grasscutter.game.props.WeaponType;
|
||||
import emu.grasscutter.net.proto.AbilityActionGenerateElemBallOuterClass.AbilityActionGenerateElemBall;
|
||||
import emu.grasscutter.net.proto.AbilityIdentifierOuterClass.AbilityIdentifier;
|
||||
import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry;
|
||||
import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult;
|
||||
import emu.grasscutter.net.proto.ChangeEnergyReasonOuterClass.ChangeEnergyReason;
|
||||
import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo;
|
||||
import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import lombok.Getter;
|
||||
|
||||
public class EnergyManager extends BasePlayerManager {
|
||||
private static final Int2ObjectMap<List<EnergyDropInfo>> energyDropData =
|
||||
new Int2ObjectOpenHashMap<>();
|
||||
private static final Int2ObjectMap<List<SkillParticleGenerationInfo>>
|
||||
skillParticleGenerationData = new Int2ObjectOpenHashMap<>();
|
||||
private final Object2IntMap<EntityAvatar> avatarNormalProbabilities;
|
||||
@Getter private boolean energyUsage; // Should energy usage be enabled for this player?
|
||||
|
||||
public EnergyManager(Player player) {
|
||||
super(player);
|
||||
this.avatarNormalProbabilities = new Object2IntOpenHashMap<>();
|
||||
this.energyUsage = GAME_OPTIONS.energyUsage;
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
// Read the data we need for monster energy drops.
|
||||
try {
|
||||
DataLoader.loadList("EnergyDrop.json", EnergyDropEntry.class)
|
||||
.forEach(
|
||||
entry -> {
|
||||
energyDropData.put(entry.getDropId(), entry.getDropList());
|
||||
});
|
||||
|
||||
Grasscutter.getLogger().debug("Energy drop data successfully loaded.");
|
||||
} catch (Exception ex) {
|
||||
Grasscutter.getLogger().error("Unable to load energy drop data.", ex);
|
||||
}
|
||||
|
||||
// Read the data for particle generation from skills
|
||||
try {
|
||||
DataLoader.loadList("SkillParticleGeneration.json", SkillParticleGenerationEntry.class)
|
||||
.forEach(
|
||||
entry -> {
|
||||
skillParticleGenerationData.put(entry.getAvatarId(), entry.getAmountList());
|
||||
});
|
||||
|
||||
Grasscutter.getLogger().debug("Skill particle generation data successfully loaded.");
|
||||
} catch (Exception ex) {
|
||||
Grasscutter.getLogger().error("Unable to load skill particle generation data data.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/** Particle creation for elemental skills. */
|
||||
private int getBallCountForAvatar(int avatarId) {
|
||||
// We default to two particles.
|
||||
int count = 2;
|
||||
|
||||
// If we don't have any data for this avatar, stop.
|
||||
if (!skillParticleGenerationData.containsKey(avatarId)) {
|
||||
Grasscutter.getLogger().warn("No particle generation data for avatarId {} found.", avatarId);
|
||||
}
|
||||
// If we do have data, roll for how many particles we should generate.
|
||||
else {
|
||||
int roll = ThreadLocalRandom.current().nextInt(0, 100);
|
||||
int percentageStack = 0;
|
||||
for (SkillParticleGenerationInfo info : skillParticleGenerationData.get(avatarId)) {
|
||||
int chance = info.getChance();
|
||||
percentageStack += chance;
|
||||
if (roll < percentageStack) {
|
||||
count = info.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Done.
|
||||
return count;
|
||||
}
|
||||
|
||||
private int getBallIdForElement(ElementType element) {
|
||||
// If we have no element, we default to an element-less particle.
|
||||
if (element == null) {
|
||||
return 2024;
|
||||
}
|
||||
|
||||
// Otherwise, we determine the particle's ID based on the element.
|
||||
return switch (element) {
|
||||
case Fire -> 2017;
|
||||
case Water -> 2018;
|
||||
case Grass -> 2019;
|
||||
case Electric -> 2020;
|
||||
case Wind -> 2021;
|
||||
case Ice -> 2022;
|
||||
case Rock -> 2023;
|
||||
default -> 2024;
|
||||
};
|
||||
}
|
||||
|
||||
public void handleGenerateElemBall(AbilityInvokeEntry invoke)
|
||||
throws InvalidProtocolBufferException {
|
||||
// ToDo:
|
||||
// This is also called when a weapon like Favonius Warbow etc. creates energy through its
|
||||
// passive.
|
||||
// We are not handling this correctly at the moment.
|
||||
|
||||
// Get action info.
|
||||
AbilityActionGenerateElemBall action =
|
||||
AbilityActionGenerateElemBall.parseFrom(invoke.getAbilityData());
|
||||
if (action == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Default to an elementless particle.
|
||||
int itemId = 2024;
|
||||
|
||||
// Generate 2 particles by default.
|
||||
int amount = 2;
|
||||
|
||||
// Try to get the casting avatar from the player's party.
|
||||
Optional<EntityAvatar> avatarEntity =
|
||||
this.getCastingAvatarEntityForEnergy(invoke.getEntityId());
|
||||
|
||||
// Bug: invokes twice sometimes, Ayato, Keqing
|
||||
// ToDo: deal with press, hold difference. deal with charge(Beidou, Yunjin)
|
||||
if (avatarEntity.isPresent()) {
|
||||
Avatar avatar = avatarEntity.get().getAvatar();
|
||||
|
||||
if (avatar != null) {
|
||||
int avatarId = avatar.getAvatarId();
|
||||
AvatarSkillDepotData skillDepotData = avatar.getSkillDepot();
|
||||
|
||||
// Determine how many particles we need to create for this avatar.
|
||||
amount = this.getBallCountForAvatar(avatarId);
|
||||
|
||||
// Determine the avatar's element, and based on that the ID of the
|
||||
// particles we have to generate.
|
||||
if (skillDepotData != null) {
|
||||
ElementType element = skillDepotData.getElementType();
|
||||
itemId = this.getBallIdForElement(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the particles.
|
||||
var pos = new Position(action.getPos());
|
||||
for (int i = 0; i < amount; i++) {
|
||||
this.generateElemBall(itemId, pos, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Energy generation for NAs/CAs.
|
||||
*
|
||||
* @param avatar The avatar.
|
||||
*/
|
||||
private void generateEnergyForNormalAndCharged(EntityAvatar avatar) {
|
||||
// This logic is based on the descriptions given in
|
||||
// https://genshin-impact.fandom.com/wiki/Energy#Energy_Generated_by_Normal_Attacks
|
||||
// https://library.keqingmains.com/combat-mechanics/energy#auto-attacking
|
||||
// Those descriptions are lacking in some information, so this implementation most likely
|
||||
// does not fully replicate the behavior of the official server. Open questions:
|
||||
// - Does the probability for a character reset after some time?
|
||||
// - Does the probability for a character reset when switching them out?
|
||||
// - Does this really count every individual hit separately?
|
||||
|
||||
// Get the avatar's weapon type.
|
||||
WeaponType weaponType = avatar.getAvatar().getAvatarData().getWeaponType();
|
||||
|
||||
// Check if we already have probability data for this avatar. If not, insert it.
|
||||
if (!this.avatarNormalProbabilities.containsKey(avatar)) {
|
||||
this.avatarNormalProbabilities.put(avatar, weaponType.getEnergyGainInitialProbability());
|
||||
}
|
||||
|
||||
// Roll for energy.
|
||||
int currentProbability = this.avatarNormalProbabilities.getInt(avatar);
|
||||
int roll = ThreadLocalRandom.current().nextInt(0, 100);
|
||||
|
||||
// If the player wins the roll, we increase the avatar's energy and reset the probability.
|
||||
if (roll < currentProbability) {
|
||||
avatar.addEnergy(1.0f, PropChangeReason.PROP_CHANGE_REASON_ABILITY, true);
|
||||
this.avatarNormalProbabilities.put(avatar, weaponType.getEnergyGainInitialProbability());
|
||||
}
|
||||
// Otherwise, we increase the probability for the next hit.
|
||||
else {
|
||||
this.avatarNormalProbabilities.put(
|
||||
avatar, currentProbability + weaponType.getEnergyGainIncreaseProbability());
|
||||
}
|
||||
}
|
||||
|
||||
public void handleAttackHit(EvtBeingHitInfo hitInfo) {
|
||||
// Get the attack result.
|
||||
AttackResult attackRes = hitInfo.getAttackResult();
|
||||
|
||||
// Make sure the attack was performed by the currently active avatar. If not, we ignore the hit.
|
||||
Optional<EntityAvatar> attackerEntity =
|
||||
this.getCastingAvatarEntityForEnergy(attackRes.getAttackerId());
|
||||
if (attackerEntity.isEmpty()
|
||||
|| this.player.getTeamManager().getCurrentAvatarEntity().getId()
|
||||
!= attackerEntity.get().getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the target is an actual enemy.
|
||||
GameEntity targetEntity = this.player.getScene().getEntityById(attackRes.getDefenseId());
|
||||
if (!(targetEntity instanceof EntityMonster targetMonster)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MonsterType targetType = targetMonster.getMonsterData().getType();
|
||||
if (targetType != MonsterType.MONSTER_ORDINARY && targetType != MonsterType.MONSTER_BOSS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the ability that caused this hit.
|
||||
AbilityIdentifier ability = attackRes.getAbilityIdentifier();
|
||||
|
||||
// Make sure there is no actual "ability" associated with the hit. For now, this is how we
|
||||
// identify normal and charged attacks. Note that this is not completely accurate:
|
||||
// - Many character's charged attacks have an ability associated with them. This means that,
|
||||
// for now, we don't identify charged attacks reliably.
|
||||
// - There might also be some cases where we incorrectly identify something as a normal or
|
||||
// charged attack that is not (Diluc's E?).
|
||||
// - Catalyst normal attacks have an ability, so we don't handle those for now.
|
||||
// ToDo: Fix all of that.
|
||||
if (ability != AbilityIdentifier.getDefaultInstance()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the energy generation.
|
||||
this.generateEnergyForNormalAndCharged(attackerEntity.get());
|
||||
}
|
||||
|
||||
/*
|
||||
* Energy logic related to using skills.
|
||||
*/
|
||||
|
||||
private void handleBurstCast(Avatar avatar, int skillId) {
|
||||
// Don't do anything if energy usage is disabled.
|
||||
if (!GAME_OPTIONS.energyUsage || !this.energyUsage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the cast skill was a burst, consume energy.
|
||||
if (avatar.getSkillDepot() != null && skillId == avatar.getSkillDepot().getEnergySkill()) {
|
||||
avatar.getAsEntity().clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START);
|
||||
}
|
||||
}
|
||||
|
||||
public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) {
|
||||
// Determine the entity that has cast the skill. Cancel if we can't find that avatar.
|
||||
Optional<EntityAvatar> caster =
|
||||
this.player.getTeamManager().getActiveTeam().stream()
|
||||
.filter(character -> character.getId() == casterId)
|
||||
.findFirst();
|
||||
|
||||
if (caster.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Avatar avatar = caster.get().getAvatar();
|
||||
|
||||
// Handle elemental burst.
|
||||
this.handleBurstCast(avatar, skillId);
|
||||
}
|
||||
|
||||
/*
|
||||
* Monster energy drops.
|
||||
*/
|
||||
|
||||
private void generateElemBallDrops(EntityMonster monster, int dropId) {
|
||||
// Generate all drops specified for the given drop id.
|
||||
if (!energyDropData.containsKey(dropId)) {
|
||||
Grasscutter.getLogger().warn("No drop data for dropId {} found.", dropId);
|
||||
return;
|
||||
}
|
||||
|
||||
for (EnergyDropInfo info : energyDropData.get(dropId)) {
|
||||
this.generateElemBall(info.getBallId(), monster.getPosition(), info.getCount());
|
||||
}
|
||||
}
|
||||
|
||||
public void handleMonsterEnergyDrop(
|
||||
EntityMonster monster, float hpBeforeDamage, float hpAfterDamage) {
|
||||
// Make sure this is actually a monster.
|
||||
// Note that some wildlife also has that type, like boars or birds.
|
||||
MonsterType type = monster.getMonsterData().getType();
|
||||
if (type != MonsterType.MONSTER_ORDINARY && type != MonsterType.MONSTER_BOSS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the HP thresholds for before and after the damage was taken.
|
||||
float maxHp = monster.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
|
||||
float thresholdBefore = hpBeforeDamage / maxHp;
|
||||
float thresholdAfter = hpAfterDamage / maxHp;
|
||||
|
||||
// Determine the thresholds the monster has passed, and generate drops based on that.
|
||||
for (HpDrops drop : monster.getMonsterData().getHpDrops()) {
|
||||
if (drop.getDropId() == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float threshold = drop.getHpPercent() / 100.0f;
|
||||
if (threshold < thresholdBefore && threshold >= thresholdAfter) {
|
||||
this.generateElemBallDrops(monster, drop.getDropId());
|
||||
}
|
||||
}
|
||||
|
||||
// Handle kill drops.
|
||||
if (hpAfterDamage <= 0 && monster.getMonsterData().getKillDropId() != 0) {
|
||||
this.generateElemBallDrops(monster, monster.getMonsterData().getKillDropId());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Utilities.
|
||||
*/
|
||||
|
||||
private void generateElemBall(int ballId, Position position, int count) {
|
||||
// Generate a particle/orb with the specified parameters.
|
||||
ItemData itemData = GameData.getItemDataMap().get(ballId);
|
||||
if (itemData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
EntityItem energyBall =
|
||||
new EntityItem(this.getPlayer().getScene(), this.getPlayer(), itemData, position, count);
|
||||
this.getPlayer().getScene().addEntity(energyBall);
|
||||
}
|
||||
|
||||
private Optional<EntityAvatar> getCastingAvatarEntityForEnergy(int invokeEntityId) {
|
||||
// To determine the avatar that has cast the skill that caused the energy particle to be
|
||||
// generated,
|
||||
// we have to look at the entity that has invoked the ability. This can either be that avatar
|
||||
// directly,
|
||||
// or it can be an `EntityClientGadget`, owned (some way up the owner hierarchy) by the avatar
|
||||
// that cast the skill.
|
||||
|
||||
// Try to get the invoking entity from the scene.
|
||||
GameEntity entity = this.player.getScene().getEntityById(invokeEntityId);
|
||||
|
||||
// Determine the ID of the entity that originally cast this skill. If the scene entity is null,
|
||||
// or not an `EntityClientGadget`, we assume that we are directly looking at the casting avatar
|
||||
// (the null case will happen if the avatar was switched out between casting the skill and the
|
||||
// particle being generated). If the scene entity is an `EntityClientGadget`, we need to find
|
||||
// the
|
||||
// ID of the original owner of that gadget.
|
||||
int avatarEntityId =
|
||||
(!(entity instanceof EntityClientGadget))
|
||||
? invokeEntityId
|
||||
: ((EntityClientGadget) entity).getOriginalOwnerEntityId();
|
||||
|
||||
// Finally, find the avatar entity in the player's team.
|
||||
return this.player.getTeamManager().getActiveTeam().stream()
|
||||
.filter(character -> character.getId() == avatarEntityId)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refills the energy of the active avatar.
|
||||
*
|
||||
* @return True if the energy was refilled, false otherwise.
|
||||
*/
|
||||
public boolean refillActiveEnergy() {
|
||||
var activeEntity = this.player.getTeamManager().getCurrentAvatarEntity();
|
||||
return activeEntity.addEnergy(
|
||||
activeEntity.getAvatar().getSkillDepot().getEnergySkillData().getCostElemVal());
|
||||
}
|
||||
|
||||
/**
|
||||
* Refills the energy of the entire team.
|
||||
*
|
||||
* @param changeReason The reason for the energy change.
|
||||
* @param isFlat Whether the energy should be added as a flat value.
|
||||
*/
|
||||
public void refillTeamEnergy(PropChangeReason changeReason, boolean isFlat) {
|
||||
for (var entityAvatar : this.player.getTeamManager().getActiveTeam()) {
|
||||
// giving the exact amount read off the AvatarSkillData.json
|
||||
entityAvatar.addEnergy(
|
||||
entityAvatar.getAvatar().getSkillDepot().getEnergySkillData().getCostElemVal(),
|
||||
changeReason,
|
||||
isFlat);
|
||||
}
|
||||
}
|
||||
|
||||
public void setEnergyUsage(boolean energyUsage) {
|
||||
this.energyUsage = energyUsage;
|
||||
if (!energyUsage) { // Refill team energy if usage is disabled
|
||||
this.refillTeamEnergy(PropChangeReason.PROP_CHANGE_REASON_GM, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -141,6 +141,7 @@ public class StaminaManager extends BasePlayerManager {
|
||||
put(242301, 0.8f);
|
||||
put(542301, 0.8f);
|
||||
}};
|
||||
|
||||
private final Logger logger = Grasscutter.getLogger();
|
||||
private final HashMap<String, BeforeUpdateStaminaListener> beforeUpdateStaminaListeners = new HashMap<>();
|
||||
private final HashMap<String, AfterUpdateStaminaListener> afterUpdateStaminaListeners = new HashMap<>();
|
||||
@ -414,13 +415,7 @@ public class StaminaManager extends BasePlayerManager {
|
||||
// Internal handler
|
||||
|
||||
private void handleImmediateStamina(GameSession session, @NotNull MotionState motionState) {
|
||||
if (currentState == motionState) {
|
||||
if (motionState.equals(MotionState.MOTION_STATE_CLIMB_JUMP)) {
|
||||
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP), true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentState == motionState) return;
|
||||
switch (motionState) {
|
||||
case MOTION_STATE_CLIMB ->
|
||||
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_START), true);
|
||||
@ -440,6 +435,73 @@ public class StaminaManager extends BasePlayerManager {
|
||||
updateStaminaRelative(session, consumption, true);
|
||||
}
|
||||
|
||||
private class SustainedStaminaHandler extends TimerTask {
|
||||
public void run() {
|
||||
boolean moving = isPlayerMoving();
|
||||
int currentCharacterStamina = getCurrentCharacterStamina();
|
||||
int maxCharacterStamina = getMaxCharacterStamina();
|
||||
int currentVehicleStamina = getCurrentVehicleStamina();
|
||||
int maxVehicleStamina = getMaxVehicleStamina();
|
||||
if (moving || (currentCharacterStamina < maxCharacterStamina) || (currentVehicleStamina < maxVehicleStamina)) {
|
||||
logger.trace("Player moving: " + moving + ", stamina full: " +
|
||||
(currentCharacterStamina >= maxCharacterStamina) + ", recalculate stamina");
|
||||
boolean isCharacterStamina = true;
|
||||
Consumption consumption;
|
||||
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
|
||||
consumption = getClimbConsumption();
|
||||
} else if (MotionStatesCategorized.get("DASH").contains(currentState)) {
|
||||
consumption = getDashConsumption();
|
||||
} else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
|
||||
consumption = getFlyConsumption();
|
||||
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
|
||||
consumption = new Consumption(ConsumptionType.RUN);
|
||||
} else if (MotionStatesCategorized.get("SKIFF").contains(currentState)) {
|
||||
consumption = getSkiffConsumption();
|
||||
isCharacterStamina = false;
|
||||
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
|
||||
consumption = new Consumption(ConsumptionType.STANDBY);
|
||||
} else if (MotionStatesCategorized.get("SWIM").contains(currentState)) {
|
||||
consumption = getSwimConsumptions();
|
||||
} else if (MotionStatesCategorized.get("WALK").contains(currentState)) {
|
||||
consumption = new Consumption(ConsumptionType.WALK);
|
||||
} else if (MotionStatesCategorized.get("NOCOST_NORECOVER").contains(currentState)) {
|
||||
consumption = new Consumption();
|
||||
} else if (MotionStatesCategorized.get("OTHER").contains(currentState)) {
|
||||
consumption = getOtherConsumptions();
|
||||
} else { // ignore
|
||||
return;
|
||||
}
|
||||
|
||||
if (consumption.amount < 0 && isCharacterStamina) {
|
||||
// Do not apply reduction factor when recovering stamina
|
||||
if (player.getTeamManager().getTeamResonances().contains(10301)) {
|
||||
consumption.amount *= 0.85f;
|
||||
}
|
||||
}
|
||||
// Delay 1 seconds before starts recovering stamina
|
||||
if (consumption.amount != 0 && cachedSession != null) {
|
||||
if (consumption.amount < 0) {
|
||||
staminaRecoverDelay = 0;
|
||||
}
|
||||
if (consumption.amount > 0
|
||||
&& consumption.type != ConsumptionType.POWERED_FLY
|
||||
&& consumption.type != ConsumptionType.POWERED_SKIFF) {
|
||||
// For POWERED_* recover immediately - things like Amber's gliding exam and skiff challenges may require this.
|
||||
if (staminaRecoverDelay < 5) {
|
||||
// For others recover after 1 seconds (5 ticks) - as official server does.
|
||||
staminaRecoverDelay++;
|
||||
consumption.amount = 0;
|
||||
logger.trace("Delaying recovery: " + staminaRecoverDelay);
|
||||
}
|
||||
}
|
||||
updateStaminaRelative(cachedSession, consumption, isCharacterStamina);
|
||||
}
|
||||
}
|
||||
previousState = currentState;
|
||||
previousCoordinates = currentCoordinates.clone();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDrowning() {
|
||||
// TODO: fix drowning waverider entity
|
||||
int stamina = getCurrentCharacterStamina();
|
||||
@ -452,6 +514,10 @@ public class StaminaManager extends BasePlayerManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Consumption Calculators
|
||||
|
||||
// Stamina Consumption Reduction: https://genshin-impact.fandom.com/wiki/Stamina
|
||||
|
||||
private Consumption getFightConsumption(int skillCasting) {
|
||||
// Talent moving
|
||||
if (TalentMovements.contains(skillCasting)) {
|
||||
@ -471,10 +537,6 @@ public class StaminaManager extends BasePlayerManager {
|
||||
};
|
||||
}
|
||||
|
||||
// Consumption Calculators
|
||||
|
||||
// Stamina Consumption Reduction: https://genshin-impact.fandom.com/wiki/Stamina
|
||||
|
||||
private Consumption getClimbConsumption() {
|
||||
Consumption consumption = new Consumption();
|
||||
if (currentState == MotionState.MOTION_STATE_CLIMB && isPlayerMoving()) {
|
||||
@ -552,6 +614,8 @@ public class StaminaManager extends BasePlayerManager {
|
||||
return new Consumption();
|
||||
}
|
||||
|
||||
// Reduction getter
|
||||
|
||||
private float getTalentCostReductionFactor(HashMap<Integer, Float> talentReductionMap) {
|
||||
// All known talents reductions are not stackable
|
||||
float reduction = 1;
|
||||
@ -568,8 +632,6 @@ public class StaminaManager extends BasePlayerManager {
|
||||
return reduction;
|
||||
}
|
||||
|
||||
// Reduction getter
|
||||
|
||||
private float getFoodCostReductionFactor(HashMap<Integer, Float> foodReductionMap) {
|
||||
// All known food reductions are not stackable
|
||||
// TODO: Check consumed food (buff?) and return proper factor
|
||||
@ -633,76 +695,11 @@ public class StaminaManager extends BasePlayerManager {
|
||||
private Consumption getSwordCost(int skillId) {
|
||||
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2000);
|
||||
// Character specific handling
|
||||
if (skillId == 10421) {
|
||||
consumption.amount = -2500;
|
||||
switch (skillId) {
|
||||
case 10421:
|
||||
consumption.amount = -2500;
|
||||
break;
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private class SustainedStaminaHandler extends TimerTask {
|
||||
public void run() {
|
||||
boolean moving = isPlayerMoving();
|
||||
int currentCharacterStamina = getCurrentCharacterStamina();
|
||||
int maxCharacterStamina = getMaxCharacterStamina();
|
||||
int currentVehicleStamina = getCurrentVehicleStamina();
|
||||
int maxVehicleStamina = getMaxVehicleStamina();
|
||||
if (moving || (currentCharacterStamina < maxCharacterStamina) || (currentVehicleStamina < maxVehicleStamina)) {
|
||||
logger.trace("Player moving: " + moving + ", stamina full: " +
|
||||
(currentCharacterStamina >= maxCharacterStamina) + ", recalculate stamina");
|
||||
boolean isCharacterStamina = true;
|
||||
Consumption consumption;
|
||||
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
|
||||
consumption = getClimbConsumption();
|
||||
} else if (MotionStatesCategorized.get("DASH").contains(currentState)) {
|
||||
consumption = getDashConsumption();
|
||||
} else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
|
||||
consumption = getFlyConsumption();
|
||||
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
|
||||
consumption = new Consumption(ConsumptionType.RUN);
|
||||
} else if (MotionStatesCategorized.get("SKIFF").contains(currentState)) {
|
||||
consumption = getSkiffConsumption();
|
||||
isCharacterStamina = false;
|
||||
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
|
||||
consumption = new Consumption(ConsumptionType.STANDBY);
|
||||
} else if (MotionStatesCategorized.get("SWIM").contains(currentState)) {
|
||||
consumption = getSwimConsumptions();
|
||||
} else if (MotionStatesCategorized.get("WALK").contains(currentState)) {
|
||||
consumption = new Consumption(ConsumptionType.WALK);
|
||||
} else if (MotionStatesCategorized.get("NOCOST_NORECOVER").contains(currentState)) {
|
||||
consumption = new Consumption();
|
||||
} else if (MotionStatesCategorized.get("OTHER").contains(currentState)) {
|
||||
consumption = getOtherConsumptions();
|
||||
} else { // ignore
|
||||
return;
|
||||
}
|
||||
|
||||
if (consumption.amount < 0 && isCharacterStamina) {
|
||||
// Do not apply reduction factor when recovering stamina
|
||||
if (player.getTeamManager().getTeamResonances().contains(10301)) {
|
||||
consumption.amount *= 0.85f;
|
||||
}
|
||||
}
|
||||
// Delay 1 seconds before starts recovering stamina
|
||||
if (consumption.amount != 0 && cachedSession != null) {
|
||||
if (consumption.amount < 0) {
|
||||
staminaRecoverDelay = 0;
|
||||
}
|
||||
if (consumption.amount > 0
|
||||
&& consumption.type != ConsumptionType.POWERED_FLY
|
||||
&& consumption.type != ConsumptionType.POWERED_SKIFF) {
|
||||
// For POWERED_* recover immediately - things like Amber's gliding exam and skiff challenges may require this.
|
||||
if (staminaRecoverDelay < 5) {
|
||||
// For others recover after 1 seconds (5 ticks) - as official server does.
|
||||
staminaRecoverDelay++;
|
||||
consumption.amount = 0;
|
||||
logger.trace("Delaying recovery: " + staminaRecoverDelay);
|
||||
}
|
||||
}
|
||||
updateStaminaRelative(cachedSession, consumption, isCharacterStamina);
|
||||
}
|
||||
}
|
||||
previousState = currentState;
|
||||
previousCoordinates = currentCoordinates.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,276 +1,303 @@
|
||||
package emu.grasscutter.game.player;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.binout.ScenePointEntry;
|
||||
import emu.grasscutter.data.excels.OpenStateData;
|
||||
import emu.grasscutter.data.excels.OpenStateData.OpenStateCondType;
|
||||
import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.game.quest.enums.QuestCond;
|
||||
import emu.grasscutter.game.quest.enums.QuestContent;
|
||||
import emu.grasscutter.game.quest.enums.QuestState;
|
||||
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
// @Entity
|
||||
public final class PlayerProgressManager extends BasePlayerDataManager {
|
||||
/******************************************************************************************************************
|
||||
******************************************************************************************************************
|
||||
* OPEN STATES
|
||||
******************************************************************************************************************
|
||||
*****************************************************************************************************************/
|
||||
|
||||
// Set of open states that are never unlocked, whether they fulfill the conditions or not.
|
||||
public static final Set<Integer> BLACKLIST_OPEN_STATES =
|
||||
Set.of(
|
||||
48 // blacklist OPEN_STATE_LIMIT_REGION_GLOBAL to make Meledy happy. =D Remove this as
|
||||
// soon as quest unlocks are fully implemented.
|
||||
);
|
||||
// Set of open states that are set per default for all accounts. Can be overwritten by an entry in
|
||||
// `map`.
|
||||
public static final Set<Integer> DEFAULT_OPEN_STATES =
|
||||
GameData.getOpenStateList().stream()
|
||||
.filter(
|
||||
s ->
|
||||
s.isDefaultState() // Actual default-opened states.
|
||||
// All states whose unlock we don't handle correctly yet.
|
||||
|| (s.getCond().stream()
|
||||
.filter(
|
||||
c ->
|
||||
c.getCondType()
|
||||
== OpenStateCondType.OPEN_STATE_COND_PLAYER_LEVEL)
|
||||
.count()
|
||||
== 0)
|
||||
// Always unlock OPEN_STATE_PAIMON, otherwise the player will not have a
|
||||
// working chat.
|
||||
|| s.getId() == 1)
|
||||
.filter(
|
||||
s ->
|
||||
!BLACKLIST_OPEN_STATES.contains(s.getId())) // Filter out states in the blacklist.
|
||||
.map(s -> s.getId())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
public PlayerProgressManager(Player player) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
/**********
|
||||
* Handler for player login.
|
||||
**********/
|
||||
public void onPlayerLogin() {
|
||||
// Try unlocking open states on player login. This handles accounts where unlock conditions were
|
||||
// already met before certain open state unlocks were implemented.
|
||||
this.tryUnlockOpenStates(false);
|
||||
|
||||
// Send notify to the client.
|
||||
player.getSession().send(new PacketOpenStateUpdateNotify(this.player));
|
||||
|
||||
// Add statue quests if necessary.
|
||||
this.addStatueQuestsOnLogin();
|
||||
|
||||
// Auto-unlock the first statue and map area, until we figure out how to make
|
||||
// that particular statue interactable.
|
||||
this.player.getUnlockedScenePoints(3).add(7);
|
||||
this.player.getUnlockedSceneAreas(3).add(1);
|
||||
}
|
||||
|
||||
/**********
|
||||
* Direct getters and setters for open states.
|
||||
**********/
|
||||
public int getOpenState(int openState) {
|
||||
return this.player.getOpenStates().getOrDefault(openState, 0);
|
||||
}
|
||||
|
||||
private void setOpenState(int openState, int value, boolean sendNotify) {
|
||||
int previousValue = this.player.getOpenStates().getOrDefault(openState, 0);
|
||||
|
||||
if (value != previousValue) {
|
||||
this.player.getOpenStates().put(openState, value);
|
||||
|
||||
if (sendNotify) {
|
||||
player.getSession().send(new PacketOpenStateChangeNotify(openState, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setOpenState(int openState, int value) {
|
||||
this.setOpenState(openState, value, true);
|
||||
}
|
||||
|
||||
/**********
|
||||
* Condition checking for setting open states.
|
||||
**********/
|
||||
private boolean areConditionsMet(OpenStateData openState) {
|
||||
// Check all conditions and test if at least one of them is violated.
|
||||
for (var condition : openState.getCond()) {
|
||||
// For level conditions, check if the player has reached the necessary level.
|
||||
if (condition.getCondType() == OpenStateCondType.OPEN_STATE_COND_PLAYER_LEVEL) {
|
||||
if (this.player.getLevel() < condition.getParam()) {
|
||||
return false;
|
||||
}
|
||||
} else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_COND_QUEST) {
|
||||
// ToDo: Implement.
|
||||
} else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_COND_PARENT_QUEST) {
|
||||
// ToDo: Implement.
|
||||
} else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_OFFERING_LEVEL) {
|
||||
// ToDo: Implement.
|
||||
} else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_CITY_REPUTATION_LEVEL) {
|
||||
// ToDo: Implement.
|
||||
}
|
||||
}
|
||||
|
||||
// Done. If we didn't find any violations, all conditions are met.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**********
|
||||
* Setting open states from the client (via `SetOpenStateReq`).
|
||||
**********/
|
||||
public void setOpenStateFromClient(int openState, int value) {
|
||||
// Get the data for this open state.
|
||||
OpenStateData data = GameData.getOpenStateDataMap().get(openState);
|
||||
if (data == null) {
|
||||
this.player.sendPacket(new PacketSetOpenStateRsp(Retcode.RET_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure that this is an open state that the client is allowed to set,
|
||||
// and that it doesn't have any further conditions attached.
|
||||
if (!data.isAllowClientOpen() || !this.areConditionsMet(data)) {
|
||||
this.player.sendPacket(new PacketSetOpenStateRsp(Retcode.RET_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set.
|
||||
this.setOpenState(openState, value);
|
||||
this.player.sendPacket(new PacketSetOpenStateRsp(openState, value));
|
||||
}
|
||||
|
||||
/** This force sets an open state, ignoring all conditions and permissions */
|
||||
public void forceSetOpenState(int openState, int value) {
|
||||
this.setOpenState(openState, value);
|
||||
}
|
||||
|
||||
/**********
|
||||
* Triggered unlocking of open states (unlock states whose conditions have been met.)
|
||||
**********/
|
||||
public void tryUnlockOpenStates(boolean sendNotify) {
|
||||
// Get list of open states that are not yet unlocked.
|
||||
var lockedStates =
|
||||
GameData.getOpenStateList().stream()
|
||||
.filter(s -> this.player.getOpenStates().getOrDefault(s, 0) == 0)
|
||||
.toList();
|
||||
|
||||
// Try unlocking all of them.
|
||||
for (var state : lockedStates) {
|
||||
// To auto-unlock a state, it has to meet three conditions:
|
||||
// * it can not be a state that is unlocked by the client,
|
||||
// * it has to meet all its unlock conditions, and
|
||||
// * it can not be in the blacklist.
|
||||
if (!state.isAllowClientOpen()
|
||||
&& this.areConditionsMet(state)
|
||||
&& !BLACKLIST_OPEN_STATES.contains(state.getId())) {
|
||||
this.setOpenState(state.getId(), 1, sendNotify);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void tryUnlockOpenStates() {
|
||||
this.tryUnlockOpenStates(true);
|
||||
}
|
||||
|
||||
/******************************************************************************************************************
|
||||
******************************************************************************************************************
|
||||
* MAP AREAS AND POINTS
|
||||
******************************************************************************************************************
|
||||
*****************************************************************************************************************/
|
||||
private void addStatueQuestsOnLogin() {
|
||||
// Get all currently existing subquests for the "unlock all statues" main quest.
|
||||
var statueMainQuest = GameData.getMainQuestDataMap().get(303);
|
||||
var statueSubQuests = statueMainQuest.getSubQuests();
|
||||
|
||||
// Add the main statue quest if it isn't active yet.
|
||||
var statueGameMainQuest = this.player.getQuestManager().getMainQuestById(303);
|
||||
if (statueGameMainQuest == null) {
|
||||
this.player.getQuestManager().addQuest(30302);
|
||||
statueGameMainQuest = this.player.getQuestManager().getMainQuestById(303);
|
||||
}
|
||||
|
||||
// Set all subquests to active if they aren't already finished.
|
||||
for (var subData : statueSubQuests) {
|
||||
var subGameQuest = statueGameMainQuest.getChildQuestById(subData.getSubId());
|
||||
if (subGameQuest != null && subGameQuest.getState() == QuestState.QUEST_STATE_UNSTARTED) {
|
||||
this.player.getQuestManager().addQuest(subData.getSubId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean unlockTransPoint(int sceneId, int pointId, boolean isStatue) {
|
||||
// Check whether the unlocked point exists and whether it is still locked.
|
||||
ScenePointEntry scenePointEntry = GameData.getScenePointEntryById(sceneId, pointId);
|
||||
|
||||
if (scenePointEntry == null || this.player.getUnlockedScenePoints(sceneId).contains(pointId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the point to the list of unlocked points for its scene.
|
||||
this.player.getUnlockedScenePoints(sceneId).add(pointId);
|
||||
|
||||
// Give primogems and Adventure EXP for unlocking.
|
||||
this.player.getInventory().addItem(201, 5, ActionReason.UnlockPointReward);
|
||||
this.player.getInventory().addItem(102, isStatue ? 50 : 10, ActionReason.UnlockPointReward);
|
||||
|
||||
// this.player.sendPacket(new
|
||||
// PacketPlayerPropChangeReasonNotify(this.player.getProperty(PlayerProperty.PROP_PLAYER_EXP),
|
||||
// PlayerProperty.PROP_PLAYER_EXP, PropChangeReason.PROP_CHANGE_REASON_PLAYER_ADD_EXP));
|
||||
|
||||
// Fire quest trigger for trans point unlock.
|
||||
this.player
|
||||
.getQuestManager()
|
||||
.queueEvent(QuestContent.QUEST_CONTENT_UNLOCK_TRANS_POINT, sceneId, pointId);
|
||||
|
||||
// Send packet.
|
||||
this.player.sendPacket(new PacketScenePointUnlockNotify(sceneId, pointId));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void unlockSceneArea(int sceneId, int areaId) {
|
||||
// Add the area to the list of unlocked areas in its scene.
|
||||
this.player.getUnlockedSceneAreas(sceneId).add(areaId);
|
||||
|
||||
// Send packet.
|
||||
this.player.sendPacket(new PacketSceneAreaUnlockNotify(sceneId, areaId));
|
||||
}
|
||||
|
||||
/** Give replace costume to player (Amber, Jean, Mona, Rosaria) */
|
||||
public void addReplaceCostumes() {
|
||||
var currentPlayerCostumes = player.getCostumeList();
|
||||
GameData.getAvatarReplaceCostumeDataMap()
|
||||
.keySet()
|
||||
.forEach(
|
||||
costumeId -> {
|
||||
if (GameData.getAvatarCostumeDataMap().get(costumeId) == null
|
||||
|| currentPlayerCostumes.contains(costumeId)) {
|
||||
return;
|
||||
}
|
||||
this.player.addCostume(costumeId);
|
||||
});
|
||||
}
|
||||
|
||||
/** Quest progress */
|
||||
public void addQuestProgress(int id, int count) {
|
||||
var newCount = player.getPlayerProgress().addToCurrentProgress(id, count);
|
||||
player.save();
|
||||
player
|
||||
.getQuestManager()
|
||||
.queueEvent(QuestContent.QUEST_CONTENT_ADD_QUEST_PROGRESS, id, newCount);
|
||||
}
|
||||
|
||||
/** Item history */
|
||||
public void addItemObtainedHistory(int id, int count) {
|
||||
var newCount = player.getPlayerProgress().addToItemHistory(id, count);
|
||||
player.save();
|
||||
player.getQuestManager().queueEvent(QuestCond.QUEST_COND_HISTORY_GOT_ANY_ITEM, id, newCount);
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.player;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.binout.ScenePointEntry;
|
||||
import emu.grasscutter.data.excels.OpenStateData;
|
||||
import emu.grasscutter.data.excels.OpenStateData.OpenStateCondType;
|
||||
import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.game.quest.enums.ParentQuestState;
|
||||
import emu.grasscutter.game.quest.enums.QuestCond;
|
||||
import emu.grasscutter.game.quest.enums.QuestContent;
|
||||
import emu.grasscutter.game.quest.enums.QuestState;
|
||||
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
|
||||
import emu.grasscutter.scripts.data.ScriptArgs;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static emu.grasscutter.scripts.constants.EventType.EVENT_UNLOCK_TRANS_POINT;
|
||||
|
||||
// @Entity
|
||||
public final class PlayerProgressManager extends BasePlayerDataManager {
|
||||
/******************************************************************************************************************
|
||||
******************************************************************************************************************
|
||||
* OPEN STATES
|
||||
******************************************************************************************************************
|
||||
*****************************************************************************************************************/
|
||||
|
||||
// Set of open states that are never unlocked, whether they fulfill the conditions or not.
|
||||
public static final Set<Integer> BLACKLIST_OPEN_STATES =
|
||||
Set.of(
|
||||
48 // blacklist OPEN_STATE_LIMIT_REGION_GLOBAL to make Meledy happy. =D Remove this as
|
||||
// soon as quest unlocks are fully implemented.
|
||||
);
|
||||
// Set of open states that are set per default for all accounts. Can be overwritten by an entry in
|
||||
// `map`.
|
||||
public static final Set<Integer> DEFAULT_OPEN_STATES =
|
||||
GameData.getOpenStateList().stream()
|
||||
.filter(
|
||||
s ->
|
||||
s.isDefaultState() && !s.isAllowClientOpen() // Actual default-opened states.
|
||||
|| ((s.getCond().size() == 1)
|
||||
&& (s.getCond().get(0).getCondType()
|
||||
== OpenStateCondType.OPEN_STATE_COND_PLAYER_LEVEL)
|
||||
&& (s.getCond().get(0).getParam() == 1))
|
||||
// All states whose unlock we don't handle correctly yet.
|
||||
|| (s.getCond().stream()
|
||||
.anyMatch(
|
||||
c ->
|
||||
c.getCondType() == OpenStateCondType.OPEN_STATE_OFFERING_LEVEL
|
||||
|| c.getCondType()
|
||||
== OpenStateCondType.OPEN_STATE_CITY_REPUTATION_LEVEL))
|
||||
// Always unlock OPEN_STATE_PAIMON, otherwise the player will not have a
|
||||
// working chat.
|
||||
|| s.getId() == 1)
|
||||
.filter(
|
||||
s ->
|
||||
!BLACKLIST_OPEN_STATES.contains(s.getId())) // Filter out states in the blacklist.
|
||||
.map(OpenStateData::getId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
public PlayerProgressManager(Player player) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
/**********
|
||||
* Handler for player login.
|
||||
**********/
|
||||
public void onPlayerLogin() {
|
||||
// Try unlocking open states on player login. This handles accounts where unlock conditions were
|
||||
// already met before certain open state unlocks were implemented.
|
||||
this.tryUnlockOpenStates(false);
|
||||
|
||||
// Send notify to the client.
|
||||
player.getSession().send(new PacketOpenStateUpdateNotify(this.player));
|
||||
|
||||
// Add statue quests if necessary.
|
||||
this.addStatueQuestsOnLogin();
|
||||
|
||||
// Auto-unlock the first statue and map area, until we figure out how to make
|
||||
// that particular statue interactable.
|
||||
this.player.getUnlockedScenePoints(3).add(7);
|
||||
this.player.getUnlockedSceneAreas(3).add(1);
|
||||
}
|
||||
|
||||
/**********
|
||||
* Direct getters and setters for open states.
|
||||
**********/
|
||||
public int getOpenState(int openState) {
|
||||
return this.player.getOpenStates().getOrDefault(openState, 0);
|
||||
}
|
||||
|
||||
private void setOpenState(int openState, int value, boolean sendNotify) {
|
||||
int previousValue = this.player.getOpenStates().getOrDefault(openState, 0);
|
||||
|
||||
if (value != previousValue) {
|
||||
this.player.getOpenStates().put(openState, value);
|
||||
|
||||
this.player
|
||||
.getQuestManager()
|
||||
.queueEvent(QuestCond.QUEST_COND_OPEN_STATE_EQUAL, openState, value);
|
||||
|
||||
if (sendNotify) {
|
||||
player.getSession().send(new PacketOpenStateChangeNotify(openState, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setOpenState(int openState, int value) {
|
||||
this.setOpenState(openState, value, true);
|
||||
}
|
||||
|
||||
/**********
|
||||
* Condition checking for setting open states.
|
||||
**********/
|
||||
private boolean areConditionsMet(OpenStateData openState) {
|
||||
// Check all conditions and test if at least one of them is violated.
|
||||
for (var condition : openState.getCond()) {
|
||||
switch (condition.getCondType()) {
|
||||
// For level conditions, check if the player has reached the necessary level.
|
||||
case OPEN_STATE_COND_PLAYER_LEVEL -> {
|
||||
if (this.player.getLevel() < condition.getParam()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
case OPEN_STATE_COND_QUEST -> {
|
||||
// check sub quest id for quest finished met requirements
|
||||
var quest = this.player.getQuestManager().getQuestById(condition.getParam());
|
||||
if (quest == null || quest.getState() != QuestState.QUEST_STATE_FINISHED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
case OPEN_STATE_COND_PARENT_QUEST -> {
|
||||
// check main quest id for quest finished met requirements
|
||||
// TODO not sure if its having or finished quest
|
||||
var mainQuest = this.player.getQuestManager().getMainQuestById(condition.getParam());
|
||||
if (mainQuest == null
|
||||
|| mainQuest.getState() != ParentQuestState.PARENT_QUEST_STATE_FINISHED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// ToDo: Implement.
|
||||
case OPEN_STATE_OFFERING_LEVEL, OPEN_STATE_CITY_REPUTATION_LEVEL -> {}
|
||||
}
|
||||
}
|
||||
|
||||
// Done. If we didn't find any violations, all conditions are met.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**********
|
||||
* Setting open states from the client (via `SetOpenStateReq`).
|
||||
**********/
|
||||
public void setOpenStateFromClient(int openState, int value) {
|
||||
// Get the data for this open state.
|
||||
OpenStateData data = GameData.getOpenStateDataMap().get(openState);
|
||||
if (data == null) {
|
||||
this.player.sendPacket(new PacketSetOpenStateRsp(Retcode.RET_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure that this is an open state that the client is allowed to set,
|
||||
// and that it doesn't have any further conditions attached.
|
||||
if (!data.isAllowClientOpen() || !this.areConditionsMet(data)) {
|
||||
this.player.sendPacket(new PacketSetOpenStateRsp(Retcode.RET_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set.
|
||||
this.setOpenState(openState, value);
|
||||
this.player.sendPacket(new PacketSetOpenStateRsp(openState, value));
|
||||
}
|
||||
|
||||
/** This force sets an open state, ignoring all conditions and permissions */
|
||||
public void forceSetOpenState(int openState, int value) {
|
||||
this.setOpenState(openState, value);
|
||||
}
|
||||
|
||||
/**********
|
||||
* Triggered unlocking of open states (unlock states whose conditions have been met.)
|
||||
**********/
|
||||
public void tryUnlockOpenStates(boolean sendNotify) {
|
||||
// Get list of open states that are not yet unlocked.
|
||||
var lockedStates =
|
||||
GameData.getOpenStateList().stream()
|
||||
.filter(s -> this.player.getOpenStates().getOrDefault(s, 0) == 0)
|
||||
.toList();
|
||||
|
||||
// Try unlocking all of them.
|
||||
for (var state : lockedStates) {
|
||||
// To auto-unlock a state, it has to meet three conditions:
|
||||
// * it can not be a state that is unlocked by the client,
|
||||
// * it has to meet all its unlock conditions, and
|
||||
// * it can not be in the blacklist.
|
||||
if (!state.isAllowClientOpen()
|
||||
&& this.areConditionsMet(state)
|
||||
&& !BLACKLIST_OPEN_STATES.contains(state.getId())) {
|
||||
this.setOpenState(state.getId(), 1, sendNotify);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void tryUnlockOpenStates() {
|
||||
this.tryUnlockOpenStates(true);
|
||||
}
|
||||
|
||||
/******************************************************************************************************************
|
||||
******************************************************************************************************************
|
||||
* MAP AREAS AND POINTS
|
||||
******************************************************************************************************************
|
||||
*****************************************************************************************************************/
|
||||
private void addStatueQuestsOnLogin() {
|
||||
// Get all currently existing subquests for the "unlock all statues" main quest.
|
||||
var statueMainQuest = GameData.getMainQuestDataMap().get(303);
|
||||
var statueSubQuests = statueMainQuest.getSubQuests();
|
||||
|
||||
// Add the main statue quest if it isn't active yet.
|
||||
var statueGameMainQuest = this.player.getQuestManager().getMainQuestById(303);
|
||||
if (statueGameMainQuest == null) {
|
||||
this.player.getQuestManager().addQuest(30302);
|
||||
statueGameMainQuest = this.player.getQuestManager().getMainQuestById(303);
|
||||
}
|
||||
|
||||
// Set all subquests to active if they aren't already finished.
|
||||
for (var subData : statueSubQuests) {
|
||||
var subGameQuest = statueGameMainQuest.getChildQuestById(subData.getSubId());
|
||||
if (subGameQuest != null && subGameQuest.getState() == QuestState.QUEST_STATE_UNSTARTED) {
|
||||
this.player.getQuestManager().addQuest(subData.getSubId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean unlockTransPoint(int sceneId, int pointId, boolean isStatue) {
|
||||
// Check whether the unlocked point exists and whether it is still locked.
|
||||
ScenePointEntry scenePointEntry = GameData.getScenePointEntryById(sceneId, pointId);
|
||||
|
||||
if (scenePointEntry == null || this.player.getUnlockedScenePoints(sceneId).contains(pointId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the point to the list of unlocked points for its scene.
|
||||
this.player.getUnlockedScenePoints(sceneId).add(pointId);
|
||||
|
||||
// Give primogems and Adventure EXP for unlocking.
|
||||
this.player.getInventory().addItem(201, 5, ActionReason.UnlockPointReward);
|
||||
this.player.getInventory().addItem(102, isStatue ? 50 : 10, ActionReason.UnlockPointReward);
|
||||
|
||||
// this.player.sendPacket(new
|
||||
// PacketPlayerPropChangeReasonNotify(this.player.getProperty(PlayerProperty.PROP_PLAYER_EXP),
|
||||
// PlayerProperty.PROP_PLAYER_EXP, PropChangeReason.PROP_CHANGE_REASON_PLAYER_ADD_EXP));
|
||||
|
||||
// Fire quest trigger for trans point unlock.
|
||||
this.player
|
||||
.getQuestManager()
|
||||
.queueEvent(QuestContent.QUEST_CONTENT_UNLOCK_TRANS_POINT, sceneId, pointId);
|
||||
this.player
|
||||
.getScene()
|
||||
.getScriptManager()
|
||||
.callEvent(new ScriptArgs(0, EVENT_UNLOCK_TRANS_POINT, sceneId, pointId));
|
||||
|
||||
// Send packet.
|
||||
this.player.sendPacket(new PacketScenePointUnlockNotify(sceneId, pointId));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void unlockSceneArea(int sceneId, int areaId) {
|
||||
// Add the area to the list of unlocked areas in its scene.
|
||||
this.player.getUnlockedSceneAreas(sceneId).add(areaId);
|
||||
|
||||
// Send packet.
|
||||
this.player.sendPacket(new PacketSceneAreaUnlockNotify(sceneId, areaId));
|
||||
}
|
||||
|
||||
/** Give replace costume to player (Amber, Jean, Mona, Rosaria) */
|
||||
public void addReplaceCostumes() {
|
||||
var currentPlayerCostumes = player.getCostumeList();
|
||||
GameData.getAvatarReplaceCostumeDataMap()
|
||||
.keySet()
|
||||
.forEach(
|
||||
costumeId -> {
|
||||
if (GameData.getAvatarCostumeDataMap().get(costumeId) == null
|
||||
|| currentPlayerCostumes.contains(costumeId)) {
|
||||
return;
|
||||
}
|
||||
this.player.addCostume(costumeId);
|
||||
});
|
||||
}
|
||||
|
||||
/** Quest progress */
|
||||
public void addQuestProgress(int id, int count) {
|
||||
var newCount = player.getPlayerProgress().addToCurrentProgress(id, count);
|
||||
player.save();
|
||||
player
|
||||
.getQuestManager()
|
||||
.queueEvent(QuestContent.QUEST_CONTENT_ADD_QUEST_PROGRESS, id, newCount);
|
||||
}
|
||||
|
||||
/** Item history */
|
||||
public void addItemObtainedHistory(int id, int count) {
|
||||
var newCount = player.getPlayerProgress().addToItemHistory(id, count);
|
||||
player.save();
|
||||
player.getQuestManager().queueEvent(QuestCond.QUEST_COND_HISTORY_GOT_ANY_ITEM, id, newCount);
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,25 +1,19 @@
|
||||
package emu.grasscutter.game.props.ItemUseAction;
|
||||
|
||||
import emu.grasscutter.game.props.ItemUseOp;
|
||||
|
||||
public class ItemUseUnlockHomeModule extends ItemUseInt {
|
||||
public ItemUseUnlockHomeModule(String[] useParam) {
|
||||
super(useParam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemUseOp getItemUseOp() {
|
||||
return ItemUseOp.ITEM_USE_UNLOCK_HOME_MODULE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useItem(UseItemParams params) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean postUseItem(UseItemParams params) {
|
||||
params.player.addRealmList(this.i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.props.ItemUseAction;
|
||||
|
||||
import emu.grasscutter.game.props.ItemUseOp;
|
||||
|
||||
public class ItemUseUnlockHomeModule extends ItemUseInt {
|
||||
public ItemUseUnlockHomeModule(String[] useParam) {
|
||||
super(useParam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemUseOp getItemUseOp() {
|
||||
return ItemUseOp.ITEM_USE_UNLOCK_HOME_MODULE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useItem(UseItemParams params) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1,95 +1,95 @@
|
||||
package emu.grasscutter.game.shop;
|
||||
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.data.excels.ShopGoodsData;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class ShopInfo {
|
||||
@Getter @Setter private int goodsId = 0;
|
||||
@Getter @Setter private ItemParamData goodsItem;
|
||||
@Getter @Setter private int scoin = 0;
|
||||
@Getter @Setter private List<ItemParamData> costItemList;
|
||||
@Getter @Setter private int boughtNum = 0;
|
||||
@Getter @Setter private int buyLimit = 0;
|
||||
@Getter @Setter private int beginTime = 0;
|
||||
@Getter @Setter private int endTime = 1924992000;
|
||||
@Getter @Setter private int minLevel = 0;
|
||||
@Getter @Setter private int maxLevel = 61;
|
||||
@Getter @Setter private List<Integer> preGoodsIdList = new ArrayList<>();
|
||||
@Getter @Setter private int mcoin = 0;
|
||||
@Getter @Setter private int hcoin = 0;
|
||||
@Getter @Setter private int disableType = 0;
|
||||
@Getter @Setter private int secondarySheetId = 0;
|
||||
|
||||
private String refreshType;
|
||||
@Setter private transient ShopRefreshType shopRefreshType;
|
||||
@Getter @Setter private int shopRefreshParam;
|
||||
|
||||
public ShopInfo(ShopGoodsData sgd) {
|
||||
this.goodsId = sgd.getGoodsId();
|
||||
this.goodsItem = new ItemParamData(sgd.getItemId(), sgd.getItemCount());
|
||||
this.scoin = sgd.getCostScoin();
|
||||
this.mcoin = sgd.getCostMcoin();
|
||||
this.hcoin = sgd.getCostHcoin();
|
||||
this.buyLimit = sgd.getBuyLimit();
|
||||
|
||||
this.minLevel = sgd.getMinPlayerLevel();
|
||||
this.maxLevel = sgd.getMaxPlayerLevel();
|
||||
this.costItemList =
|
||||
sgd.getCostItems().stream()
|
||||
.filter(x -> x.getId() != 0)
|
||||
.map(x -> new ItemParamData(x.getId(), x.getCount()))
|
||||
.toList();
|
||||
this.secondarySheetId = sgd.getSubTabId();
|
||||
this.shopRefreshType = sgd.getRefreshType();
|
||||
this.shopRefreshParam = sgd.getRefreshParam();
|
||||
}
|
||||
|
||||
public ShopRefreshType getShopRefreshType() {
|
||||
if (refreshType == null) return ShopRefreshType.NONE;
|
||||
return switch (refreshType) {
|
||||
case "SHOP_REFRESH_DAILY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_DAILY;
|
||||
case "SHOP_REFRESH_WEEKLY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_WEEKLY;
|
||||
case "SHOP_REFRESH_MONTHLY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_MONTHLY;
|
||||
default -> ShopInfo.ShopRefreshType.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean evaluateVirtualCost(ItemParamData item) {
|
||||
return switch (item.getId()) {
|
||||
case 201 -> {
|
||||
this.hcoin += item.getCount();
|
||||
yield true;
|
||||
}
|
||||
case 203 -> {
|
||||
this.mcoin += item.getCount();
|
||||
yield true;
|
||||
}
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public void removeVirtualCosts() {
|
||||
if (this.costItemList != null) this.costItemList.removeIf(item -> evaluateVirtualCost(item));
|
||||
}
|
||||
|
||||
public enum ShopRefreshType {
|
||||
NONE(0),
|
||||
SHOP_REFRESH_DAILY(1),
|
||||
SHOP_REFRESH_WEEKLY(2),
|
||||
SHOP_REFRESH_MONTHLY(3);
|
||||
|
||||
private final int value;
|
||||
|
||||
ShopRefreshType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int value() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
package emu.grasscutter.game.shop;
|
||||
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
import emu.grasscutter.data.excels.ShopGoodsData;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class ShopInfo {
|
||||
@Getter @Setter private int goodsId = 0;
|
||||
@Getter @Setter private ItemParamData goodsItem;
|
||||
@Getter @Setter private int scoin = 0;
|
||||
@Getter @Setter private List<ItemParamData> costItemList;
|
||||
@Getter @Setter private int boughtNum = 0;
|
||||
@Getter @Setter private int buyLimit = 0;
|
||||
@Getter @Setter private int beginTime = 0;
|
||||
@Getter @Setter private int endTime = 1924992000;
|
||||
@Getter @Setter private int minLevel = 0;
|
||||
@Getter @Setter private int maxLevel = 61;
|
||||
@Getter @Setter private List<Integer> preGoodsIdList = new ArrayList<>();
|
||||
@Getter @Setter private int mcoin = 0;
|
||||
@Getter @Setter private int hcoin = 0;
|
||||
@Getter @Setter private int disableType = 0;
|
||||
@Getter @Setter private int secondarySheetId = 0;
|
||||
|
||||
private String refreshType;
|
||||
@Setter private transient ShopRefreshType shopRefreshType;
|
||||
@Getter @Setter private int shopRefreshParam;
|
||||
|
||||
public ShopInfo(ShopGoodsData sgd) {
|
||||
this.goodsId = sgd.getGoodsId();
|
||||
this.goodsItem = new ItemParamData(sgd.getItemId(), sgd.getItemCount());
|
||||
this.scoin = sgd.getCostScoin();
|
||||
this.mcoin = sgd.getCostMcoin();
|
||||
this.hcoin = sgd.getCostHcoin();
|
||||
this.buyLimit = sgd.getBuyLimit();
|
||||
|
||||
this.minLevel = sgd.getMinPlayerLevel();
|
||||
this.maxLevel = sgd.getMaxPlayerLevel();
|
||||
this.costItemList =
|
||||
sgd.getCostItems().stream()
|
||||
.filter(x -> x.getId() != 0)
|
||||
.map(x -> new ItemParamData(x.getId(), x.getCount()))
|
||||
.toList();
|
||||
this.secondarySheetId = sgd.getSubTabId();
|
||||
this.shopRefreshType = sgd.getRefreshType();
|
||||
this.shopRefreshParam = sgd.getRefreshParam();
|
||||
}
|
||||
|
||||
public ShopRefreshType getShopRefreshType() {
|
||||
if (refreshType == null) return ShopRefreshType.NONE;
|
||||
return switch (refreshType) {
|
||||
case "SHOP_REFRESH_DAILY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_DAILY;
|
||||
case "SHOP_REFRESH_WEEKLY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_WEEKLY;
|
||||
case "SHOP_REFRESH_MONTHLY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_MONTHLY;
|
||||
default -> ShopInfo.ShopRefreshType.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean evaluateVirtualCost(ItemParamData item) {
|
||||
return switch (item.getId()) {
|
||||
case 201 -> {
|
||||
this.hcoin += item.getCount();
|
||||
yield true;
|
||||
}
|
||||
case 203 -> {
|
||||
this.mcoin += item.getCount();
|
||||
yield true;
|
||||
}
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public void removeVirtualCosts() {
|
||||
if (this.costItemList != null) this.costItemList.removeIf(item -> evaluateVirtualCost(item));
|
||||
}
|
||||
|
||||
public enum ShopRefreshType {
|
||||
NONE(0),
|
||||
SHOP_REFRESH_DAILY(1),
|
||||
SHOP_REFRESH_WEEKLY(2),
|
||||
SHOP_REFRESH_MONTHLY(3);
|
||||
|
||||
private final int value;
|
||||
|
||||
ShopRefreshType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int value() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user