|
|
|
|
@ -32,21 +32,6 @@ import { getViewport } from '../shared/telegram';
|
|
|
|
|
* - Thought bubble display (server-driven rest state)
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// ---- Thought Bubble Pool ----
|
|
|
|
|
|
|
|
|
|
const HERO_THOUGHTS = [
|
|
|
|
|
"I wonder what's ahead...",
|
|
|
|
|
'Need better gear...',
|
|
|
|
|
'Getting stronger!',
|
|
|
|
|
'This forest is huge',
|
|
|
|
|
'I smell adventure',
|
|
|
|
|
'Gold... need more gold',
|
|
|
|
|
'What was that sound?',
|
|
|
|
|
'Time to rest a bit',
|
|
|
|
|
'My sword needs sharpening',
|
|
|
|
|
'Almost leveled up!',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/** Drift threshold in world tiles before snapping to server position. */
|
|
|
|
|
const POSITION_DRIFT_SNAP_THRESHOLD = 2.0;
|
|
|
|
|
|
|
|
|
|
@ -95,6 +80,10 @@ export class GameEngine {
|
|
|
|
|
/** Thought bubble text shown during rest pauses (null = hidden). */
|
|
|
|
|
private _thoughtText: string | null = null;
|
|
|
|
|
private _thoughtStartMs = 0;
|
|
|
|
|
/** True if current bubble text came from client random pool (not applyAdventureLogLine). */
|
|
|
|
|
private _thoughtIsRandomPool = false;
|
|
|
|
|
/** Returns localized idle thought; set from App using current locale (e.g. randomRoadsideThoughtLine). */
|
|
|
|
|
private _restThoughtPicker: (() => string) | null = null;
|
|
|
|
|
|
|
|
|
|
/** Towns for map rendering */
|
|
|
|
|
private _towns: TownData[] = [];
|
|
|
|
|
@ -159,6 +148,23 @@ export class GameEngine {
|
|
|
|
|
this._allNPCs = npcs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Picks thought bubble text when resting / in town before server sends adventure_log_line.
|
|
|
|
|
* Call with a function that reads current UI locale (e.g. from a ref).
|
|
|
|
|
*/
|
|
|
|
|
setRestThoughtPicker(picker: (() => string) | null): void {
|
|
|
|
|
this._restThoughtPicker = picker;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** After language change: refresh only client-random rest thoughts, not server narration. */
|
|
|
|
|
refreshRandomRestThoughtForLocale(): void {
|
|
|
|
|
if (!this._thoughtIsRandomPool || !this._thoughtText) return;
|
|
|
|
|
const phase = this._gameState.phase;
|
|
|
|
|
if (phase !== GamePhase.Resting && phase !== GamePhase.InTown) return;
|
|
|
|
|
this._showThought();
|
|
|
|
|
this._notifyStateChange();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Interpolated hero position in world space (for UI proximity checks). */
|
|
|
|
|
getHeroDisplayWorldPosition(): { x: number; y: number } {
|
|
|
|
|
return { x: this._heroDisplayX, y: this._heroDisplayY };
|
|
|
|
|
@ -254,6 +260,7 @@ export class GameEngine {
|
|
|
|
|
) {
|
|
|
|
|
this._gameState = { ...this._gameState, phase: GamePhase.Walking };
|
|
|
|
|
this._thoughtText = null;
|
|
|
|
|
this._thoughtIsRandomPool = false;
|
|
|
|
|
} else if (this._gameState.phase !== GamePhase.Walking) {
|
|
|
|
|
// Don't override fighting/dead phase from move messages
|
|
|
|
|
}
|
|
|
|
|
@ -290,6 +297,7 @@ export class GameEngine {
|
|
|
|
|
) {
|
|
|
|
|
this._gameState = { ...this._gameState, phase: GamePhase.Walking };
|
|
|
|
|
this._thoughtText = null;
|
|
|
|
|
this._thoughtIsRandomPool = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -312,6 +320,7 @@ export class GameEngine {
|
|
|
|
|
routeWaypoints: waypoints.length >= 2 ? waypoints.map((p) => ({ x: p.x, y: p.y })) : null,
|
|
|
|
|
};
|
|
|
|
|
this._thoughtText = null;
|
|
|
|
|
this._thoughtIsRandomPool = false;
|
|
|
|
|
this._notifyStateChange();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -381,6 +390,7 @@ export class GameEngine {
|
|
|
|
|
enemyOnScreenRight: undefined,
|
|
|
|
|
};
|
|
|
|
|
this._thoughtText = null;
|
|
|
|
|
this._thoughtIsRandomPool = false;
|
|
|
|
|
} else if (this._gameState.phase !== GamePhase.Fighting) {
|
|
|
|
|
if (activity === 'resting') {
|
|
|
|
|
this._gameState = { ...this._gameState, phase: GamePhase.Resting };
|
|
|
|
|
@ -391,6 +401,7 @@ export class GameEngine {
|
|
|
|
|
} else if (activity === 'walking') {
|
|
|
|
|
this._gameState = { ...this._gameState, phase: GamePhase.Walking };
|
|
|
|
|
this._thoughtText = null;
|
|
|
|
|
this._thoughtIsRandomPool = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -429,6 +440,7 @@ export class GameEngine {
|
|
|
|
|
};
|
|
|
|
|
this._lootTimerMs = 0;
|
|
|
|
|
this._thoughtText = null;
|
|
|
|
|
this._thoughtIsRandomPool = false;
|
|
|
|
|
this._notifyStateChange();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -554,6 +566,7 @@ export class GameEngine {
|
|
|
|
|
applyAdventureLogLine(message: string): void {
|
|
|
|
|
if (!message) return;
|
|
|
|
|
this._thoughtText = message;
|
|
|
|
|
this._thoughtIsRandomPool = false;
|
|
|
|
|
this._thoughtStartMs = performance.now();
|
|
|
|
|
this._notifyStateChange();
|
|
|
|
|
}
|
|
|
|
|
@ -567,6 +580,7 @@ export class GameEngine {
|
|
|
|
|
phase: GamePhase.Walking,
|
|
|
|
|
};
|
|
|
|
|
this._thoughtText = null;
|
|
|
|
|
this._thoughtIsRandomPool = false;
|
|
|
|
|
this._notifyStateChange();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -932,9 +946,9 @@ export class GameEngine {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _showThought(): void {
|
|
|
|
|
this._thoughtText =
|
|
|
|
|
HERO_THOUGHTS[Math.floor(Math.random() * HERO_THOUGHTS.length)] ??
|
|
|
|
|
null;
|
|
|
|
|
const line = this._restThoughtPicker?.().trim() ?? '';
|
|
|
|
|
this._thoughtText = line || null;
|
|
|
|
|
this._thoughtIsRandomPool = Boolean(line);
|
|
|
|
|
this._thoughtStartMs = performance.now();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|