Android build v1.1.10
This commit is contained in:
+49
-15
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user