Commit 2a3708ee authored by gentlespoon's avatar gentlespoon Committed by Melledy
Browse files

Talent moving stamina cost

parent 99dbac8a
...@@ -2,27 +2,22 @@ package emu.grasscutter.game.ability; ...@@ -2,27 +2,22 @@ package emu.grasscutter.game.ability;
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.InvalidProtocolBufferException;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.custom.AbilityModifier;
import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction; import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierActionType;
import emu.grasscutter.data.def.ItemData; import emu.grasscutter.data.def.ItemData;
import emu.grasscutter.data.custom.AbilityModifierEntry; import emu.grasscutter.data.custom.AbilityModifierEntry;
import emu.grasscutter.game.entity.EntityItem; import emu.grasscutter.game.entity.EntityItem;
import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.proto.AbilityActionGenerateElemBallOuterClass.AbilityActionGenerateElemBall; import emu.grasscutter.net.proto.AbilityActionGenerateElemBallOuterClass.AbilityActionGenerateElemBall;
import emu.grasscutter.net.proto.AbilityInvokeArgumentOuterClass.AbilityInvokeArgument;
import emu.grasscutter.net.proto.AbilityInvokeEntryHeadOuterClass.AbilityInvokeEntryHead; import emu.grasscutter.net.proto.AbilityInvokeEntryHeadOuterClass.AbilityInvokeEntryHead;
import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry;
import emu.grasscutter.net.proto.AbilityMetaModifierChangeOuterClass.AbilityMetaModifierChange; import emu.grasscutter.net.proto.AbilityMetaModifierChangeOuterClass.AbilityMetaModifierChange;
import emu.grasscutter.net.proto.AbilityMetaReInitOverrideMapOuterClass.AbilityMetaReInitOverrideMap; import emu.grasscutter.net.proto.AbilityMetaReInitOverrideMapOuterClass.AbilityMetaReInitOverrideMap;
import emu.grasscutter.net.proto.AbilityScalarTypeOuterClass.AbilityScalarType; import emu.grasscutter.net.proto.AbilityMixinCostStaminaOuterClass.AbilityMixinCostStamina;
import emu.grasscutter.net.proto.AbilityScalarValueEntryOuterClass.AbilityScalarValueEntry; import emu.grasscutter.net.proto.AbilityScalarValueEntryOuterClass.AbilityScalarValueEntry;
import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction; import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction;
import emu.grasscutter.utils.Position; import emu.grasscutter.utils.Position;
import emu.grasscutter.utils.Utils;
public class AbilityManager { public class AbilityManager {
private Player player; private Player player;
...@@ -138,13 +133,9 @@ public class AbilityManager { ...@@ -138,13 +133,9 @@ public class AbilityManager {
} }
} }
private void handleMixinCostStamina(AbilityInvokeEntry invoke) { private void handleMixinCostStamina(AbilityInvokeEntry invoke) throws InvalidProtocolBufferException {
// Not the right way of doing this AbilityMixinCostStamina costStamina = AbilityMixinCostStamina.parseFrom((invoke.getAbilityData()));
if (Grasscutter.getConfig().OpenStamina) { getPlayer().getStaminaManager().handleMixinCostStamina(costStamina.getIsSwim());
// getPlayer().getStaminaManager().updateStamina(getPlayer().getSession(), -450);
// TODO
// set flag in stamina/movement manager that specifies the player is currently using an alternate sprint
}
} }
private void handleGenerateElemBall(AbilityInvokeEntry invoke) throws InvalidProtocolBufferException { private void handleGenerateElemBall(AbilityInvokeEntry invoke) throws InvalidProtocolBufferException {
......
package emu.grasscutter.game.managers.StaminaManager; package emu.grasscutter.game.managers.StaminaManager;
public class Consumption { public class Consumption {
public ConsumptionType consumptionType; public ConsumptionType type = ConsumptionType.None;
public int amount; public int amount = 0;
public Consumption(ConsumptionType ct, int a) { public Consumption(ConsumptionType type, int amount) {
consumptionType = ct; this.type = type;
amount = a; this.amount = amount;
} }
public Consumption(ConsumptionType ct) { public Consumption(ConsumptionType type) {
this(ct, ct.amount); this(type, type.amount);
}
public Consumption() {
} }
} }
...@@ -4,23 +4,29 @@ public enum ConsumptionType { ...@@ -4,23 +4,29 @@ public enum ConsumptionType {
None(0), None(0),
// consume // consume
CLIMB_START(-500),
CLIMBING(-150), CLIMBING(-150),
CLIMB_START(-500),
CLIMB_JUMP(-2500), CLIMB_JUMP(-2500),
SPRINT(-1800),
DASH(-360), DASH(-360),
FIGHT(0), // See StaminaManager.getFightConsumption()
FLY(-60), FLY(-60),
// Slow swimming is handled per movement, not per second.
// Arm movement frequency depends on gender/age/height.
// TODO: Instead of cost -80 per tick, find a proper way to calculate cost.
SKIFF(-300), // TODO: Get real value
SPRINT(-1800),
SWIM_DASH_START(-20), SWIM_DASH_START(-20),
SWIM_DASH(-204), SWIM_DASH(-204), // -10.2 per second, 5Hz = -204 each tick
SWIMMING(-80), // TODO: Slow swimming is handled per movement, not per second. Movement frequency depends on gender/age/height. SWIMMING(-80),
FIGHT(0), // See StaminaManager.getFightConsumption() TALENT_DASH(-300), // -1500 per second, 5Hz = -300 each tick
TALENT_DASH_START(-1000),
// restore // restore
STANDBY(500), POWERED_FLY(500), // TODO: Get real value
POWERED_SKIFF(2000), // TODO: Get real value
RUN(500), RUN(500),
WALK(500), STANDBY(500),
STANDBY_MOVE(500), WALK(500);
POWERED_FLY(500);
public final int amount; public final int amount;
......
...@@ -8,7 +8,6 @@ import emu.grasscutter.game.props.FightProperty; ...@@ -8,7 +8,6 @@ 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.EntityMoveInfo; 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;
...@@ -16,13 +15,94 @@ import emu.grasscutter.net.proto.VectorOuterClass.Vector; ...@@ -16,13 +15,94 @@ 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 StaminaManager { public class StaminaManager {
// TODO: Skiff state detection?
private final Player player; private final Player player;
private HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>(); private final HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>() {{
put("CLIMB", new HashSet<>(List.of(
MotionState.MOTION_CLIMB, // sustained, when not moving no cost no recover
MotionState.MOTION_STANDBY_TO_CLIMB // NOT OBSERVED, see MOTION_JUMP_UP_WALL_FOR_STANDBY
)));
put("DASH", new HashSet<>(List.of(
MotionState.MOTION_DANGER_DASH, // sustained
MotionState.MOTION_DASH // sustained
)));
put("FLY", new HashSet<>(List.of(
MotionState.MOTION_FLY, // sustained
MotionState.MOTION_FLY_FAST, // sustained
MotionState.MOTION_FLY_SLOW, // sustained
MotionState.MOTION_POWERED_FLY // sustained, recover
)));
put("RUN", new HashSet<>(List.of(
MotionState.MOTION_DANGER_RUN, // sustained, recover
MotionState.MOTION_RUN // sustained, recover
)));
put("SKIFF", new HashSet<>(List.of(
MotionState.MOTION_SKIFF_BOARDING, // NOT OBSERVED even when boarding
MotionState.MOTION_SKIFF_DASH, // NOT OBSERVED even when dashing
MotionState.MOTION_SKIFF_NORMAL, // sustained, OBSERVED when both normal and dashing
MotionState.MOTION_SKIFF_POWERED_DASH // sustained, recover
)));
put("STANDBY", new HashSet<>(List.of(
MotionState.MOTION_DANGER_STANDBY_MOVE, // sustained, recover
MotionState.MOTION_DANGER_STANDBY, // sustained, recover
MotionState.MOTION_LADDER_TO_STANDBY, // NOT OBSERVED
MotionState.MOTION_STANDBY_MOVE, // sustained, recover
MotionState.MOTION_STANDBY // sustained, recover
)));
put("SWIM", new HashSet<>(List.of(
MotionState.MOTION_SWIM_IDLE, // sustained
MotionState.MOTION_SWIM_DASH, // immediate and sustained
MotionState.MOTION_SWIM_JUMP, // NOT OBSERVED
MotionState.MOTION_SWIM_MOVE // sustained
)));
put("WALK", new HashSet<>(List.of(
MotionState.MOTION_DANGER_WALK, // sustained, recover
MotionState.MOTION_WALK // sustained, recover
)));
put("OTHER", new HashSet<>(List.of(
MotionState.MOTION_CLIMB_JUMP, // cost only once if repeated without switching state
MotionState.MOTION_DASH_BEFORE_SHAKE, // immediate one time sprint charge.
MotionState.MOTION_FIGHT, // immediate, if sustained then subsequent will be MOTION_NOTIFY
MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY, // immediate, observed when RUN/WALK->CLIMB
MotionState.MOTION_NOTIFY, // can be either cost or recover - check previous state and check skill casting
MotionState.MOTION_SIT_IDLE, // sustained, recover
MotionState.MOTION_JUMP // recover
)));
put("NOCOST_NORECOVER", new HashSet<>(List.of(
MotionState.MOTION_LADDER_SLIP, // NOT OBSERVED
MotionState.MOTION_SLIP, // sustained, no cost no recover
MotionState.MOTION_FLY_IDLE // NOT OBSERVED
)));
put("IGNORE", new HashSet<>(List.of(
// these states have no impact on stamina
MotionState.MOTION_CROUCH_IDLE,
MotionState.MOTION_CROUCH_MOVE,
MotionState.MOTION_CROUCH_ROLL,
MotionState.MOTION_DESTROY_VEHICLE,
MotionState.MOTION_FALL_ON_GROUND,
MotionState.MOTION_FOLLOW_ROUTE,
MotionState.MOTION_FORCE_SET_POS,
MotionState.MOTION_GO_UPSTAIRS,
MotionState.MOTION_JUMP_OFF_WALL,
MotionState.MOTION_LADDER_IDLE,
MotionState.MOTION_LADDER_MOVE,
MotionState.MOTION_LAND_SPEED,
MotionState.MOTION_MOVE_FAIL_ACK,
MotionState.MOTION_NONE,
MotionState.MOTION_NUM,
MotionState.MOTION_QUEST_FORCE_DRAG,
MotionState.MOTION_RESET,
MotionState.MOTION_STANDBY_TO_LADDER,
MotionState.MOTION_WATERFALL
)));
}};
public final static int GlobalMaximumStamina = 24000; public final static int GlobalMaximumStamina = 24000;
private Position currentCoordinates = new Position(0, 0, 0); private Position currentCoordinates = new Position(0, 0, 0);
...@@ -33,71 +113,24 @@ public class StaminaManager { ...@@ -33,71 +113,24 @@ 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 final HashMap<String, BeforeUpdateStaminaListener> beforeUpdateStaminaListeners = new HashMap<>();
private final HashMap<String, AfterUpdateStaminaListener> afterUpdateStaminaListeners = new HashMap<>();
private int lastSkillId = 0;
private int lastSkillCasterId = 0;
private boolean lastSkillFirstTick = true;
private HashMap<String, BeforeUpdateStaminaListener> beforeUpdateStaminaListeners = new HashMap<>();
private HashMap<String, AfterUpdateStaminaListener> afterUpdateStaminaListeners = new HashMap<>();
public StaminaManager(Player player) { public StaminaManager(Player player) {
this.player = player; this.player = player;
}
MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList( // Accessors
MotionState.MOTION_SWIM_MOVE,
MotionState.MOTION_SWIM_IDLE,
MotionState.MOTION_SWIM_DASH,
MotionState.MOTION_SWIM_JUMP
)));
MotionStatesCategorized.put("STANDBY", new HashSet<>(Arrays.asList(
MotionState.MOTION_STANDBY,
MotionState.MOTION_STANDBY_MOVE,
MotionState.MOTION_DANGER_STANDBY,
MotionState.MOTION_DANGER_STANDBY_MOVE,
MotionState.MOTION_LADDER_TO_STANDBY,
MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY
)));
MotionStatesCategorized.put("CLIMB", new HashSet<>(Arrays.asList(
MotionState.MOTION_CLIMB,
MotionState.MOTION_CLIMB_JUMP,
MotionState.MOTION_STANDBY_TO_CLIMB,
MotionState.MOTION_LADDER_IDLE,
MotionState.MOTION_LADDER_MOVE,
MotionState.MOTION_LADDER_SLIP,
MotionState.MOTION_STANDBY_TO_LADDER
)));
MotionStatesCategorized.put("FLY", new HashSet<>(Arrays.asList(
MotionState.MOTION_FLY,
MotionState.MOTION_FLY_IDLE,
MotionState.MOTION_FLY_SLOW,
MotionState.MOTION_FLY_FAST,
MotionState.MOTION_POWERED_FLY
)));
MotionStatesCategorized.put("RUN", new HashSet<>(Arrays.asList(
MotionState.MOTION_DASH,
MotionState.MOTION_DANGER_DASH,
MotionState.MOTION_DASH_BEFORE_SHAKE,
MotionState.MOTION_RUN,
MotionState.MOTION_DANGER_RUN,
MotionState.MOTION_WALK,
MotionState.MOTION_DANGER_WALK
)));
MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList(
MotionState.MOTION_FIGHT
)));
MotionStatesCategorized.put("SKIFF", new HashSet<>(Arrays.asList( public void setSkillCast(int skillId, int skillCasterId) {
MotionState.MOTION_SKIFF_BOARDING, lastSkillId = skillId;
MotionState.MOTION_SKIFF_NORMAL, lastSkillCasterId = skillCasterId;
MotionState.MOTION_SKIFF_DASH,
MotionState.MOTION_SKIFF_POWERED_DASH
)));
} }
// Listeners
public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) { public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) {
if (beforeUpdateStaminaListeners.containsKey(listenerName)) { if (beforeUpdateStaminaListeners.containsKey(listenerName)) {
return false; return false;
...@@ -146,17 +179,17 @@ public class StaminaManager { ...@@ -146,17 +179,17 @@ public class StaminaManager {
} }
// notify will update // notify will update
for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) { for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) {
Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.consumptionType.toString(), consumption); Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.type.toString(), consumption);
if ((overriddenConsumption.consumptionType != consumption.consumptionType) && (overriddenConsumption.amount != consumption.amount)) { if ((overriddenConsumption.type != consumption.type) && (overriddenConsumption.amount != consumption.amount)) {
Grasscutter.getLogger().debug("[StaminaManager] Stamina update relative(" + Grasscutter.getLogger().debug("[StaminaManager] Stamina update relative(" +
consumption.consumptionType.toString() + ", " + consumption.amount + ") overridden to relative(" + consumption.type.toString() + ", " + consumption.amount + ") overridden to relative(" +
consumption.consumptionType.toString() + ", " + consumption.amount + ") by: " + listener.getKey()); consumption.type.toString() + ", " + consumption.amount + ") by: " + listener.getKey());
return currentStamina; return currentStamina;
} }
} }
int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA); int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
Grasscutter.getLogger().trace(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" + Grasscutter.getLogger().trace(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" +
(isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.consumptionType + "," + (isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.type + "," +
consumption.amount + ")"); consumption.amount + ")");
int newStamina = currentStamina + consumption.amount; int newStamina = currentStamina + consumption.amount;
if (newStamina < 0) { if (newStamina < 0) {
...@@ -164,7 +197,7 @@ public class StaminaManager { ...@@ -164,7 +197,7 @@ public class StaminaManager {
} else if (newStamina > playerMaxStamina) { } else if (newStamina > playerMaxStamina) {
newStamina = playerMaxStamina; newStamina = playerMaxStamina;
} }
return setStamina(session, consumption.consumptionType.toString(), newStamina); return setStamina(session, consumption.type.toString(), newStamina);
} }
public int updateStaminaAbsolute(GameSession session, String reason, int newStamina) { public int updateStaminaAbsolute(GameSession session, String reason, int newStamina) {
...@@ -236,11 +269,24 @@ public class StaminaManager { ...@@ -236,11 +269,24 @@ public class StaminaManager {
// External trigger handler // External trigger handler
public void handleEvtDoSkillSuccNotify(GameSession session, EvtDoSkillSuccNotify notify) { public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) {
handleImmediateStamina(session, notify); // Ignore if skill not cast by not current active
if (casterId != player.getTeamManager().getCurrentAvatarEntity().getId()) {
return;
}
setSkillCast(skillId, casterId);
handleImmediateStamina(session, skillId);
} }
public void handleCombatInvocationsNotify(GameSession session, EntityMoveInfo moveInfo, GameEntity entity) { public void handleMixinCostStamina(boolean isSwim) {
// Talent moving and claymore avatar charged attack duration
// Grasscutter.getLogger().trace("abilityMixinCostStamina: isSwim: " + isSwim);
if (lastSkillCasterId == player.getTeamManager().getCurrentAvatarEntity().getId()) {
handleImmediateStamina(cachedSession, lastSkillId);
}
}
public void handleCombatInvocationsNotify(@NotNull GameSession session, @NotNull EntityMoveInfo moveInfo, @NotNull GameEntity entity) {
// cache info for later use in SustainedStaminaHandler tick // cache info for later use in SustainedStaminaHandler tick
cachedSession = session; cachedSession = session;
cachedEntity = entity; cachedEntity = entity;
...@@ -252,20 +298,25 @@ public class StaminaManager { ...@@ -252,20 +298,25 @@ public class StaminaManager {
return; return;
} }
currentState = motionState; currentState = motionState;
// Grasscutter.getLogger().trace("" + currentState);
Vector posVector = motionInfo.getPos(); Vector posVector = motionInfo.getPos();
Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ()); Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ());
if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) { if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) {
currentCoordinates = newPos; currentCoordinates = newPos;
} }
startSustainedStaminaHandler(); startSustainedStaminaHandler();
handleImmediateStamina(session, motionInfo, motionState, entity); handleImmediateStamina(session, motionState);
} }
// Internal handler // Internal handler
private void handleImmediateStamina(GameSession session, MotionInfo motionInfo, MotionState motionState, private void handleImmediateStamina(GameSession session, @NotNull MotionState motionState) {
GameEntity entity) {
switch (motionState) { switch (motionState) {
case MOTION_CLIMB:
if (currentState != MotionState.MOTION_CLIMB) {
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_START));
}
break;
case MOTION_DASH_BEFORE_SHAKE: case MOTION_DASH_BEFORE_SHAKE:
if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) { if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) {
updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT)); updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT));
...@@ -284,8 +335,10 @@ public class StaminaManager { ...@@ -284,8 +335,10 @@ public class StaminaManager {
} }
} }
private void handleImmediateStamina(GameSession session, EvtDoSkillSuccNotify notify) { private void handleImmediateStamina(GameSession session, int skillId) {
Consumption consumption = getFightConsumption(notify.getSkillId()); // Non-claymore avatar attacks
// TODO: differentiate charged vs normal attack
Consumption consumption = getFightConsumption(skillId);
updateStaminaRelative(session, consumption); updateStaminaRelative(session, consumption);
} }
...@@ -298,32 +351,45 @@ public class StaminaManager { ...@@ -298,32 +351,45 @@ public class StaminaManager {
Grasscutter.getLogger().trace("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;
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
consumption = getClimbSustainedConsumption(); consumption = getClimbConsumption();
} else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { } else if (MotionStatesCategorized.get("DASH").contains(currentState)) {
consumption = getSwimSustainedConsumptions(); consumption = getDashConsumption();
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
consumption = getRunWalkDashSustainedConsumption();
} else if (MotionStatesCategorized.get("FLY").contains(currentState)) { } else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
consumption = getFlySustainedConsumption(); consumption = getFlyConsumption();
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
consumption = new Consumption(ConsumptionType.RUN);
} else if (MotionStatesCategorized.get("SKIFF").contains(currentState)) {
consumption = getSkiffConsumption();
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
consumption = getStandSustainedConsumption(); consumption = new Consumption(ConsumptionType.STANDBY);
} else if (MotionStatesCategorized.get("SWIM").contains((currentState))) {
consumption = getSwimConsumptions();
} else if (MotionStatesCategorized.get("WALK").contains((currentState))) {
consumption = new Consumption(ConsumptionType.WALK);
} else if (MotionStatesCategorized.get("OTHER").contains((currentState))) {
consumption = getOtherConsumptions();
} else {
// ignore
return;
} }
if (consumption.amount < 0) {
/* /* Do not apply reduction factor when recovering stamina
TODO: Reductions that apply to all motion types: TODO: Reductions that apply to all motion types:
Elemental Resonance Elemental Resonance
Wind: -15% Wind: -15%
Skills Skills
Diona E: -10% while shield lasts Diona E: -10% while shield lasts - applies to SP+MP
Barbara E: -12% while lasts Barbara E: -12% while lasts - applies to SP+MP
*/ */
}
// Delay 2 seconds before starts recovering stamina
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.type != ConsumptionType.POWERED_FLY) {
// For POWERED_FLY recover immediately - things like Amber's gliding exam may require this. // 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. // For others recover after 2 seconds (10 ticks) - as official server does.
...@@ -384,91 +450,137 @@ public class StaminaManager { ...@@ -384,91 +450,137 @@ public class StaminaManager {
*/ */
// TODO: Currently only handling Ayaka and Mona's talent moving initial costs. // TODO: Currently only handling Ayaka and Mona's talent moving initial costs.
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption();
HashMap<Integer, Integer> fightingCost = new HashMap<>() {{
put(10013, -1000); // Kamisato Ayaka // Talent moving
put(10413, -1000); // Mona HashMap<Integer, List<Consumption>> talentMovementConsumptions = new HashMap<>() {{
// List[0] = initial cost, [1] = sustained cost. Sustained costs are divided by 3 per second as MixinStaminaCost is triggered at 3Hz.
put(10013, List.of(new Consumption(ConsumptionType.TALENT_DASH_START, -1000), new Consumption(ConsumptionType.TALENT_DASH, -500))); // Kamisato Ayaka
put(10413, List.of(new Consumption(ConsumptionType.TALENT_DASH_START, -1000), new Consumption(ConsumptionType.TALENT_DASH, -500))); // Mona
}}; }};
if (fightingCost.containsKey(skillCasting)) { if (talentMovementConsumptions.containsKey(skillCasting)) {
consumption = new Consumption(ConsumptionType.FIGHT, fightingCost.get(skillCasting)); if (lastSkillFirstTick) {
consumption = talentMovementConsumptions.get(skillCasting).get(1);
} else {
lastSkillFirstTick = false;
consumption = talentMovementConsumptions.get(skillCasting).get(0);
}
} }
// TODO: Claymore avatar charged attack
// HashMap<Integer, Integer> fightConsumptions = new HashMap<>();
// TODO: Non-claymore avatar charged attack
return consumption; return consumption;
} }
private Consumption getClimbSustainedConsumption() { private Consumption getClimbConsumption() {
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption();
if (currentState == MotionState.MOTION_CLIMB && isPlayerMoving()) { if (currentState == MotionState.MOTION_CLIMB && isPlayerMoving()) {
consumption = new Consumption(ConsumptionType.CLIMBING); consumption.type = ConsumptionType.CLIMBING;
if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) { consumption.amount = ConsumptionType.CLIMBING.amount;
consumption = new Consumption(ConsumptionType.CLIMB_START);
}
} }
// TODO: Foods // Climbing specific reductions
// TODO: create a food cost reduction map
HashMap<Integer, Float> foodReductionMap = new HashMap<>() {{
// TODO: get real talent id
put(0, 0.8f); // Sample food
}};
consumption.amount *= getFoodCostReductionFactor(foodReductionMap);
HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{
// TODO: get real talent id
put(0, 0.8f); // Xiao
}};
consumption.amount *= getTalentCostReductionFactor(talentReductionMap);
return consumption; return consumption;
} }
private Consumption getSwimSustainedConsumptions() { private Consumption getSwimConsumptions() {
handleDrowning(); handleDrowning();
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption();
if (currentState == MotionState.MOTION_SWIM_MOVE) { if (currentState == MotionState.MOTION_SWIM_MOVE) {
consumption = new Consumption(ConsumptionType.SWIMMING); consumption.type = ConsumptionType.SWIMMING;
consumption.amount = ConsumptionType.SWIMMING.amount;
} }
if (currentState == MotionState.MOTION_SWIM_DASH) { if (currentState == MotionState.MOTION_SWIM_DASH) {
consumption = new Consumption(ConsumptionType.SWIM_DASH); consumption.type = ConsumptionType.SWIM_DASH;
consumption.amount = ConsumptionType.SWIM_DASH.amount;
} }
// Reductions
HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{
// TODO: get real talent id
put(0, 0.8f); // Beidou
put(1, 0.8f); // Sangonomiya Kokomi
}};
consumption.amount *= getTalentCostReductionFactor(talentReductionMap);
return consumption; return consumption;
} }
private Consumption getRunWalkDashSustainedConsumption() { private Consumption getDashConsumption() {
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption();
if (currentState == MotionState.MOTION_DASH) { if (currentState == MotionState.MOTION_DASH) {
consumption = new Consumption(ConsumptionType.DASH); consumption.type = ConsumptionType.DASH;
// TODO: Foods consumption.amount = ConsumptionType.DASH.amount;
} // TODO: Dashing specific reductions
if (currentState == MotionState.MOTION_RUN) { // Foods:
consumption = new Consumption(ConsumptionType.RUN);
}
if (currentState == MotionState.MOTION_WALK) {
consumption = new Consumption(ConsumptionType.WALK);
} }
return consumption; return consumption;
} }
private Consumption getFlySustainedConsumption() { private Consumption getFlyConsumption() {
// POWERED_FLY, e.g. wind tunnel // POWERED_FLY, e.g. wind tunnel
if (currentState == MotionState.MOTION_POWERED_FLY) { if (currentState == MotionState.MOTION_POWERED_FLY) {
return new Consumption(ConsumptionType.POWERED_FLY); return new Consumption(ConsumptionType.POWERED_FLY);
} }
Consumption consumption = new Consumption(ConsumptionType.FLY); Consumption consumption = new Consumption(ConsumptionType.FLY);
// Talent // Passive Talents
HashMap<Integer, Float> glidingCostReduction = new HashMap<>() {{ HashMap<Integer, Float> talentReductionMap = new HashMap<>() {{
put(212301, 0.8f); // Amber put(212301, 0.8f); // Amber
put(222301, 0.8f); // Venti put(222301, 0.8f); // Venti
}}; }};
consumption.amount *= getTalentCostReductionFactor(talentReductionMap);
// TODO: Foods
return consumption;
}
private Consumption getSkiffConsumption() {
// POWERED_SKIFF, e.g. wind tunnel
if (currentState == MotionState.MOTION_SKIFF_POWERED_DASH) {
return new Consumption(ConsumptionType.POWERED_SKIFF);
}
Consumption consumption = new Consumption(ConsumptionType.SKIFF);
// No known reduction for skiffing.
return consumption;
}
private Consumption getOtherConsumptions() {
// TODO: Add logic
return new Consumption();
}
// Reduction getter
private float getTalentCostReductionFactor(HashMap<Integer, Float> talentReductionMap) {
// All known talents reductions are not stackable
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 (talentReductionMap.containsKey(skillId)) {
float potentialLowerReduction = glidingCostReduction.get(skillId); float potentialLowerReduction = talentReductionMap.get(skillId);
if (potentialLowerReduction < reduction) { if (potentialLowerReduction < reduction) {
reduction = potentialLowerReduction; reduction = potentialLowerReduction;
} }
} }
} }
} }
consumption.amount *= reduction; return reduction;
// TODO: Foods
return consumption;
} }
private Consumption getStandSustainedConsumption() { private float getFoodCostReductionFactor(HashMap<Integer, Float> foodReductionMap) {
Consumption consumption = new Consumption(ConsumptionType.None); // All known food reductions are not stackable
if (currentState == MotionState.MOTION_STANDBY) { // TODO: Check consumed food (buff?) and return proper factor
consumption = new Consumption(ConsumptionType.STANDBY); float reduction = 1;
} return reduction;
if (currentState == MotionState.MOTION_STANDBY_MOVE) {
consumption = new Consumption(ConsumptionType.STANDBY_MOVE);
}
return consumption;
} }
} }
...@@ -30,7 +30,7 @@ public enum PlayerProperty { ...@@ -30,7 +30,7 @@ public enum PlayerProperty {
// his gems and then got a money refund, so negative is allowed. // his gems and then got a money refund, so negative is allowed.
PROP_PLAYER_SCOIN (10016), // Mora [0, +inf) PROP_PLAYER_SCOIN (10016), // Mora [0, +inf)
PROP_PLAYER_MP_SETTING_TYPE (10017), // Do you allow other players to join your game? [0=no 1=direct 2=approval] PROP_PLAYER_MP_SETTING_TYPE (10017), // Do you allow other players to join your game? [0=no 1=direct 2=approval]
PROP_IS_MP_MODE_AVAILABLE (10018), // Are you not in a quest or something that disables MP? [0, 1] PROP_IS_MP_MODE_AVAILABLE (10018), // 0 if in quest or something that disables MP [0, 1]
PROP_PLAYER_WORLD_LEVEL (10019), // [0, 8] PROP_PLAYER_WORLD_LEVEL (10019), // [0, 8]
PROP_PLAYER_RESIN (10020), // Original Resin [0, +inf) PROP_PLAYER_RESIN (10020), // Original Resin [0, +inf)
PROP_PLAYER_WAIT_SUB_HCOIN (10022), PROP_PLAYER_WAIT_SUB_HCOIN (10022),
......
...@@ -4,7 +4,9 @@ import emu.grasscutter.net.packet.Opcodes; ...@@ -4,7 +4,9 @@ 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;
import emu.grasscutter.net.proto.EvtDoSkillSuccNotifyOuterClass.EvtDoSkillSuccNotify; import emu.grasscutter.net.proto.EvtDoSkillSuccNotifyOuterClass.EvtDoSkillSuccNotify;
import emu.grasscutter.net.proto.VectorOuterClass.Vector;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.utils.Position;
@Opcodes(PacketOpcodes.EvtDoSkillSuccNotify) @Opcodes(PacketOpcodes.EvtDoSkillSuccNotify)
public class HandlerEvtDoSkillSuccNotify extends PacketHandler { public class HandlerEvtDoSkillSuccNotify extends PacketHandler {
...@@ -12,9 +14,10 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler { ...@@ -12,9 +14,10 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler {
@Override @Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
EvtDoSkillSuccNotify notify = EvtDoSkillSuccNotify.parseFrom(payload); EvtDoSkillSuccNotify notify = EvtDoSkillSuccNotify.parseFrom(payload);
// TODO: Will be used for deducting stamina for charged skills. int skillId = notify.getSkillId();
int casterId = notify.getCasterId();
session.getPlayer().getStaminaManager().handleEvtDoSkillSuccNotify(session, notify); Vector forwardVector = notify.getForward();
Position forward = new Position(forwardVector.getX(), forwardVector.getY(), forwardVector.getZ());
session.getPlayer().getStaminaManager().handleEvtDoSkillSuccNotify(session, skillId, casterId);
} }
} }
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