Android build v1.1.10

This commit is contained in:
Warren H
2026-07-02 22:42:48 -04:00
parent 6560a049f2
commit f00ad8655b
6 changed files with 310 additions and 59 deletions
+49 -15
View File
@@ -55,6 +55,21 @@ textarea:focus-visible,
box-shadow: 0 0 0 5px #8b6726, 0 0 18px rgba(229, 185, 95, 0.65);
}
html[data-input-device='controller'] button:focus,
html[data-input-device='controller'] input:focus,
html[data-input-device='controller'] select:focus,
html[data-input-device='controller'] textarea:focus,
html[data-input-device='controller'] [tabindex]:focus {
outline: 3px solid #fff4a8 !important;
box-shadow: 0 0 0 5px #8b6726, 0 0 18px rgba(229, 185, 95, 0.65);
}
html[data-input-device='controller'] [data-game-nav-active='true'] button:focus:not(.game-selected),
html[data-input-device='controller'] [data-game-nav-active='true'] [tabindex]:focus:not(.game-selected) {
outline: 2px solid #42414c !important;
box-shadow: none;
}
.settings-heading {
align-items: end;
border-bottom: 2px solid #34343d;
@@ -1849,10 +1864,13 @@ h2 {
.main-menu-grid {
display: grid;
gap: 15px;
gap: 10px;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: repeat(4, minmax(0, 1fr));
height: min(100%, 388px);
margin: 0 auto;
max-width: 930px;
max-width: 900px;
width: 100%;
}
.roguelike-mode-grid {
@@ -1901,24 +1919,33 @@ h2 {
color: var(--ink);
cursor: pointer;
display: flex;
gap: 16px;
min-height: 105px;
gap: 12px;
min-height: 0;
outline: 2px solid #42414c;
padding: 16px;
padding: 12px;
text-align: left;
}
.menu-card:first-child {
grid-column: 1 / -1;
}
.menu-card:hover {
outline-color: var(--gold);
transform: translateY(-2px);
}
.menu-card.game-selected {
outline: 3px solid #fff4a8;
box-shadow: 0 0 0 5px #8b6726, 0 0 18px rgba(229, 185, 95, 0.65);
}
.menu-card.game-selected > span {
background: var(--gold);
color: #13141a;
}
.cloud-sync-card {
cursor: default;
display: grid;
gap: 10px;
grid-template-columns: auto minmax(0, 1fr);
justify-content: space-between;
}
@@ -1931,6 +1958,13 @@ h2 {
display: grid;
flex: 1;
gap: 6px;
min-width: 0;
}
.cloud-sync-card .text-button {
grid-column: 1 / -1;
min-height: 28px;
padding: 5px 8px;
}
.cloud-sync-card .text-button:disabled {
@@ -1948,10 +1982,10 @@ h2 {
border: 2px solid var(--gold);
color: var(--gold);
display: flex;
flex: 0 0 58px;
flex: 0 0 48px;
font-family: var(--pixel-font);
font-size: 19px;
height: 58px;
font-size: 16px;
height: 48px;
justify-content: center;
}
@@ -1962,13 +1996,13 @@ h2 {
.menu-card strong {
font-family: var(--pixel-font);
font-size: 11px;
margin-bottom: 9px;
font-size: 10px;
margin-bottom: 5px;
}
.menu-card small {
color: var(--muted);
font-size: 18px;
font-size: 14px;
line-height: 1.05;
}
+80 -27
View File
@@ -19,7 +19,7 @@ import {
syncCloudSave,
type GameMode,
} from './gameRepository'
import { focusFirstControl } from './input.tsx'
import { focusFirstControl, useGameAction } from './input.tsx'
import { barFillStyle } from './components/barStyles'
const CombatScreen = lazy(() => import('./components/CombatScreen').then((module) => ({ default: module.CombatScreen })))
@@ -62,6 +62,7 @@ const MENU_ITEMS: Array<{
const LAST_DIFFICULTY_KEY = 'i-want-to-heal:last-difficulty'
const SHOW_LEADERBOARDS = false
const ACTIVITY_PAGE_SIZE = 4
const HOME_MENU_COLUMNS = 2
function activityInitials(name: string) {
return name
@@ -115,6 +116,7 @@ function App() {
const [error, setError] = useState('')
const [syncingCloud, setSyncingCloud] = useState(false)
const [syncMessage, setSyncMessage] = useState('')
const [homeSelectedIndex, setHomeSelectedIndex] = useState(0)
useEffect(() => {
loadAuthSession()
@@ -336,6 +338,49 @@ function App() {
}
}
const cloudSync = getCloudSyncStatus()
const canShowCloudSync = Boolean(account && account.id !== -1 && cloudSync.available)
const homeMenuOffset = canShowCloudSync ? 1 : 0
const homeMenuEntryCount = MENU_ITEMS.length + homeMenuOffset
const homeActiveIndex = Math.min(homeSelectedIndex, homeMenuEntryCount - 1)
function openHomeMenuIndex(index: number) {
if (canShowCloudSync && index === 0) {
if (!syncingCloud && cloudSync.dirty) void syncSaveNow()
return
}
const item = MENU_ITEMS[index - homeMenuOffset]
if (!item) return
if (item.screen === 'pvp') {
setRoguelikeVariant('pvp')
setScreen('roguelike')
return
}
setScreen(item.screen)
}
useGameAction((action, device) => {
if (device !== 'controller' || screen !== 'menu') return
if (action === 'confirm') {
openHomeMenuIndex(homeActiveIndex)
return
}
if (!action.startsWith('navigate')) return
setHomeSelectedIndex((current) => {
const bounded = Math.min(current, homeMenuEntryCount - 1)
const column = bounded % HOME_MENU_COLUMNS
if (action === 'navigateLeft') return column > 0 ? bounded - 1 : bounded
if (action === 'navigateRight') {
const next = bounded + 1
return column < HOME_MENU_COLUMNS - 1 && next < homeMenuEntryCount ? next : bounded
}
if (action === 'navigateUp') return bounded >= HOME_MENU_COLUMNS ? bounded - HOME_MENU_COLUMNS : bounded
const next = bounded + HOME_MENU_COLUMNS
if (next < homeMenuEntryCount) return next
return column > 0 ? homeMenuEntryCount - 1 : bounded
})
})
if (error) {
return (
<main className="game-shell">
@@ -455,9 +500,6 @@ function App() {
const activity = selectedActivityOption ?? dungeon
const selectedDifficulty = selectedDifficultyOption ?? activity.difficulties[0]
const difficultyLocked = profile.character.level < selectedDifficulty.unlockLevel
const cloudSync = getCloudSyncStatus()
const canShowCloudSync = account.id !== -1 && cloudSync.available
return (
<main className={`game-shell ${screen === 'dungeons' || screen === 'raids' ? 'dungeon-shell' : ''} ${screen === 'customize' ? 'workshop-shell' : ''} ${screen === 'settings' ? 'settings-shell' : ''}`}>
{screen !== 'hunter-profile' && (
@@ -484,10 +526,14 @@ function App() {
)}
{screen === 'menu' && (
<section className="menu-screen">
<section className="menu-screen" data-game-nav-active="true">
<div className="main-menu-grid">
{canShowCloudSync && (
<div className="menu-card cloud-sync-card">
<div
className={`menu-card cloud-sync-card ${homeActiveIndex === 0 ? 'game-selected' : ''}`}
data-game-selected={homeActiveIndex === 0 ? 'true' : undefined}
onPointerDown={() => setHomeSelectedIndex(0)}
>
<span>{cloudSync.dirty ? 'S' : 'C'}</span>
<div>
<strong>Cloud Save</strong>
@@ -500,6 +546,7 @@ function App() {
</div>
<button
className="text-button"
data-controller-nav="skip"
disabled={syncingCloud || !cloudSync.dirty}
onClick={syncSaveNow}
type="button"
@@ -508,27 +555,33 @@ function App() {
</button>
</div>
)}
{MENU_ITEMS.map((item) => (
<button
className="menu-card"
key={item.screen}
onClick={() => {
if (item.screen === 'pvp') {
setRoguelikeVariant('pvp')
setScreen('roguelike')
return
}
setScreen(item.screen)
}}
type="button"
>
<span>{item.glyph}</span>
<div>
<strong>{item.label}</strong>
<small>{item.description}</small>
</div>
</button>
))}
{MENU_ITEMS.map((item, index) => {
const homeIndex = index + homeMenuOffset
return (
<button
className={`menu-card ${homeActiveIndex === homeIndex ? 'game-selected' : ''}`}
data-controller-nav="skip"
data-game-selected={homeActiveIndex === homeIndex ? 'true' : undefined}
key={item.screen}
onClick={() => {
if (item.screen === 'pvp') {
setRoguelikeVariant('pvp')
setScreen('roguelike')
return
}
setScreen(item.screen)
}}
onPointerDown={() => setHomeSelectedIndex(homeIndex)}
type="button"
>
<span>{item.glyph}</span>
<div>
<strong>{item.label}</strong>
<small>{item.description}</small>
</div>
</button>
)
})}
</div>
</section>
)}
+40 -14
View File
@@ -312,10 +312,17 @@ function focusControl(element: HTMLElement) {
element.scrollIntoView({ block: 'nearest', inline: 'nearest' })
}
function currentFocusableControl(candidates = focusableElements()) {
function currentFocusableControl(candidates = focusableElements(), preferControllerFocus = false) {
if (
preferControllerFocus
&& lastControllerFocus
&& candidates.includes(lastControllerFocus)
&& isVisible(lastControllerFocus)
) {
return lastControllerFocus
}
const active = document.activeElement
if (active instanceof HTMLElement && candidates.includes(active)) {
rememberFocusableControl(active)
return active
}
if (lastControllerFocus && candidates.includes(lastControllerFocus) && isVisible(lastControllerFocus)) {
@@ -347,10 +354,10 @@ export function focusFirstControl() {
return first
}
function moveFocus(action: InputAction) {
function moveFocus(action: InputAction, preferControllerFocus = false) {
const candidates = focusableElements()
if (candidates.length === 0) return
const current = currentFocusableControl(candidates)
const current = currentFocusableControl(candidates, preferControllerFocus)
if (!current) {
focusFirstControl()
return
@@ -415,6 +422,18 @@ function hasUiOverlay() {
).some(isVisible)
}
function hasDedicatedGameNavigation() {
return Array.from(
document.querySelectorAll<HTMLElement>('[data-game-nav-active="true"]'),
).some(isVisible)
}
function dispatchGameAction(action: InputAction, device: InputDevice) {
window.dispatchEvent(new CustomEvent(GAME_ACTION_EVENT, {
detail: { action, device },
}))
}
const BUTTON_LABELS: Record<number, string> = {
0: 'A / Cross',
1: 'B / Circle',
@@ -585,10 +604,17 @@ export function InputProvider({ children }: { children: ReactNode }) {
const dispatchAction = useCallback((action: InputAction, device: InputDevice) => {
const uiOverlay = hasUiOverlay()
const combatActive = Boolean(document.querySelector('[data-combat-active="true"]'))
const controllerUiInput = device === 'controller' && (uiOverlay || !combatActive)
const dedicatedNavAction = action.startsWith('navigate') || action === 'confirm' || action === 'back'
setLastDevice(device)
document.documentElement.dataset.inputDevice = device
if (controllerUiInput && dedicatedNavAction && hasDedicatedGameNavigation()) {
dispatchGameAction(action, device)
return
}
if (action === 'toggleTouchLock') {
setPreferences((current) => ({
...current,
@@ -602,22 +628,24 @@ export function InputProvider({ children }: { children: ReactNode }) {
}, 1400)
} else if (action.startsWith('navigate')) {
if (uiOverlay || !combatActive) {
const active = currentFocusableControl()
const active = currentFocusableControl(focusableElements(), controllerUiInput)
if (
active instanceof HTMLSelectElement
&& (action === 'navigateUp' || action === 'navigateDown')
&& changeSelectOption(active, action === 'navigateUp' ? -1 : 1)
) {
if (controllerUiInput) focusControl(active)
return
}
moveFocus(action)
moveFocus(action, controllerUiInput)
}
} else if (action === 'confirm') {
const active = currentFocusableControl()
const active = currentFocusableControl(focusableElements(), controllerUiInput)
if (isTextInput(active)) {
setKeyboardInput(active)
window.requestAnimationFrame(() => focusFirstControl())
} else if (active instanceof HTMLSelectElement) {
if (controllerUiInput) focusControl(active)
const select = active as HTMLSelectElement & { showPicker?: () => void }
if (select.showPicker) select.showPicker()
else active.click()
@@ -626,6 +654,7 @@ export function InputProvider({ children }: { children: ReactNode }) {
&& active.matches('button:not(:disabled), [role="button"]')
&& isVisible(active)
) {
if (controllerUiInput) focusControl(active)
active.click()
} else {
focusFirstControl()
@@ -641,9 +670,7 @@ export function InputProvider({ children }: { children: ReactNode }) {
}
}
window.dispatchEvent(new CustomEvent(GAME_ACTION_EVENT, {
detail: { action, device },
}))
dispatchGameAction(action, device)
}, [closeKeyboard])
const dispatchControllerToken = useCallback((token: string, repeat = false) => {
@@ -743,6 +770,7 @@ export function InputProvider({ children }: { children: ReactNode }) {
const target = event.target
if (!(target instanceof HTMLElement)) return
if (!target.matches(FOCUSABLE_SELECTOR) || !isVisible(target)) return
if (document.documentElement.dataset.inputDevice !== 'controller') return
rememberFocusableControl(target)
}
const onPointerDown = (event: PointerEvent) => {
@@ -753,7 +781,7 @@ export function InputProvider({ children }: { children: ReactNode }) {
if (!(target instanceof Element)) return
const control = target.closest<HTMLElement>(FOCUSABLE_SELECTOR)
if (!control || !isVisible(control)) return
rememberFocusableControl(control)
if (event.pointerType !== 'touch') rememberFocusableControl(control)
}
document.addEventListener('focusin', onFocusIn)
document.addEventListener('pointerdown', onPointerDown, { capture: true })
@@ -1042,7 +1070,5 @@ export function dispatchExternalGameAction(
action: InputAction,
device: InputDevice,
) {
window.dispatchEvent(new CustomEvent(GAME_ACTION_EVENT, {
detail: { action, device },
}))
dispatchGameAction(action, device)
}