Commit 8e99cb4f authored by gentlespoon's avatar gentlespoon Committed by Melledy
Browse files

More reliable stamina calculation

by separately handling immediate one-time cost and cost over time.
parent 43c27c46
...@@ -7,22 +7,31 @@ import emu.grasscutter.game.player.Player; ...@@ -7,22 +7,31 @@ import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.LifeState; import emu.grasscutter.game.props.LifeState;
import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.props.PlayerProperty;
import emu.grasscutter.net.proto.EntityMoveInfoOuterClass; 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.MotionInfoOuterClass.MotionInfo;
import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState;
import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType;
import emu.grasscutter.net.proto.VectorOuterClass; import emu.grasscutter.net.proto.VectorOuterClass.Vector;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.Position; import emu.grasscutter.utils.Position;
import org.jetbrains.annotations.NotNull;
import java.lang.Math; import java.lang.Math;
import java.util.*; import java.util.*;
public class MovementManager { public class MovementManager {
private final Player player;
public HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>(); private HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>();
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 boolean isInSkillMove = false;
private enum ConsumptionType { private enum ConsumptionType {
None(0), None(0),
...@@ -31,8 +40,8 @@ public class MovementManager { ...@@ -31,8 +40,8 @@ public class MovementManager {
CLIMB_START(-500), CLIMB_START(-500),
CLIMBING(-150), CLIMBING(-150),
CLIMB_JUMP(-2500), CLIMB_JUMP(-2500),
DASH(-1800), SPRINT(-1800),
SPRINT(-360), DASH(-360),
FLY(-60), FLY(-60),
SWIM_DASH_START(-200), SWIM_DASH_START(-200),
SWIM_DASH(-200), SWIM_DASH(-200),
...@@ -47,6 +56,7 @@ public class MovementManager { ...@@ -47,6 +56,7 @@ public class MovementManager {
POWERED_FLY(500); POWERED_FLY(500);
public final int amount; public final int amount;
ConsumptionType(int amount) { ConsumptionType(int amount) {
this.amount = amount; this.amount = amount;
} }
...@@ -55,33 +65,26 @@ public class MovementManager { ...@@ -55,33 +65,26 @@ public class MovementManager {
private class Consumption { private class Consumption {
public ConsumptionType consumptionType; public ConsumptionType consumptionType;
public int amount; public int amount;
public Consumption(ConsumptionType ct, int a) { public Consumption(ConsumptionType ct, int a) {
consumptionType = ct; consumptionType = ct;
amount = a; amount = a;
} }
public Consumption(ConsumptionType ct) { public Consumption(ConsumptionType ct) {
this(ct, ct.amount); this(ct, ct.amount);
} }
} }
private MotionState previousState = MotionState.MOTION_STANDBY; public boolean getIsInSkillMove() {
private MotionState currentState = MotionState.MOTION_STANDBY; return isInSkillMove;
private Position previousCoordinates = new Position(0, 0, 0); }
private Position currentCoordinates = new Position(0, 0, 0);
private final Player player;
private float landSpeed = 0; public void setIsInSkillMove(boolean b) {
private long landTimeMillisecond = 0; isInSkillMove = b;
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) { public MovementManager(Player player) {
previousCoordinates.add(new Position(0,0,0));
this.player = player; this.player = player;
MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList( MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList(
...@@ -131,250 +134,223 @@ public class MovementManager { ...@@ -131,250 +134,223 @@ public class MovementManager {
MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList( MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList(
MotionState.MOTION_FIGHT 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() { private boolean isPlayerMoving() {
float diffX = currentCoordinates.getX() - previousCoordinates.getX(); float diffX = currentCoordinates.getX() - previousCoordinates.getX();
float diffY = currentCoordinates.getY() - previousCoordinates.getY(); float diffY = currentCoordinates.getY() - previousCoordinates.getY();
float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ(); float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ();
// Grasscutter.getLogger().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates + ", " + diffX + ", " + diffY + ", " + diffZ); Grasscutter.getLogger().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates +
return Math.abs(diffX) > 0.2 || Math.abs(diffY) > 0.1 || Math.abs(diffZ) > 0.2; ", " + diffX + ", " + diffY + ", " + diffZ);
} return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3;
private int getCurrentStamina() {
return player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
} }
private int getMaximumStamina() { // Returns new stamina and sends PlayerPropNotify
return player.getProperty(PlayerProperty.PROP_MAX_STAMINA); public int updateStamina(GameSession session, Consumption consumption) {
} int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
if (consumption.amount == 0) {
// 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; return currentStamina;
} }
int playerMaxStamina = session.getPlayer().getProperty(PlayerProperty.PROP_MAX_STAMINA); int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
int newStamina = currentStamina + amount; Grasscutter.getLogger().debug(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" +
(isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.consumptionType + "," +
consumption.amount + ")");
int newStamina = currentStamina + consumption.amount;
if (newStamina < 0) { if (newStamina < 0) {
newStamina = 0; newStamina = 0;
} }
if (newStamina > playerMaxStamina) { if (newStamina > playerMaxStamina) {
newStamina = playerMaxStamina; newStamina = playerMaxStamina;
} }
session.getPlayer().setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina);
session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA)); session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA));
return newStamina; return newStamina;
} }
private void handleFallOnGround(@NotNull MotionInfo motionInfo) { // Kills avatar, removes entity and sends notification.
MotionState state = motionInfo.getState(); // TODO: Probably move this to Avatar class? since other components may also need to kill avatar.
// land speed and fall on ground event arrive in different packets public void killAvatar(GameSession session, GameEntity entity, PlayerDieType dieType) {
// cache land speed session.send(new PacketAvatarLifeStateChangeNotify(player.getTeamManager().getCurrentAvatarEntity().getAvatar(),
if (state == MotionState.MOTION_LAND_SPEED) { LifeState.LIFE_DEAD, dieType));
landSpeed = motionInfo.getSpeed().getY(); session.send(new PacketLifeStateChangeNotify(entity, LifeState.LIFE_DEAD, dieType));
landTimeMillisecond = System.currentTimeMillis(); 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);
} }
if (state == MotionState.MOTION_FALL_ON_GROUND) {
// if not received immediately after MOTION_LAND_SPEED, discard this packet. public void startSustainedStaminaHandler() {
// TODO: Test in high latency. if (sustainedStaminaHandlerTimer == null) {
int maxDelay = 200; sustainedStaminaHandlerTimer = new Timer();
if ((System.currentTimeMillis() - landTimeMillisecond) > maxDelay) { sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200);
Grasscutter.getLogger().debug("MOTION_FALL_ON_GROUND received after " + maxDelay + "ms, discard."); Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started");
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); public void stopSustainedStaminaHandler() {
Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
sustainedStaminaHandlerTimer.cancel();
sustainedStaminaHandlerTimer = null;
} }
if (landSpeed < -26.5) {
damage = (float)(maxHP * 0.66); // Handlers
// External trigger handler
public void handleEvtDoSkillSuccNotify(GameSession session, EvtDoSkillSuccNotify notify) {
handleImmediateStamina(session, notify);
} }
if (landSpeed < -28) {
damage = (maxHP * 1); 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;
} }
float newHP = currentHP - damage;
if (newHP < 0) {
newHP = 0;
} }
Grasscutter.getLogger().debug("Max: " + maxHP + "\tCurr: " + currentHP + "\tDamage: " + damage + "\tnewHP: " + newHP); startSustainedStaminaHandler();
cachedEntity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP); handleImmediateStamina(session, motionInfo, motionState, entity);
cachedEntity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(cachedEntity, FightProperty.FIGHT_PROP_CUR_HP));
if (newHP == 0) {
killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_FALL);
} }
landSpeed = 0;
// 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) {
updateStamina(session, new Consumption(ConsumptionType.SPRINT));
} }
break;
case MOTION_CLIMB_JUMP:
if (previousState != MotionState.MOTION_CLIMB_JUMP) {
updateStamina(session, new Consumption(ConsumptionType.CLIMB_JUMP));
} }
break;
private void handleDrowning() { case MOTION_SWIM_DASH:
int stamina = getCurrentStamina(); if (previousState != MotionState.MOTION_SWIM_DASH) {
if (stamina < 10) { updateStamina(session, new Consumption(ConsumptionType.SWIM_DASH_START));
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);
} }
break;
} }
} }
public void killAvatar(GameSession session, GameEntity entity, PlayerDieType dieType) { private void handleImmediateStamina(GameSession session, EvtDoSkillSuccNotify notify) {
cachedSession.send(new PacketAvatarLifeStateChangeNotify( Consumption consumption = getFightConsumption(notify.getSkillId());
cachedSession.getPlayer().getTeamManager().getCurrentAvatarEntity().getAvatar(), updateStamina(session, consumption);
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 private class SustainedStaminaHandler extends TimerTask {
{
public void run() { public void run() {
if (Grasscutter.getConfig().OpenStamina) { if (Grasscutter.getConfig().OpenStamina) {
boolean moving = isPlayerMoving(); boolean moving = isPlayerMoving();
if (moving || (getCurrentStamina() < getMaximumStamina())) { int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
// Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " + (getCurrentStamina() >= getMaximumStamina()) + ", recalculate stamina"); int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
if (moving || (currentStamina < maxStamina)) {
Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " +
(currentStamina >= maxStamina) + ", recalculate stamina");
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption(ConsumptionType.None);
if (!isInSkillMove) {
// TODO: refactor these conditions.
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
consumption = getClimbConsumption(); consumption = getClimbSustainedConsumption();
} else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) {
consumption = getSwimConsumptions(); consumption = getSwimSustainedConsumptions();
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) { } else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
consumption = getRunWalkDashConsumption(); consumption = getRunWalkDashSustainedConsumption();
} else if (MotionStatesCategorized.get("FLY").contains(currentState)) { } else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
consumption = getFlyConsumption(); consumption = getFlySustainedConsumption();
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
consumption = getStandConsumption(); consumption = getStandSustainedConsumption();
} else if (MotionStatesCategorized.get("FIGHT").contains(currentState)) { }
consumption = getFightConsumption();
} }
// delay 2 seconds before start recovering - as official server does.
if (cachedSession != null) { if (cachedSession != null) {
if (consumption.amount < 0) { if (consumption.amount < 0) {
staminaRecoverDelay = 0; staminaRecoverDelay = 0;
} }
if (consumption.amount > 0 && consumption.consumptionType != ConsumptionType.POWERED_FLY) { 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) { if (staminaRecoverDelay < 10) {
// For others recover after 2 seconds (10 ticks) - as official server does.
staminaRecoverDelay++; staminaRecoverDelay++;
consumption = new Consumption(ConsumptionType.None); consumption = new Consumption(ConsumptionType.None);
} }
} }
// Grasscutter.getLogger().debug(getCurrentStamina() + "/" + getMaximumStamina() + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t(" + consumption.consumptionType + "," + consumption.amount + ")"); updateStamina(cachedSession, consumption);
updateStamina(cachedSession, consumption.amount);
} }
// tick triggered
handleDrowning(); handleDrowning();
} }
} }
previousState = currentState; previousState = currentState;
previousCoordinates = new Position(currentCoordinates.getX(), previousCoordinates = new Position(
currentCoordinates.getY(), currentCoordinates.getZ());; currentCoordinates.getX(),
currentCoordinates.getY(),
currentCoordinates.getZ()
);
}
}
private void handleDrowning() {
int stamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
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);
} }
} }
}
// Consumption Calculators
private Consumption getClimbConsumption() { private Consumption getFightConsumption(int skillCasting) {
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_CLIMB) { 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); consumption = new Consumption(ConsumptionType.CLIMBING);
if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) { if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) {
consumption = new Consumption(ConsumptionType.CLIMB_START); 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; return consumption;
} }
private Consumption getSwimConsumptions() { private Consumption getSwimSustainedConsumptions() {
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_SWIM_MOVE) { if (currentState == MotionState.MOTION_SWIM_MOVE) {
consumption = new Consumption(ConsumptionType.SWIMMING); consumption = new Consumption(ConsumptionType.SWIMMING);
} }
if (currentState == MotionState.MOTION_SWIM_DASH) { 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); consumption = new Consumption(ConsumptionType.SWIM_DASH);
} }
}
return consumption; return consumption;
} }
private Consumption getRunWalkDashConsumption() { private Consumption getRunWalkDashSustainedConsumption() {
Consumption consumption = new Consumption(ConsumptionType.None); 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) { if (currentState == MotionState.MOTION_DASH) {
consumption = new Consumption(ConsumptionType.SPRINT); consumption = new Consumption(ConsumptionType.DASH);
} }
if (currentState == MotionState.MOTION_RUN) { if (currentState == MotionState.MOTION_RUN) {
consumption = new Consumption(ConsumptionType.RUN); consumption = new Consumption(ConsumptionType.RUN);
...@@ -385,22 +361,21 @@ public class MovementManager { ...@@ -385,22 +361,21 @@ public class MovementManager {
return consumption; return consumption;
} }
private Consumption getFlyConsumption() { private Consumption getFlySustainedConsumption() {
Consumption consumption = new Consumption(ConsumptionType.FLY); Consumption consumption = new Consumption(ConsumptionType.FLY);
HashMap<Integer, Float> glidingCostReduction = new HashMap<>() {{ HashMap<Integer, Float> glidingCostReduction = new HashMap<>() {{
put(212301, 0.8f); // Amber put(212301, 0.8f); // Amber
put(222301, 0.8f); // Venti put(222301, 0.8f); // Venti
}}; }};
float reduction = 1; float reduction = 1;
for (EntityAvatar entity: cachedSession.getPlayer().getTeamManager().getActiveTeam()) { for (EntityAvatar entity : cachedSession.getPlayer().getTeamManager().getActiveTeam()) {
for (int skillId: entity.getAvatar().getProudSkillList()) { for (int skillId : entity.getAvatar().getProudSkillList()) {
if (glidingCostReduction.containsKey(skillId)) { if (glidingCostReduction.containsKey(skillId)) {
reduction = glidingCostReduction.get(skillId); reduction = glidingCostReduction.get(skillId);
} }
} }
} }
consumption.amount *= reduction; consumption.amount *= reduction;
// POWERED_FLY, e.g. wind tunnel // POWERED_FLY, e.g. wind tunnel
if (currentState == MotionState.MOTION_POWERED_FLY) { if (currentState == MotionState.MOTION_POWERED_FLY) {
consumption = new Consumption(ConsumptionType.POWERED_FLY); consumption = new Consumption(ConsumptionType.POWERED_FLY);
...@@ -408,7 +383,7 @@ public class MovementManager { ...@@ -408,7 +383,7 @@ public class MovementManager {
return consumption; return consumption;
} }
private Consumption getStandConsumption() { private Consumption getStandSustainedConsumption() {
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_STANDBY) { if (currentState == MotionState.MOTION_STANDBY) {
consumption = new Consumption(ConsumptionType.STANDBY); consumption = new Consumption(ConsumptionType.STANDBY);
...@@ -418,25 +393,4 @@ public class MovementManager { ...@@ -418,25 +393,4 @@ public class MovementManager {
} }
return consumption; 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;
}
} }
...@@ -1152,7 +1152,7 @@ public class Player { ...@@ -1152,7 +1152,7 @@ public class Player {
public void onLogout() { public void onLogout() {
// stop stamina calculation // stop stamina calculation
getMovementManager().resetTimer(); getMovementManager().stopSustainedStaminaHandler();
// force to leave the dungeon // force to leave the dungeon
if (getScene().getSceneType() == SceneType.SCENE_DUNGEON) { if (getScene().getSceneType() == SceneType.SCENE_DUNGEON) {
......
...@@ -557,7 +557,7 @@ public class TeamManager { ...@@ -557,7 +557,7 @@ public class TeamManager {
// return; // return;
// } // }
// } // }
player.getMovementManager().resetTimer(); // prevent drowning immediately after respawn player.getMovementManager().stopSustainedStaminaHandler(); // prevent drowning immediately after respawn
// Revive all team members // Revive all team members
for (EntityAvatar entity : getActiveTeam()) { for (EntityAvatar entity : getActiveTeam()) {
......
package emu.grasscutter.server.packet.recv; package emu.grasscutter.server.packet.recv;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.CombatInvocationsNotifyOuterClass.CombatInvocationsNotify; import emu.grasscutter.net.proto.CombatInvocationsNotifyOuterClass.CombatInvocationsNotify;
...@@ -8,11 +10,21 @@ import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry; ...@@ -8,11 +10,21 @@ import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry;
import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo; import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo;
import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo; import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo;
import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState;
import emu.grasscutter.net.proto.PlayerDieTypeOuterClass;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
import java.util.HashMap;
@Opcodes(PacketOpcodes.CombatInvocationsNotify) @Opcodes(PacketOpcodes.CombatInvocationsNotify)
public class HandlerCombatInvocationsNotify extends PacketHandler { public class HandlerCombatInvocationsNotify extends PacketHandler {
private float cachedLandingSpeed = 0;
private long cachedLandingTimeMillisecond = 0;
private boolean monitorLandingEvent = false;
@Override @Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
CombatInvocationsNotify notif = CombatInvocationsNotify.parseFrom(payload); CombatInvocationsNotify notif = CombatInvocationsNotify.parseFrom(payload);
...@@ -28,7 +40,33 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { ...@@ -28,7 +40,33 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
EntityMoveInfo moveInfo = EntityMoveInfo.parseFrom(entry.getCombatData()); EntityMoveInfo moveInfo = EntityMoveInfo.parseFrom(entry.getCombatData());
GameEntity entity = session.getPlayer().getScene().getEntityById(moveInfo.getEntityId()); GameEntity entity = session.getPlayer().getScene().getEntityById(moveInfo.getEntityId());
if (entity != null) { if (entity != null) {
session.getPlayer().getMovementManager().handle(session, moveInfo, entity); // Move player
MotionInfo motionInfo = moveInfo.getMotionInfo();
entity.getPosition().set(motionInfo.getPos());
entity.getRotation().set(motionInfo.getRot());
entity.setLastMoveSceneTimeMs(moveInfo.getSceneTime());
entity.setLastMoveReliableSeq(moveInfo.getReliableSeq());
MotionState motionState = motionInfo.getState();
entity.setMotionState(motionState);
session.getPlayer().getMovementManager().handleCombatInvocationsNotify(session, moveInfo, entity);
// TODO: handle MOTION_FIGHT landing
// For plunge attacks, LAND_SPEED is always -30 and is not useful.
// May need the height when starting plunge attack.
if (monitorLandingEvent) {
if (motionState == MotionState.MOTION_FALL_ON_GROUND) {
monitorLandingEvent = false;
handleFallOnGround(session, entity, motionState);
}
}
if (motionState == MotionState.MOTION_LAND_SPEED) {
// MOTION_LAND_SPEED and MOTION_FALL_ON_GROUND arrive in different packet. Cache land speed for later use.
cachedLandingSpeed = motionInfo.getSpeed().getY();
cachedLandingTimeMillisecond = System.currentTimeMillis();
monitorLandingEvent = true;
}
} }
break; break;
default: default:
...@@ -47,5 +85,39 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { ...@@ -47,5 +85,39 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
} }
} }
private void handleFallOnGround(GameSession session, GameEntity entity, MotionState motionState) {
// If not received immediately after MOTION_LAND_SPEED, discard this packet.
int maxDelay = 200;
long actualDelay = System.currentTimeMillis() - cachedLandingTimeMillisecond;
Grasscutter.getLogger().debug("MOTION_FALL_ON_GROUND received after " + actualDelay + "/" + maxDelay + "ms." + (actualDelay > maxDelay ? " Discard" : ""));
if (actualDelay > maxDelay) {
return;
}
float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
float damage = 0;
if (cachedLandingSpeed < -23.5) {
damage = (float) (maxHP * 0.33);
}
if (cachedLandingSpeed < -25) {
damage = (float) (maxHP * 0.5);
}
if (cachedLandingSpeed < -26.5) {
damage = (float) (maxHP * 0.66);
}
if (cachedLandingSpeed < -28) {
damage = (maxHP * 1);
}
float newHP = currentHP - damage;
if (newHP < 0) {
newHP = 0;
}
Grasscutter.getLogger().debug(currentHP + "/" + maxHP + "\t" + "\tDamage: " + damage + "\tnewHP: " + newHP);
entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP);
entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
if (newHP == 0) {
session.getPlayer().getMovementManager().killAvatar(session, entity, PlayerDieTypeOuterClass.PlayerDieType.PLAYER_DIE_FALL);
}
cachedLandingSpeed = 0;
}
} }
package emu.grasscutter.server.packet.recv; package emu.grasscutter.server.packet.recv;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
...@@ -15,10 +14,7 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler { ...@@ -15,10 +14,7 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler {
EvtDoSkillSuccNotify notify = EvtDoSkillSuccNotify.parseFrom(payload); EvtDoSkillSuccNotify notify = EvtDoSkillSuccNotify.parseFrom(payload);
// TODO: Will be used for deducting stamina for charged skills. // TODO: Will be used for deducting stamina for charged skills.
int caster = notify.getCasterId(); session.getPlayer().getMovementManager().handleEvtDoSkillSuccNotify(session, notify);
int skillId = notify.getSkillId();
session.getPlayer().getMovementManager().notifySkill(caster, skillId);
} }
} }
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment