mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-01-26 18:22:54 +08:00
Merge branch 'tower' of https://github.com/Akka0/Grasscutter into tower
This commit is contained in:
commit
ccdce68434
@ -0,0 +1,12 @@
|
|||||||
|
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);
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
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);
|
||||||
|
}
|
@ -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),
|
||||||
|
@ -0,0 +1,73 @@
|
|||||||
|
# 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) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@ -29,100 +29,174 @@ public class StaminaManager {
|
|||||||
private Position previousCoordinates = new Position(0, 0, 0);
|
private Position previousCoordinates = new Position(0, 0, 0);
|
||||||
private MotionState currentState = MotionState.MOTION_STANDBY;
|
private MotionState currentState = MotionState.MOTION_STANDBY;
|
||||||
private MotionState previousState = MotionState.MOTION_STANDBY;
|
private MotionState previousState = MotionState.MOTION_STANDBY;
|
||||||
private final Timer sustainedStaminaHandlerTimer = new Timer();
|
private Timer sustainedStaminaHandlerTimer;
|
||||||
private final SustainedStaminaHandler handleSustainedStamina = new SustainedStaminaHandler();
|
|
||||||
private boolean timerRunning = false;
|
|
||||||
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;
|
||||||
|
|
||||||
MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList(
|
MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList(
|
||||||
MotionState.MOTION_SWIM_MOVE,
|
MotionState.MOTION_SWIM_MOVE,
|
||||||
MotionState.MOTION_SWIM_IDLE,
|
MotionState.MOTION_SWIM_IDLE,
|
||||||
MotionState.MOTION_SWIM_DASH,
|
MotionState.MOTION_SWIM_DASH,
|
||||||
MotionState.MOTION_SWIM_JUMP
|
MotionState.MOTION_SWIM_JUMP
|
||||||
)));
|
)));
|
||||||
|
|
||||||
MotionStatesCategorized.put("STANDBY", new HashSet<>(Arrays.asList(
|
MotionStatesCategorized.put("STANDBY", new HashSet<>(Arrays.asList(
|
||||||
MotionState.MOTION_STANDBY,
|
MotionState.MOTION_STANDBY,
|
||||||
MotionState.MOTION_STANDBY_MOVE,
|
MotionState.MOTION_STANDBY_MOVE,
|
||||||
MotionState.MOTION_DANGER_STANDBY,
|
MotionState.MOTION_DANGER_STANDBY,
|
||||||
MotionState.MOTION_DANGER_STANDBY_MOVE,
|
MotionState.MOTION_DANGER_STANDBY_MOVE,
|
||||||
MotionState.MOTION_LADDER_TO_STANDBY,
|
MotionState.MOTION_LADDER_TO_STANDBY,
|
||||||
MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY
|
MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY
|
||||||
)));
|
)));
|
||||||
|
|
||||||
MotionStatesCategorized.put("CLIMB", new HashSet<>(Arrays.asList(
|
MotionStatesCategorized.put("CLIMB", new HashSet<>(Arrays.asList(
|
||||||
MotionState.MOTION_CLIMB,
|
MotionState.MOTION_CLIMB,
|
||||||
MotionState.MOTION_CLIMB_JUMP,
|
MotionState.MOTION_CLIMB_JUMP,
|
||||||
MotionState.MOTION_STANDBY_TO_CLIMB,
|
MotionState.MOTION_STANDBY_TO_CLIMB,
|
||||||
MotionState.MOTION_LADDER_IDLE,
|
MotionState.MOTION_LADDER_IDLE,
|
||||||
MotionState.MOTION_LADDER_MOVE,
|
MotionState.MOTION_LADDER_MOVE,
|
||||||
MotionState.MOTION_LADDER_SLIP,
|
MotionState.MOTION_LADDER_SLIP,
|
||||||
MotionState.MOTION_STANDBY_TO_LADDER
|
MotionState.MOTION_STANDBY_TO_LADDER
|
||||||
)));
|
)));
|
||||||
|
|
||||||
MotionStatesCategorized.put("FLY", new HashSet<>(Arrays.asList(
|
MotionStatesCategorized.put("FLY", new HashSet<>(Arrays.asList(
|
||||||
MotionState.MOTION_FLY,
|
MotionState.MOTION_FLY,
|
||||||
MotionState.MOTION_FLY_IDLE,
|
MotionState.MOTION_FLY_IDLE,
|
||||||
MotionState.MOTION_FLY_SLOW,
|
MotionState.MOTION_FLY_SLOW,
|
||||||
MotionState.MOTION_FLY_FAST,
|
MotionState.MOTION_FLY_FAST,
|
||||||
MotionState.MOTION_POWERED_FLY
|
MotionState.MOTION_POWERED_FLY
|
||||||
)));
|
)));
|
||||||
|
|
||||||
MotionStatesCategorized.put("RUN", new HashSet<>(Arrays.asList(
|
MotionStatesCategorized.put("RUN", new HashSet<>(Arrays.asList(
|
||||||
MotionState.MOTION_DASH,
|
MotionState.MOTION_DASH,
|
||||||
MotionState.MOTION_DANGER_DASH,
|
MotionState.MOTION_DANGER_DASH,
|
||||||
MotionState.MOTION_DASH_BEFORE_SHAKE,
|
MotionState.MOTION_DASH_BEFORE_SHAKE,
|
||||||
MotionState.MOTION_RUN,
|
MotionState.MOTION_RUN,
|
||||||
MotionState.MOTION_DANGER_RUN,
|
MotionState.MOTION_DANGER_RUN,
|
||||||
MotionState.MOTION_WALK,
|
MotionState.MOTION_WALK,
|
||||||
MotionState.MOTION_DANGER_WALK
|
MotionState.MOTION_DANGER_WALK
|
||||||
)));
|
)));
|
||||||
|
|
||||||
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) {
|
||||||
if (newStamina > playerMaxStamina) {
|
|
||||||
newStamina = playerMaxStamina;
|
newStamina = playerMaxStamina;
|
||||||
}
|
}
|
||||||
|
return setStamina(session, consumption.consumptionType.toString(), newStamina);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int updateStaminaAbsolute(GameSession session, String reason, int newStamina) {
|
||||||
|
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
|
||||||
|
// notify will update
|
||||||
|
for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) {
|
||||||
|
int overriddenNewStamina = listener.getValue().onBeforeUpdateStamina(reason, newStamina);
|
||||||
|
if (overriddenNewStamina != newStamina) {
|
||||||
|
Grasscutter.getLogger().debug("[StaminaManager] Stamina update absolute(" +
|
||||||
|
reason + ", " + newStamina + ") overridden to absolute(" +
|
||||||
|
reason + ", " + newStamina + ") by: " + listener.getKey());
|
||||||
|
return currentStamina;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
|
||||||
|
if (newStamina < 0) {
|
||||||
|
newStamina = 0;
|
||||||
|
} else if (newStamina > playerMaxStamina) {
|
||||||
|
newStamina = playerMaxStamina;
|
||||||
|
}
|
||||||
|
return setStamina(session, reason, newStamina);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns new stamina and sends PlayerPropNotify
|
||||||
|
public int setStamina(GameSession session, String reason, int newStamina) {
|
||||||
|
// set stamina
|
||||||
player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,22 +210,22 @@ public class StaminaManager {
|
|||||||
entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
|
entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
|
||||||
entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD));
|
entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD));
|
||||||
player.getScene().removeEntity(entity);
|
player.getScene().removeEntity(entity);
|
||||||
((EntityAvatar)entity).onDeath(dieType, 0);
|
((EntityAvatar) entity).onDeath(dieType, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startSustainedStaminaHandler() {
|
public void startSustainedStaminaHandler() {
|
||||||
if (!player.isPaused() && !timerRunning) {
|
if (!player.isPaused() && sustainedStaminaHandlerTimer == null) {
|
||||||
timerRunning = true;
|
sustainedStaminaHandlerTimer = new Timer();
|
||||||
sustainedStaminaHandlerTimer.scheduleAtFixedRate(handleSustainedStamina, 0, 200);
|
sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200);
|
||||||
// Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started");
|
Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopSustainedStaminaHandler() {
|
public void stopSustainedStaminaHandler() {
|
||||||
if (timerRunning) {
|
if (sustainedStaminaHandlerTimer != null) {
|
||||||
timerRunning = false;
|
|
||||||
sustainedStaminaHandlerTimer.cancel();
|
sustainedStaminaHandlerTimer.cancel();
|
||||||
// Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
|
sustainedStaminaHandlerTimer = null;
|
||||||
|
Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,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;
|
||||||
}
|
}
|
||||||
@ -208,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 {
|
||||||
@ -218,22 +292,30 @@ 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))) {
|
consumption = getSwimSustainedConsumptions();
|
||||||
consumption = getSwimSustainedConsumptions();
|
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
|
||||||
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
|
consumption = getRunWalkDashSustainedConsumption();
|
||||||
consumption = getRunWalkDashSustainedConsumption();
|
} else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
|
||||||
} else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
|
consumption = getFlySustainedConsumption();
|
||||||
consumption = getFlySustainedConsumption();
|
} 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;
|
||||||
@ -243,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;
|
||||||
@ -263,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -274,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
|
||||||
@ -294,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);
|
||||||
@ -312,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);
|
||||||
@ -323,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
|
||||||
@ -332,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;
|
consumption.amount *= reduction;
|
||||||
// POWERED_FLY, e.g. wind tunnel
|
// TODO: Foods
|
||||||
if (currentState == MotionState.MOTION_POWERED_FLY) {
|
|
||||||
consumption = new Consumption(ConsumptionType.POWERED_FLY);
|
|
||||||
}
|
|
||||||
return consumption;
|
return consumption;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -95,17 +95,20 @@
|
|||||||
"create": "已建立账号,UID 为 %s 。",
|
"create": "已建立账号,UID 为 %s 。",
|
||||||
"delete": "账号已刪除。",
|
"delete": "账号已刪除。",
|
||||||
"no_account": "账号不存在。",
|
"no_account": "账号不存在。",
|
||||||
"command_usage": "用法:account <create|delete> <username> [uid]"
|
"command_usage": "用法:account <create|delete> <username> [uid]",
|
||||||
|
"description": "创建或删除账号。"
|
||||||
},
|
},
|
||||||
"broadcast": {
|
"broadcast": {
|
||||||
"command_usage": "用法:broadcast <消息>",
|
"command_usage": "用法:broadcast <消息>",
|
||||||
"message_sent": "公告已发送。"
|
"message_sent": "公告已发送。",
|
||||||
|
"description": "向所有玩家发送公告。"
|
||||||
},
|
},
|
||||||
"changescene": {
|
"changescene": {
|
||||||
"usage": "用法:changescene <scene id>",
|
"usage": "用法:changescene <scene id>",
|
||||||
"already_in_scene": "你已经在这个秘境中了。",
|
"already_in_scene": "你已经在这个秘境中了。",
|
||||||
"success": "已切换至秘境 %s.",
|
"success": "已切换至秘境 %s.",
|
||||||
"exists_error": "此秘境不存在。"
|
"exists_error": "此秘境不存在。",
|
||||||
|
"description": "切换指定秘境。"
|
||||||
},
|
},
|
||||||
"clear": {
|
"clear": {
|
||||||
"command_usage": "用法: clear <all|wp|art|mat>",
|
"command_usage": "用法: clear <all|wp|art|mat>",
|
||||||
@ -115,35 +118,41 @@
|
|||||||
"furniture": "已将 %s 的尘歌壶家具清空。",
|
"furniture": "已将 %s 的尘歌壶家具清空。",
|
||||||
"displays": "已清除 %s 的显示。",
|
"displays": "已清除 %s 的显示。",
|
||||||
"virtuals": "已将 %s 的所有货币和经验值清空。",
|
"virtuals": "已将 %s 的所有货币和经验值清空。",
|
||||||
"everything": "已将 %s 的所有物品清空。"
|
"everything": "已将 %s 的所有物品清空。",
|
||||||
|
"description": "从您的背包中删除所有未装备且已解锁的物品,包括稀有物品。"
|
||||||
},
|
},
|
||||||
"coop": {
|
"coop": {
|
||||||
"usage": "用法:coop <playerId> <target playerId>",
|
"usage": "用法:coop <playerId> <target playerId>",
|
||||||
"success": "已强制召唤 %s 到 %s的世界"
|
"success": "已强制召唤 %s 到 %s的世界",
|
||||||
|
"description": "强制召唤指定用户到他人的世界。"
|
||||||
},
|
},
|
||||||
"enter_dungeon": {
|
"enter_dungeon": {
|
||||||
"usage": "用法:enterdungeon <dungeon id>",
|
"usage": "用法:enterdungeon <dungeon id>",
|
||||||
"changed": "已进入秘境 %s",
|
"changed": "已进入秘境 %s",
|
||||||
"not_found_error": "此秘境不存在。",
|
"not_found_error": "此秘境不存在。",
|
||||||
"in_dungeon_error": "你已经在秘境中了。"
|
"in_dungeon_error": "你已经在秘境中了。",
|
||||||
|
"description": "进入指定秘境。"
|
||||||
},
|
},
|
||||||
"giveAll": {
|
"giveAll": {
|
||||||
"usage": "用法:giveall [player] [amount]",
|
"usage": "用法:giveall [player] [amount]",
|
||||||
"started": "正在给予全部物品...",
|
"started": "正在给予全部物品...",
|
||||||
"success": "已给予全部物品。",
|
"success": "已给予全部物品。",
|
||||||
"invalid_amount_or_playerId": "无效的数量/玩家ID。"
|
"invalid_amount_or_playerId": "无效的数量/玩家ID。",
|
||||||
|
"description": "给予所有物品。"
|
||||||
},
|
},
|
||||||
"giveArtifact": {
|
"giveArtifact": {
|
||||||
"usage": "用法:giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]",
|
"usage": "用法:giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]",
|
||||||
"id_error": "无效的圣遗物ID。",
|
"id_error": "无效的圣遗物ID。",
|
||||||
"success": "已将 %s 给予 %s。"
|
"success": "已将 %s 给予 %s。",
|
||||||
|
"description": "给予指定圣遗物。"
|
||||||
},
|
},
|
||||||
"giveChar": {
|
"giveChar": {
|
||||||
"usage": "用法:givechar <player> <itemId|itemName> [amount]",
|
"usage": "用法:givechar <player> <itemId|itemName> [amount]",
|
||||||
"given": "给予角色 %s 等级 %s 向UID %s.",
|
"given": "给予角色 %s 等级 %s 向UID %s.",
|
||||||
"invalid_avatar_id": "无效的角色ID。",
|
"invalid_avatar_id": "无效的角色ID。",
|
||||||
"invalid_avatar_level": "无效的角色等級。.",
|
"invalid_avatar_level": "无效的角色等級。.",
|
||||||
"invalid_avatar_or_player_id": "无效的角色ID/玩家ID。"
|
"invalid_avatar_or_player_id": "无效的角色ID/玩家ID。",
|
||||||
|
"description": "给予指定角色。"
|
||||||
},
|
},
|
||||||
"give": {
|
"give": {
|
||||||
"usage": "用法:give <player> <itemId|itemName> [amount] [level] [refinement]",
|
"usage": "用法:give <player> <itemId|itemName> [amount] [level] [refinement]",
|
||||||
@ -151,29 +160,36 @@
|
|||||||
"refinement_must_between_1_and_5": "精炼等阶必须在 1 到 5 之间。",
|
"refinement_must_between_1_and_5": "精炼等阶必须在 1 到 5 之间。",
|
||||||
"given": "已将 %s 个 %s 给予 %s。",
|
"given": "已将 %s 个 %s 给予 %s。",
|
||||||
"given_with_level_and_refinement": "已将 %s [等級%s, 精炼%s] %s个给予 %s",
|
"given_with_level_and_refinement": "已将 %s [等級%s, 精炼%s] %s个给予 %s",
|
||||||
"given_level": "已将 %s 等级 %s %s 个给予UID %s"
|
"given_level": "已将 %s 等级 %s %s 个给予UID %s",
|
||||||
|
"description": "给予指定物品。"
|
||||||
},
|
},
|
||||||
"godmode": {
|
"godmode": {
|
||||||
"success": "上帝模式已被设置为 %s 。 [用户:%s]"
|
"success": "上帝模式已被设置为 %s 。 [用户:%s]",
|
||||||
|
"description": "防止你受到伤害。"
|
||||||
},
|
},
|
||||||
"heal": {
|
"heal": {
|
||||||
"success": "所有角色已被治疗。"
|
"success": "所有角色已被治疗。",
|
||||||
|
"description": "治疗所选队伍的角色。"
|
||||||
},
|
},
|
||||||
"kick": {
|
"kick": {
|
||||||
"player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出",
|
"player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出",
|
||||||
"server_kick_player": "正在踢出玩家 [%s:%s]"
|
"server_kick_player": "正在踢出玩家 [%s:%s]",
|
||||||
|
"description": "从服务器内踢出指定玩家。"
|
||||||
},
|
},
|
||||||
"kill": {
|
"kill": {
|
||||||
"usage": "用法:killall [playerUid] [sceneId]",
|
"usage": "用法:killall [playerUid] [sceneId]",
|
||||||
"scene_not_found_in_player_world": "未在玩家世界中找到此场景",
|
"scene_not_found_in_player_world": "未在玩家世界中找到此场景",
|
||||||
"kill_monsters_in_scene": "已杀死 %s 个怪物。 [场景ID: %s]"
|
"kill_monsters_in_scene": "已杀死 %s 个怪物。 [场景ID: %s]",
|
||||||
|
"description": "杀死所有怪物"
|
||||||
},
|
},
|
||||||
"killCharacter": {
|
"killCharacter": {
|
||||||
"usage": "用法:/killcharacter [playerId]",
|
"usage": "用法:/killcharacter [playerId]",
|
||||||
"success": "已杀死 %s 目前使用的角色。"
|
"success": "已杀死 %s 目前使用的角色。",
|
||||||
|
"description": "杀死目前使用的角色"
|
||||||
},
|
},
|
||||||
"list": {
|
"list": {
|
||||||
"success": "目前在线人数:%s"
|
"success": "目前在线人数:%s",
|
||||||
|
"description": "查看所有玩家"
|
||||||
},
|
},
|
||||||
"permission": {
|
"permission": {
|
||||||
"usage": "用法:permission <add|remove> <username> <permission>",
|
"usage": "用法:permission <add|remove> <username> <permission>",
|
||||||
@ -181,21 +197,26 @@
|
|||||||
"has_error": "此玩家已拥有此权限!",
|
"has_error": "此玩家已拥有此权限!",
|
||||||
"remove": "权限已移除。",
|
"remove": "权限已移除。",
|
||||||
"not_have_error": "此玩家未拥有权限!",
|
"not_have_error": "此玩家未拥有权限!",
|
||||||
"account_error": "账号不存在!"
|
"account_error": "账号不存在!",
|
||||||
|
"description": "给予或移除指定玩家的权限。"
|
||||||
},
|
},
|
||||||
"position": {
|
"position": {
|
||||||
"success": "坐标:%.3f, %.3f, %.3f\n场景ID:%d"
|
"success": "坐标:%.3f, %.3f, %.3f\n场景ID:%d",
|
||||||
|
"description": "获取所在位置。"
|
||||||
},
|
},
|
||||||
"reload": {
|
"reload": {
|
||||||
"reload_start": "正在重载配置文件和数据。",
|
"reload_start": "正在重载配置文件和数据。",
|
||||||
"reload_done": "重载完毕。"
|
"reload_done": "重载完毕。",
|
||||||
|
"description": "重载配置文件和数据。"
|
||||||
},
|
},
|
||||||
"resetConst": {
|
"resetConst": {
|
||||||
"reset_all": "重置所有角色的命座。",
|
"reset_all": "重置所有角色的命座。",
|
||||||
"success": "已重置 %s 的命座,重新登录后将会生效。"
|
"success": "已重置 %s 的命座,重新登录后将会生效。",
|
||||||
|
"description": "重置当前角色的命之座,执行命令后需重新登录以生效。"
|
||||||
},
|
},
|
||||||
"resetShopLimit": {
|
"resetShopLimit": {
|
||||||
"usage": "用法:/resetshop <player id>"
|
"usage": "用法:/resetshop <player id>",
|
||||||
|
"description": "重置所选玩家的商店刷新时间。"
|
||||||
},
|
},
|
||||||
"sendMail": {
|
"sendMail": {
|
||||||
"usage": "用法:give [player] <itemId|itemName> [amount]",
|
"usage": "用法:give [player] <itemId|itemName> [amount]",
|
||||||
@ -217,17 +238,20 @@
|
|||||||
"message": "<正文>",
|
"message": "<正文>",
|
||||||
"sender": "<发件人>",
|
"sender": "<发件人>",
|
||||||
"arguments": "<itemId|itemName|finish> [数量] [等级]",
|
"arguments": "<itemId|itemName|finish> [数量] [等级]",
|
||||||
"error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。"
|
"error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。",
|
||||||
|
"description": "向指定用户发送邮件。 此命令的用法可根据附加的参数而变化。"
|
||||||
},
|
},
|
||||||
"sendMessage": {
|
"sendMessage": {
|
||||||
"usage": "用法:sendmessage <player> <message>",
|
"usage": "用法:sendmessage <player> <message>",
|
||||||
"success": "消息已发送。"
|
"success": "消息已发送。",
|
||||||
|
"description": "向指定玩家发送消息"
|
||||||
},
|
},
|
||||||
"setFetterLevel": {
|
"setFetterLevel": {
|
||||||
"usage": "用法:setfetterlevel <level>",
|
"usage": "用法:setfetterlevel <level>",
|
||||||
"range_error": "好感度等级必须在 0 到 10 之间。",
|
"range_error": "好感度等级必须在 0 到 10 之间。",
|
||||||
"fetter_set_level": "好感度已设置为 %s 级",
|
"fetter_set_level": "好感度已设置为 %s 级",
|
||||||
"level_error": "无效的好感度等级。"
|
"level_error": "无效的好感度等级。",
|
||||||
|
"description": "设置当前角色的好感度等级。"
|
||||||
},
|
},
|
||||||
"setStats": {
|
"setStats": {
|
||||||
"usage_console": "用法:setstats|stats @<UID> <stat> <value>",
|
"usage_console": "用法:setstats|stats @<UID> <stat> <value>",
|
||||||
@ -238,20 +262,24 @@
|
|||||||
"player_error": "玩家不存在或已离线。",
|
"player_error": "玩家不存在或已离线。",
|
||||||
"set_self": "%s 已经设置为 %s。",
|
"set_self": "%s 已经设置为 %s。",
|
||||||
"set_for_uid": "%s 的使用者 %s 更改为 %s。",
|
"set_for_uid": "%s 的使用者 %s 更改为 %s。",
|
||||||
"set_max_hp": "最大生命值更改为 %s。"
|
"set_max_hp": "最大生命值更改为 %s。",
|
||||||
|
"description": "设置当前角色的属性。"
|
||||||
},
|
},
|
||||||
"setWorldLevel": {
|
"setWorldLevel": {
|
||||||
"usage": "用法:setworldlevel <level>",
|
"usage": "用法:setworldlevel <level>",
|
||||||
"value_error": "世界等级必须设置在0-8之间。",
|
"value_error": "世界等级必须设置在0-8之间。",
|
||||||
"success": "已将世界等级设为%s。",
|
"success": "已将世界等级设为%s。",
|
||||||
"invalid_world_level": "无效的世界等级。"
|
"invalid_world_level": "无效的世界等级。",
|
||||||
|
"description": "设置世界等级,执行命令后需重新登录以生效。"
|
||||||
},
|
},
|
||||||
"spawn": {
|
"spawn": {
|
||||||
"usage": "用法:spawn <entityId> [amount] [level(仅限怪物]",
|
"usage": "用法:spawn <entityId> [amount] [level(仅限怪物]",
|
||||||
"success": "已生成 %s 个 %s。"
|
"success": "已生成 %s 个 %s。",
|
||||||
|
"description": "在你附近生成一个生物。"
|
||||||
},
|
},
|
||||||
"stop": {
|
"stop": {
|
||||||
"success": "正在关闭服务器..."
|
"success": "正在关闭服务器...",
|
||||||
|
"description": "停止服务器"
|
||||||
},
|
},
|
||||||
"talent": {
|
"talent": {
|
||||||
"usage_1": "设置天赋等级:/talent set <talentID> <value>",
|
"usage_1": "设置天赋等级:/talent set <talentID> <value>",
|
||||||
@ -267,32 +295,41 @@
|
|||||||
"invalid_level": "无效的天赋等级。",
|
"invalid_level": "无效的天赋等级。",
|
||||||
"normal_attack_id": "普通攻击的 ID 为 %s。",
|
"normal_attack_id": "普通攻击的 ID 为 %s。",
|
||||||
"e_skill_id": "元素战技ID %s。",
|
"e_skill_id": "元素战技ID %s。",
|
||||||
"q_skill_id": "元素爆发ID %s。"
|
"q_skill_id": "元素爆发ID %s。",
|
||||||
|
"description": "设置当前角色的天赋等级。"
|
||||||
},
|
},
|
||||||
"teleportAll": {
|
"teleportAll": {
|
||||||
"success": "已将全部玩家传送到你的位置",
|
"success": "已将全部玩家传送到你的位置",
|
||||||
"error": "命令仅限处于多人游戏状态下使用。"
|
"error": "命令仅限处于多人游戏状态下使用。",
|
||||||
|
"description": "将你世界中的所有玩家传送到你所在的位置。"
|
||||||
},
|
},
|
||||||
"teleport": {
|
"teleport": {
|
||||||
"usage_server": "用法:/tp @<player id> <x> <y> <z> [scene id]",
|
"usage_server": "用法:/tp @<player id> <x> <y> <z> [scene id]",
|
||||||
"usage": "用法:/tp [@<player id>] <x> <y> <z> [scene id]",
|
"usage": "用法:/tp [@<player id>] <x> <y> <z> [scene id]",
|
||||||
"specify_player_id": "你必须指定一个玩家ID。",
|
"specify_player_id": "你必须指定一个玩家ID。",
|
||||||
"invalid_position": "无效的位置。",
|
"invalid_position": "无效的位置。",
|
||||||
"success": "传送 %s 到坐标 %s,%s,%s,场景为 %s"
|
"success": "传送 %s 到坐标 %s,%s,%s,场景为 %s",
|
||||||
|
"description": "改变指定玩家的位置。"
|
||||||
},
|
},
|
||||||
"weather": {
|
"weather": {
|
||||||
"usage": "用法:weather <weatherId> [climateId]",
|
"usage": "用法:weather <weatherId> [climateId]",
|
||||||
"success": "已将当前天气设定为 %s,气候为 %s。",
|
"success": "已将当前天气设定为 %s,气候为 %s。",
|
||||||
"invalid_id": "无效的天气ID。"
|
"invalid_id": "无效的天气ID。",
|
||||||
|
"description": "改变天气"
|
||||||
},
|
},
|
||||||
"drop": {
|
"drop": {
|
||||||
"command_usage": "用法:drop <itemId|itemName> [amount]",
|
"command_usage": "用法:drop <itemId|itemName> [amount]",
|
||||||
"success": "已将 %s x %s 丟在附近。"
|
"success": "已将 %s x %s 丟在附近。",
|
||||||
|
"description": "在你附近丢一个物品。"
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"usage": "用法:",
|
"usage": "用法:",
|
||||||
"aliases": "別名:",
|
"aliases": "別名:",
|
||||||
"available_commands": "可用指令:"
|
"available_commands": "可用指令:",
|
||||||
|
"description": "发送帮助信息或显示指定命令的信息。"
|
||||||
|
},
|
||||||
|
"restart": {
|
||||||
|
"description": "重新启动服务器。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<Configuration>
|
<Configuration>
|
||||||
|
<variable name="LOG_LEVEL" value="${LOG_LEVEL:-INFO}" />
|
||||||
|
|
||||||
<appender name="STDOUT" class="emu.grasscutter.utils.JlineLogbackAppender">
|
<appender name="STDOUT" class="emu.grasscutter.utils.JlineLogbackAppender">
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>[%d{HH:mm:ss}] [%highlight(%level)] %msg%n</pattern>
|
<pattern>[%d{HH:mm:ss}] [%highlight(%level)] %msg%n</pattern>
|
||||||
@ -14,7 +16,10 @@
|
|||||||
<pattern>%d{yyyy-MM-dd'T'HH:mm:ss'Z'} - %m%n</pattern>
|
<pattern>%d{yyyy-MM-dd'T'HH:mm:ss'Z'} - %m%n</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
<logger name="org.reflections" level="OFF"/>
|
|
||||||
|
<logger name="org.reflections" level="OFF" />
|
||||||
|
<logger name="emu.grasscutter" level="${LOG_LEVEL}" />
|
||||||
|
|
||||||
<root level="INFO">
|
<root level="INFO">
|
||||||
<appender-ref ref="STDOUT" />
|
<appender-ref ref="STDOUT" />
|
||||||
<appender-ref ref="FILE" />
|
<appender-ref ref="FILE" />
|
||||||
|
Loading…
Reference in New Issue
Block a user