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