You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

281 lines
7.8 KiB
TypeScript

import { useEffect, useState, type CSSProperties } from 'react';
import type { AdventureLogEntry, EquipmentItem, HeroQuest, HeroState } from '../game/types';
import { EquipmentPaperDoll } from './EquipmentPaperDoll';
import { InventoryGrid } from './InventoryGrid';
import { HeroStatsContent } from './HeroPanel';
import { AdventureLogEntries } from './AdventureLog';
import { QuestLogList } from './QuestLog';
import { useT, useLocale, type Locale } from '../i18n';
export type HeroSheetTab = 'stats' | 'character' | 'inventory' | 'journal' | 'quests' | 'settings';
const overlay: CSSProperties = {
position: 'fixed',
inset: 0,
zIndex: 800,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 12,
pointerEvents: 'auto',
};
const backdrop: CSSProperties = {
position: 'absolute',
inset: 0,
backgroundColor: 'rgba(0, 0, 0, 0.55)',
backdropFilter: 'blur(4px)',
};
const panel: CSSProperties = {
position: 'relative',
width: '100%',
maxWidth: 420,
height: 'min(88vh, 640px)',
maxHeight: 'min(88vh, 640px)',
display: 'flex',
flexDirection: 'column',
borderRadius: 14,
border: '1px solid rgba(255, 255, 255, 0.12)',
backgroundColor: 'rgba(12, 14, 22, 0.94)',
boxShadow: '0 12px 48px rgba(0,0,0,0.55)',
overflow: 'hidden',
};
const header: CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '10px 12px',
borderBottom: '1px solid rgba(255,255,255,0.08)',
flexShrink: 0,
};
const titleStyle: CSSProperties = {
fontSize: 15,
fontWeight: 700,
color: '#e8e8e8',
};
const closeBtn: CSSProperties = {
background: 'none',
border: 'none',
color: '#888',
fontSize: 22,
cursor: 'pointer',
padding: '2px 8px',
lineHeight: 1,
};
const tabsRow: CSSProperties = {
display: 'flex',
borderBottom: '1px solid rgba(255,255,255,0.08)',
flexShrink: 0,
};
const tabBtn = (active: boolean): CSSProperties => ({
flex: 1,
padding: '8px 2px',
fontSize: 9,
fontWeight: 700,
border: 'none',
cursor: 'pointer',
fontFamily: 'inherit',
textTransform: 'uppercase',
letterSpacing: 0.4,
background: active ? 'rgba(80, 120, 200, 0.22)' : 'transparent',
color: active ? '#c8d8ff' : '#778',
borderBottom: active ? '2px solid #6a9eef' : '2px solid transparent',
WebkitTapHighlightColor: 'transparent',
});
const body: CSSProperties = {
flex: 1,
minHeight: 0,
overflowY: 'auto',
padding: '12px 12px 16px',
fontSize: 12,
color: '#ccc',
WebkitOverflowScrolling: 'touch',
};
const goldBar: CSSProperties = {
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 10,
padding: '8px 10px',
borderRadius: 8,
background: 'rgba(255, 215, 0, 0.08)',
border: '1px solid rgba(255, 215, 0, 0.2)',
color: '#ffd700',
fontWeight: 700,
fontSize: 14,
};
interface HeroSheetModalProps {
open: boolean;
onClose: () => void;
initialTab?: HeroSheetTab;
hero: HeroState;
nowMs: number;
equipment: Record<string, EquipmentItem>;
logEntries: AdventureLogEntry[];
quests: HeroQuest[];
onQuestClaim: (heroQuestId: number) => void;
onQuestAbandon: (heroQuestId: number) => void;
/** Disable claim while hero is dead (HP 0 / death phase). */
questClaimDisabled?: boolean;
}
export function HeroSheetModal({
open,
onClose,
initialTab = 'stats',
hero,
nowMs,
equipment,
logEntries,
quests,
onQuestClaim,
onQuestAbandon,
questClaimDisabled = false,
}: HeroSheetModalProps) {
const [tab, setTab] = useState<HeroSheetTab>(initialTab);
const tr = useT();
const { locale, setLocale } = useLocale();
useEffect(() => {
if (open) setTab(initialTab);
}, [open, initialTab]);
useEffect(() => {
if (!open) return;
const h = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', h);
return () => window.removeEventListener('keydown', h);
}, [open, onClose]);
if (!open) return null;
return (
<div style={overlay}>
<div style={backdrop} onClick={onClose} aria-hidden />
<div style={panel} role="dialog" aria-modal aria-labelledby="hero-sheet-title">
<div style={header}>
<span id="hero-sheet-title" style={titleStyle}>
{tr.hero} · {tr.level}.{hero.level}
</span>
<button type="button" style={closeBtn} onClick={onClose} aria-label="Close">
{'\u2715'}
</button>
</div>
<div style={tabsRow}>
<button type="button" style={tabBtn(tab === 'stats')} onClick={() => setTab('stats')}>
{tr.stats}
</button>
<button type="button" style={tabBtn(tab === 'character')} onClick={() => setTab('character')}>
{tr.character}
</button>
<button type="button" style={tabBtn(tab === 'inventory')} onClick={() => setTab('inventory')}>
{tr.inventory}
</button>
<button type="button" style={tabBtn(tab === 'journal')} onClick={() => setTab('journal')}>
{tr.journal}
</button>
<button type="button" style={tabBtn(tab === 'quests')} onClick={() => setTab('quests')}>
{tr.quests}
</button>
<button type="button" style={tabBtn(tab === 'settings')} onClick={() => setTab('settings')}>
{'\u2699\uFE0F'}
</button>
</div>
<div style={body}>
{tab === 'stats' && <HeroStatsContent hero={hero} nowMs={nowMs} />}
{tab === 'character' && <EquipmentPaperDoll equipment={equipment} />}
{tab === 'inventory' && (
<>
<div style={goldBar}>
<span style={{ fontSize: 18 }}>{'\uD83E\uDE99'}</span>
<span>{hero.gold.toLocaleString()} {tr.gold.toLowerCase()}</span>
</div>
<InventoryGrid items={hero.inventory} />
</>
)}
{tab === 'journal' && <AdventureLogEntries entries={logEntries} />}
{tab === 'quests' && (
<QuestLogList
quests={quests}
onClaim={onQuestClaim}
onAbandon={onQuestAbandon}
claimDisabled={questClaimDisabled}
/>
)}
{tab === 'settings' && (
<SettingsContent locale={locale} setLocale={setLocale} tr={tr} />
)}
</div>
</div>
</div>
);
}
// ---- Settings Tab ----
const settingRow: CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '10px 0',
borderBottom: '1px solid rgba(255,255,255,0.06)',
};
const langBtn = (active: boolean): CSSProperties => ({
padding: '6px 16px',
borderRadius: 6,
border: active ? '2px solid #ffd700' : '1px solid rgba(255,255,255,0.15)',
background: active ? 'rgba(255, 215, 0, 0.15)' : 'rgba(255,255,255,0.04)',
color: active ? '#ffd700' : '#aaa',
fontWeight: active ? 700 : 400,
fontSize: 13,
cursor: 'pointer',
transition: 'all 150ms ease',
});
interface SettingsContentProps {
locale: Locale;
setLocale: (l: Locale) => void;
tr: { settings: string; language: string; english: string; russian: string };
}
function SettingsContent({ locale, setLocale, tr }: SettingsContentProps) {
return (
<div>
<div style={{ fontSize: 14, fontWeight: 700, color: '#e8e8e8', marginBottom: 12 }}>
{tr.settings}
</div>
<div style={settingRow}>
<span style={{ color: '#ccc', fontSize: 13 }}>{tr.language}</span>
<div style={{ display: 'flex', gap: 8 }}>
<button
type="button"
style={langBtn(locale === 'en')}
onClick={() => setLocale('en')}
>
{tr.english}
</button>
<button
type="button"
style={langBtn(locale === 'ru')}
onClick={() => setLocale('ru')}
>
{tr.russian}
</button>
</div>
</div>
</div>
);
}