mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-01-10 21:12:57 +08:00
Talent moving stamina cost
This commit is contained in:
parent
99dbac8ad3
commit
2a3708ee46
@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
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 {
|
|||||||
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),
|
||||||
FLY(-60),
|
|
||||||
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()
|
FIGHT(0), // See StaminaManager.getFightConsumption()
|
||||||
|
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(-204), // -10.2 per second, 5Hz = -204 each tick
|
||||||
|
SWIMMING(-80),
|
||||||
|
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;
|
|||||||
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;
|
|||||||
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,70 +113,23 @@ 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(
|
|
||||||
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(
|
|
||||||
MotionState.MOTION_SKIFF_BOARDING,
|
|
||||||
MotionState.MOTION_SKIFF_NORMAL,
|
|
||||||
MotionState.MOTION_SKIFF_DASH,
|
|
||||||
MotionState.MOTION_SKIFF_POWERED_DASH
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listeners
|
// Accessors
|
||||||
|
|
||||||
|
public void setSkillCast(int skillId, int skillCasterId) {
|
||||||
|
lastSkillId = skillId;
|
||||||
|
lastSkillCasterId = skillCasterId;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) {
|
public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) {
|
||||||
if (beforeUpdateStaminaListeners.containsKey(listenerName)) {
|
if (beforeUpdateStaminaListeners.containsKey(listenerName)) {
|
||||||
@ -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 {
|
|||||||
} 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 {
|
|||||||
|
|
||||||
// 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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
}
|
// Climbing specific reductions
|
||||||
// TODO: Foods
|
// 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 {
|
|||||||
// 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;
|
|||||||
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 {
|
|||||||
@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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user