StaminaManager.java 17.2 KB
Newer Older
1
package emu.grasscutter.game.managers;
2
3
4
5
6
7
8
9

import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.LifeState;
import emu.grasscutter.game.props.PlayerProperty;
10
11
import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo;
import emu.grasscutter.net.proto.EvtDoSkillSuccNotifyOuterClass.EvtDoSkillSuccNotify;
12
13
14
import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo;
import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState;
import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType;
15
import emu.grasscutter.net.proto.VectorOuterClass.Vector;
16
17
18
19
20
21
22
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.Position;

import java.lang.Math;
import java.util.*;

23
public class StaminaManager {
24
25
    private final Player player;
    private HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>();
26
27

    public final static int GlobalMaximumStamina = 24000;
28
29
30
31
32
33
34
35
36
    private Position currentCoordinates = new Position(0, 0, 0);
    private Position previousCoordinates = new Position(0, 0, 0);
    private MotionState currentState = MotionState.MOTION_STANDBY;
    private MotionState previousState = MotionState.MOTION_STANDBY;
    private Timer sustainedStaminaHandlerTimer;
    private GameSession cachedSession = null;
    private GameEntity cachedEntity = null;
    private int staminaRecoverDelay = 0;
    private boolean isInSkillMove = false;
37

38
    private enum ConsumptionType {
39
40
41
42
43
44
        None(0),

        // consume
        CLIMB_START(-500),
        CLIMBING(-150),
        CLIMB_JUMP(-2500),
45
46
        SPRINT(-1800),
        DASH(-360),
47
48
49
50
        FLY(-60),
        SWIM_DASH_START(-200),
        SWIM_DASH(-200),
        SWIMMING(-80),
51
        FIGHT(0),
52
53
54
55
56
57
58
59
60

        // restore
        STANDBY(500),
        RUN(500),
        WALK(500),
        STANDBY_MOVE(500),
        POWERED_FLY(500);

        public final int amount;
61

62
        ConsumptionType(int amount) {
63
64
65
66
            this.amount = amount;
        }
    }

67
68
69
    private class Consumption {
        public ConsumptionType consumptionType;
        public int amount;
70

71
72
73
74
        public Consumption(ConsumptionType ct, int a) {
            consumptionType = ct;
            amount = a;
        }
75

76
77
78
79
        public Consumption(ConsumptionType ct) {
            this(ct, ct.amount);
        }
    }
80

81
82
83
    public boolean getIsInSkillMove() {
        return isInSkillMove;
    }
84

85
86
87
    public void setIsInSkillMove(boolean b) {
        isInSkillMove = b;
    }
88

89
    public StaminaManager(Player player) {
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
        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
        )));
135
136

        MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList(
137
            MotionState.MOTION_FIGHT
138
        )));
139
140
141
142
143
144
    }

    private boolean isPlayerMoving() {
        float diffX = currentCoordinates.getX() - previousCoordinates.getX();
        float diffY = currentCoordinates.getY() - previousCoordinates.getY();
        float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ();
145
146
147
        Grasscutter.getLogger().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates +
                ", " + diffX + ", " + diffY + ", " + diffZ);
        return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3;
148
149
    }

150
151
152
153
    // Returns new stamina and sends PlayerPropNotify
    public int updateStamina(GameSession session, Consumption consumption) {
        int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
        if (consumption.amount == 0) {
154
155
            return currentStamina;
        }
156
157
158
159
160
        int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
        Grasscutter.getLogger().debug(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" +
                (isPlayerMoving() ? "moving" : "      ") + "\t(" + consumption.consumptionType + "," +
                consumption.amount + ")");
        int newStamina = currentStamina + consumption.amount;
161
162
163
164
165
166
        if (newStamina < 0) {
            newStamina = 0;
        }
        if (newStamina > playerMaxStamina) {
            newStamina = playerMaxStamina;
        }
167
        player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina);
168
        session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA));
169
170
171
        return newStamina;
    }

172
173
174
175
176
177
178
179
180
181
182
183
184
185
    // Kills avatar, removes entity and sends notification.
    // TODO: Probably move this to Avatar class? since other components may also need to kill avatar.
    public void killAvatar(GameSession session, GameEntity entity, PlayerDieType dieType) {
        session.send(new PacketAvatarLifeStateChangeNotify(player.getTeamManager().getCurrentAvatarEntity().getAvatar(),
                LifeState.LIFE_DEAD, dieType));
        session.send(new PacketLifeStateChangeNotify(entity, LifeState.LIFE_DEAD, dieType));
        entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0);
        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);
    }

    public void startSustainedStaminaHandler() {
186
187
188
189
190
191
        if (!player.isPaused()) {
            if (sustainedStaminaHandlerTimer == null) {
                sustainedStaminaHandlerTimer = new Timer();
                sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200);
                Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started");
            }
192
        }
193

194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
    }

    public void stopSustainedStaminaHandler() {
        Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
        sustainedStaminaHandlerTimer.cancel();
        sustainedStaminaHandlerTimer = null;
    }

    // Handlers

    // External trigger handler

    public void handleEvtDoSkillSuccNotify(GameSession session, EvtDoSkillSuccNotify notify) {
        handleImmediateStamina(session, notify);
    }

    public void handleCombatInvocationsNotify(GameSession session, EntityMoveInfo moveInfo, GameEntity entity) {
        // cache info for later use in SustainedStaminaHandler tick
        cachedSession = session;
        cachedEntity = entity;
        MotionInfo motionInfo = moveInfo.getMotionInfo();
        MotionState motionState = motionInfo.getState();
        boolean isReliable = moveInfo.getIsReliable();
        Grasscutter.getLogger().trace("" + motionState + "\t" + (isReliable ? "reliable" : ""));
        if (isReliable) {
            currentState = motionState;
            Vector posVector = motionInfo.getPos();
            Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ());
            if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) {
                currentCoordinates = newPos;
224
225
            }
        }
226
227
        startSustainedStaminaHandler();
        handleImmediateStamina(session, motionInfo, motionState, entity);
228
229
    }

230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
    // Internal handler

    private void handleImmediateStamina(GameSession session, MotionInfo motionInfo, MotionState motionState,
                                        GameEntity entity) {
        switch (motionState) {
            case MOTION_DASH_BEFORE_SHAKE:
                if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) {
                    updateStamina(session, new Consumption(ConsumptionType.SPRINT));
                }
                break;
            case MOTION_CLIMB_JUMP:
                if (previousState != MotionState.MOTION_CLIMB_JUMP) {
                    updateStamina(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));
                }
                break;
250
251
252
        }
    }

253
254
255
    private void handleImmediateStamina(GameSession session, EvtDoSkillSuccNotify notify) {
        Consumption consumption = getFightConsumption(notify.getSkillId());
        updateStamina(session, consumption);
256
257
    }

258
    private class SustainedStaminaHandler extends TimerTask {
259
260
261
        public void run() {
            if (Grasscutter.getConfig().OpenStamina) {
                boolean moving = isPlayerMoving();
262
263
264
265
266
                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: " +
                            (currentStamina >= maxStamina) + ", recalculate stamina");
267
                    Consumption consumption = new Consumption(ConsumptionType.None);
268
269
270
271
272
273
274
275
276
277
278
279
                    if (!isInSkillMove) {
                        if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
                            consumption = getClimbSustainedConsumption();
                        } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) {
                            consumption = getSwimSustainedConsumptions();
                        } else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
                            consumption = getRunWalkDashSustainedConsumption();
                        } else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
                            consumption = getFlySustainedConsumption();
                        } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
                            consumption = getStandSustainedConsumption();
                        }
280
281
282
283
284
                    }
                    if (cachedSession != null) {
                        if (consumption.amount < 0) {
                            staminaRecoverDelay = 0;
                        }
285
                        if (consumption.amount > 0 && consumption.consumptionType != ConsumptionType.POWERED_FLY) {
286
                            // For POWERED_FLY recover immediately - things like Amber's gliding exam may require this.
gentlespoon's avatar
gentlespoon committed
287
                            if (staminaRecoverDelay < 10) {
288
                                // For others recover after 2 seconds (10 ticks) - as official server does.
289
                                staminaRecoverDelay++;
290
                                consumption = new Consumption(ConsumptionType.None);
291
292
                            }
                        }
293
                        updateStamina(cachedSession, consumption);
294
                    }
295
                    handleDrowning();
296
297
298
                }
            }
            previousState = currentState;
299
300
301
302
303
            previousCoordinates = new Position(
                    currentCoordinates.getX(),
                    currentCoordinates.getY(),
                    currentCoordinates.getZ()
            );
304
305
        }
    }
306

307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
    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) {
                killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN);
            }
        }
    }

    // Consumption Calculators

    private Consumption getFightConsumption(int skillCasting) {
322
        Consumption consumption = new Consumption(ConsumptionType.None);
323
324
325
326
327
328
329
330
331
332
333
334
335
        HashMap<Integer, Integer> fightingCost = new HashMap<>() {{
            put(10013, -1000); // Kamisato Ayaka
            put(10413, -1000); // Mona
        }};
        if (fightingCost.containsKey(skillCasting)) {
            consumption = new Consumption(ConsumptionType.FIGHT, fightingCost.get(skillCasting));
        }
        return consumption;
    }

    private Consumption getClimbSustainedConsumption() {
        Consumption consumption = new Consumption(ConsumptionType.None);
        if (currentState == MotionState.MOTION_CLIMB && isPlayerMoving()) {
336
337
338
339
340
341
342
343
            consumption = new Consumption(ConsumptionType.CLIMBING);
            if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) {
                consumption = new Consumption(ConsumptionType.CLIMB_START);
            }
        }
        return consumption;
    }

344
    private Consumption getSwimSustainedConsumptions() {
345
346
347
348
349
        Consumption consumption = new Consumption(ConsumptionType.None);
        if (currentState == MotionState.MOTION_SWIM_MOVE) {
            consumption = new Consumption(ConsumptionType.SWIMMING);
        }
        if (currentState == MotionState.MOTION_SWIM_DASH) {
350
            consumption = new Consumption(ConsumptionType.SWIM_DASH);
351
352
353
354
        }
        return consumption;
    }

355
    private Consumption getRunWalkDashSustainedConsumption() {
356
357
        Consumption consumption = new Consumption(ConsumptionType.None);
        if (currentState == MotionState.MOTION_DASH) {
358
            consumption = new Consumption(ConsumptionType.DASH);
359
360
361
362
363
364
365
366
367
368
        }
        if (currentState == MotionState.MOTION_RUN) {
            consumption = new Consumption(ConsumptionType.RUN);
        }
        if (currentState == MotionState.MOTION_WALK) {
            consumption = new Consumption(ConsumptionType.WALK);
        }
        return consumption;
    }

369
    private Consumption getFlySustainedConsumption() {
370
371
372
373
374
375
        Consumption consumption = new Consumption(ConsumptionType.FLY);
        HashMap<Integer, Float> glidingCostReduction = new HashMap<>() {{
            put(212301, 0.8f); // Amber
            put(222301, 0.8f); // Venti
        }};
        float reduction = 1;
376
377
        for (EntityAvatar entity : cachedSession.getPlayer().getTeamManager().getActiveTeam()) {
            for (int skillId : entity.getAvatar().getProudSkillList()) {
378
379
380
381
382
383
384
385
386
387
388
389
390
                if (glidingCostReduction.containsKey(skillId)) {
                    reduction = glidingCostReduction.get(skillId);
                }
            }
        }
        consumption.amount *= reduction;
        // POWERED_FLY, e.g. wind tunnel
        if (currentState == MotionState.MOTION_POWERED_FLY) {
            consumption = new Consumption(ConsumptionType.POWERED_FLY);
        }
        return consumption;
    }

391
    private Consumption getStandSustainedConsumption() {
392
393
394
395
396
397
398
399
400
        Consumption consumption = new Consumption(ConsumptionType.None);
        if (currentState == MotionState.MOTION_STANDBY) {
            consumption = new Consumption(ConsumptionType.STANDBY);
        }
        if (currentState == MotionState.MOTION_STANDBY_MOVE) {
            consumption = new Consumption(ConsumptionType.STANDBY_MOVE);
        }
        return consumption;
    }
401
}