|
|
|
@ -45,10 +45,15 @@ import { hapticImpact, hapticNotification, onThemeChanged, getTelegramUserId } f
|
|
|
|
import { Rarity } from './game/types';
|
|
|
|
import { Rarity } from './game/types';
|
|
|
|
import type { HeroState, BuffChargeState } from './game/types';
|
|
|
|
import type { HeroState, BuffChargeState } from './game/types';
|
|
|
|
import { useUiClock } from './hooks/useUiClock';
|
|
|
|
import { useUiClock } from './hooks/useUiClock';
|
|
|
|
import { adventureEntriesFromServerLog } from './game/adventureLogMap';
|
|
|
|
import {
|
|
|
|
|
|
|
|
adventureEntriesFromServerLog,
|
|
|
|
|
|
|
|
appendAdventureLogMessage,
|
|
|
|
|
|
|
|
} from './game/adventureLogMap';
|
|
|
|
|
|
|
|
import { parseAdventureLogLine } from './game/adventureLogMarkers';
|
|
|
|
import { HUD } from './ui/HUD';
|
|
|
|
import { HUD } from './ui/HUD';
|
|
|
|
import { DeathScreen } from './ui/DeathScreen';
|
|
|
|
import { DeathScreen } from './ui/DeathScreen';
|
|
|
|
import { FloatingDamage } from './ui/FloatingDamage';
|
|
|
|
import { FloatingDamage } from './ui/FloatingDamage';
|
|
|
|
|
|
|
|
import { CombatLogPanel } from './ui/CombatLogPanel';
|
|
|
|
import { GameToast } from './ui/GameToast';
|
|
|
|
import { GameToast } from './ui/GameToast';
|
|
|
|
import { OfflineReport } from './ui/OfflineReport';
|
|
|
|
import { OfflineReport } from './ui/OfflineReport';
|
|
|
|
import { HeroSheetModal, type HeroSheetTab } from './ui/HeroSheetModal';
|
|
|
|
import { HeroSheetModal, type HeroSheetTab } from './ui/HeroSheetModal';
|
|
|
|
@ -320,6 +325,8 @@ export function App() {
|
|
|
|
const [connectionError, setConnectionError] = useState<string | null>(null);
|
|
|
|
const [connectionError, setConnectionError] = useState<string | null>(null);
|
|
|
|
const [toast, setToast] = useState<{ message: string; color: string } | null>(null);
|
|
|
|
const [toast, setToast] = useState<{ message: string; color: string } | null>(null);
|
|
|
|
const [logEntries, setLogEntries] = useState<AdventureLogEntry[]>([]);
|
|
|
|
const [logEntries, setLogEntries] = useState<AdventureLogEntry[]>([]);
|
|
|
|
|
|
|
|
/** Live combat narration (mirrors prefixed adventure log lines). */
|
|
|
|
|
|
|
|
const [combatLogLines, setCombatLogLines] = useState<string[]>([]);
|
|
|
|
const [offlineReport, setOfflineReport] = useState<OfflineReportData | null>(null);
|
|
|
|
const [offlineReport, setOfflineReport] = useState<OfflineReportData | null>(null);
|
|
|
|
const [needsName, setNeedsName] = useState(false);
|
|
|
|
const [needsName, setNeedsName] = useState(false);
|
|
|
|
const logIdCounter = useRef(0);
|
|
|
|
const logIdCounter = useRef(0);
|
|
|
|
@ -349,14 +356,19 @@ export function App() {
|
|
|
|
|
|
|
|
|
|
|
|
const sheetNowMs = useUiClock(100);
|
|
|
|
const sheetNowMs = useUiClock(100);
|
|
|
|
|
|
|
|
|
|
|
|
const addLogEntry = useCallback((message: string) => {
|
|
|
|
const appendLogLine = useCallback((rawMessage: string) => {
|
|
|
|
|
|
|
|
setLogEntries((prev) =>
|
|
|
|
|
|
|
|
appendAdventureLogMessage(prev, rawMessage, () => {
|
|
|
|
logIdCounter.current += 1;
|
|
|
|
logIdCounter.current += 1;
|
|
|
|
const entry: AdventureLogEntry = {
|
|
|
|
return logIdCounter.current;
|
|
|
|
id: logIdCounter.current,
|
|
|
|
}),
|
|
|
|
message,
|
|
|
|
);
|
|
|
|
timestamp: Date.now(),
|
|
|
|
const parsed = parseAdventureLogLine(rawMessage);
|
|
|
|
};
|
|
|
|
if (parsed.type === 'encounter') {
|
|
|
|
setLogEntries((prev) => [...prev, entry]);
|
|
|
|
setCombatLogLines([parsed.title]);
|
|
|
|
|
|
|
|
} else if (parsed.type === 'battle') {
|
|
|
|
|
|
|
|
setCombatLogLines((prev) => [...prev, parsed.text].slice(-5));
|
|
|
|
|
|
|
|
}
|
|
|
|
}, []);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const refreshEquipment = useCallback(() => {
|
|
|
|
const refreshEquipment = useCallback(() => {
|
|
|
|
@ -590,6 +602,10 @@ export function App() {
|
|
|
|
|
|
|
|
|
|
|
|
// Wire WS handler -- routes server messages to engine + UI callbacks
|
|
|
|
// Wire WS handler -- routes server messages to engine + UI callbacks
|
|
|
|
wireWSHandler(ws, engine, {
|
|
|
|
wireWSHandler(ws, engine, {
|
|
|
|
|
|
|
|
onCombatStart: () => {
|
|
|
|
|
|
|
|
setCombatLogLines([]);
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onHeroStateReceived: (payload) => {
|
|
|
|
onHeroStateReceived: (payload) => {
|
|
|
|
// Convert raw payload to HeroResponse shape and apply
|
|
|
|
// Convert raw payload to HeroResponse shape and apply
|
|
|
|
const res = payload as unknown as HeroResponse;
|
|
|
|
const res = payload as unknown as HeroResponse;
|
|
|
|
@ -598,6 +614,7 @@ export function App() {
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onCombatEnd: (p) => {
|
|
|
|
onCombatEnd: (p) => {
|
|
|
|
|
|
|
|
setCombatLogLines([]);
|
|
|
|
const loot = buildLootFromCombatEnd(p);
|
|
|
|
const loot = buildLootFromCombatEnd(p);
|
|
|
|
engine.applyLoot(loot);
|
|
|
|
engine.applyLoot(loot);
|
|
|
|
hapticNotification('success');
|
|
|
|
hapticNotification('success');
|
|
|
|
@ -636,6 +653,7 @@ export function App() {
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onHeroRevived: () => {
|
|
|
|
onHeroRevived: () => {
|
|
|
|
|
|
|
|
setCombatLogLines([]);
|
|
|
|
setToast({ message: tr.heroRevived, color: '#44cc44' });
|
|
|
|
setToast({ message: tr.heroRevived, color: '#44cc44' });
|
|
|
|
// "Hero revived" comes from server log + WS
|
|
|
|
// "Hero revived" comes from server log + WS
|
|
|
|
},
|
|
|
|
},
|
|
|
|
@ -648,7 +666,7 @@ export function App() {
|
|
|
|
const town = townsRef.current.find((t) => t.id === p.townId) ?? null;
|
|
|
|
const town = townsRef.current.find((t) => t.id === p.townId) ?? null;
|
|
|
|
setCurrentTown(town);
|
|
|
|
setCurrentTown(town);
|
|
|
|
setToast({ message: t(tr.entering, { townName: p.townName }), color: '#daa520' });
|
|
|
|
setToast({ message: t(tr.entering, { townName: p.townName }), color: '#daa520' });
|
|
|
|
addLogEntry(`Entered ${p.townName}`);
|
|
|
|
appendLogLine(`Entered ${p.townName}`);
|
|
|
|
setNearestNPC(null);
|
|
|
|
setNearestNPC(null);
|
|
|
|
setNpcVisitAwaitingProximity(null);
|
|
|
|
setNpcVisitAwaitingProximity(null);
|
|
|
|
setSelectedNPC(null);
|
|
|
|
setSelectedNPC(null);
|
|
|
|
@ -656,7 +674,7 @@ export function App() {
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onAdventureLogLine: (p) => {
|
|
|
|
onAdventureLogLine: (p) => {
|
|
|
|
addLogEntry(p.message);
|
|
|
|
appendLogLine(p.message);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onTownNPCVisit: (p) => {
|
|
|
|
onTownNPCVisit: (p) => {
|
|
|
|
@ -689,7 +707,7 @@ export function App() {
|
|
|
|
|
|
|
|
|
|
|
|
onNPCEncounterEnd: (p) => {
|
|
|
|
onNPCEncounterEnd: (p) => {
|
|
|
|
if (p.reason === 'timeout') {
|
|
|
|
if (p.reason === 'timeout') {
|
|
|
|
addLogEntry('Wandering merchant moved on');
|
|
|
|
appendLogLine('Wandering merchant moved on');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setWanderingNPC(null);
|
|
|
|
setWanderingNPC(null);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
@ -1135,8 +1153,8 @@ export function App() {
|
|
|
|
sendNPCAlmsDecline(ws);
|
|
|
|
sendNPCAlmsDecline(ws);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setWanderingNPC(null);
|
|
|
|
setWanderingNPC(null);
|
|
|
|
addLogEntry('Declined wandering merchant');
|
|
|
|
appendLogLine('Declined wandering merchant');
|
|
|
|
}, [addLogEntry]);
|
|
|
|
}, [appendLogLine]);
|
|
|
|
|
|
|
|
|
|
|
|
// Show NPC interaction when near an NPC and not dismissed
|
|
|
|
// Show NPC interaction when near an NPC and not dismissed
|
|
|
|
const showNPCInteraction =
|
|
|
|
const showNPCInteraction =
|
|
|
|
@ -1191,6 +1209,14 @@ export function App() {
|
|
|
|
{/* Floating Damage Numbers */}
|
|
|
|
{/* Floating Damage Numbers */}
|
|
|
|
<FloatingDamage damages={damages} />
|
|
|
|
<FloatingDamage damages={damages} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<CombatLogPanel
|
|
|
|
|
|
|
|
visible={
|
|
|
|
|
|
|
|
gameState.phase === GamePhase.Fighting || gameState.phase === GamePhase.Dead
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
lines={combatLogLines}
|
|
|
|
|
|
|
|
anchor={gameState.enemyOnScreenRight !== false ? 'left' : 'right'}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Name Entry Screen */}
|
|
|
|
{/* Name Entry Screen */}
|
|
|
|
{needsName && <NameEntryScreen onNameSet={handleNameSet} />}
|
|
|
|
{needsName && <NameEntryScreen onNameSet={handleNameSet} />}
|
|
|
|
|
|
|
|
|
|
|
|
|