fix dead screen

master
Denis Ranneft 1 month ago
parent 42e3a9b19e
commit 0c52a369cb

@ -1565,8 +1565,15 @@ func (e *Engine) processMovementTick(now time.Time) {
}
for heroID, hm := range e.movements {
if hm == nil {
continue
}
// Do not run movement FSM, AdvanceTick, or encounters for dead heroes.
if hm.skipMovementSimulation() {
continue
}
ProcessSingleHeroMovementTick(heroID, hm, e.roadGraph, now, e.sender, startCombat, nil, e.adventureLog, e.persistHeroAfterTownEnter, nil)
if e.heroStore == nil || hm == nil || hm.Hero == nil {
if e.heroStore == nil || hm.Hero == nil {
continue
}
if sig, ok := hm.TownPausePersistDue(); ok {
@ -1701,6 +1708,9 @@ func (e *Engine) processPositionSync(now time.Time) {
if hm == nil {
continue
}
if hm.skipMovementSimulation() {
continue
}
if sender != nil && hm.State == model.StateWalking {
sender.SendToHero(heroID, "position_sync", hm.PositionSyncPayload(now))
}

@ -546,6 +546,9 @@ func (hm *HeroMovement) refreshSpeed(now time.Time) {
// AdvanceTick moves the hero along the road for one movement tick.
// Returns true if the hero reached the destination town this tick.
func (hm *HeroMovement) AdvanceTick(now time.Time, graph *RoadGraph) (reachedTown bool) {
if hm.skipMovementSimulation() {
return false
}
if hm.Road == nil || len(hm.Road.Waypoints) < 2 {
return false
}
@ -1033,6 +1036,36 @@ func (hm *HeroMovement) Die() {
hm.State = model.StateDead
}
// canSimulateMovement is true when the hero may advance along roads, towns, rests, and encounters.
// Dead heroes must not run the movement FSM or AdvanceTick.
func (hm *HeroMovement) canSimulateMovement() bool {
if hm == nil || hm.Hero == nil {
return false
}
if hm.State == model.StateDead {
return false
}
if hm.Hero.HP <= 0 || hm.Hero.State == model.StateDead {
return false
}
return true
}
// skipMovementSimulation returns true if the hero must not run movement this step.
// When the model is dead but FSM is not yet StateDead, aligns with Die().
func (hm *HeroMovement) skipMovementSimulation() bool {
if hm == nil {
return true
}
if hm.canSimulateMovement() {
return false
}
if hm.Hero != nil && (hm.Hero.HP <= 0 || hm.Hero.State == model.StateDead) {
hm.Die()
}
return true
}
// worldPositionAt returns hero world (x,y) matching SyncToHero / hero_move (spine + display offset).
func (hm *HeroMovement) worldPositionAt(now time.Time) (x, y float64) {
if hm == nil || hm.Hero == nil {
@ -1751,6 +1784,10 @@ func ProcessSingleHeroMovementTick(
return
}
if hm.skipMovementSimulation() {
return
}
switch hm.State {
case model.StateFighting, model.StateDead:
return

@ -600,3 +600,22 @@ func TestLowHP_DoesNotStartRestWhileFighting(t *testing.T) {
t.Fatalf("expected no rest kind, got %s", hm.ActiveRestKind)
}
}
// TestProcessMovementTick_DeadHeroIgnoresWalkingFSM ensures HP 0 never runs the walking
// tick (no hero_move) even if hm.State was incorrectly left as Walking.
func TestProcessMovementTick_DeadHeroIgnoresWalkingFSM(t *testing.T) {
graph := testGraph()
hero := testHeroOnRoad(1, 0, 1000)
now := time.Now()
hm := NewHeroMovement(hero, graph, now)
if hm.State != model.StateDead {
t.Fatalf("NewHeroMovement should set dead for HP=0, got %s", hm.State)
}
hm.State = model.StateWalking // simulate FSM / snapshot desync
ProcessSingleHeroMovementTick(hero.ID, hm, graph, now.Add(time.Second), nil, nil, nil, nil, nil, nil)
if hm.State != model.StateDead {
t.Fatalf("expected StateDead after tick, got %s", hm.State)
}
}

@ -294,8 +294,12 @@ export class GameEngine {
this._gameState.hero.position.y = y;
}
// Sync phase from server state string
if (state === 'walking' && this._gameState.phase !== GamePhase.Fighting) {
// Sync phase from server state string (never clear Death or active combat)
if (
state === 'walking' &&
this._gameState.phase !== GamePhase.Fighting &&
this._gameState.phase !== GamePhase.Dead
) {
this._gameState = { ...this._gameState, phase: GamePhase.Walking };
this._thoughtText = null;
}
@ -312,9 +316,11 @@ export class GameEngine {
this._routeWaypoints = waypoints;
this._heroSpeed = speed;
this._syncWorldTerrainContext();
const hp = this._gameState.hero?.hp ?? 1;
const phase = hp <= 0 ? GamePhase.Dead : GamePhase.Walking;
this._gameState = {
...this._gameState,
phase: GamePhase.Walking,
phase,
routeWaypoints: waypoints.length >= 2 ? waypoints.map((p) => ({ x: p.x, y: p.y })) : null,
};
this._thoughtText = null;
@ -371,16 +377,23 @@ export class GameEngine {
const newX = hero.position.x || 0;
const newY = hero.position.y || 0;
const activity = hero.serverActivityState?.toLowerCase();
const isDead = hero.hp <= 0 || activity === 'dead';
this._gameState = {
...this._gameState,
hero,
};
const activity = hero.serverActivityState?.toLowerCase();
if (
this._gameState.phase !== GamePhase.Fighting &&
this._gameState.phase !== GamePhase.Dead
) {
if (isDead) {
this._gameState = {
...this._gameState,
phase: GamePhase.Dead,
enemy: null,
enemyOnScreenRight: undefined,
};
this._thoughtText = null;
} else if (this._gameState.phase !== GamePhase.Fighting) {
if (activity === 'resting') {
this._gameState = { ...this._gameState, phase: GamePhase.Resting };
if (!this._thoughtText) this._showThought();
@ -507,9 +520,11 @@ export class GameEngine {
* Transitions back to walking phase.
*/
applyCombatEnd(): void {
const hp = this._gameState.hero?.hp ?? 0;
const phase = hp <= 0 ? GamePhase.Dead : GamePhase.Walking;
this._gameState = {
...this._gameState,
phase: GamePhase.Walking,
phase,
enemy: null,
enemyOnScreenRight: undefined,
};

Loading…
Cancel
Save