diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 01644b0..36d965f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -78,6 +78,7 @@ import { Minimap } from './ui/Minimap'; import { NPCInteraction } from './ui/NPCInteraction'; import { WanderingNPCPopup } from './ui/WanderingNPCPopup'; import { I18nContext, t, detectLocale, getTranslations, type Locale } from './i18n'; +import { randomRoadsideThoughtLine } from './i18n/loadLocales'; const appStyle: CSSProperties = { width: '100%', @@ -361,6 +362,10 @@ export function App() { i18nForLogRef.current = { locale, tr: translations }; const nearbyIntervalRef = useRef | null>(null); + useEffect(() => { + engineRef.current?.refreshRandomRestThoughtForLocale(); + }, [locale]); + // Quest system state const [towns, setTowns] = useState([]); const townsRef = useRef([]); @@ -613,6 +618,9 @@ export function App() { // ---- Game Engine ---- const engine = new GameEngine(); engineRef.current = engine; + engine.setRestThoughtPicker(() => + randomRoadsideThoughtLine(i18nForLogRef.current.locale), + ); engine.onStateChange((state) => { setGameState(state); diff --git a/frontend/src/game/engine.ts b/frontend/src/game/engine.ts index 8cfffd0..aedf958 100644 --- a/frontend/src/game/engine.ts +++ b/frontend/src/game/engine.ts @@ -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(); } } diff --git a/frontend/src/i18n/loadLocales.ts b/frontend/src/i18n/loadLocales.ts index 7e237a0..34b7df8 100644 --- a/frontend/src/i18n/loadLocales.ts +++ b/frontend/src/i18n/loadLocales.ts @@ -122,6 +122,15 @@ export function enemyTypeLabel(locale: Locale, enemyTypeSlug: string): string { return map[slug] ?? ''; } +/** Random roadside rest line for the hero thought bubble (same pool as server adventure_log). */ +export function randomRoadsideThoughtLine(locale: Locale): string { + const map = locale === 'ru' ? ruDoc.roadside : enDoc.roadside; + const keys = Object.keys(map); + if (!keys.length) return ''; + const slug = keys[Math.floor(Math.random() * keys.length)]!; + return map[slug] ?? ''; +} + /** Phrase code `roadside.` → localized line. */ export function roadsidePhraseText(locale: Locale, phraseCode: string): string { if (!phraseCode.startsWith('roadside.')) return '';