mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-12-17 09:25:06 +01:00
Merge remote-tracking branch 'origin/development' into tower
This commit is contained in:
@@ -96,7 +96,7 @@ public class GachaBanner {
|
||||
return toProto("");
|
||||
}
|
||||
public GachaInfo toProto(String sessionKey) {
|
||||
String record = "https://"
|
||||
String record = "http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://"
|
||||
+ (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ?
|
||||
Grasscutter.getConfig().getDispatchOptions().Ip :
|
||||
Grasscutter.getConfig().getDispatchOptions().PublicIp)
|
||||
|
||||
@@ -1,442 +0,0 @@
|
||||
package emu.grasscutter.game.managers.MovementManager;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.game.entity.EntityAvatar;
|
||||
import emu.grasscutter.game.entity.GameEntity;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.game.props.LifeState;
|
||||
import emu.grasscutter.game.props.PlayerProperty;
|
||||
import emu.grasscutter.net.proto.EntityMoveInfoOuterClass;
|
||||
import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo;
|
||||
import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState;
|
||||
import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType;
|
||||
import emu.grasscutter.net.proto.VectorOuterClass;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.Math;
|
||||
import java.util.*;
|
||||
|
||||
public class MovementManager {
|
||||
|
||||
public HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>();
|
||||
|
||||
private enum ConsumptionType {
|
||||
None(0),
|
||||
|
||||
// consume
|
||||
CLIMB_START(-500),
|
||||
CLIMBING(-150),
|
||||
CLIMB_JUMP(-2500),
|
||||
DASH(-1800),
|
||||
SPRINT(-360),
|
||||
FLY(-60),
|
||||
SWIM_DASH_START(-200),
|
||||
SWIM_DASH(-200),
|
||||
SWIMMING(-80),
|
||||
FIGHT(0),
|
||||
|
||||
// restore
|
||||
STANDBY(500),
|
||||
RUN(500),
|
||||
WALK(500),
|
||||
STANDBY_MOVE(500),
|
||||
POWERED_FLY(500);
|
||||
|
||||
public final int amount;
|
||||
ConsumptionType(int amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
|
||||
private class Consumption {
|
||||
public ConsumptionType consumptionType;
|
||||
public int amount;
|
||||
public Consumption(ConsumptionType ct, int a) {
|
||||
consumptionType = ct;
|
||||
amount = a;
|
||||
}
|
||||
public Consumption(ConsumptionType ct) {
|
||||
this(ct, ct.amount);
|
||||
}
|
||||
}
|
||||
|
||||
private MotionState previousState = MotionState.MOTION_STANDBY;
|
||||
private MotionState currentState = MotionState.MOTION_STANDBY;
|
||||
private Position previousCoordinates = new Position(0, 0, 0);
|
||||
private Position currentCoordinates = new Position(0, 0, 0);
|
||||
|
||||
private final Player player;
|
||||
|
||||
private float landSpeed = 0;
|
||||
private long landTimeMillisecond = 0;
|
||||
private Timer movementManagerTickTimer;
|
||||
private GameSession cachedSession = null;
|
||||
private GameEntity cachedEntity = null;
|
||||
private int staminaRecoverDelay = 0;
|
||||
private int skillCaster = 0;
|
||||
private int skillCasting = 0;
|
||||
|
||||
public MovementManager(Player player) {
|
||||
previousCoordinates.add(new Position(0,0,0));
|
||||
this.player = player;
|
||||
|
||||
MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList(
|
||||
MotionState.MOTION_SWIM_MOVE,
|
||||
MotionState.MOTION_SWIM_IDLE,
|
||||
MotionState.MOTION_SWIM_DASH,
|
||||
MotionState.MOTION_SWIM_JUMP
|
||||
)));
|
||||
|
||||
MotionStatesCategorized.put("STANDBY", new HashSet<>(Arrays.asList(
|
||||
MotionState.MOTION_STANDBY,
|
||||
MotionState.MOTION_STANDBY_MOVE,
|
||||
MotionState.MOTION_DANGER_STANDBY,
|
||||
MotionState.MOTION_DANGER_STANDBY_MOVE,
|
||||
MotionState.MOTION_LADDER_TO_STANDBY,
|
||||
MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY
|
||||
)));
|
||||
|
||||
MotionStatesCategorized.put("CLIMB", new HashSet<>(Arrays.asList(
|
||||
MotionState.MOTION_CLIMB,
|
||||
MotionState.MOTION_CLIMB_JUMP,
|
||||
MotionState.MOTION_STANDBY_TO_CLIMB,
|
||||
MotionState.MOTION_LADDER_IDLE,
|
||||
MotionState.MOTION_LADDER_MOVE,
|
||||
MotionState.MOTION_LADDER_SLIP,
|
||||
MotionState.MOTION_STANDBY_TO_LADDER
|
||||
)));
|
||||
|
||||
MotionStatesCategorized.put("FLY", new HashSet<>(Arrays.asList(
|
||||
MotionState.MOTION_FLY,
|
||||
MotionState.MOTION_FLY_IDLE,
|
||||
MotionState.MOTION_FLY_SLOW,
|
||||
MotionState.MOTION_FLY_FAST,
|
||||
MotionState.MOTION_POWERED_FLY
|
||||
)));
|
||||
|
||||
MotionStatesCategorized.put("RUN", new HashSet<>(Arrays.asList(
|
||||
MotionState.MOTION_DASH,
|
||||
MotionState.MOTION_DANGER_DASH,
|
||||
MotionState.MOTION_DASH_BEFORE_SHAKE,
|
||||
MotionState.MOTION_RUN,
|
||||
MotionState.MOTION_DANGER_RUN,
|
||||
MotionState.MOTION_WALK,
|
||||
MotionState.MOTION_DANGER_WALK
|
||||
)));
|
||||
|
||||
MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList(
|
||||
MotionState.MOTION_FIGHT
|
||||
)));
|
||||
|
||||
|
||||
}
|
||||
|
||||
public void handle(GameSession session, EntityMoveInfoOuterClass.EntityMoveInfo moveInfo, GameEntity entity) {
|
||||
if (movementManagerTickTimer == null) {
|
||||
movementManagerTickTimer = new Timer();
|
||||
movementManagerTickTimer.scheduleAtFixedRate(new MotionManagerTick(), 0, 200);
|
||||
}
|
||||
// cache info for later use in tick
|
||||
cachedSession = session;
|
||||
cachedEntity = entity;
|
||||
|
||||
MotionInfo motionInfo = moveInfo.getMotionInfo();
|
||||
moveEntity(entity, moveInfo);
|
||||
VectorOuterClass.Vector posVector = motionInfo.getPos();
|
||||
Position newPos = new Position(posVector.getX(),
|
||||
posVector.getY(), posVector.getZ());;
|
||||
if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) {
|
||||
currentCoordinates = newPos;
|
||||
}
|
||||
currentState = motionInfo.getState();
|
||||
Grasscutter.getLogger().debug("" + currentState + "\t" + (moveInfo.getIsReliable() ? "reliable" : ""));
|
||||
handleFallOnGround(motionInfo);
|
||||
}
|
||||
|
||||
public void resetTimer() {
|
||||
Grasscutter.getLogger().debug("MovementManager ticker stopped");
|
||||
movementManagerTickTimer.cancel();
|
||||
movementManagerTickTimer = null;
|
||||
}
|
||||
|
||||
private void moveEntity(GameEntity entity, EntityMoveInfoOuterClass.EntityMoveInfo moveInfo) {
|
||||
entity.getPosition().set(moveInfo.getMotionInfo().getPos());
|
||||
entity.getRotation().set(moveInfo.getMotionInfo().getRot());
|
||||
entity.setLastMoveSceneTimeMs(moveInfo.getSceneTime());
|
||||
entity.setLastMoveReliableSeq(moveInfo.getReliableSeq());
|
||||
entity.setMotionState(moveInfo.getMotionInfo().getState());
|
||||
}
|
||||
|
||||
private boolean isPlayerMoving() {
|
||||
float diffX = currentCoordinates.getX() - previousCoordinates.getX();
|
||||
float diffY = currentCoordinates.getY() - previousCoordinates.getY();
|
||||
float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ();
|
||||
// Grasscutter.getLogger().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates + ", " + diffX + ", " + diffY + ", " + diffZ);
|
||||
return Math.abs(diffX) > 0.2 || Math.abs(diffY) > 0.1 || Math.abs(diffZ) > 0.2;
|
||||
}
|
||||
|
||||
private int getCurrentStamina() {
|
||||
return player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
|
||||
}
|
||||
|
||||
private int getMaximumStamina() {
|
||||
return player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
|
||||
}
|
||||
|
||||
// Returns new stamina
|
||||
public int updateStamina(GameSession session, int amount) {
|
||||
int currentStamina = session.getPlayer().getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
|
||||
if (amount == 0) {
|
||||
return currentStamina;
|
||||
}
|
||||
int playerMaxStamina = session.getPlayer().getProperty(PlayerProperty.PROP_MAX_STAMINA);
|
||||
int newStamina = currentStamina + amount;
|
||||
if (newStamina < 0) {
|
||||
newStamina = 0;
|
||||
}
|
||||
if (newStamina > playerMaxStamina) {
|
||||
newStamina = playerMaxStamina;
|
||||
}
|
||||
session.getPlayer().setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina);
|
||||
session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA));
|
||||
return newStamina;
|
||||
}
|
||||
|
||||
private void handleFallOnGround(@NotNull MotionInfo motionInfo) {
|
||||
MotionState state = motionInfo.getState();
|
||||
// land speed and fall on ground event arrive in different packets
|
||||
// cache land speed
|
||||
if (state == MotionState.MOTION_LAND_SPEED) {
|
||||
landSpeed = motionInfo.getSpeed().getY();
|
||||
landTimeMillisecond = System.currentTimeMillis();
|
||||
}
|
||||
if (state == MotionState.MOTION_FALL_ON_GROUND) {
|
||||
// if not received immediately after MOTION_LAND_SPEED, discard this packet.
|
||||
// TODO: Test in high latency.
|
||||
int maxDelay = 200;
|
||||
if ((System.currentTimeMillis() - landTimeMillisecond) > maxDelay) {
|
||||
Grasscutter.getLogger().debug("MOTION_FALL_ON_GROUND received after " + maxDelay + "ms, discard.");
|
||||
return;
|
||||
}
|
||||
float currentHP = cachedEntity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
|
||||
float maxHP = cachedEntity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
|
||||
float damage = 0;
|
||||
Grasscutter.getLogger().debug("LandSpeed: " + landSpeed);
|
||||
if (landSpeed < -23.5) {
|
||||
damage = (float)(maxHP * 0.33);
|
||||
}
|
||||
if (landSpeed < -25) {
|
||||
damage = (float)(maxHP * 0.5);
|
||||
}
|
||||
if (landSpeed < -26.5) {
|
||||
damage = (float)(maxHP * 0.66);
|
||||
}
|
||||
if (landSpeed < -28) {
|
||||
damage = (maxHP * 1);
|
||||
}
|
||||
float newHP = currentHP - damage;
|
||||
if (newHP < 0) {
|
||||
newHP = 0;
|
||||
}
|
||||
Grasscutter.getLogger().debug("Max: " + maxHP + "\tCurr: " + currentHP + "\tDamage: " + damage + "\tnewHP: " + newHP);
|
||||
cachedEntity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP);
|
||||
cachedEntity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(cachedEntity, FightProperty.FIGHT_PROP_CUR_HP));
|
||||
if (newHP == 0) {
|
||||
killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_FALL);
|
||||
}
|
||||
landSpeed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDrowning() {
|
||||
int stamina = getCurrentStamina();
|
||||
if (stamina < 10) {
|
||||
boolean isSwimming = MotionStatesCategorized.get("SWIM").contains(currentState);
|
||||
Grasscutter.getLogger().debug(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState + "\t" + isSwimming);
|
||||
if (isSwimming && currentState != MotionState.MOTION_SWIM_IDLE) {
|
||||
killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void killAvatar(GameSession session, GameEntity entity, PlayerDieType dieType) {
|
||||
cachedSession.send(new PacketAvatarLifeStateChangeNotify(
|
||||
cachedSession.getPlayer().getTeamManager().getCurrentAvatarEntity().getAvatar(),
|
||||
LifeState.LIFE_DEAD,
|
||||
dieType
|
||||
));
|
||||
cachedSession.send(new PacketLifeStateChangeNotify(
|
||||
cachedEntity,
|
||||
LifeState.LIFE_DEAD,
|
||||
dieType
|
||||
));
|
||||
cachedEntity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0);
|
||||
cachedEntity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(cachedEntity, FightProperty.FIGHT_PROP_CUR_HP));
|
||||
entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD));
|
||||
session.getPlayer().getScene().removeEntity(entity);
|
||||
((EntityAvatar)entity).onDeath(dieType, 0);
|
||||
}
|
||||
|
||||
private class MotionManagerTick extends TimerTask
|
||||
{
|
||||
public void run() {
|
||||
if (Grasscutter.getConfig().OpenStamina) {
|
||||
boolean moving = isPlayerMoving();
|
||||
if (moving || (getCurrentStamina() < getMaximumStamina())) {
|
||||
// Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " + (getCurrentStamina() >= getMaximumStamina()) + ", recalculate stamina");
|
||||
Consumption consumption = new Consumption(ConsumptionType.None);
|
||||
|
||||
// TODO: refactor these conditions.
|
||||
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
|
||||
consumption = getClimbConsumption();
|
||||
} else if (MotionStatesCategorized.get("SWIM").contains((currentState))) {
|
||||
consumption = getSwimConsumptions();
|
||||
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
|
||||
consumption = getRunWalkDashConsumption();
|
||||
} else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
|
||||
consumption = getFlyConsumption();
|
||||
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
|
||||
consumption = getStandConsumption();
|
||||
} else if (MotionStatesCategorized.get("FIGHT").contains(currentState)) {
|
||||
consumption = getFightConsumption();
|
||||
}
|
||||
|
||||
// delay 2 seconds before start recovering - as official server does.
|
||||
if (cachedSession != null) {
|
||||
if (consumption.amount < 0) {
|
||||
staminaRecoverDelay = 0;
|
||||
}
|
||||
if (consumption.amount > 0 && consumption.consumptionType != ConsumptionType.POWERED_FLY) {
|
||||
if (staminaRecoverDelay < 10) {
|
||||
staminaRecoverDelay++;
|
||||
consumption = new Consumption(ConsumptionType.None);
|
||||
}
|
||||
}
|
||||
// Grasscutter.getLogger().debug(getCurrentStamina() + "/" + getMaximumStamina() + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t(" + consumption.consumptionType + "," + consumption.amount + ")");
|
||||
updateStamina(cachedSession, consumption.amount);
|
||||
}
|
||||
|
||||
// tick triggered
|
||||
handleDrowning();
|
||||
}
|
||||
}
|
||||
|
||||
previousState = currentState;
|
||||
previousCoordinates = new Position(currentCoordinates.getX(),
|
||||
currentCoordinates.getY(), currentCoordinates.getZ());;
|
||||
}
|
||||
}
|
||||
|
||||
private Consumption getClimbConsumption() {
|
||||
Consumption consumption = new Consumption(ConsumptionType.None);
|
||||
if (currentState == MotionState.MOTION_CLIMB) {
|
||||
consumption = new Consumption(ConsumptionType.CLIMBING);
|
||||
if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) {
|
||||
consumption = new Consumption(ConsumptionType.CLIMB_START);
|
||||
}
|
||||
if (!isPlayerMoving()) {
|
||||
consumption = new Consumption(ConsumptionType.None);
|
||||
}
|
||||
}
|
||||
if (currentState == MotionState.MOTION_CLIMB_JUMP) {
|
||||
if (previousState != MotionState.MOTION_CLIMB_JUMP) {
|
||||
consumption = new Consumption(ConsumptionType.CLIMB_JUMP);
|
||||
}
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private Consumption getSwimConsumptions() {
|
||||
Consumption consumption = new Consumption(ConsumptionType.None);
|
||||
if (currentState == MotionState.MOTION_SWIM_MOVE) {
|
||||
consumption = new Consumption(ConsumptionType.SWIMMING);
|
||||
}
|
||||
if (currentState == MotionState.MOTION_SWIM_DASH) {
|
||||
consumption = new Consumption(ConsumptionType.SWIM_DASH_START);
|
||||
if (previousState == MotionState.MOTION_SWIM_DASH) {
|
||||
consumption = new Consumption(ConsumptionType.SWIM_DASH);
|
||||
}
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private Consumption getRunWalkDashConsumption() {
|
||||
Consumption consumption = new Consumption(ConsumptionType.None);
|
||||
if (currentState == MotionState.MOTION_DASH_BEFORE_SHAKE) {
|
||||
consumption = new Consumption(ConsumptionType.DASH);
|
||||
if (previousState == MotionState.MOTION_DASH_BEFORE_SHAKE) {
|
||||
// only charge once
|
||||
consumption = new Consumption(ConsumptionType.SPRINT);
|
||||
}
|
||||
}
|
||||
if (currentState == MotionState.MOTION_DASH) {
|
||||
consumption = new Consumption(ConsumptionType.SPRINT);
|
||||
}
|
||||
if (currentState == MotionState.MOTION_RUN) {
|
||||
consumption = new Consumption(ConsumptionType.RUN);
|
||||
}
|
||||
if (currentState == MotionState.MOTION_WALK) {
|
||||
consumption = new Consumption(ConsumptionType.WALK);
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private Consumption getFlyConsumption() {
|
||||
Consumption consumption = new Consumption(ConsumptionType.FLY);
|
||||
HashMap<Integer, Float> glidingCostReduction = new HashMap<>() {{
|
||||
put(212301, 0.8f); // Amber
|
||||
put(222301, 0.8f); // Venti
|
||||
}};
|
||||
float reduction = 1;
|
||||
for (EntityAvatar entity: cachedSession.getPlayer().getTeamManager().getActiveTeam()) {
|
||||
for (int skillId: entity.getAvatar().getProudSkillList()) {
|
||||
if (glidingCostReduction.containsKey(skillId)) {
|
||||
reduction = glidingCostReduction.get(skillId);
|
||||
}
|
||||
}
|
||||
}
|
||||
consumption.amount *= reduction;
|
||||
|
||||
// POWERED_FLY, e.g. wind tunnel
|
||||
if (currentState == MotionState.MOTION_POWERED_FLY) {
|
||||
consumption = new Consumption(ConsumptionType.POWERED_FLY);
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private Consumption getStandConsumption() {
|
||||
Consumption consumption = new Consumption(ConsumptionType.None);
|
||||
if (currentState == MotionState.MOTION_STANDBY) {
|
||||
consumption = new Consumption(ConsumptionType.STANDBY);
|
||||
}
|
||||
if (currentState == MotionState.MOTION_STANDBY_MOVE) {
|
||||
consumption = new Consumption(ConsumptionType.STANDBY_MOVE);
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private Consumption getFightConsumption() {
|
||||
Consumption consumption = new Consumption(ConsumptionType.None);
|
||||
HashMap<Integer, Integer> fightingCost = new HashMap<>() {{
|
||||
put(10013, -1000); // Kamisato Ayaka
|
||||
put(10413, -1000); // Mona
|
||||
}};
|
||||
if (fightingCost.containsKey(skillCasting)) {
|
||||
consumption = new Consumption(ConsumptionType.FIGHT, fightingCost.get(skillCasting));
|
||||
// only handle once, so reset.
|
||||
skillCasting = 0;
|
||||
skillCaster = 0;
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
|
||||
public void notifySkill(int caster, int skillId) {
|
||||
skillCaster = caster;
|
||||
skillCasting = skillId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package emu.grasscutter.game.managers.SotSManager;
|
||||
package emu.grasscutter.game.managers;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.game.avatar.Avatar;
|
||||
import emu.grasscutter.game.entity.EntityAvatar;
|
||||
import emu.grasscutter.game.entity.GameEntity;
|
||||
import emu.grasscutter.game.managers.MovementManager.MovementManager;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.game.props.PlayerProperty;
|
||||
import emu.grasscutter.game.world.World;
|
||||
import emu.grasscutter.net.proto.ChangeHpReasonOuterClass;
|
||||
import emu.grasscutter.net.proto.PropChangeReasonOuterClass;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
@@ -29,6 +26,8 @@ public class SotSManager {
|
||||
private final Player player;
|
||||
private Timer autoRecoverTimer;
|
||||
|
||||
public final static int GlobalMaximumSpringVolume = 8500000;
|
||||
|
||||
public SotSManager(Player player) {
|
||||
this.player = player;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package emu.grasscutter.game.managers.StaminaManager;
|
||||
|
||||
public interface AfterUpdateStaminaListener {
|
||||
/**
|
||||
* onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
|
||||
* This gives listeners a chance to intercept this update.
|
||||
*
|
||||
* @param reason Why updating stamina.
|
||||
* @param newStamina New Stamina value.
|
||||
*/
|
||||
void onAfterUpdateStamina(String reason, int newStamina);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package emu.grasscutter.game.managers.StaminaManager;
|
||||
|
||||
public interface BeforeUpdateStaminaListener {
|
||||
/**
|
||||
* onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
|
||||
* This gives listeners a chance to intercept this update.
|
||||
* @param reason Why updating stamina.
|
||||
* @param newStamina New ABSOLUTE stamina value.
|
||||
* @return true if you want to cancel this update, otherwise false.
|
||||
*/
|
||||
int onBeforeUpdateStamina(String reason, int newStamina);
|
||||
/**
|
||||
* onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
|
||||
* This gives listeners a chance to intercept this update.
|
||||
* @param reason Why updating stamina.
|
||||
* @param consumption ConsumptionType and RELATIVE stamina change amount.
|
||||
* @return true if you want to cancel this update, otherwise false.
|
||||
*/
|
||||
Consumption onBeforeUpdateStamina(String reason, Consumption consumption);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package emu.grasscutter.game.managers.StaminaManager;
|
||||
|
||||
public class Consumption {
|
||||
public ConsumptionType consumptionType;
|
||||
public int amount;
|
||||
|
||||
public Consumption(ConsumptionType ct, int a) {
|
||||
consumptionType = ct;
|
||||
amount = a;
|
||||
}
|
||||
|
||||
public Consumption(ConsumptionType ct) {
|
||||
this(ct, ct.amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package emu.grasscutter.game.managers.StaminaManager;
|
||||
|
||||
public enum ConsumptionType {
|
||||
None(0),
|
||||
|
||||
// consume
|
||||
CLIMB_START(-500),
|
||||
CLIMBING(-150),
|
||||
CLIMB_JUMP(-2500),
|
||||
SPRINT(-1800),
|
||||
DASH(-360),
|
||||
FLY(-60),
|
||||
SWIM_DASH_START(-20),
|
||||
SWIM_DASH(-204),
|
||||
SWIMMING(-80), // TODO: Slow swimming is handled per movement, not per second. Movement frequency depends on gender/age/height.
|
||||
FIGHT(0), // See StaminaManager.getFightConsumption()
|
||||
|
||||
// restore
|
||||
STANDBY(500),
|
||||
RUN(500),
|
||||
WALK(500),
|
||||
STANDBY_MOVE(500),
|
||||
POWERED_FLY(500);
|
||||
|
||||
public final int amount;
|
||||
|
||||
ConsumptionType(int amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
# Stamina Manager
|
||||
|
||||
---
|
||||
## UpdateStamina
|
||||
```java
|
||||
// will use consumption.consumptionType as reason
|
||||
public int updateStaminaRelative(GameSession session, Consumption consumption);
|
||||
```
|
||||
```java
|
||||
public int updateStaminaAbsolute(GameSession session, String reason, int newStamina)
|
||||
```
|
||||
|
||||
---
|
||||
## Pause and Resume
|
||||
```java
|
||||
public void startSustainedStaminaHandler()
|
||||
```
|
||||
```java
|
||||
public void stopSustainedStaminaHandler()
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
## Stamina change listeners and intercepting
|
||||
### BeforeUpdateStaminaListener
|
||||
```java
|
||||
|
||||
import emu.grasscutter.game.managers.StaminaManager.BeforeUpdateStaminaListener;
|
||||
|
||||
// Listener sample: plugin disable CLIMB_JUMP stamina cost.
|
||||
private class MyClass implements BeforeUpdateStaminaListener {
|
||||
// Make your class implement the listener, and pass in your class as a listener.
|
||||
|
||||
public MyClass() {
|
||||
getStaminaManager().registerBeforeUpdateStaminaListener("myClass", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBeforeUpdateStamina(String reason, int newStamina) {
|
||||
// do not intercept this update
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBeforeUpdateStamina(String reason, Consumption consumption) {
|
||||
// Try to intercept if this update is CLIMB_JUMP
|
||||
if (consumption.consumptionType == ConsumptionType.CLIMB_JUMP) {
|
||||
return true;
|
||||
}
|
||||
// If it is not CLIMB_JUMP, do not intercept.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
### AfterUpdateStaminaListener
|
||||
```java
|
||||
|
||||
import emu.grasscutter.game.managers.StaminaManager.AfterUpdateStaminaListener;
|
||||
|
||||
// Listener sample: plugin listens for changes already made.
|
||||
private class MyClass implements AfterUpdateStaminaListener {
|
||||
// Make your class implement the listener, and pass in your class as a listener.
|
||||
|
||||
public MyClass() {
|
||||
registerAfterUpdateStaminaListener("myClass", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAfterUpdateStamina(String reason, int newStamina) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,472 @@
|
||||
package emu.grasscutter.game.managers.StaminaManager;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.game.entity.EntityAvatar;
|
||||
import emu.grasscutter.game.entity.GameEntity;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
import emu.grasscutter.game.props.LifeState;
|
||||
import emu.grasscutter.game.props.PlayerProperty;
|
||||
import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo;
|
||||
import emu.grasscutter.net.proto.EvtDoSkillSuccNotifyOuterClass.EvtDoSkillSuccNotify;
|
||||
import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo;
|
||||
import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState;
|
||||
import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType;
|
||||
import emu.grasscutter.net.proto.VectorOuterClass.Vector;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import emu.grasscutter.utils.Position;
|
||||
|
||||
import java.lang.Math;
|
||||
import java.util.*;
|
||||
|
||||
public class StaminaManager {
|
||||
private final Player player;
|
||||
private HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>();
|
||||
|
||||
public final static int GlobalMaximumStamina = 24000;
|
||||
private Position currentCoordinates = new Position(0, 0, 0);
|
||||
private Position previousCoordinates = new Position(0, 0, 0);
|
||||
private MotionState currentState = MotionState.MOTION_STANDBY;
|
||||
private MotionState previousState = MotionState.MOTION_STANDBY;
|
||||
private Timer sustainedStaminaHandlerTimer;
|
||||
private GameSession cachedSession = null;
|
||||
private GameEntity cachedEntity = null;
|
||||
private int staminaRecoverDelay = 0;
|
||||
|
||||
private HashMap<String, BeforeUpdateStaminaListener> beforeUpdateStaminaListeners = new HashMap<>();
|
||||
private HashMap<String, AfterUpdateStaminaListener> afterUpdateStaminaListeners = new HashMap<>();
|
||||
|
||||
public StaminaManager(Player player) {
|
||||
this.player = player;
|
||||
|
||||
MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList(
|
||||
MotionState.MOTION_SWIM_MOVE,
|
||||
MotionState.MOTION_SWIM_IDLE,
|
||||
MotionState.MOTION_SWIM_DASH,
|
||||
MotionState.MOTION_SWIM_JUMP
|
||||
)));
|
||||
|
||||
MotionStatesCategorized.put("STANDBY", new HashSet<>(Arrays.asList(
|
||||
MotionState.MOTION_STANDBY,
|
||||
MotionState.MOTION_STANDBY_MOVE,
|
||||
MotionState.MOTION_DANGER_STANDBY,
|
||||
MotionState.MOTION_DANGER_STANDBY_MOVE,
|
||||
MotionState.MOTION_LADDER_TO_STANDBY,
|
||||
MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY
|
||||
)));
|
||||
|
||||
MotionStatesCategorized.put("CLIMB", new HashSet<>(Arrays.asList(
|
||||
MotionState.MOTION_CLIMB,
|
||||
MotionState.MOTION_CLIMB_JUMP,
|
||||
MotionState.MOTION_STANDBY_TO_CLIMB,
|
||||
MotionState.MOTION_LADDER_IDLE,
|
||||
MotionState.MOTION_LADDER_MOVE,
|
||||
MotionState.MOTION_LADDER_SLIP,
|
||||
MotionState.MOTION_STANDBY_TO_LADDER
|
||||
)));
|
||||
|
||||
MotionStatesCategorized.put("FLY", new HashSet<>(Arrays.asList(
|
||||
MotionState.MOTION_FLY,
|
||||
MotionState.MOTION_FLY_IDLE,
|
||||
MotionState.MOTION_FLY_SLOW,
|
||||
MotionState.MOTION_FLY_FAST,
|
||||
MotionState.MOTION_POWERED_FLY
|
||||
)));
|
||||
|
||||
MotionStatesCategorized.put("RUN", new HashSet<>(Arrays.asList(
|
||||
MotionState.MOTION_DASH,
|
||||
MotionState.MOTION_DANGER_DASH,
|
||||
MotionState.MOTION_DASH_BEFORE_SHAKE,
|
||||
MotionState.MOTION_RUN,
|
||||
MotionState.MOTION_DANGER_RUN,
|
||||
MotionState.MOTION_WALK,
|
||||
MotionState.MOTION_DANGER_WALK
|
||||
)));
|
||||
|
||||
MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList(
|
||||
MotionState.MOTION_FIGHT
|
||||
)));
|
||||
|
||||
MotionStatesCategorized.put("SKIFF", new HashSet<>(Arrays.asList(
|
||||
MotionState.MOTION_SKIFF_BOARDING,
|
||||
MotionState.MOTION_SKIFF_NORMAL,
|
||||
MotionState.MOTION_SKIFF_DASH,
|
||||
MotionState.MOTION_SKIFF_POWERED_DASH
|
||||
)));
|
||||
}
|
||||
|
||||
// Listeners
|
||||
|
||||
public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) {
|
||||
if (beforeUpdateStaminaListeners.containsKey(listenerName)) {
|
||||
return false;
|
||||
}
|
||||
beforeUpdateStaminaListeners.put(listenerName, listener);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean unregisterBeforeUpdateStaminaListener(String listenerName) {
|
||||
if (!beforeUpdateStaminaListeners.containsKey(listenerName)) {
|
||||
return false;
|
||||
}
|
||||
beforeUpdateStaminaListeners.remove(listenerName);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean registerAfterUpdateStaminaListener(String listenerName, AfterUpdateStaminaListener listener) {
|
||||
if (afterUpdateStaminaListeners.containsKey(listenerName)) {
|
||||
return false;
|
||||
}
|
||||
afterUpdateStaminaListeners.put(listenerName, listener);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean unregisterAfterUpdateStaminaListener(String listenerName) {
|
||||
if (!afterUpdateStaminaListeners.containsKey(listenerName)) {
|
||||
return false;
|
||||
}
|
||||
afterUpdateStaminaListeners.remove(listenerName);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isPlayerMoving() {
|
||||
float diffX = currentCoordinates.getX() - previousCoordinates.getX();
|
||||
float diffY = currentCoordinates.getY() - previousCoordinates.getY();
|
||||
float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ();
|
||||
Grasscutter.getLogger().trace("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates +
|
||||
", " + diffX + ", " + diffY + ", " + diffZ);
|
||||
return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3;
|
||||
}
|
||||
|
||||
public int updateStaminaRelative(GameSession session, Consumption consumption) {
|
||||
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
|
||||
if (consumption.amount == 0) {
|
||||
return currentStamina;
|
||||
}
|
||||
// notify will update
|
||||
for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) {
|
||||
Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.consumptionType.toString(), consumption);
|
||||
if ((overriddenConsumption.consumptionType != consumption.consumptionType) && (overriddenConsumption.amount != consumption.amount)) {
|
||||
Grasscutter.getLogger().debug("[StaminaManager] Stamina update relative(" +
|
||||
consumption.consumptionType.toString() + ", " + consumption.amount + ") overridden to relative(" +
|
||||
consumption.consumptionType.toString() + ", " + consumption.amount + ") by: " + listener.getKey());
|
||||
return currentStamina;
|
||||
}
|
||||
}
|
||||
int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
|
||||
Grasscutter.getLogger().trace(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" +
|
||||
(isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.consumptionType + "," +
|
||||
consumption.amount + ")");
|
||||
int newStamina = currentStamina + consumption.amount;
|
||||
if (newStamina < 0) {
|
||||
newStamina = 0;
|
||||
} else if (newStamina > playerMaxStamina) {
|
||||
newStamina = playerMaxStamina;
|
||||
}
|
||||
return setStamina(session, consumption.consumptionType.toString(), newStamina);
|
||||
}
|
||||
|
||||
public int updateStaminaAbsolute(GameSession session, String reason, int newStamina) {
|
||||
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
|
||||
// notify will update
|
||||
for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) {
|
||||
int overriddenNewStamina = listener.getValue().onBeforeUpdateStamina(reason, newStamina);
|
||||
if (overriddenNewStamina != newStamina) {
|
||||
Grasscutter.getLogger().debug("[StaminaManager] Stamina update absolute(" +
|
||||
reason + ", " + newStamina + ") overridden to absolute(" +
|
||||
reason + ", " + newStamina + ") by: " + listener.getKey());
|
||||
return currentStamina;
|
||||
}
|
||||
}
|
||||
int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
|
||||
if (newStamina < 0) {
|
||||
newStamina = 0;
|
||||
} else if (newStamina > playerMaxStamina) {
|
||||
newStamina = playerMaxStamina;
|
||||
}
|
||||
return setStamina(session, reason, newStamina);
|
||||
}
|
||||
|
||||
// Returns new stamina and sends PlayerPropNotify
|
||||
public int setStamina(GameSession session, String reason, int newStamina) {
|
||||
// set stamina
|
||||
player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina);
|
||||
session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA));
|
||||
// notify updated
|
||||
for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) {
|
||||
listener.getValue().onAfterUpdateStamina(reason, newStamina);
|
||||
}
|
||||
return newStamina;
|
||||
}
|
||||
|
||||
// Kills avatar, removes entity and sends notification.
|
||||
// TODO: Probably move this to Avatar class? since other components may also need to kill avatar.
|
||||
public void killAvatar(GameSession session, GameEntity entity, PlayerDieType dieType) {
|
||||
session.send(new PacketAvatarLifeStateChangeNotify(player.getTeamManager().getCurrentAvatarEntity().getAvatar(),
|
||||
LifeState.LIFE_DEAD, dieType));
|
||||
session.send(new PacketLifeStateChangeNotify(entity, LifeState.LIFE_DEAD, dieType));
|
||||
entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0);
|
||||
entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
|
||||
entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD));
|
||||
player.getScene().removeEntity(entity);
|
||||
((EntityAvatar) entity).onDeath(dieType, 0);
|
||||
}
|
||||
|
||||
public void startSustainedStaminaHandler() {
|
||||
if (!player.isPaused() && sustainedStaminaHandlerTimer == null) {
|
||||
sustainedStaminaHandlerTimer = new Timer();
|
||||
sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200);
|
||||
Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started");
|
||||
}
|
||||
}
|
||||
|
||||
public void stopSustainedStaminaHandler() {
|
||||
if (sustainedStaminaHandlerTimer != null) {
|
||||
sustainedStaminaHandlerTimer.cancel();
|
||||
sustainedStaminaHandlerTimer = null;
|
||||
Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
|
||||
}
|
||||
}
|
||||
|
||||
// Handlers
|
||||
|
||||
// External trigger handler
|
||||
|
||||
public void handleEvtDoSkillSuccNotify(GameSession session, EvtDoSkillSuccNotify notify) {
|
||||
handleImmediateStamina(session, notify);
|
||||
}
|
||||
|
||||
public void handleCombatInvocationsNotify(GameSession session, EntityMoveInfo moveInfo, GameEntity entity) {
|
||||
// cache info for later use in SustainedStaminaHandler tick
|
||||
cachedSession = session;
|
||||
cachedEntity = entity;
|
||||
MotionInfo motionInfo = moveInfo.getMotionInfo();
|
||||
MotionState motionState = motionInfo.getState();
|
||||
boolean isReliable = moveInfo.getIsReliable();
|
||||
Grasscutter.getLogger().trace("" + motionState + "\t" + (isReliable ? "reliable" : ""));
|
||||
if (isReliable) {
|
||||
currentState = motionState;
|
||||
Vector posVector = motionInfo.getPos();
|
||||
Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ());
|
||||
if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) {
|
||||
currentCoordinates = newPos;
|
||||
}
|
||||
}
|
||||
startSustainedStaminaHandler();
|
||||
handleImmediateStamina(session, motionInfo, motionState, entity);
|
||||
}
|
||||
|
||||
// Internal handler
|
||||
|
||||
private void handleImmediateStamina(GameSession session, MotionInfo motionInfo, MotionState motionState,
|
||||
GameEntity entity) {
|
||||
switch (motionState) {
|
||||
case MOTION_DASH_BEFORE_SHAKE:
|
||||
if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) {
|
||||
updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT));
|
||||
}
|
||||
break;
|
||||
case MOTION_CLIMB_JUMP:
|
||||
if (previousState != MotionState.MOTION_CLIMB_JUMP) {
|
||||
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP));
|
||||
}
|
||||
break;
|
||||
case MOTION_SWIM_DASH:
|
||||
if (previousState != MotionState.MOTION_SWIM_DASH) {
|
||||
updateStaminaRelative(session, new Consumption(ConsumptionType.SWIM_DASH_START));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleImmediateStamina(GameSession session, EvtDoSkillSuccNotify notify) {
|
||||
Consumption consumption = getFightConsumption(notify.getSkillId());
|
||||
updateStaminaRelative(session, consumption);
|
||||
}
|
||||
|
||||
private class SustainedStaminaHandler extends TimerTask {
|
||||
public void run() {
|
||||
if (Grasscutter.getConfig().OpenStamina) {
|
||||
boolean moving = isPlayerMoving();
|
||||
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
|
||||
int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
|
||||
if (moving || (currentStamina < maxStamina)) {
|
||||
Grasscutter.getLogger().trace("Player moving: " + moving + ", stamina full: " +
|
||||
(currentStamina >= maxStamina) + ", recalculate stamina");
|
||||
|
||||
Consumption consumption = new Consumption(ConsumptionType.None);
|
||||
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
|
||||
consumption = getClimbSustainedConsumption();
|
||||
} else if (MotionStatesCategorized.get("SWIM").contains((currentState))) {
|
||||
consumption = getSwimSustainedConsumptions();
|
||||
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
|
||||
consumption = getRunWalkDashSustainedConsumption();
|
||||
} else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
|
||||
consumption = getFlySustainedConsumption();
|
||||
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
|
||||
consumption = getStandSustainedConsumption();
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: Reductions that apply to all motion types:
|
||||
Elemental Resonance
|
||||
Wind: -15%
|
||||
Skills
|
||||
Diona E: -10% while shield lasts
|
||||
Barbara E: -12% while lasts
|
||||
*/
|
||||
if (cachedSession != null) {
|
||||
if (consumption.amount < 0) {
|
||||
staminaRecoverDelay = 0;
|
||||
}
|
||||
if (consumption.amount > 0 && consumption.consumptionType != ConsumptionType.POWERED_FLY) {
|
||||
// For POWERED_FLY recover immediately - things like Amber's gliding exam may require this.
|
||||
if (staminaRecoverDelay < 10) {
|
||||
// For others recover after 2 seconds (10 ticks) - as official server does.
|
||||
staminaRecoverDelay++;
|
||||
consumption.amount = 0;
|
||||
Grasscutter.getLogger().trace("[StaminaManager] Delaying recovery: " + staminaRecoverDelay);
|
||||
}
|
||||
}
|
||||
updateStaminaRelative(cachedSession, consumption);
|
||||
}
|
||||
}
|
||||
}
|
||||
previousState = currentState;
|
||||
previousCoordinates = new Position(
|
||||
currentCoordinates.getX(),
|
||||
currentCoordinates.getY(),
|
||||
currentCoordinates.getZ()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDrowning() {
|
||||
int stamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
|
||||
if (stamina < 10) {
|
||||
Grasscutter.getLogger().trace(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" +
|
||||
player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState);
|
||||
if (currentState != MotionState.MOTION_SWIM_IDLE) {
|
||||
killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Consumption Calculators
|
||||
|
||||
// Stamina Consumption Reduction: https://genshin-impact.fandom.com/wiki/Stamina
|
||||
|
||||
private Consumption getFightConsumption(int skillCasting) {
|
||||
/* TODO:
|
||||
Instead of handling here, consider call StaminaManager.updateStamina****() with a Consumption object with
|
||||
type=FIGHT and a modified amount when handling attacks for more accurate attack start/end time and
|
||||
other info. Handling it here could be very complicated.
|
||||
Charged attack
|
||||
Default:
|
||||
Polearm: (-2500)
|
||||
Claymore: (-4000 per second, -800 each tick)
|
||||
Catalyst: (-5000)
|
||||
Talent:
|
||||
Ningguang: When Ningguang is in possession of Star Jades, her Charged Attack does not consume Stamina. (Catalyst * 0)
|
||||
Klee: When Jumpy Dumpty and Normal Attacks deal DMG, Klee has a 50% chance to obtain an Explosive Spark.
|
||||
This Explosive Spark is consumed by the next Charged Attack, which costs no Stamina. (Catalyst * 0)
|
||||
Constellations:
|
||||
Hu Tao: While in a Paramita Papilio state activated by Guide to Afterlife, Hu Tao's Charge Attacks do not consume Stamina. (Polearm * 0)
|
||||
Character Specific:
|
||||
Keqing: (-2500)
|
||||
Diluc: (Claymore * 0.5)
|
||||
Talent Moving: (Those are skills too)
|
||||
Ayaka: (-1000 initial) (-1500 per second) When the Cryo application at the end of Kamisato Art: Senho hits an opponent (+1000)
|
||||
Mona: (-1000 initial) (-1500 per second)
|
||||
*/
|
||||
|
||||
// TODO: Currently only handling Ayaka and Mona's talent moving initial costs.
|
||||
Consumption consumption = new Consumption(ConsumptionType.None);
|
||||
HashMap<Integer, Integer> fightingCost = new HashMap<>() {{
|
||||
put(10013, -1000); // Kamisato Ayaka
|
||||
put(10413, -1000); // Mona
|
||||
}};
|
||||
if (fightingCost.containsKey(skillCasting)) {
|
||||
consumption = new Consumption(ConsumptionType.FIGHT, fightingCost.get(skillCasting));
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private Consumption getClimbSustainedConsumption() {
|
||||
Consumption consumption = new Consumption(ConsumptionType.None);
|
||||
if (currentState == MotionState.MOTION_CLIMB && isPlayerMoving()) {
|
||||
consumption = new Consumption(ConsumptionType.CLIMBING);
|
||||
if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) {
|
||||
consumption = new Consumption(ConsumptionType.CLIMB_START);
|
||||
}
|
||||
}
|
||||
// TODO: Foods
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private Consumption getSwimSustainedConsumptions() {
|
||||
handleDrowning();
|
||||
Consumption consumption = new Consumption(ConsumptionType.None);
|
||||
if (currentState == MotionState.MOTION_SWIM_MOVE) {
|
||||
consumption = new Consumption(ConsumptionType.SWIMMING);
|
||||
}
|
||||
if (currentState == MotionState.MOTION_SWIM_DASH) {
|
||||
consumption = new Consumption(ConsumptionType.SWIM_DASH);
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private Consumption getRunWalkDashSustainedConsumption() {
|
||||
Consumption consumption = new Consumption(ConsumptionType.None);
|
||||
if (currentState == MotionState.MOTION_DASH) {
|
||||
consumption = new Consumption(ConsumptionType.DASH);
|
||||
// TODO: Foods
|
||||
}
|
||||
if (currentState == MotionState.MOTION_RUN) {
|
||||
consumption = new Consumption(ConsumptionType.RUN);
|
||||
}
|
||||
if (currentState == MotionState.MOTION_WALK) {
|
||||
consumption = new Consumption(ConsumptionType.WALK);
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private Consumption getFlySustainedConsumption() {
|
||||
// POWERED_FLY, e.g. wind tunnel
|
||||
if (currentState == MotionState.MOTION_POWERED_FLY) {
|
||||
return new Consumption(ConsumptionType.POWERED_FLY);
|
||||
}
|
||||
Consumption consumption = new Consumption(ConsumptionType.FLY);
|
||||
// Talent
|
||||
HashMap<Integer, Float> glidingCostReduction = new HashMap<>() {{
|
||||
put(212301, 0.8f); // Amber
|
||||
put(222301, 0.8f); // Venti
|
||||
}};
|
||||
float reduction = 1;
|
||||
for (EntityAvatar entity : cachedSession.getPlayer().getTeamManager().getActiveTeam()) {
|
||||
for (int skillId : entity.getAvatar().getProudSkillList()) {
|
||||
if (glidingCostReduction.containsKey(skillId)) {
|
||||
float potentialLowerReduction = glidingCostReduction.get(skillId);
|
||||
if (potentialLowerReduction < reduction) {
|
||||
reduction = potentialLowerReduction;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
consumption.amount *= reduction;
|
||||
// TODO: Foods
|
||||
return consumption;
|
||||
}
|
||||
|
||||
private Consumption getStandSustainedConsumption() {
|
||||
Consumption consumption = new Consumption(ConsumptionType.None);
|
||||
if (currentState == MotionState.MOTION_STANDBY) {
|
||||
consumption = new Consumption(ConsumptionType.STANDBY);
|
||||
}
|
||||
if (currentState == MotionState.MOTION_STANDBY_MOVE) {
|
||||
consumption = new Consumption(ConsumptionType.STANDBY_MOVE);
|
||||
}
|
||||
return consumption;
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ import emu.grasscutter.game.inventory.GameItem;
|
||||
import emu.grasscutter.game.inventory.Inventory;
|
||||
import emu.grasscutter.game.mail.Mail;
|
||||
import emu.grasscutter.game.mail.MailHandler;
|
||||
import emu.grasscutter.game.managers.MovementManager.MovementManager;
|
||||
import emu.grasscutter.game.managers.SotSManager.SotSManager;
|
||||
import emu.grasscutter.game.managers.StaminaManager.StaminaManager;
|
||||
import emu.grasscutter.game.managers.SotSManager;
|
||||
import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.game.props.EntityType;
|
||||
import emu.grasscutter.game.props.PlayerProperty;
|
||||
@@ -62,9 +62,6 @@ import java.util.concurrent.LinkedBlockingQueue;
|
||||
@Entity(value = "players", useDiscriminator = false)
|
||||
public class Player {
|
||||
|
||||
@Transient private static int GlobalMaximumSpringVolume = 8500000;
|
||||
@Transient private static int GlobalMaximumStamina = 24000;
|
||||
|
||||
@Id private int id;
|
||||
@Indexed(options = @IndexOptions(unique = true)) private String accountId;
|
||||
|
||||
@@ -132,7 +129,7 @@ public class Player {
|
||||
@Transient private final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler;
|
||||
|
||||
private MapMarksManager mapMarksManager;
|
||||
@Transient private MovementManager movementManager;
|
||||
@Transient private StaminaManager staminaManager;
|
||||
|
||||
private long springLastUsed;
|
||||
|
||||
@@ -178,7 +175,7 @@ public class Player {
|
||||
this.expeditionInfo = new HashMap<>();
|
||||
this.messageHandler = null;
|
||||
this.mapMarksManager = new MapMarksManager();
|
||||
this.movementManager = new MovementManager(this);
|
||||
this.staminaManager = new StaminaManager(this);
|
||||
this.sotsManager = new SotSManager(this);
|
||||
}
|
||||
|
||||
@@ -206,7 +203,7 @@ public class Player {
|
||||
this.getRotation().set(0, 307, 0);
|
||||
this.messageHandler = null;
|
||||
this.mapMarksManager = new MapMarksManager();
|
||||
this.movementManager = new MovementManager(this);
|
||||
this.staminaManager = new StaminaManager(this);
|
||||
this.sotsManager = new SotSManager(this);
|
||||
}
|
||||
|
||||
@@ -875,11 +872,11 @@ public class Player {
|
||||
}
|
||||
|
||||
public void onPause() {
|
||||
|
||||
getStaminaManager().stopSustainedStaminaHandler();
|
||||
}
|
||||
|
||||
public void onUnpause() {
|
||||
|
||||
getStaminaManager().startSustainedStaminaHandler();
|
||||
}
|
||||
|
||||
public void sendPacket(BasePacket packet) {
|
||||
@@ -1024,7 +1021,7 @@ public class Player {
|
||||
return mapMarksManager;
|
||||
}
|
||||
|
||||
public MovementManager getMovementManager() { return movementManager; }
|
||||
public StaminaManager getStaminaManager() { return staminaManager; }
|
||||
|
||||
public SotSManager getSotSManager() { return sotsManager; }
|
||||
|
||||
@@ -1152,7 +1149,7 @@ public class Player {
|
||||
|
||||
public void onLogout() {
|
||||
// stop stamina calculation
|
||||
getMovementManager().resetTimer();
|
||||
getStaminaManager().stopSustainedStaminaHandler();
|
||||
|
||||
// force to leave the dungeon
|
||||
if (getScene().getSceneType() == SceneType.SCENE_DUNGEON) {
|
||||
@@ -1214,7 +1211,7 @@ public class Player {
|
||||
} else if (prop == PlayerProperty.PROP_LAST_CHANGE_AVATAR_TIME) { // 10001
|
||||
// TODO: implement sanity check
|
||||
} else if (prop == PlayerProperty.PROP_MAX_SPRING_VOLUME) { // 10002
|
||||
if (!(value >= 0 && value <= GlobalMaximumSpringVolume)) { return false; }
|
||||
if (!(value >= 0 && value <= getSotSManager().GlobalMaximumSpringVolume)) { return false; }
|
||||
} else if (prop == PlayerProperty.PROP_CUR_SPRING_VOLUME) { // 10003
|
||||
int playerMaximumSpringVolume = getProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME);
|
||||
if (!(value >= 0 && value <= playerMaximumSpringVolume)) { return false; }
|
||||
@@ -1231,7 +1228,7 @@ public class Player {
|
||||
} else if (prop == PlayerProperty.PROP_IS_TRANSFERABLE) { // 10009
|
||||
if (!(0 <= value && value <= 1)) { return false; }
|
||||
} else if (prop == PlayerProperty.PROP_MAX_STAMINA) { // 10010
|
||||
if (!(value >= 0 && value <= GlobalMaximumStamina)) { return false; }
|
||||
if (!(value >= 0 && value <= getStaminaManager().GlobalMaximumStamina)) { return false; }
|
||||
} else if (prop == PlayerProperty.PROP_CUR_PERSIST_STAMINA) { // 10011
|
||||
int playerMaximumStamina = getProperty(PlayerProperty.PROP_MAX_STAMINA);
|
||||
if (!(value >= 0 && value <= playerMaximumStamina)) { return false; }
|
||||
@@ -1242,7 +1239,7 @@ public class Player {
|
||||
} else if (prop == PlayerProperty.PROP_PLAYER_EXP) { // 10014
|
||||
if (!(0 <= value)) { return false; }
|
||||
} else if (prop == PlayerProperty.PROP_PLAYER_HCOIN) { // 10015
|
||||
// see 10015
|
||||
// see PlayerProperty.PROP_PLAYER_HCOIN comments
|
||||
} else if (prop == PlayerProperty.PROP_PLAYER_SCOIN) { // 10016
|
||||
// See 10015
|
||||
} else if (prop == PlayerProperty.PROP_PLAYER_MP_SETTING_TYPE) { // 10017
|
||||
|
||||
@@ -557,7 +557,7 @@ public class TeamManager {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
player.getMovementManager().resetTimer(); // prevent drowning immediately after respawn
|
||||
player.getStaminaManager().stopSustainedStaminaHandler(); // prevent drowning immediately after respawn
|
||||
|
||||
// Revive all team members
|
||||
for (EntityAvatar entity : getActiveTeam()) {
|
||||
|
||||
Reference in New Issue
Block a user