Skip to content
Snippets Groups Projects
Commit d7834852 authored by gentlespoon's avatar gentlespoon Committed by Melledy
Browse files

Update StaminaManager

parent a09723f0
Branches
Tags
No related merge requests found
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);
}
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);
}
\ No newline at end of file
......@@ -10,10 +10,10 @@ public enum ConsumptionType {
SPRINT(-1800),
DASH(-360),
FLY(-60),
SWIM_DASH_START(-200),
SWIM_DASH(-200),
SWIMMING(-80),
FIGHT(0),
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),
......
# 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) {
// ...
}
}
```
\ No newline at end of file
......@@ -33,94 +33,170 @@ public class StaminaManager {
private GameSession cachedSession = null;
private GameEntity cachedEntity = null;
private int staminaRecoverDelay = 0;
private boolean isInSkillMove = false;
public boolean getIsInSkillMove() {
return isInSkillMove;
}
public void setIsInSkillMove(boolean b) {
isInSkillMove = b;
}
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
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
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
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
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
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
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().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates +
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;
}
// Returns new stamina and sends PlayerPropNotify
public int updateStamina(GameSession session, Consumption consumption) {
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().debug(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" +
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;
}
if (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;
}
......@@ -141,7 +217,7 @@ public class StaminaManager {
if (!player.isPaused() && sustainedStaminaHandlerTimer == null) {
sustainedStaminaHandlerTimer = new Timer();
sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200);
// Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started");
Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started");
}
}
......@@ -149,7 +225,7 @@ public class StaminaManager {
if (sustainedStaminaHandlerTimer != null) {
sustainedStaminaHandlerTimer.cancel();
sustainedStaminaHandlerTimer = null;
// Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
}
}
......@@ -188,17 +264,17 @@ public class StaminaManager {
switch (motionState) {
case MOTION_DASH_BEFORE_SHAKE:
if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) {
updateStamina(session, new Consumption(ConsumptionType.SPRINT));
updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT));
}
break;
case MOTION_CLIMB_JUMP:
if (previousState != MotionState.MOTION_CLIMB_JUMP) {
updateStamina(session, new Consumption(ConsumptionType.CLIMB_JUMP));
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP));
}
break;
case MOTION_SWIM_DASH:
if (previousState != MotionState.MOTION_SWIM_DASH) {
updateStamina(session, new Consumption(ConsumptionType.SWIM_DASH_START));
updateStaminaRelative(session, new Consumption(ConsumptionType.SWIM_DASH_START));
}
break;
}
......@@ -206,7 +282,7 @@ public class StaminaManager {
private void handleImmediateStamina(GameSession session, EvtDoSkillSuccNotify notify) {
Consumption consumption = getFightConsumption(notify.getSkillId());
updateStamina(session, consumption);
updateStaminaRelative(session, consumption);
}
private class SustainedStaminaHandler extends TimerTask {
......@@ -216,22 +292,30 @@ public class StaminaManager {
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
if (moving || (currentStamina < maxStamina)) {
Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " +
Grasscutter.getLogger().trace("Player moving: " + moving + ", stamina full: " +
(currentStamina >= maxStamina) + ", recalculate stamina");
Consumption consumption = new Consumption(ConsumptionType.None);
if (!isInSkillMove) {
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();
}
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;
......@@ -241,12 +325,12 @@ public class StaminaManager {
if (staminaRecoverDelay < 10) {
// For others recover after 2 seconds (10 ticks) - as official server does.
staminaRecoverDelay++;
consumption = new Consumption(ConsumptionType.None);
consumption.amount = 0;
Grasscutter.getLogger().trace("[StaminaManager] Delaying recovery: " + staminaRecoverDelay);
}
}
updateStamina(cachedSession, consumption);
updateStaminaRelative(cachedSession, consumption);
}
handleDrowning();
}
}
previousState = currentState;
......@@ -261,10 +345,9 @@ public class StaminaManager {
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) {
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);
}
}
......@@ -272,7 +355,33 @@ public class StaminaManager {
// 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
......@@ -292,10 +401,12 @@ public class StaminaManager {
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);
......@@ -310,6 +421,7 @@ public class StaminaManager {
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);
......@@ -321,7 +433,12 @@ public class StaminaManager {
}
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
......@@ -330,15 +447,15 @@ public class StaminaManager {
for (EntityAvatar entity : cachedSession.getPlayer().getTeamManager().getActiveTeam()) {
for (int skillId : entity.getAvatar().getProudSkillList()) {
if (glidingCostReduction.containsKey(skillId)) {
reduction = glidingCostReduction.get(skillId);
float potentialLowerReduction = glidingCostReduction.get(skillId);
if (potentialLowerReduction < reduction) {
reduction = potentialLowerReduction;
}
}
}
}
consumption.amount *= reduction;
// POWERED_FLY, e.g. wind tunnel
if (currentState == MotionState.MOTION_POWERED_FLY) {
consumption = new Consumption(ConsumptionType.POWERED_FLY);
}
// TODO: Foods
return consumption;
}
......
......@@ -49,22 +49,23 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
session.getPlayer().getStaminaManager().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.
// TODO: handle MOTION_FIGHT landing which has a different damage factor
// Also, for plunge attacks, LAND_SPEED is always -30 and is not useful.
// May need the height when starting plunge attack.
// MOTION_LAND_SPEED and MOTION_FALL_ON_GROUND arrive in different packets.
// Cache land speed for later use.
if (motionState == MotionState.MOTION_LAND_SPEED) {
cachedLandingSpeed = motionInfo.getSpeed().getY();
cachedLandingTimeMillisecond = System.currentTimeMillis();
monitorLandingEvent = true;
}
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;
default:
......@@ -84,33 +85,42 @@ 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.
// People have reported that after plunge attack (client sends a FIGHT instead of FALL_ON_GROUND) they will die
// if they talk to an NPC (this is when the client sends a FALL_ON_GROUND) without jumping again.
// A dirty patch: if not received immediately after MOTION_LAND_SPEED, discard this packet.
// 200ms seems to be a reasonable delay.
int maxDelay = 200;
long actualDelay = System.currentTimeMillis() - cachedLandingTimeMillisecond;
Grasscutter.getLogger().debug("MOTION_FALL_ON_GROUND received after " + actualDelay + "/" + maxDelay + "ms." + (actualDelay > maxDelay ? " Discard" : ""));
Grasscutter.getLogger().trace("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;
float damageFactor = 0;
if (cachedLandingSpeed < -23.5) {
damage = (float) (maxHP * 0.33);
damageFactor = 0.33f;
}
if (cachedLandingSpeed < -25) {
damage = (float) (maxHP * 0.5);
damageFactor = 0.5f;
}
if (cachedLandingSpeed < -26.5) {
damage = (float) (maxHP * 0.66);
damageFactor = 0.66f;
}
if (cachedLandingSpeed < -28) {
damage = (maxHP * 1);
damageFactor = 1f;
}
float damage = maxHP * damageFactor;
float newHP = currentHP - damage;
if (newHP < 0) {
newHP = 0;
}
Grasscutter.getLogger().debug(currentHP + "/" + maxHP + "\t" + "\tDamage: " + damage + "\tnewHP: " + newHP);
if (damageFactor > 0) {
Grasscutter.getLogger().debug(currentHP + "/" + maxHP + "\tLandingSpeed: " + cachedLandingSpeed +
"\tDamageFactor: " + damageFactor + "\tDamage: " + damage + "\tNewHP: " + newHP);
} else {
Grasscutter.getLogger().trace(currentHP + "/" + maxHP + "\tLandingSpeed: 0\tNo damage");
}
entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP);
entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
if (newHP == 0) {
......
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