275 lines
9.2 KiB
TypeScript
275 lines
9.2 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import {
|
|
ACTION_LABELS,
|
|
INPUT_ACTIONS,
|
|
useInput,
|
|
type InputDevice,
|
|
} from '../input'
|
|
import {
|
|
ControllerBindingLabel,
|
|
ControllerStylePreview,
|
|
} from './ControllerIcons'
|
|
|
|
const CONTROLLER_STYLE_LABELS = {
|
|
xbox: 'Xbox',
|
|
playstation: 'PlayStation',
|
|
nintendo: 'Nintendo',
|
|
} as const
|
|
import { useDualScreen } from '../dualScreen'
|
|
import {
|
|
getNativeDisplays,
|
|
hasNativeDualScreenBridge,
|
|
type AndroidDisplay,
|
|
} from '../nativeDualScreen'
|
|
|
|
export function SettingsScreen({ onBack }: { onBack: () => void }) {
|
|
const [device, setDevice] = useState<InputDevice>('controller')
|
|
const [settingsTab, setSettingsTab] = useState<'display' | 'input' | 'bindings'>('display')
|
|
const [displayMessage, setDisplayMessage] = useState('')
|
|
const [androidDisplays, setAndroidDisplays] = useState<AndroidDisplay[]>([])
|
|
const {
|
|
bindings,
|
|
capture,
|
|
controllerIconStyle,
|
|
directPartyTargeting,
|
|
beginCapture,
|
|
cancelCapture,
|
|
resetBindings,
|
|
setControllerIconStyle,
|
|
setDirectPartyTargeting,
|
|
} = useInput()
|
|
const {
|
|
enabled: dualScreenEnabled,
|
|
connected: topDisplayConnected,
|
|
setEnabled: setDualScreenEnabled,
|
|
openTopDisplay,
|
|
} = useDualScreen()
|
|
const nativeDualScreen = hasNativeDualScreenBridge()
|
|
const directTargetActions = new Set([
|
|
'targetParty1',
|
|
'targetParty2',
|
|
'targetParty3',
|
|
'targetParty4',
|
|
'targetParty5',
|
|
'targetParty6',
|
|
'toggleTargetGroup',
|
|
])
|
|
const visibleActions = INPUT_ACTIONS.filter((action) => (
|
|
directPartyTargeting
|
|
? action !== 'previousTarget' && action !== 'nextTarget'
|
|
: !directTargetActions.has(action)
|
|
))
|
|
|
|
async function refreshNativeDisplays() {
|
|
if (!nativeDualScreen) return
|
|
try {
|
|
const result = await getNativeDisplays()
|
|
setAndroidDisplays(result.displays)
|
|
} catch {
|
|
setAndroidDisplays([])
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!nativeDualScreen) return
|
|
getNativeDisplays()
|
|
.then((result) => setAndroidDisplays(result.displays))
|
|
.catch(() => setAndroidDisplays([]))
|
|
}, [nativeDualScreen])
|
|
|
|
async function launchTopDisplay() {
|
|
const opened = await openTopDisplay()
|
|
setDisplayMessage(opened
|
|
? nativeDualScreen
|
|
? 'Android placed the game on the larger display and controls on the smaller display.'
|
|
: 'Companion display opened. Move it to the Thor screen you want and select Fullscreen.'
|
|
: 'No usable second display was found. Check the Thor display mode and try again.')
|
|
await refreshNativeDisplays()
|
|
}
|
|
|
|
return (
|
|
<section className="content-screen settings-screen">
|
|
<div className="screen-heading">
|
|
<div>
|
|
<p className="eyebrow">Game Options</p>
|
|
<h1>Settings</h1>
|
|
</div>
|
|
<button className="back-button" onClick={onBack} type="button">Back</button>
|
|
</div>
|
|
|
|
<nav className="settings-tabs" role="tablist" aria-label="Settings sections">
|
|
{([
|
|
{ key: 'display', label: 'Display' },
|
|
{ key: 'input', label: 'Input' },
|
|
{ key: 'bindings', label: 'Bindings' },
|
|
] as const).map((tab) => (
|
|
<button
|
|
aria-selected={settingsTab === tab.key}
|
|
className={settingsTab === tab.key ? 'selected' : ''}
|
|
key={tab.key}
|
|
onClick={() => setSettingsTab(tab.key)}
|
|
role="tab"
|
|
type="button"
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
|
|
{settingsTab === 'display' && (
|
|
<section className="dual-screen-settings settings-tab-panel">
|
|
<div>
|
|
<p className="eyebrow">Display</p>
|
|
<h2>AYN Thor Dual-Screen Mode</h2>
|
|
<p>
|
|
The upper display shows enemy and party health. The lower display
|
|
keeps targeting, resources, skills, and cooldowns.
|
|
</p>
|
|
</div>
|
|
<div className="dual-screen-actions">
|
|
<button
|
|
className={dualScreenEnabled ? 'selected' : ''}
|
|
onClick={() => {
|
|
setDualScreenEnabled(!dualScreenEnabled)
|
|
setDisplayMessage('')
|
|
}}
|
|
type="button"
|
|
>
|
|
{dualScreenEnabled ? 'Dual-Screen Enabled' : 'Enable Dual-Screen'}
|
|
</button>
|
|
<button onClick={launchTopDisplay} type="button">
|
|
{topDisplayConnected ? 'Companion Connected' : 'Open Companion Display'}
|
|
</button>
|
|
</div>
|
|
<small>
|
|
{displayMessage || (
|
|
topDisplayConnected
|
|
? 'The companion display is connected and receiving live combat data.'
|
|
: 'Open the companion display before starting combat.'
|
|
)}
|
|
</small>
|
|
{nativeDualScreen && androidDisplays.length > 0 && (
|
|
<div className="android-display-list">
|
|
{androidDisplays.map((display) => (
|
|
<span key={display.id}>
|
|
<strong>{display.isCurrent ? 'Current' : 'Secondary'} #{display.id}</strong>
|
|
{display.width}x{display.height} at {Math.round(display.refreshRate)} Hz
|
|
{display.isPresentation ? ' - Presentation' : ''}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
{settingsTab === 'input' && (
|
|
<section className="controller-preferences settings-tab-panel">
|
|
<div>
|
|
<p className="eyebrow">Targeting</p>
|
|
<h3>Direct Party Keybinds</h3>
|
|
<p>
|
|
Assign party slots directly. In raids, use the group-switch binding
|
|
to alternate between members 1-6, 7-12, and 13-18.
|
|
</p>
|
|
</div>
|
|
<button
|
|
aria-pressed={directPartyTargeting}
|
|
className={directPartyTargeting ? 'selected' : ''}
|
|
onClick={() => setDirectPartyTargeting(!directPartyTargeting)}
|
|
type="button"
|
|
>
|
|
{directPartyTargeting ? 'Direct Targeting On' : 'Direct Targeting Off'}
|
|
</button>
|
|
<div className="controller-icon-options">
|
|
<span>Controller Icons</span>
|
|
{(['xbox', 'playstation', 'nintendo'] as const).map((style) => (
|
|
<button
|
|
aria-pressed={controllerIconStyle === style}
|
|
className={controllerIconStyle === style ? 'selected' : ''}
|
|
key={style}
|
|
onClick={() => setControllerIconStyle(style)}
|
|
type="button"
|
|
>
|
|
<ControllerStylePreview iconStyle={style} />
|
|
<span className="controller-style-name">{CONTROLLER_STYLE_LABELS[style]}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{settingsTab === 'bindings' && (
|
|
<section className="settings-bindings-panel settings-tab-panel">
|
|
<div className="settings-heading">
|
|
<div>
|
|
<p className="eyebrow">Input</p>
|
|
<h2>Keybindings</h2>
|
|
</div>
|
|
<p>Select an action, then press the new key or controller control.</p>
|
|
</div>
|
|
|
|
<div className="binding-tabs">
|
|
<button
|
|
className={device === 'controller' ? 'selected' : ''}
|
|
onClick={() => setDevice('controller')}
|
|
type="button"
|
|
>
|
|
Controller
|
|
</button>
|
|
<button
|
|
className={device === 'pc' ? 'selected' : ''}
|
|
onClick={() => setDevice('pc')}
|
|
type="button"
|
|
>
|
|
PC
|
|
</button>
|
|
</div>
|
|
|
|
<div className="binding-list">
|
|
{visibleActions.map((action) => (
|
|
<button
|
|
className={capture?.device === device && capture.action === action ? 'listening' : ''}
|
|
key={action}
|
|
onClick={() => beginCapture(device, action)}
|
|
type="button"
|
|
>
|
|
<span>{ACTION_LABELS[action]}</span>
|
|
<kbd>
|
|
{capture?.device === device && capture.action === action
|
|
? 'Press a control...'
|
|
: (
|
|
<ControllerBindingLabel
|
|
binding={bindings[device][action]}
|
|
iconStyle={controllerIconStyle}
|
|
/>
|
|
)}
|
|
</kbd>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<footer className="settings-footer">
|
|
<span>Bindings are saved automatically on this device.</span>
|
|
<button className="text-button" onClick={() => resetBindings(device)} type="button">
|
|
Reset {device === 'pc' ? 'PC' : 'Controller'} Defaults
|
|
</button>
|
|
</footer>
|
|
</section>
|
|
)}
|
|
|
|
{capture && (
|
|
<div className="binding-capture" role="dialog" aria-modal="true">
|
|
<div>
|
|
<p className="eyebrow">Remapping</p>
|
|
<h2>{ACTION_LABELS[capture.action]}</h2>
|
|
<p>
|
|
Press any {capture.device === 'pc' ? 'keyboard key' : 'controller button or move a stick'}.
|
|
</p>
|
|
<button onClick={cancelCapture} type="button">Cancel</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|