Android build v1.1.5
This commit is contained in:
+114
-9
@@ -37,6 +37,7 @@ export const INPUT_ACTIONS = [
|
||||
'targetParty6',
|
||||
'toggleTargetGroup',
|
||||
'toggleSpeed',
|
||||
'toggleTouchLock',
|
||||
'pause',
|
||||
] as const
|
||||
|
||||
@@ -66,6 +67,7 @@ export const ACTION_LABELS: Record<InputAction, string> = {
|
||||
targetParty6: 'Target Party Member 6',
|
||||
toggleTargetGroup: 'Switch Raid Target Group',
|
||||
toggleSpeed: 'Toggle 2x Speed',
|
||||
toggleTouchLock: 'Toggle Combat Touch Lock',
|
||||
pause: 'Pause Menu',
|
||||
}
|
||||
|
||||
@@ -93,6 +95,7 @@ export const DEFAULT_BINDINGS: Record<InputDevice, InputBindings> = {
|
||||
targetParty6: 'F6',
|
||||
toggleTargetGroup: 'Tab',
|
||||
toggleSpeed: 'Backquote',
|
||||
toggleTouchLock: 'F7',
|
||||
pause: 'Escape',
|
||||
},
|
||||
controller: {
|
||||
@@ -115,9 +118,10 @@ export const DEFAULT_BINDINGS: Record<InputDevice, InputBindings> = {
|
||||
targetParty3: 'Button15',
|
||||
targetParty4: 'Button13',
|
||||
targetParty5: 'Button4',
|
||||
targetParty6: 'Button10',
|
||||
toggleTargetGroup: 'Button6',
|
||||
targetParty6: 'Button6',
|
||||
toggleTargetGroup: 'Button8',
|
||||
toggleSpeed: 'Button11',
|
||||
toggleTouchLock: 'Button10',
|
||||
pause: 'Button9',
|
||||
},
|
||||
}
|
||||
@@ -144,11 +148,13 @@ type InputContextValue = {
|
||||
lastDevice: InputDevice
|
||||
controllerIconStyle: ControllerIconStyle
|
||||
directPartyTargeting: boolean
|
||||
combatTouchLocked: boolean
|
||||
beginCapture: (device: InputDevice, action: InputAction) => void
|
||||
cancelCapture: () => void
|
||||
resetBindings: (device: InputDevice) => void
|
||||
setControllerIconStyle: (style: ControllerIconStyle) => void
|
||||
setDirectPartyTargeting: (enabled: boolean) => void
|
||||
setCombatTouchLocked: (locked: boolean) => void
|
||||
}
|
||||
|
||||
const InputContext = createContext<InputContextValue | null>(null)
|
||||
@@ -187,6 +193,21 @@ function loadBindings(): Record<InputDevice, InputBindings> {
|
||||
if (savedController?.targetParty6 === 'Button11') {
|
||||
controller.targetParty6 = DEFAULT_BINDINGS.controller.targetParty6
|
||||
}
|
||||
if (
|
||||
savedController?.targetParty6 === undefined
|
||||
|| savedController.targetParty6 === 'Button10'
|
||||
) {
|
||||
controller.targetParty6 = DEFAULT_BINDINGS.controller.targetParty6
|
||||
}
|
||||
if (
|
||||
savedController?.toggleTargetGroup === undefined
|
||||
|| savedController.toggleTargetGroup === 'Button6'
|
||||
) {
|
||||
controller.toggleTargetGroup = DEFAULT_BINDINGS.controller.toggleTargetGroup
|
||||
}
|
||||
if (savedController?.toggleTouchLock === undefined) {
|
||||
controller.toggleTouchLock = DEFAULT_BINDINGS.controller.toggleTouchLock
|
||||
}
|
||||
return {
|
||||
pc: { ...DEFAULT_BINDINGS.pc, ...saved.pc },
|
||||
controller,
|
||||
@@ -201,15 +222,18 @@ function loadPreferences() {
|
||||
const saved = JSON.parse(localStorage.getItem(PREFERENCES_STORAGE_KEY) ?? '{}') as {
|
||||
controllerIconStyle?: ControllerIconStyle
|
||||
directPartyTargeting?: boolean
|
||||
combatTouchLocked?: boolean
|
||||
}
|
||||
return {
|
||||
controllerIconStyle: saved.controllerIconStyle ?? 'xbox',
|
||||
directPartyTargeting: saved.directPartyTargeting ?? false,
|
||||
combatTouchLocked: saved.combatTouchLocked ?? Capacitor.isNativePlatform(),
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
controllerIconStyle: 'xbox' as ControllerIconStyle,
|
||||
directPartyTargeting: false,
|
||||
combatTouchLocked: Capacitor.isNativePlatform(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,6 +247,7 @@ function bindingGroup(action: InputAction) {
|
||||
if (action.startsWith('targetParty') || action === 'toggleTargetGroup') return 'direct-targeting'
|
||||
if (action === 'previousTarget' || action === 'nextTarget') return 'relative-targeting'
|
||||
if (action === 'pause') return 'pause'
|
||||
if (action === 'toggleTouchLock' || action === 'toggleSpeed') return 'system'
|
||||
return 'navigation'
|
||||
}
|
||||
|
||||
@@ -428,6 +453,7 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
const [preferences, setPreferences] = useState(loadPreferences)
|
||||
const [keyboardInput, setKeyboardInput] = useState<HTMLInputElement | HTMLTextAreaElement | null>(null)
|
||||
const [keyboardShift, setKeyboardShift] = useState(false)
|
||||
const [touchLockMessage, setTouchLockMessage] = useState('')
|
||||
const bindingsRef = useRef(bindings)
|
||||
const preferencesRef = useRef(preferences)
|
||||
const captureRef = useRef(capture)
|
||||
@@ -435,6 +461,7 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
const previousTokensRef = useRef(new Set<string>())
|
||||
const repeatRef = useRef<Record<string, number>>({})
|
||||
const gamepadConnectedRef = useRef(Capacitor.isNativePlatform())
|
||||
const touchLockMessageTimerRef = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
bindingsRef.current = bindings
|
||||
@@ -485,7 +512,18 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
setLastDevice(device)
|
||||
document.documentElement.dataset.inputDevice = device
|
||||
|
||||
if (action.startsWith('navigate')) {
|
||||
if (action === 'toggleTouchLock') {
|
||||
setPreferences((current) => ({
|
||||
...current,
|
||||
combatTouchLocked: !current.combatTouchLocked,
|
||||
}))
|
||||
window.clearTimeout(touchLockMessageTimerRef.current)
|
||||
const nextLocked = !preferencesRef.current.combatTouchLocked
|
||||
setTouchLockMessage(`Combat touch ${nextLocked ? 'locked' : 'unlocked'}`)
|
||||
touchLockMessageTimerRef.current = window.setTimeout(() => {
|
||||
setTouchLockMessage('')
|
||||
}, 1400)
|
||||
} else if (action.startsWith('navigate')) {
|
||||
if (uiOverlay || !combatActive) moveFocus(action)
|
||||
} else if (action === 'confirm') {
|
||||
const active = currentFocusableControl()
|
||||
@@ -551,8 +589,10 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
'targetParty6',
|
||||
'toggleTargetGroup',
|
||||
'toggleSpeed',
|
||||
'toggleTouchLock',
|
||||
] satisfies InputAction[]
|
||||
const combatPriority = [
|
||||
'toggleTouchLock',
|
||||
'pause',
|
||||
'toggleSpeed',
|
||||
'ability1',
|
||||
@@ -621,7 +661,9 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
rememberFocusableControl(target)
|
||||
}
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
document.documentElement.dataset.inputDevice = 'pc'
|
||||
if (event.pointerType !== 'touch') {
|
||||
document.documentElement.dataset.inputDevice = 'pc'
|
||||
}
|
||||
const target = event.target
|
||||
if (!(target instanceof Element)) return
|
||||
const control = target.closest<HTMLElement>(FOCUSABLE_SELECTOR)
|
||||
@@ -645,8 +687,14 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
return () => window.removeEventListener(NATIVE_CONTROLLER_EVENT, listener)
|
||||
}, [dispatchControllerToken])
|
||||
|
||||
useEffect(() => () => {
|
||||
window.clearTimeout(touchLockMessageTimerRef.current)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let focusFrame = 0
|
||||
const ensureFocus = () => {
|
||||
focusFrame = 0
|
||||
const combatActive = document.querySelector('[data-combat-active="true"]')
|
||||
if (combatActive) return
|
||||
const candidates = focusableElements()
|
||||
@@ -663,22 +711,37 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
}
|
||||
}
|
||||
const scheduleEnsureFocus = () => {
|
||||
if (focusFrame) return
|
||||
focusFrame = window.requestAnimationFrame(ensureFocus)
|
||||
}
|
||||
const observer = new MutationObserver(() => {
|
||||
window.requestAnimationFrame(ensureFocus)
|
||||
scheduleEnsureFocus()
|
||||
})
|
||||
observer.observe(document.getElementById('root') ?? document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['aria-hidden', 'class', 'disabled', 'hidden', 'style'],
|
||||
attributeFilter: ['aria-hidden', 'class', 'disabled', 'hidden'],
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
window.requestAnimationFrame(ensureFocus)
|
||||
return () => observer.disconnect()
|
||||
scheduleEnsureFocus()
|
||||
return () => {
|
||||
if (focusFrame) window.cancelAnimationFrame(focusFrame)
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let timer = 0
|
||||
if (Capacitor.isNativePlatform() && capture?.device !== 'controller') {
|
||||
previousTokensRef.current = new Set()
|
||||
repeatRef.current = {}
|
||||
return undefined
|
||||
}
|
||||
const nextDelay = () => {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
return GAMEPAD_COMBAT_POLL_MS
|
||||
}
|
||||
if (!Capacitor.isNativePlatform() && !gamepadConnectedRef.current) {
|
||||
return GAMEPAD_BROWSER_DISCONNECTED_POLL_MS
|
||||
}
|
||||
@@ -735,7 +798,39 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
window.removeEventListener('gamepadconnected', onGamepadConnected)
|
||||
window.removeEventListener('gamepaddisconnected', onGamepadDisconnected)
|
||||
}
|
||||
}, [assignBinding, dispatchControllerToken])
|
||||
}, [assignBinding, capture, dispatchControllerToken])
|
||||
|
||||
useEffect(() => {
|
||||
const shouldBlockTouch = () => (
|
||||
Capacitor.isNativePlatform()
|
||||
&& preferencesRef.current.combatTouchLocked
|
||||
&& isCombatActive()
|
||||
&& !hasUiOverlay()
|
||||
)
|
||||
const blockTouch = (event: Event) => {
|
||||
if (
|
||||
typeof PointerEvent !== 'undefined'
|
||||
&& event instanceof PointerEvent
|
||||
&& event.pointerType !== 'touch'
|
||||
) return
|
||||
if (!shouldBlockTouch()) return
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
const options = { capture: true, passive: false }
|
||||
document.addEventListener('pointerdown', blockTouch, options)
|
||||
document.addEventListener('pointerup', blockTouch, options)
|
||||
document.addEventListener('touchstart', blockTouch, options)
|
||||
document.addEventListener('touchmove', blockTouch, options)
|
||||
document.addEventListener('touchend', blockTouch, options)
|
||||
return () => {
|
||||
document.removeEventListener('pointerdown', blockTouch, options)
|
||||
document.removeEventListener('pointerup', blockTouch, options)
|
||||
document.removeEventListener('touchstart', blockTouch, options)
|
||||
document.removeEventListener('touchmove', blockTouch, options)
|
||||
document.removeEventListener('touchend', blockTouch, options)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const contextValue = useMemo<InputContextValue>(() => ({
|
||||
bindings,
|
||||
@@ -743,6 +838,7 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
lastDevice,
|
||||
controllerIconStyle: preferences.controllerIconStyle,
|
||||
directPartyTargeting: preferences.directPartyTargeting,
|
||||
combatTouchLocked: preferences.combatTouchLocked,
|
||||
beginCapture: (device, action) => setCapture({ device, action }),
|
||||
cancelCapture: () => setCapture(null),
|
||||
resetBindings: (device) => setBindings((current) => ({
|
||||
@@ -757,6 +853,10 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
...current,
|
||||
directPartyTargeting,
|
||||
})),
|
||||
setCombatTouchLocked: (combatTouchLocked) => setPreferences((current) => ({
|
||||
...current,
|
||||
combatTouchLocked,
|
||||
})),
|
||||
}), [bindings, capture, lastDevice, preferences])
|
||||
|
||||
function typeKeyboardKey(key: string) {
|
||||
@@ -821,6 +921,11 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
{touchLockMessage && (
|
||||
<div className="combat-touch-lock-status" aria-live="polite">
|
||||
{touchLockMessage}
|
||||
</div>
|
||||
)}
|
||||
</InputContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user