Android build v1.1.6
This commit is contained in:
+110
-26
@@ -130,10 +130,20 @@ const STORAGE_KEY = 'ashen-halls-input-bindings-v1'
|
||||
const PREFERENCES_STORAGE_KEY = 'ashen-halls-input-preferences-v1'
|
||||
const GAME_ACTION_EVENT = 'ashen-halls-game-action'
|
||||
const NATIVE_CONTROLLER_EVENT = 'ashen-halls-native-controller'
|
||||
const FOCUSABLE_SELECTOR = 'button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])'
|
||||
const FOCUSABLE_SELECTOR = 'button:not(:disabled):not([data-controller-nav="skip"]), input:not(:disabled):not([data-controller-nav="skip"]), select:not(:disabled):not([data-controller-nav="skip"]), textarea:not(:disabled):not([data-controller-nav="skip"]), [tabindex]:not([tabindex="-1"]):not([data-controller-nav="skip"])'
|
||||
const MAIN_CONTENT_SELECTOR = '.menu-screen, .content-screen, .dungeon-run-screen'
|
||||
const HEADER_CONTENT_SELECTOR = '.app-header'
|
||||
const GAMEPAD_COMBAT_POLL_MS = 1000 / 60
|
||||
const GAMEPAD_MENU_POLL_MS = 1000 / 30
|
||||
const GAMEPAD_BROWSER_DISCONNECTED_POLL_MS = 250
|
||||
const CONTROLLER_REPEAT_INITIAL_MS = 260
|
||||
const CONTROLLER_REPEAT_MS = 85
|
||||
const DPAD_NAV_ACTIONS: Partial<Record<string, InputAction>> = {
|
||||
Button12: 'navigateUp',
|
||||
Button13: 'navigateDown',
|
||||
Button14: 'navigateLeft',
|
||||
Button15: 'navigateRight',
|
||||
}
|
||||
|
||||
let lastControllerFocus: HTMLElement | null = null
|
||||
|
||||
@@ -256,6 +266,16 @@ function isVisible(element: HTMLElement) {
|
||||
return element.getClientRects().length > 0
|
||||
}
|
||||
|
||||
function focusableDescendants(scope: ParentNode) {
|
||||
return Array.from(
|
||||
scope.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),
|
||||
).filter(isVisible)
|
||||
}
|
||||
|
||||
function uniqueElements(elements: HTMLElement[]) {
|
||||
return elements.filter((element, index) => elements.indexOf(element) === index)
|
||||
}
|
||||
|
||||
function focusableElements() {
|
||||
const keyboard = document.querySelector<HTMLElement>('.controller-keyboard')
|
||||
const pauseMenu = document.querySelector<HTMLElement>('.pause-screen')
|
||||
@@ -264,10 +284,22 @@ function focusableElements() {
|
||||
'.result-screen, .binding-capture, .dual-startup-prompt',
|
||||
),
|
||||
).find(isVisible)
|
||||
const scope: ParentNode = keyboard ?? pauseMenu ?? dialog ?? document
|
||||
return Array.from(
|
||||
scope.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),
|
||||
const mainContent = Array.from(
|
||||
document.querySelectorAll<HTMLElement>(MAIN_CONTENT_SELECTOR),
|
||||
).find(isVisible)
|
||||
const overlay = keyboard ?? pauseMenu ?? dialog
|
||||
if (overlay) return focusableDescendants(overlay)
|
||||
const visibleHeaders = Array.from(
|
||||
document.querySelectorAll<HTMLElement>(HEADER_CONTENT_SELECTOR),
|
||||
).filter(isVisible)
|
||||
if (!mainContent) return visibleHeaders.length > 0 ? [] : focusableDescendants(document)
|
||||
const headerControls = visibleHeaders.flatMap(
|
||||
(header) => focusableDescendants(header),
|
||||
)
|
||||
return uniqueElements([
|
||||
...focusableDescendants(mainContent),
|
||||
...headerControls,
|
||||
])
|
||||
}
|
||||
|
||||
function rememberFocusableControl(element: HTMLElement) {
|
||||
@@ -291,6 +323,23 @@ function currentFocusableControl(candidates = focusableElements()) {
|
||||
return null
|
||||
}
|
||||
|
||||
function rangeDistance(startA: number, endA: number, startB: number, endB: number) {
|
||||
if (endA < startB) return startB - endA
|
||||
if (endB < startA) return startA - endB
|
||||
return 0
|
||||
}
|
||||
|
||||
function changeSelectOption(select: HTMLSelectElement, direction: -1 | 1) {
|
||||
const options = Array.from(select.options).filter((option) => !option.disabled)
|
||||
const currentIndex = options.findIndex((option) => option.index === select.selectedIndex)
|
||||
const nextOption = options[currentIndex + direction]
|
||||
if (!nextOption) return false
|
||||
select.selectedIndex = nextOption.index
|
||||
select.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
return true
|
||||
}
|
||||
|
||||
export function focusFirstControl() {
|
||||
const first = focusableElements()[0]
|
||||
if (first) focusControl(first)
|
||||
@@ -311,21 +360,48 @@ function moveFocus(action: InputAction) {
|
||||
const currentY = currentRect.top + currentRect.height / 2
|
||||
const vertical = action === 'navigateUp' || action === 'navigateDown'
|
||||
const direction = action === 'navigateUp' || action === 'navigateLeft' ? -1 : 1
|
||||
const currentIndex = candidates.indexOf(current)
|
||||
|
||||
const adjacent = candidates[currentIndex + direction]
|
||||
const fallbackToAdjacent = () => {
|
||||
if (adjacent) focusControl(adjacent)
|
||||
}
|
||||
|
||||
const ranked = candidates
|
||||
.filter((candidate) => candidate !== current)
|
||||
.map((candidate) => {
|
||||
.map((candidate, index) => {
|
||||
const rect = candidate.getBoundingClientRect()
|
||||
const x = rect.left + rect.width / 2
|
||||
const y = rect.top + rect.height / 2
|
||||
const primary = vertical ? y - currentY : x - currentX
|
||||
const secondary = vertical ? Math.abs(x - currentX) : Math.abs(y - currentY)
|
||||
return { candidate, primary, score: Math.abs(primary) + secondary * 2.5 }
|
||||
const primary = vertical
|
||||
? direction > 0
|
||||
? rect.top - currentRect.bottom
|
||||
: currentRect.top - rect.bottom
|
||||
: direction > 0
|
||||
? rect.left - currentRect.right
|
||||
: currentRect.left - rect.right
|
||||
const secondary = vertical
|
||||
? rangeDistance(currentRect.left, currentRect.right, rect.left, rect.right)
|
||||
: rangeDistance(currentRect.top, currentRect.bottom, rect.top, rect.bottom)
|
||||
return {
|
||||
candidate,
|
||||
index,
|
||||
primary,
|
||||
secondary,
|
||||
score: Math.max(0, primary) + secondary * 2.5 + (vertical ? Math.abs(x - currentX) : Math.abs(y - currentY)) * 0.1,
|
||||
}
|
||||
})
|
||||
.filter(({ primary }) => Math.sign(primary) === direction)
|
||||
.sort((a, b) => a.score - b.score)
|
||||
.filter(({ primary }) => primary >= -4)
|
||||
const sameRowHorizontal = !vertical
|
||||
? ranked.filter(({ secondary }) => secondary === 0)
|
||||
: ranked
|
||||
if (sameRowHorizontal.length === 0) {
|
||||
fallbackToAdjacent()
|
||||
return
|
||||
}
|
||||
sameRowHorizontal.sort((a, b) => a.score - b.score || a.secondary - b.secondary || a.index - b.index)
|
||||
|
||||
const next = ranked[0]?.candidate
|
||||
const next = sameRowHorizontal[0]?.candidate
|
||||
if (!next) return
|
||||
focusControl(next)
|
||||
}
|
||||
@@ -524,12 +600,26 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
setTouchLockMessage('')
|
||||
}, 1400)
|
||||
} else if (action.startsWith('navigate')) {
|
||||
if (uiOverlay || !combatActive) moveFocus(action)
|
||||
if (uiOverlay || !combatActive) {
|
||||
const active = currentFocusableControl()
|
||||
if (
|
||||
active instanceof HTMLSelectElement
|
||||
&& (action === 'navigateUp' || action === 'navigateDown')
|
||||
&& changeSelectOption(active, action === 'navigateUp' ? -1 : 1)
|
||||
) {
|
||||
return
|
||||
}
|
||||
moveFocus(action)
|
||||
}
|
||||
} else if (action === 'confirm') {
|
||||
const active = currentFocusableControl()
|
||||
if (isTextInput(active)) {
|
||||
setKeyboardInput(active)
|
||||
window.requestAnimationFrame(() => focusFirstControl())
|
||||
} else if (active instanceof HTMLSelectElement) {
|
||||
const select = active as HTMLSelectElement & { showPicker?: () => void }
|
||||
if (select.showPicker) select.showPicker()
|
||||
else active.click()
|
||||
} else if (
|
||||
active
|
||||
&& active.matches('button:not(:disabled), [role="button"]')
|
||||
@@ -566,12 +656,6 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
document.querySelector('[data-combat-active="true"]'),
|
||||
)
|
||||
const uiOverlay = hasUiOverlay()
|
||||
const menuDpadActions: Partial<Record<string, InputAction>> = {
|
||||
Button12: 'navigateUp',
|
||||
Button13: 'navigateDown',
|
||||
Button14: 'navigateLeft',
|
||||
Button15: 'navigateRight',
|
||||
}
|
||||
const uiPriority = [
|
||||
'navigateUp',
|
||||
'navigateDown',
|
||||
@@ -608,18 +692,18 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
'navigateLeft',
|
||||
'navigateRight',
|
||||
] satisfies InputAction[]
|
||||
const action = menuDpadActions[token] && (!combatActive || uiOverlay)
|
||||
? menuDpadActions[token]
|
||||
const action = DPAD_NAV_ACTIONS[token] && (!combatActive || uiOverlay)
|
||||
? DPAD_NAV_ACTIONS[token]
|
||||
: uiOverlay
|
||||
? uiPriority.find((candidate) => bindingsRef.current.controller[candidate] === token)
|
||||
: combatActive && preferencesRef.current.directPartyTargeting
|
||||
? [...directTargetActions, ...combatPriority].find(
|
||||
(candidate) => bindingsRef.current.controller[candidate] === token,
|
||||
)
|
||||
: combatActive && menuDpadActions[token]
|
||||
? menuDpadActions[token]
|
||||
: !combatActive && menuDpadActions[token]
|
||||
? menuDpadActions[token]
|
||||
: combatActive && DPAD_NAV_ACTIONS[token]
|
||||
? DPAD_NAV_ACTIONS[token]
|
||||
: !combatActive && DPAD_NAV_ACTIONS[token]
|
||||
? DPAD_NAV_ACTIONS[token]
|
||||
: (combatActive ? combatPriority : INPUT_ACTIONS).find(
|
||||
(candidate) => bindingsRef.current.controller[candidate] === token,
|
||||
)
|
||||
@@ -768,11 +852,11 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
||||
const action = INPUT_ACTIONS.find(
|
||||
(candidate) => bindingsRef.current.controller[candidate] === token,
|
||||
)
|
||||
const canRepeat = action?.startsWith('navigate') ?? false
|
||||
const canRepeat = Boolean(action?.startsWith('navigate') || DPAD_NAV_ACTIONS[token])
|
||||
const nextRepeat = repeatRef.current[token] ?? 0
|
||||
if (pressed || (canRepeat && time >= nextRepeat)) {
|
||||
dispatchControllerToken(token, !pressed)
|
||||
repeatRef.current[token] = time + (pressed ? 360 : 125)
|
||||
repeatRef.current[token] = time + (pressed ? CONTROLLER_REPEAT_INITIAL_MS : CONTROLLER_REPEAT_MS)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user