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 { ...@@ -10,10 +10,10 @@ public enum ConsumptionType {
SPRINT(-1800), SPRINT(-1800),
DASH(-360), DASH(-360),
FLY(-60), FLY(-60),
SWIM_DASH_START(-200), SWIM_DASH_START(-20),
SWIM_DASH(-200), SWIM_DASH(-204),
SWIMMING(-80), SWIMMING(-80), // TODO: Slow swimming is handled per movement, not per second. Movement frequency depends on gender/age/height.
FIGHT(0), FIGHT(0), // See StaminaManager.getFightConsumption()
// restore // restore
STANDBY(500), 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,13 +33,9 @@ public class StaminaManager { ...@@ -33,13 +33,9 @@ public class StaminaManager {
private GameSession cachedSession = null; private GameSession cachedSession = null;
private GameEntity cachedEntity = null; private GameEntity cachedEntity = null;
private int staminaRecoverDelay = 0; private int staminaRecoverDelay = 0;
private boolean isInSkillMove = false;
public boolean getIsInSkillMove() { private HashMap<String, BeforeUpdateStaminaListener> beforeUpdateStaminaListeners = new HashMap<>();
return isInSkillMove; private HashMap<String, AfterUpdateStaminaListener> afterUpdateStaminaListeners = new HashMap<>();
}
public void setIsInSkillMove(boolean b) {
isInSkillMove = b;
}
public StaminaManager(Player player) { public StaminaManager(Player player) {
this.player = player; this.player = player;
...@@ -91,36 +87,116 @@ public class StaminaManager { ...@@ -91,36 +87,116 @@ public class StaminaManager {
MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList( 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() { 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 + Grasscutter.getLogger().trace("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates +
", " + diffX + ", " + diffY + ", " + diffZ); ", " + diffX + ", " + diffY + ", " + diffZ);
return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3; return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3;
} }
// Returns new stamina and sends PlayerPropNotify public int updateStaminaRelative(GameSession session, Consumption consumption) {
public int updateStamina(GameSession session, Consumption consumption) {
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
if (consumption.amount == 0) { if (consumption.amount == 0) {
return currentStamina; 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); 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 + "," + (isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.consumptionType + "," +
consumption.amount + ")"); consumption.amount + ")");
int newStamina = currentStamina + consumption.amount; int newStamina = currentStamina + consumption.amount;
if (newStamina < 0) { if (newStamina < 0) {
newStamina = 0; newStamina = 0;
} else if (newStamina > playerMaxStamina) {
newStamina = playerMaxStamina;
}
return setStamina(session, consumption.consumptionType.toString(), newStamina);
} }
if (newStamina > playerMaxStamina) {
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; 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); 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));
// notify updated
for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) {
listener.getValue().onAfterUpdateStamina(reason, newStamina);
}
return newStamina; return newStamina;
} }
...@@ -141,7 +217,7 @@ public class StaminaManager { ...@@ -141,7 +217,7 @@ public class StaminaManager {
if (!player.isPaused() && sustainedStaminaHandlerTimer == null) { if (!player.isPaused() && sustainedStaminaHandlerTimer == null) {
sustainedStaminaHandlerTimer = new Timer(); sustainedStaminaHandlerTimer = new Timer();
sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200); 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 { ...@@ -149,7 +225,7 @@ public class StaminaManager {
if (sustainedStaminaHandlerTimer != null) { if (sustainedStaminaHandlerTimer != null) {
sustainedStaminaHandlerTimer.cancel(); sustainedStaminaHandlerTimer.cancel();
sustainedStaminaHandlerTimer = null; sustainedStaminaHandlerTimer = null;
// Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped"); Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
} }
} }
...@@ -188,17 +264,17 @@ public class StaminaManager { ...@@ -188,17 +264,17 @@ public class StaminaManager {
switch (motionState) { switch (motionState) {
case MOTION_DASH_BEFORE_SHAKE: case MOTION_DASH_BEFORE_SHAKE:
if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) { if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) {
updateStamina(session, new Consumption(ConsumptionType.SPRINT)); updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT));
} }
break; break;
case MOTION_CLIMB_JUMP: case MOTION_CLIMB_JUMP:
if (previousState != MotionState.MOTION_CLIMB_JUMP) { if (previousState != MotionState.MOTION_CLIMB_JUMP) {
updateStamina(session, new Consumption(ConsumptionType.CLIMB_JUMP)); updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP));
} }
break; break;
case MOTION_SWIM_DASH: case MOTION_SWIM_DASH:
if (previousState != MotionState.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; break;
} }
...@@ -206,7 +282,7 @@ public class StaminaManager { ...@@ -206,7 +282,7 @@ public class StaminaManager {
private void handleImmediateStamina(GameSession session, EvtDoSkillSuccNotify notify) { private void handleImmediateStamina(GameSession session, EvtDoSkillSuccNotify notify) {
Consumption consumption = getFightConsumption(notify.getSkillId()); Consumption consumption = getFightConsumption(notify.getSkillId());
updateStamina(session, consumption); updateStaminaRelative(session, consumption);
} }
private class SustainedStaminaHandler extends TimerTask { private class SustainedStaminaHandler extends TimerTask {
...@@ -216,10 +292,10 @@ public class StaminaManager { ...@@ -216,10 +292,10 @@ public class StaminaManager {
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
if (moving || (currentStamina < maxStamina)) { if (moving || (currentStamina < maxStamina)) {
Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " + Grasscutter.getLogger().trace("Player moving: " + moving + ", stamina full: " +
(currentStamina >= maxStamina) + ", recalculate stamina"); (currentStamina >= maxStamina) + ", recalculate stamina");
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption(ConsumptionType.None);
if (!isInSkillMove) {
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
consumption = getClimbSustainedConsumption(); consumption = getClimbSustainedConsumption();
} else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) {
...@@ -231,7 +307,15 @@ public class StaminaManager { ...@@ -231,7 +307,15 @@ public class StaminaManager {
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
consumption = getStandSustainedConsumption(); 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 (cachedSession != null) {
if (consumption.amount < 0) { if (consumption.amount < 0) {
staminaRecoverDelay = 0; staminaRecoverDelay = 0;
...@@ -241,12 +325,12 @@ public class StaminaManager { ...@@ -241,12 +325,12 @@ public class StaminaManager {
if (staminaRecoverDelay < 10) { if (staminaRecoverDelay < 10) {
// For others recover after 2 seconds (10 ticks) - as official server does. // For others recover after 2 seconds (10 ticks) - as official server does.
staminaRecoverDelay++; 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; previousState = currentState;
...@@ -261,10 +345,9 @@ public class StaminaManager { ...@@ -261,10 +345,9 @@ public class StaminaManager {
private void handleDrowning() { private void handleDrowning() {
int stamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); int stamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
if (stamina < 10) { if (stamina < 10) {
boolean isSwimming = MotionStatesCategorized.get("SWIM").contains(currentState); Grasscutter.getLogger().trace(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" +
Grasscutter.getLogger().debug(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" + player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState);
player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState + "\t" + isSwimming); if (currentState != MotionState.MOTION_SWIM_IDLE) {
if (isSwimming && currentState != MotionState.MOTION_SWIM_IDLE) {
killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN); killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN);
} }
} }
...@@ -272,7 +355,33 @@ public class StaminaManager { ...@@ -272,7 +355,33 @@ public class StaminaManager {
// Consumption Calculators // Consumption Calculators
// Stamina Consumption Reduction: https://genshin-impact.fandom.com/wiki/Stamina
private Consumption getFightConsumption(int skillCasting) { 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); Consumption consumption = new Consumption(ConsumptionType.None);
HashMap<Integer, Integer> fightingCost = new HashMap<>() {{ HashMap<Integer, Integer> fightingCost = new HashMap<>() {{
put(10013, -1000); // Kamisato Ayaka put(10013, -1000); // Kamisato Ayaka
...@@ -292,10 +401,12 @@ public class StaminaManager { ...@@ -292,10 +401,12 @@ public class StaminaManager {
consumption = new Consumption(ConsumptionType.CLIMB_START); consumption = new Consumption(ConsumptionType.CLIMB_START);
} }
} }
// TODO: Foods
return consumption; return consumption;
} }
private Consumption getSwimSustainedConsumptions() { private Consumption getSwimSustainedConsumptions() {
handleDrowning();
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);
...@@ -310,6 +421,7 @@ public class StaminaManager { ...@@ -310,6 +421,7 @@ public class StaminaManager {
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_DASH) { if (currentState == MotionState.MOTION_DASH) {
consumption = new Consumption(ConsumptionType.DASH); consumption = new Consumption(ConsumptionType.DASH);
// TODO: Foods
} }
if (currentState == MotionState.MOTION_RUN) { if (currentState == MotionState.MOTION_RUN) {
consumption = new Consumption(ConsumptionType.RUN); consumption = new Consumption(ConsumptionType.RUN);
...@@ -321,7 +433,12 @@ public class StaminaManager { ...@@ -321,7 +433,12 @@ public class StaminaManager {
} }
private Consumption getFlySustainedConsumption() { 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); Consumption consumption = new Consumption(ConsumptionType.FLY);
// Talent
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
...@@ -330,15 +447,15 @@ public class StaminaManager { ...@@ -330,15 +447,15 @@ public class StaminaManager {
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); 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);
} }
consumption.amount *= reduction;
// TODO: Foods
return consumption; return consumption;
} }
......
...@@ -49,22 +49,23 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { ...@@ -49,22 +49,23 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
session.getPlayer().getStaminaManager().handleCombatInvocationsNotify(session, moveInfo, entity); session.getPlayer().getStaminaManager().handleCombatInvocationsNotify(session, moveInfo, entity);
// TODO: handle MOTION_FIGHT landing // TODO: handle MOTION_FIGHT landing which has a different damage factor
// For plunge attacks, LAND_SPEED is always -30 and is not useful. // Also, for plunge attacks, LAND_SPEED is always -30 and is not useful.
// May need the height when starting plunge attack. // 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 (monitorLandingEvent) {
if (motionState == MotionState.MOTION_FALL_ON_GROUND) { if (motionState == MotionState.MOTION_FALL_ON_GROUND) {
monitorLandingEvent = false; monitorLandingEvent = false;
handleFallOnGround(session, entity, motionState); 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:
...@@ -84,33 +85,42 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { ...@@ -84,33 +85,42 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
} }
private void handleFallOnGround(GameSession session, GameEntity entity, MotionState motionState) { 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; int maxDelay = 200;
long actualDelay = System.currentTimeMillis() - cachedLandingTimeMillisecond; 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) { if (actualDelay > maxDelay) {
return; return;
} }
float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
float damage = 0; float damageFactor = 0;
if (cachedLandingSpeed < -23.5) { if (cachedLandingSpeed < -23.5) {
damage = (float) (maxHP * 0.33); damageFactor = 0.33f;
} }
if (cachedLandingSpeed < -25) { if (cachedLandingSpeed < -25) {
damage = (float) (maxHP * 0.5); damageFactor = 0.5f;
} }
if (cachedLandingSpeed < -26.5) { if (cachedLandingSpeed < -26.5) {
damage = (float) (maxHP * 0.66); damageFactor = 0.66f;
} }
if (cachedLandingSpeed < -28) { if (cachedLandingSpeed < -28) {
damage = (maxHP * 1); damageFactor = 1f;
} }
float damage = maxHP * damageFactor;
float newHP = currentHP - damage; float newHP = currentHP - damage;
if (newHP < 0) { if (newHP < 0) {
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.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP);
entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
if (newHP == 0) { 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