localization

master
Denis Ranneft 1 month ago
parent 7aaa1bd8d4
commit 32caac9e55

@ -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<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
engineRef.current?.refreshRandomRestThoughtForLocale();
}, [locale]);
// Quest system state
const [towns, setTowns] = useState<Town[]>([]);
const townsRef = useRef<Town[]>([]);
@ -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);

@ -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();
}
}

@ -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.<slug>` → localized line. */
export function roadsidePhraseText(locale: Locale, phraseCode: string): string {
if (!phraseCode.startsWith('roadside.')) return '';

Loading…
Cancel
Save