import { useMemo } from "react"; import { useGameStore } from "../game/store"; import { DEFAULT_THREAT_METER_POSITION, type NormalizedHudPosition, } from "../game/hudLayout"; import { useMenuController, type MenuAction } from "../input/useMenuController"; import { ControllerButton } from "./ControllerButton"; export interface OptionsPanelSettings { showMobHealthBars: boolean; showTargetFrame: boolean; showThreatMeter: boolean; threatMeterPosition: NormalizedHudPosition; showFloatingCombatText: boolean; autoTargetNearest: boolean; enableScreenShake: boolean; masterVolume: number; uiScale: number; minimapRotation: "north-up" | "player-up"; } export interface OptionsPanelProps { settings: OptionsPanelSettings; onChange: (patch: Partial) => void; onReset: () => void; open?: boolean; onClose?: () => void; } interface ToggleOption { id: keyof Pick; label: string; description: string; } const TOGGLES: readonly ToggleOption[] = [ { id: "showMobHealthBars", label: "Mob health bars", description: "Show health above nearby enemies." }, { id: "showTargetFrame", label: "Target frame", description: "Show detailed status for your current target." }, { id: "showThreatMeter", label: "Threat meter", description: "Show the compact movable threat table for your selected enemy." }, { id: "showFloatingCombatText", label: "Combat numbers", description: "Show damage, healing, and absorb feedback." }, { id: "autoTargetNearest", label: "Automatic targeting", description: "Pick the nearest valid enemy when an offensive ability needs one." }, { id: "enableScreenShake", label: "Screen shake", description: "Use restrained camera impact for major hits." }, ] as const; function quantize(value: number, step: number, minimum: number, maximum: number): number { const next = Math.round(value / step) * step; return Math.max(minimum, Math.min(maximum, Number(next.toFixed(2)))); } function OptionsDialog({ settings, onChange, onReset, onClose }: Required>) { const actions = useMemo(() => [ ...TOGGLES.map((option) => ({ id: `option-${option.id}`, run: () => onChange({ [option.id]: !settings[option.id] }), })), { id: "option-volume-down", run: () => onChange({ masterVolume: quantize(settings.masterVolume - 0.1, 0.1, 0, 1) }) }, { id: "option-volume-up", run: () => onChange({ masterVolume: quantize(settings.masterVolume + 0.1, 0.1, 0, 1) }) }, { id: "option-scale-down", run: () => onChange({ uiScale: quantize(settings.uiScale - 0.05, 0.05, 0.75, 1.25) }) }, { id: "option-scale-up", run: () => onChange({ uiScale: quantize(settings.uiScale + 0.05, 0.05, 0.75, 1.25) }) }, { id: "option-minimap", run: () => onChange({ minimapRotation: settings.minimapRotation === "north-up" ? "player-up" : "north-up" }) }, { id: "option-threat-position", run: () => onChange({ threatMeterPosition: DEFAULT_THREAT_METER_POSITION }) }, { id: "option-reset", run: onReset }, ], [onChange, onReset, settings]); const controller = useMenuController(actions, { columns: 1, onBack: onClose }); const optionButton = (id: string, label: string, run: () => void, disabled = false) => ( {label} ); return (

Gameplay and interface

Options

{TOGGLES.map((option) => (
{option.label}{option.description} onChange({ [option.id]: !settings[option.id] })} >{settings[option.id] ? "On" : "Off"}
))}
Master volumeOverall game audio volume.
{optionButton("option-volume-down", "−", () => onChange({ masterVolume: quantize(settings.masterVolume - 0.1, 0.1, 0, 1) }), settings.masterVolume <= 0)} {Math.round(settings.masterVolume * 100)}% {optionButton("option-volume-up", "+", () => onChange({ masterVolume: quantize(settings.masterVolume + 0.1, 0.1, 0, 1) }), settings.masterVolume >= 1)}
Interface scaleResize HUD and menu elements for either Thor display or PC.
{optionButton("option-scale-down", "−", () => onChange({ uiScale: quantize(settings.uiScale - 0.05, 0.05, 0.75, 1.25) }), settings.uiScale <= 0.75)} {Math.round(settings.uiScale * 100)}% {optionButton("option-scale-up", "+", () => onChange({ uiScale: quantize(settings.uiScale + 0.05, 0.05, 0.75, 1.25) }), settings.uiScale >= 1.25)}
Minimap orientationKeep north fixed or rotate the map with your character. {optionButton("option-minimap", settings.minimapRotation === "north-up" ? "North up" : "Player up", () => onChange({ minimapRotation: settings.minimapRotation === "north-up" ? "player-up" : "north-up", }))}
Threat meter positionDrag its title bar during combat, or restore its suggested location. {optionButton("option-threat-position", "Reset position", () => onChange({ threatMeterPosition: DEFAULT_THREAT_METER_POSITION, }))}
Restore defaults
A ChangeB Pause menu
); } export function OptionsPanel(props: OptionsPanelProps) { const storeOpen = useGameStore((state) => state.overlay === "options"); const openOverlay = useGameStore((state) => state.openOverlay); const close = props.onClose ?? (() => openOverlay("pause")); if (!(props.open ?? storeOpen)) return null; return ; }