Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
ziqian zhang
Grasscutter
Commits
ccdce684
Commit
ccdce684
authored
May 08, 2022
by
Akka
Browse files
Merge branch 'tower' of
https://github.com/Akka0/Grasscutter
into tower
parents
d468edcf
219a8508
Changes
8
Show whitespace changes
Inline
Side-by-side
src/main/java/emu/grasscutter/game/managers/StaminaManager/AfterUpdateStaminaListener.java
0 → 100644
View file @
ccdce684
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
);
}
src/main/java/emu/grasscutter/game/managers/StaminaManager/BeforeUpdateStaminaListener.java
0 → 100644
View file @
ccdce684
package
emu.grasscutter.game.managers.StaminaManager
;
public
interface
BeforeUpdateStaminaListener
{
/**
* onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
* This gives listeners a chance to intercept this update.
* @param reason Why updating stamina.
* @param newStamina New ABSOLUTE stamina value.
* @return true if you want to cancel this update, otherwise false.
*/
int
onBeforeUpdateStamina
(
String
reason
,
int
newStamina
);
/**
* onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
* This gives listeners a chance to intercept this update.
* @param reason Why updating stamina.
* @param consumption ConsumptionType and RELATIVE stamina change amount.
* @return true if you want to cancel this update, otherwise false.
*/
Consumption
onBeforeUpdateStamina
(
String
reason
,
Consumption
consumption
);
}
\ No newline at end of file
src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java
View file @
ccdce684
...
...
@@ -10,10 +10,10 @@ public enum ConsumptionType {
SPRINT
(-
1800
),
DASH
(-
360
),
FLY
(-
60
),
SWIM_DASH_START
(-
20
0
),
SWIM_DASH
(-
20
0
),
SWIMMING
(-
80
),
FIGHT
(
0
),
SWIM_DASH_START
(-
20
),
SWIM_DASH
(-
20
4
),
SWIMMING
(-
80
),
// TODO: Slow swimming is handled per movement, not per second. Movement frequency depends on gender/age/height.
FIGHT
(
0
),
// See StaminaManager.getFightConsumption()
// restore
STANDBY
(
500
),
...
...
src/main/java/emu/grasscutter/game/managers/StaminaManager/README.md
0 → 100644
View file @
ccdce684
# Stamina Manager
---
## UpdateStamina
```
java
// will use consumption.consumptionType as reason
public
int
updateStaminaRelative
(
GameSession
session
,
Consumption
consumption
);
```
```
java
public
int
updateStaminaAbsolute
(
GameSession
session
,
String
reason
,
int
newStamina
)
```
---
## Pause and Resume
```
java
public
void
startSustainedStaminaHandler
()
```
```
java
public
void
stopSustainedStaminaHandler
()
```
---
## Stamina change listeners and intercepting
### BeforeUpdateStaminaListener
```
java
import
emu.grasscutter.game.managers.StaminaManager.BeforeUpdateStaminaListener
;
// Listener sample: plugin disable CLIMB_JUMP stamina cost.
private
class
MyClass
implements
BeforeUpdateStaminaListener
{
// Make your class implement the listener, and pass in your class as a listener.
public
MyClass
()
{
getStaminaManager
().
registerBeforeUpdateStaminaListener
(
"myClass"
,
this
);
}
@Override
public
boolean
onBeforeUpdateStamina
(
String
reason
,
int
newStamina
)
{
// do not intercept this update
return
false
;
}
@Override
public
boolean
onBeforeUpdateStamina
(
String
reason
,
Consumption
consumption
)
{
// Try to intercept if this update is CLIMB_JUMP
if
(
consumption
.
consumptionType
==
ConsumptionType
.
CLIMB_JUMP
)
{
return
true
;
}
// If it is not CLIMB_JUMP, do not intercept.
return
false
;
}
}
```
### AfterUpdateStaminaListener
```
java
import
emu.grasscutter.game.managers.StaminaManager.AfterUpdateStaminaListener
;
// Listener sample: plugin listens for changes already made.
private
class
MyClass
implements
AfterUpdateStaminaListener
{
// Make your class implement the listener, and pass in your class as a listener.
public
MyClass
()
{
registerAfterUpdateStaminaListener
(
"myClass"
,
this
);
}
@Override
public
void
onAfterUpdateStamina
(
String
reason
,
int
newStamina
)
{
// ...
}
}
```
\ No newline at end of file
src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java
View file @
ccdce684
...
...
@@ -29,19 +29,13 @@ public class StaminaManager {
private
Position
previousCoordinates
=
new
Position
(
0
,
0
,
0
);
private
MotionState
currentState
=
MotionState
.
MOTION_STANDBY
;
private
MotionState
previousState
=
MotionState
.
MOTION_STANDBY
;
private
final
Timer
sustainedStaminaHandlerTimer
=
new
Timer
();
private
final
SustainedStaminaHandler
handleSustainedStamina
=
new
SustainedStaminaHandler
();
private
boolean
timerRunning
=
false
;
private
Timer
sustainedStaminaHandlerTimer
;
private
GameSession
cachedSession
=
null
;
private
GameEntity
cachedEntity
=
null
;
private
int
staminaRecoverDelay
=
0
;
private
boolean
isInSkillMove
=
false
;
public
boolean
getIsInSkillMove
()
{
return
isInSkillMove
;
}
public
void
setIsInSkillMove
(
boolean
b
)
{
isInSkillMove
=
b
;
}
private
HashMap
<
String
,
BeforeUpdateStaminaListener
>
beforeUpdateStaminaListeners
=
new
HashMap
<>();
private
HashMap
<
String
,
AfterUpdateStaminaListener
>
afterUpdateStaminaListeners
=
new
HashMap
<>();
public
StaminaManager
(
Player
player
)
{
this
.
player
=
player
;
...
...
@@ -93,36 +87,116 @@ public class StaminaManager {
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
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
()
{
float
diffX
=
currentCoordinates
.
getX
()
-
previousCoordinates
.
getX
();
float
diffY
=
currentCoordinates
.
getY
()
-
previousCoordinates
.
getY
();
float
diffZ
=
currentCoordinates
.
getZ
()
-
previousCoordinates
.
getZ
();
Grasscutter
.
getLogger
().
debug
(
"isPlayerMoving: "
+
previousCoordinates
+
", "
+
currentCoordinates
+
Grasscutter
.
getLogger
().
trace
(
"isPlayerMoving: "
+
previousCoordinates
+
", "
+
currentCoordinates
+
", "
+
diffX
+
", "
+
diffY
+
", "
+
diffZ
);
return
Math
.
abs
(
diffX
)
>
0.3
||
Math
.
abs
(
diffY
)
>
0.2
||
Math
.
abs
(
diffZ
)
>
0.3
;
}
// Returns new stamina and sends PlayerPropNotify
public
int
updateStamina
(
GameSession
session
,
Consumption
consumption
)
{
public
int
updateStaminaRelative
(
GameSession
session
,
Consumption
consumption
)
{
int
currentStamina
=
player
.
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
);
if
(
consumption
.
amount
==
0
)
{
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
);
Grasscutter
.
getLogger
().
debug
(
currentStamina
+
"/"
+
playerMaxStamina
+
"\t"
+
currentState
+
"\t"
+
Grasscutter
.
getLogger
().
trace
(
currentStamina
+
"/"
+
playerMaxStamina
+
"\t"
+
currentState
+
"\t"
+
(
isPlayerMoving
()
?
"moving"
:
" "
)
+
"\t("
+
consumption
.
consumptionType
+
","
+
consumption
.
amount
+
")"
);
int
newStamina
=
currentStamina
+
consumption
.
amount
;
if
(
newStamina
<
0
)
{
newStamina
=
0
;
}
else
if
(
newStamina
>
playerMaxStamina
)
{
newStamina
=
playerMaxStamina
;
}
return
setStamina
(
session
,
consumption
.
consumptionType
.
toString
(),
newStamina
);
}
if
(
newStamina
>
playerMaxStamina
)
{
public
int
updateStaminaAbsolute
(
GameSession
session
,
String
reason
,
int
newStamina
)
{
int
currentStamina
=
player
.
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
);
// notify will update
for
(
Map
.
Entry
<
String
,
BeforeUpdateStaminaListener
>
listener
:
beforeUpdateStaminaListeners
.
entrySet
())
{
int
overriddenNewStamina
=
listener
.
getValue
().
onBeforeUpdateStamina
(
reason
,
newStamina
);
if
(
overriddenNewStamina
!=
newStamina
)
{
Grasscutter
.
getLogger
().
debug
(
"[StaminaManager] Stamina update absolute("
+
reason
+
", "
+
newStamina
+
") overridden to absolute("
+
reason
+
", "
+
newStamina
+
") by: "
+
listener
.
getKey
());
return
currentStamina
;
}
}
int
playerMaxStamina
=
player
.
getProperty
(
PlayerProperty
.
PROP_MAX_STAMINA
);
if
(
newStamina
<
0
)
{
newStamina
=
0
;
}
else
if
(
newStamina
>
playerMaxStamina
)
{
newStamina
=
playerMaxStamina
;
}
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
);
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
;
}
...
...
@@ -136,22 +210,22 @@ public class StaminaManager {
entity
.
getWorld
().
broadcastPacket
(
new
PacketEntityFightPropUpdateNotify
(
entity
,
FightProperty
.
FIGHT_PROP_CUR_HP
));
entity
.
getWorld
().
broadcastPacket
(
new
PacketLifeStateChangeNotify
(
0
,
entity
,
LifeState
.
LIFE_DEAD
));
player
.
getScene
().
removeEntity
(
entity
);
((
EntityAvatar
)
entity
).
onDeath
(
dieType
,
0
);
((
EntityAvatar
)
entity
).
onDeath
(
dieType
,
0
);
}
public
void
startSustainedStaminaHandler
()
{
if
(!
player
.
isPaused
()
&&
!
timerRunning
)
{
timerRunning
=
true
;
sustainedStaminaHandlerTimer
.
scheduleAtFixedRate
(
handle
SustainedStamina
,
0
,
200
);
//
Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started");
if
(!
player
.
isPaused
()
&&
sustainedStaminaHandlerTimer
==
null
)
{
sustainedStaminaHandlerTimer
=
new
Timer
()
;
sustainedStaminaHandlerTimer
.
scheduleAtFixedRate
(
new
SustainedStamina
Handler
()
,
0
,
200
);
Grasscutter
.
getLogger
().
debug
(
"[MovementManager] SustainedStaminaHandlerTimer started"
);
}
}
public
void
stopSustainedStaminaHandler
()
{
if
(
timerRunning
)
{
timerRunning
=
false
;
if
(
sustainedStaminaHandlerTimer
!=
null
)
{
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
)
{
case
MOTION_DASH_BEFORE_SHAKE:
if
(
previousState
!=
MotionState
.
MOTION_DASH_BEFORE_SHAKE
)
{
updateStamina
(
session
,
new
Consumption
(
ConsumptionType
.
SPRINT
));
updateStamina
Relative
(
session
,
new
Consumption
(
ConsumptionType
.
SPRINT
));
}
break
;
case
MOTION_CLIMB_JUMP:
if
(
previousState
!=
MotionState
.
MOTION_CLIMB_JUMP
)
{
updateStamina
(
session
,
new
Consumption
(
ConsumptionType
.
CLIMB_JUMP
));
updateStamina
Relative
(
session
,
new
Consumption
(
ConsumptionType
.
CLIMB_JUMP
));
}
break
;
case
MOTION_SWIM_DASH:
if
(
previousState
!=
MotionState
.
MOTION_SWIM_DASH
)
{
updateStamina
(
session
,
new
Consumption
(
ConsumptionType
.
SWIM_DASH_START
));
updateStamina
Relative
(
session
,
new
Consumption
(
ConsumptionType
.
SWIM_DASH_START
));
}
break
;
}
...
...
@@ -208,7 +282,7 @@ public class StaminaManager {
private
void
handleImmediateStamina
(
GameSession
session
,
EvtDoSkillSuccNotify
notify
)
{
Consumption
consumption
=
getFightConsumption
(
notify
.
getSkillId
());
updateStamina
(
session
,
consumption
);
updateStamina
Relative
(
session
,
consumption
);
}
private
class
SustainedStaminaHandler
extends
TimerTask
{
...
...
@@ -218,10 +292,10 @@ public class StaminaManager {
int
currentStamina
=
player
.
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
);
int
maxStamina
=
player
.
getProperty
(
PlayerProperty
.
PROP_MAX_STAMINA
);
if
(
moving
||
(
currentStamina
<
maxStamina
))
{
Grasscutter
.
getLogger
().
debug
(
"Player moving: "
+
moving
+
", stamina full: "
+
Grasscutter
.
getLogger
().
trace
(
"Player moving: "
+
moving
+
", stamina full: "
+
(
currentStamina
>=
maxStamina
)
+
", recalculate stamina"
);
Consumption
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
if
(!
isInSkillMove
)
{
if
(
MotionStatesCategorized
.
get
(
"CLIMB"
).
contains
(
currentState
))
{
consumption
=
getClimbSustainedConsumption
();
}
else
if
(
MotionStatesCategorized
.
get
(
"SWIM"
).
contains
((
currentState
)))
{
...
...
@@ -233,7 +307,15 @@ public class StaminaManager {
}
else
if
(
MotionStatesCategorized
.
get
(
"STANDBY"
).
contains
(
currentState
))
{
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
(
consumption
.
amount
<
0
)
{
staminaRecoverDelay
=
0
;
...
...
@@ -243,12 +325,12 @@ public class StaminaManager {
if
(
staminaRecoverDelay
<
10
)
{
// For others recover after 2 seconds (10 ticks) - as official server does.
staminaRecoverDelay
++;
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
consumption
.
amount
=
0
;
Grasscutter
.
getLogger
().
trace
(
"[StaminaManager] Delaying recovery: "
+
staminaRecoverDelay
);
}
}
updateStamina
(
cachedSession
,
consumption
);
updateStamina
Relative
(
cachedSession
,
consumption
);
}
handleDrowning
();
}
}
previousState
=
currentState
;
...
...
@@ -263,10 +345,9 @@ public class StaminaManager {
private
void
handleDrowning
()
{
int
stamina
=
player
.
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
);
if
(
stamina
<
10
)
{
boolean
isSwimming
=
MotionStatesCategorized
.
get
(
"SWIM"
).
contains
(
currentState
);
Grasscutter
.
getLogger
().
debug
(
player
.
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
)
+
"/"
+
player
.
getProperty
(
PlayerProperty
.
PROP_MAX_STAMINA
)
+
"\t"
+
currentState
+
"\t"
+
isSwimming
);
if
(
isSwimming
&&
currentState
!=
MotionState
.
MOTION_SWIM_IDLE
)
{
Grasscutter
.
getLogger
().
trace
(
player
.
getProperty
(
PlayerProperty
.
PROP_CUR_PERSIST_STAMINA
)
+
"/"
+
player
.
getProperty
(
PlayerProperty
.
PROP_MAX_STAMINA
)
+
"\t"
+
currentState
);
if
(
currentState
!=
MotionState
.
MOTION_SWIM_IDLE
)
{
killAvatar
(
cachedSession
,
cachedEntity
,
PlayerDieType
.
PLAYER_DIE_DRAWN
);
}
}
...
...
@@ -274,7 +355,33 @@ public class StaminaManager {
// Consumption Calculators
// Stamina Consumption Reduction: https://genshin-impact.fandom.com/wiki/Stamina
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
);
HashMap
<
Integer
,
Integer
>
fightingCost
=
new
HashMap
<>()
{{
put
(
10013
,
-
1000
);
// Kamisato Ayaka
...
...
@@ -294,10 +401,12 @@ public class StaminaManager {
consumption
=
new
Consumption
(
ConsumptionType
.
CLIMB_START
);
}
}
// TODO: Foods
return
consumption
;
}
private
Consumption
getSwimSustainedConsumptions
()
{
handleDrowning
();
Consumption
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
if
(
currentState
==
MotionState
.
MOTION_SWIM_MOVE
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
SWIMMING
);
...
...
@@ -312,6 +421,7 @@ public class StaminaManager {
Consumption
consumption
=
new
Consumption
(
ConsumptionType
.
None
);
if
(
currentState
==
MotionState
.
MOTION_DASH
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
DASH
);
// TODO: Foods
}
if
(
currentState
==
MotionState
.
MOTION_RUN
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
RUN
);
...
...
@@ -323,7 +433,12 @@ public class StaminaManager {
}
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
);
// Talent
HashMap
<
Integer
,
Float
>
glidingCostReduction
=
new
HashMap
<>()
{{
put
(
212301
,
0.8f
);
// Amber
put
(
222301
,
0.8f
);
// Venti
...
...
@@ -332,15 +447,15 @@ public class StaminaManager {
for
(
EntityAvatar
entity
:
cachedSession
.
getPlayer
().
getTeamManager
().
getActiveTeam
())
{
for
(
int
skillId
:
entity
.
getAvatar
().
getProudSkillList
())
{
if
(
glidingCostReduction
.
containsKey
(
skillId
))
{
reduction
=
glidingCostReduction
.
get
(
skillId
);
float
potentialLowerReduction
=
glidingCostReduction
.
get
(
skillId
);
if
(
potentialLowerReduction
<
reduction
)
{
reduction
=
potentialLowerReduction
;
}
}
}
consumption
.
amount
*=
reduction
;
// POWERED_FLY, e.g. wind tunnel
if
(
currentState
==
MotionState
.
MOTION_POWERED_FLY
)
{
consumption
=
new
Consumption
(
ConsumptionType
.
POWERED_FLY
);
}
consumption
.
amount
*=
reduction
;
// TODO: Foods
return
consumption
;
}
...
...
src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java
View file @
ccdce684
...
...
@@ -49,22 +49,23 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
session
.
getPlayer
().
getStaminaManager
().
handleCombatInvocationsNotify
(
session
,
moveInfo
,
entity
);
// TODO: handle MOTION_FIGHT landing
//
F
or plunge attacks, LAND_SPEED is always -30 and is not useful.
// TODO: handle MOTION_FIGHT landing
which has a different damage factor
//
Also, f
or plunge attacks, LAND_SPEED is always -30 and is not useful.
// 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
(
motionState
==
MotionState
.
MOTION_FALL_ON_GROUND
)
{
monitorLandingEvent
=
false
;
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
;
default
:
...
...
@@ -84,33 +85,42 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
}
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
;
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
)
{
return
;
}
float
currentHP
=
entity
.
getFightProperty
(
FightProperty
.
FIGHT_PROP_CUR_HP
);
float
maxHP
=
entity
.
getFightProperty
(
FightProperty
.
FIGHT_PROP_MAX_HP
);
float
damage
=
0
;
float
damage
Factor
=
0
;
if
(
cachedLandingSpeed
<
-
23.5
)
{
damage
=
(
float
)
(
maxHP
*
0.33
)
;
damage
Factor
=
0.33
f
;
}
if
(
cachedLandingSpeed
<
-
25
)
{
damage
=
(
float
)
(
maxHP
*
0.5
)
;
damage
Factor
=
0.5
f
;
}
if
(
cachedLandingSpeed
<
-
26.5
)
{
damage
=
(
float
)
(
maxHP
*
0.66
)
;
damage
Factor
=
0.66
f
;
}
if
(
cachedLandingSpeed
<
-
28
)
{
damage
=
(
maxHP
*
1
)
;
damage
Factor
=
1
f
;
}
float
damage
=
maxHP
*
damageFactor
;
float
newHP
=
currentHP
-
damage
;
if
(
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
.
getWorld
().
broadcastPacket
(
new
PacketEntityFightPropUpdateNotify
(
entity
,
FightProperty
.
FIGHT_PROP_CUR_HP
));
if
(
newHP
==
0
)
{
...
...
src/main/resources/languages/zh-CN.json
View file @
ccdce684
...
...
@@ -95,17 +95,20 @@
"create"
:
"已建立账号,UID 为 %s 。"
,
"delete"
:
"账号已刪除。"
,
"no_account"
:
"账号不存在。"
,
"command_usage"
:
"用法:account <create|delete> <username> [uid]"
"command_usage"
:
"用法:account <create|delete> <username> [uid]"
,
"description"
:
"创建或删除账号。"
},
"broadcast"
:
{
"command_usage"
:
"用法:broadcast <消息>"
,
"message_sent"
:
"公告已发送。"
"message_sent"
:
"公告已发送。"
,
"description"
:
"向所有玩家发送公告。"
},
"changescene"
:
{
"usage"
:
"用法:changescene <scene id>"
,
"already_in_scene"
:
"你已经在这个秘境中了。"
,
"success"
:
"已切换至秘境 %s."
,
"exists_error"
:
"此秘境不存在。"
"exists_error"
:
"此秘境不存在。"
,
"description"
:
"切换指定秘境。"
},
"clear"
:
{
"command_usage"
:
"用法: clear <all|wp|art|mat>"
,
...
...
@@ -115,35 +118,41 @@
"furniture"
:
"已将 %s 的尘歌壶家具清空。"
,
"displays"
:
"已清除 %s 的显示。"
,
"virtuals"
:
"已将 %s 的所有货币和经验值清空。"
,
"everything"
:
"已将 %s 的所有物品清空。"
"everything"
:
"已将 %s 的所有物品清空。"
,
"description"
:
"从您的背包中删除所有未装备且已解锁的物品,包括稀有物品。"
},
"coop"
:
{
"usage"
:
"用法:coop <playerId> <target playerId>"
,
"success"
:
"已强制召唤 %s 到 %s的世界"
"success"
:
"已强制召唤 %s 到 %s的世界"
,
"description"
:
"强制召唤指定用户到他人的世界。"
},
"enter_dungeon"
:
{
"usage"
:
"用法:enterdungeon <dungeon id>"
,
"changed"
:
"已进入秘境 %s"
,
"not_found_error"
:
"此秘境不存在。"
,
"in_dungeon_error"
:
"你已经在秘境中了。"
"in_dungeon_error"
:
"你已经在秘境中了。"
,
"description"
:
"进入指定秘境。"
},
"giveAll"
:
{
"usage"
:
"用法:giveall [player] [amount]"
,
"started"
:
"正在给予全部物品..."
,
"success"
:
"已给予全部物品。"
,
"invalid_amount_or_playerId"
:
"无效的数量/玩家ID。"
"invalid_amount_or_playerId"
:
"无效的数量/玩家ID。"
,
"description"
:
"给予所有物品。"
},
"giveArtifact"
:
{
"usage"
:
"用法:giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]"
,
"id_error"
:
"无效的圣遗物ID。"
,
"success"
:
"已将 %s 给予 %s。"
"success"
:
"已将 %s 给予 %s。"
,
"description"
:
"给予指定圣遗物。"
},
"giveChar"
:
{
"usage"
:
"用法:givechar <player> <itemId|itemName> [amount]"
,
"given"
:
"给予角色 %s 等级 %s 向UID %s."
,
"invalid_avatar_id"
:
"无效的角色ID。"
,
"invalid_avatar_level"
:
"无效的角色等級。."
,
"invalid_avatar_or_player_id"
:
"无效的角色ID/玩家ID。"
"invalid_avatar_or_player_id"
:
"无效的角色ID/玩家ID。"
,
"description"
:
"给予指定角色。"
},
"give"
:
{
"usage"
:
"用法:give <player> <itemId|itemName> [amount] [level] [refinement]"
,
...
...
@@ -151,29 +160,36 @@
"refinement_must_between_1_and_5"
:
"精炼等阶必须在 1 到 5 之间。"
,
"given"
:
"已将 %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"
:
{
"success"
:
"上帝模式已被设置为 %s 。 [用户:%s]"
"success"
:
"上帝模式已被设置为 %s 。 [用户:%s]"
,
"description"
:
"防止你受到伤害。"
},
"heal"
:
{
"success"
:
"所有角色已被治疗。"
"success"
:
"所有角色已被治疗。"
,
"description"
:
"治疗所选队伍的角色。"
},
"kick"
:
{
"player_kick_player"
:
"玩家 [%s:%s] 已将 [%s:%s] 踢出"
,
"server_kick_player"
:
"正在踢出玩家 [%s:%s]"
"server_kick_player"
:
"正在踢出玩家 [%s:%s]"
,
"description"
:
"从服务器内踢出指定玩家。"
},
"kill"
:
{
"usage"
:
"用法:killall [playerUid] [sceneId]"
,
"scene_not_found_in_player_world"
:
"未在玩家世界中找到此场景"
,
"kill_monsters_in_scene"
:
"已杀死 %s 个怪物。 [场景ID: %s]"
"kill_monsters_in_scene"
:
"已杀死 %s 个怪物。 [场景ID: %s]"
,
"description"
:
"杀死所有怪物"
},
"killCharacter"
:
{
"usage"
:
"用法:/killcharacter [playerId]"
,
"success"
:
"已杀死 %s 目前使用的角色。"
"success"
:
"已杀死 %s 目前使用的角色。"
,
"description"
:
"杀死目前使用的角色"
},
"list"
:
{
"success"
:
"目前在线人数:%s"
"success"
:
"目前在线人数:%s"
,
"description"
:
"查看所有玩家"
},
"permission"
:
{
"usage"
:
"用法:permission <add|remove> <username> <permission>"
,
...
...
@@ -181,21 +197,26 @@
"has_error"
:
"此玩家已拥有此权限!"
,
"remove"
:
"权限已移除。"
,
"not_have_error"
:
"此玩家未拥有权限!"
,
"account_error"
:
"账号不存在!"
"account_error"
:
"账号不存在!"
,
"description"
:
"给予或移除指定玩家的权限。"
},
"position"
:
{
"success"
:
"坐标:%.3f, %.3f, %.3f
\n
场景ID:%d"
"success"
:
"坐标:%.3f, %.3f, %.3f
\n
场景ID:%d"
,
"description"
:
"获取所在位置。"
},
"reload"
:
{
"reload_start"
:
"正在重载配置文件和数据。"
,
"reload_done"
:
"重载完毕。"
"reload_done"
:
"重载完毕。"
,
"description"
:
"重载配置文件和数据。"
},
"resetConst"
:
{
"reset_all"
:
"重置所有角色的命座。"
,
"success"
:
"已重置 %s 的命座,重新登录后将会生效。"
"success"
:
"已重置 %s 的命座,重新登录后将会生效。"
,
"description"
:
"重置当前角色的命之座,执行命令后需重新登录以生效。"
},
"resetShopLimit"
:
{
"usage"
:
"用法:/resetshop <player id>"
"usage"
:
"用法:/resetshop <player id>"
,
"description"
:
"重置所选玩家的商店刷新时间。"
},
"sendMail"
:
{
"usage"
:
"用法:give [player] <itemId|itemName> [amount]"
,
...
...
@@ -217,17 +238,20 @@
"message"
:
"<正文>"
,
"sender"
:
"<发件人>"
,
"arguments"
:
"<itemId|itemName|finish> [数量] [等级]"
,
"error"
:
"错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。"
"error"
:
"错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。"
,
"description"
:
"向指定用户发送邮件。 此命令的用法可根据附加的参数而变化。"
},
"sendMessage"
:
{
"usage"
:
"用法:sendmessage <player> <message>"
,
"success"
:
"消息已发送。"
"success"
:
"消息已发送。"
,
"description"
:
"向指定玩家发送消息"
},
"setFetterLevel"
:
{
"usage"
:
"用法:setfetterlevel <level>"
,
"range_error"
:
"好感度等级必须在 0 到 10 之间。"
,
"fetter_set_level"
:
"好感度已设置为 %s 级"
,
"level_error"
:
"无效的好感度等级。"
"level_error"
:
"无效的好感度等级。"
,
"description"
:
"设置当前角色的好感度等级。"
},
"setStats"
:
{
"usage_console"
:
"用法:setstats|stats @<UID> <stat> <value>"
,
...
...
@@ -238,20 +262,24 @@
"player_error"
:
"玩家不存在或已离线。"
,
"set_self"
:
"%s 已经设置为 %s。"
,
"set_for_uid"
:
"%s 的使用者 %s 更改为 %s。"
,
"set_max_hp"
:
"最大生命值更改为 %s。"
"set_max_hp"
:
"最大生命值更改为 %s。"
,
"description"
:
"设置当前角色的属性。"
},
"setWorldLevel"
:
{
"usage"
:
"用法:setworldlevel <level>"
,
"value_error"
:
"世界等级必须设置在0-8之间。"
,
"success"
:
"已将世界等级设为%s。"
,
"invalid_world_level"
:
"无效的世界等级。"
"invalid_world_level"
:
"无效的世界等级。"
,
"description"
:
"设置世界等级,执行命令后需重新登录以生效。"
},
"spawn"
:
{
"usage"
:
"用法:spawn <entityId> [amount] [level(仅限怪物]"
,
"success"
:
"已生成 %s 个 %s。"
"success"
:
"已生成 %s 个 %s。"
,
"description"
:
"在你附近生成一个生物。"
},
"stop"
:
{
"success"
:
"正在关闭服务器..."
"success"
:
"正在关闭服务器..."
,
"description"
:
"停止服务器"
},
"talent"
:
{
"usage_1"
:
"设置天赋等级:/talent set <talentID> <value>"
,
...
...
@@ -267,32 +295,41 @@
"invalid_level"
:
"无效的天赋等级。"
,
"normal_attack_id"
:
"普通攻击的 ID 为 %s。"
,
"e_skill_id"
:
"元素战技ID %s。"
,
"q_skill_id"
:
"元素爆发ID %s。"
"q_skill_id"
:
"元素爆发ID %s。"
,
"description"
:
"设置当前角色的天赋等级。"
},
"teleportAll"
:
{
"success"
:
"已将全部玩家传送到你的位置"
,
"error"
:
"命令仅限处于多人游戏状态下使用。"
"error"
:
"命令仅限处于多人游戏状态下使用。"
,
"description"
:
"将你世界中的所有玩家传送到你所在的位置。"
},
"teleport"
:
{
"usage_server"
:
"用法:/tp @<player id> <x> <y> <z> [scene id]"
,
"usage"
:
"用法:/tp [@<player id>] <x> <y> <z> [scene id]"
,
"specify_player_id"
:
"你必须指定一个玩家ID。"
,
"invalid_position"
:
"无效的位置。"
,
"success"
:
"传送 %s 到坐标 %s,%s,%s,场景为 %s"
"success"
:
"传送 %s 到坐标 %s,%s,%s,场景为 %s"
,
"description"
:
"改变指定玩家的位置。"
},
"weather"
:
{
"usage"
:
"用法:weather <weatherId> [climateId]"
,
"success"
:
"已将当前天气设定为 %s,气候为 %s。"
,
"invalid_id"
:
"无效的天气ID。"
"invalid_id"
:
"无效的天气ID。"
,
"description"
:
"改变天气"
},
"drop"
:
{
"command_usage"
:
"用法:drop <itemId|itemName> [amount]"
,
"success"
:
"已将 %s x %s 丟在附近。"
"success"
:
"已将 %s x %s 丟在附近。"
,
"description"
:
"在你附近丢一个物品。"
},
"help"
:
{
"usage"
:
"用法:"
,
"aliases"
:
"別名:"
,
"available_commands"
:
"可用指令:"
"available_commands"
:
"可用指令:"
,
"description"
:
"发送帮助信息或显示指定命令的信息。"
},
"restart"
:
{
"description"
:
"重新启动服务器。"
}
}
}
src/main/resources/logback.xml
View file @
ccdce684
<Configuration>
<variable
name=
"LOG_LEVEL"
value=
"${LOG_LEVEL:-INFO}"
/>
<appender
name=
"STDOUT"
class=
"emu.grasscutter.utils.JlineLogbackAppender"
>
<encoder>
<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>
</encoder>
</appender>
<logger
name=
"org.reflections"
level=
"OFF"
/>
<logger
name=
"org.reflections"
level=
"OFF"
/>
<logger
name=
"emu.grasscutter"
level=
"${LOG_LEVEL}"
/>
<root
level=
"INFO"
>
<appender-ref
ref=
"STDOUT"
/>
<appender-ref
ref=
"FILE"
/>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment