Commit d7834852 authored by gentlespoon's avatar gentlespoon Committed by Melledy
Browse files

Update StaminaManager

parent a09723f0
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) {
......
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