Android build v1.1.11

This commit is contained in:
Warren H
2026-07-02 23:03:52 -04:00
parent f00ad8655b
commit c6e2c61d0a
4 changed files with 243 additions and 37 deletions
Binary file not shown.
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.warren.iwanttoheal" applicationId "com.warren.iwanttoheal"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 89 versionCode 90
versionName "1.1.10" versionName "1.1.11"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+5
View File
@@ -70,6 +70,11 @@ html[data-input-device='controller'] [data-game-nav-active='true'] [tabindex]:fo
box-shadow: none; box-shadow: none;
} }
.game-selected {
outline: 3px solid #fff4a8 !important;
box-shadow: 0 0 0 5px #8b6726, 0 0 18px rgba(229, 185, 95, 0.65) !important;
}
.settings-heading { .settings-heading {
align-items: end; align-items: end;
border-bottom: 2px solid #34343d; border-bottom: 2px solid #34343d;
+221 -20
View File
@@ -63,6 +63,19 @@ const LAST_DIFFICULTY_KEY = 'i-want-to-heal:last-difficulty'
const SHOW_LEADERBOARDS = false const SHOW_LEADERBOARDS = false
const ACTIVITY_PAGE_SIZE = 4 const ACTIVITY_PAGE_SIZE = 4
const HOME_MENU_COLUMNS = 2 const HOME_MENU_COLUMNS = 2
const DUNGEON_NAV_COLUMNS = 2
type DungeonNavEntry =
| { kind: 'spacer'; disabled: true }
| { kind: 'back'; disabled?: boolean }
| { kind: 'pagePrev'; disabled?: boolean }
| { kind: 'pageNext'; disabled?: boolean }
| { kind: 'activity'; index: number; disabled?: boolean }
| { kind: 'tier'; index: number; disabled?: boolean }
| { kind: 'start'; disabled?: boolean }
| { kind: 'marathon'; disabled?: boolean }
| { kind: 'loot'; disabled?: boolean }
| { kind: 'lootSort'; disabled?: boolean }
function activityInitials(name: string) { function activityInitials(name: string) {
return name return name
@@ -117,6 +130,7 @@ function App() {
const [syncingCloud, setSyncingCloud] = useState(false) const [syncingCloud, setSyncingCloud] = useState(false)
const [syncMessage, setSyncMessage] = useState('') const [syncMessage, setSyncMessage] = useState('')
const [homeSelectedIndex, setHomeSelectedIndex] = useState(0) const [homeSelectedIndex, setHomeSelectedIndex] = useState(0)
const [dungeonSelectedIndex, setDungeonSelectedIndex] = useState(0)
useEffect(() => { useEffect(() => {
loadAuthSession() loadAuthSession()
@@ -356,11 +370,163 @@ function App() {
setScreen('roguelike') setScreen('roguelike')
return return
} }
if (item.screen === 'dungeons' || item.screen === 'raids') {
const nextOptions = item.screen === 'raids' ? raidOptions : dungeonOptions
setActivityPage(0)
setDungeonSelectedIndex(firstActivityDungeonEntryIndexForPageCount(
Math.max(1, Math.ceil(nextOptions.length / ACTIVITY_PAGE_SIZE)),
))
}
setScreen(item.screen) setScreen(item.screen)
} }
function dungeonEntries() {
const difficulty = selectedDifficultyOption ?? selectedActivityOption?.difficulties[0]
const locked = profile && difficulty ? profile.character.level < difficulty.unlockLevel : true
const entries: DungeonNavEntry[] = [
{ kind: 'back' },
]
if (activityPageCount > 1) {
entries.push(
{ kind: 'pagePrev', disabled: currentActivityPage === 0 },
{ kind: 'pageNext', disabled: currentActivityPage >= activityPageCount - 1 },
)
}
if (entries.length % DUNGEON_NAV_COLUMNS !== 0) {
entries.push({ kind: 'spacer', disabled: true })
}
pagedActivityOptions.forEach((candidate, index) => {
const candidateDifficulty = difficulty
? candidate.difficulties.find(
(option) => option.droppedItemLevel === difficulty.droppedItemLevel,
) ?? candidate.difficulties[0]
: candidate.difficulties[0]
entries.push({
kind: 'activity',
index,
disabled: !profile || profile.character.level < candidateDifficulty.unlockLevel,
})
})
tierOptions.forEach((difficultyOption, index) => {
entries.push({
kind: 'tier',
index,
disabled: !profile || profile.character.level < difficultyOption.unlockLevel,
})
})
entries.push(
{ kind: 'start', disabled: locked },
{ kind: 'marathon', disabled: locked },
{ kind: 'loot' },
)
if (showLoot) entries.push({ kind: 'lootSort' })
return entries
}
function firstEnabledDungeonEntry(entries: DungeonNavEntry[]) {
return Math.max(0, entries.findIndex((entry) => !entry.disabled))
}
function firstActivityDungeonEntryIndexForPageCount(pageCount: number) {
const entryCountBeforeActivities = pageCount > 1 ? 3 : 1
return entryCountBeforeActivities % DUNGEON_NAV_COLUMNS === 0
? entryCountBeforeActivities
: entryCountBeforeActivities + 1
}
function activeDungeonEntry(entries = dungeonEntries()) {
if (entries[dungeonSelectedIndex] && !entries[dungeonSelectedIndex].disabled) {
return entries[dungeonSelectedIndex]
}
const firstEnabled = firstEnabledDungeonEntry(entries)
return entries[firstEnabled] ?? entries[0]
}
function dungeonEntrySelected(entry: DungeonNavEntry['kind'], index?: number) {
const active = activeDungeonEntry()
return active?.kind === entry && ('index' in active ? active.index === index : index === undefined)
}
function moveDungeonSelection(action: string) {
const entries = dungeonEntries()
if (entries.length === 0) return
setDungeonSelectedIndex((current) => {
const bounded = entries[current] && !entries[current].disabled
? current
: firstEnabledDungeonEntry(entries)
const column = bounded % DUNGEON_NAV_COLUMNS
const direction = action === 'navigateLeft' || action === 'navigateUp' ? -1 : 1
const step = action === 'navigateUp' || action === 'navigateDown' ? DUNGEON_NAV_COLUMNS : 1
if (action === 'navigateLeft' && column === 0) return bounded
if (action === 'navigateRight' && column === DUNGEON_NAV_COLUMNS - 1) return bounded
for (let next = bounded + direction * step; next >= 0 && next < entries.length; next += direction * step) {
if (!entries[next].disabled) return next
}
return bounded
})
}
function selectActivityByPageIndex(index: number) {
const candidate = pagedActivityOptions[index]
if (!candidate) return
const difficulty = selectedDifficultyOption
? candidate.difficulties.find(
(option) => option.droppedItemLevel === selectedDifficultyOption.droppedItemLevel,
) ?? candidate.difficulties[0]
: candidate.difficulties[0]
if (profile && profile.character.level < difficulty.unlockLevel) return
if (screen === 'raids') setSelectedRaidId(candidate.id)
else setSelectedDungeonId(candidate.id)
setSelectedDifficultyId(difficulty.id)
}
function selectTierByIndex(index: number) {
const difficulty = tierOptions[index]
const activity = selectedActivityOption ?? activityOptions[0]
if (!difficulty || !activity || (profile && profile.character.level < difficulty.unlockLevel)) return
setActivityPage(0)
const nextActivity = activity.difficulties.some(
(candidate) => candidate.droppedItemLevel === difficulty.droppedItemLevel,
)
? activity
: activityOptions.find((option) =>
option.difficulties.some((candidate) => candidate.droppedItemLevel === difficulty.droppedItemLevel),
)
if (!nextActivity) return
if (screen === 'raids') setSelectedRaidId(nextActivity.id)
else setSelectedDungeonId(nextActivity.id)
const nextDifficulty = nextActivity.difficulties.find(
(candidate) => candidate.droppedItemLevel === difficulty.droppedItemLevel,
)
if (nextDifficulty) setSelectedDifficultyId(nextDifficulty.id)
}
function startSelectedRun(marathon: boolean) {
const activity = selectedActivityOption ?? activityOptions[0]
const difficulty = selectedDifficultyOption ?? activity?.difficulties[0]
if (!activity || !difficulty || (profile && profile.character.level < difficulty.unlockLevel)) return
setSelectedMarathonMode(marathon)
setCombatContentId(activity.id)
setSelectedDifficultyId(difficulty.id)
setScreen('combat')
}
function openDungeonEntry(entry: DungeonNavEntry | undefined) {
if (!entry || entry.disabled) return
if (entry.kind === 'back') setScreen('menu')
else if (entry.kind === 'pagePrev') setActivityPage((page) => Math.max(0, page - 1))
else if (entry.kind === 'pageNext') setActivityPage((page) => Math.min(activityPageCount - 1, page + 1))
else if (entry.kind === 'activity') selectActivityByPageIndex(entry.index)
else if (entry.kind === 'tier') selectTierByIndex(entry.index)
else if (entry.kind === 'start') startSelectedRun(false)
else if (entry.kind === 'marathon') startSelectedRun(true)
else if (entry.kind === 'loot') setShowLoot((current) => !current)
else if (entry.kind === 'lootSort') setLootSort((current) => current === 'sequence' ? 'boss' : 'sequence')
}
useGameAction((action, device) => { useGameAction((action, device) => {
if (device !== 'controller' || screen !== 'menu') return if (device !== 'controller') return
if (screen === 'menu') {
if (action === 'confirm') { if (action === 'confirm') {
openHomeMenuIndex(homeActiveIndex) openHomeMenuIndex(homeActiveIndex)
return return
@@ -379,6 +545,19 @@ function App() {
if (next < homeMenuEntryCount) return next if (next < homeMenuEntryCount) return next
return column > 0 ? homeMenuEntryCount - 1 : bounded return column > 0 ? homeMenuEntryCount - 1 : bounded
}) })
return
}
if (screen === 'dungeons' || screen === 'raids') {
if (action === 'back') {
setScreen('menu')
return
}
if (action === 'confirm') {
openDungeonEntry(activeDungeonEntry())
return
}
if (action.startsWith('navigate')) moveDungeonSelection(action)
}
}) })
if (error) { if (error) {
@@ -563,14 +742,7 @@ function App() {
data-controller-nav="skip" data-controller-nav="skip"
data-game-selected={homeActiveIndex === homeIndex ? 'true' : undefined} data-game-selected={homeActiveIndex === homeIndex ? 'true' : undefined}
key={item.screen} key={item.screen}
onClick={() => { onClick={() => openHomeMenuIndex(homeIndex)}
if (item.screen === 'pvp') {
setRoguelikeVariant('pvp')
setScreen('roguelike')
return
}
setScreen(item.screen)
}}
onPointerDown={() => setHomeSelectedIndex(homeIndex)} onPointerDown={() => setHomeSelectedIndex(homeIndex)}
type="button" type="button"
> >
@@ -787,7 +959,7 @@ function App() {
)} )}
{(screen === 'dungeons' || screen === 'raids') && ( {(screen === 'dungeons' || screen === 'raids') && (
<section className="content-screen dungeon-run-screen"> <section className="content-screen dungeon-run-screen" data-game-nav-active="true">
<div className="dungeon-run-board"> <div className="dungeon-run-board">
<div className="dungeon-run-main"> <div className="dungeon-run-main">
<article className="run-summary-card dungeon-focus-card"> <article className="run-summary-card dungeon-focus-card">
@@ -798,7 +970,15 @@ function App() {
<p className="eyebrow">Selected Run</p> <p className="eyebrow">Selected Run</p>
<div className="run-title-row"> <div className="run-title-row">
<h2>{activity.name}</h2> <h2>{activity.name}</h2>
<button className="back-button inline-back-button" onClick={() => setScreen('menu')} type="button">Back</button> <button
className={`back-button inline-back-button ${dungeonEntrySelected('back') ? 'game-selected' : ''}`}
data-controller-nav="skip"
onClick={() => setScreen('menu')}
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'back'))}
type="button"
>
Back
</button>
</div> </div>
<p>{activity.description}</p> <p>{activity.description}</p>
<div className="tag-row"> <div className="tag-row">
@@ -820,16 +1000,22 @@ function App() {
{activityPageCount > 1 ? ( {activityPageCount > 1 ? (
<div className="activity-pager" aria-label={`${screen === 'raids' ? 'Raid' : 'Dungeon'} pages`}> <div className="activity-pager" aria-label={`${screen === 'raids' ? 'Raid' : 'Dungeon'} pages`}>
<button <button
className={dungeonEntrySelected('pagePrev') ? 'game-selected' : ''}
data-controller-nav="skip"
disabled={currentActivityPage === 0} disabled={currentActivityPage === 0}
onClick={() => setActivityPage((page) => Math.max(0, page - 1))} onClick={() => setActivityPage((page) => Math.max(0, page - 1))}
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'pagePrev'))}
type="button" type="button"
> >
Prev Prev
</button> </button>
<span>{activityPageStart}-{activityPageEnd} of {activityOptions.length}</span> <span>{activityPageStart}-{activityPageEnd} of {activityOptions.length}</span>
<button <button
className={dungeonEntrySelected('pageNext') ? 'game-selected' : ''}
data-controller-nav="skip"
disabled={currentActivityPage >= activityPageCount - 1} disabled={currentActivityPage >= activityPageCount - 1}
onClick={() => setActivityPage((page) => Math.min(activityPageCount - 1, page + 1))} onClick={() => setActivityPage((page) => Math.min(activityPageCount - 1, page + 1))}
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'pageNext'))}
type="button" type="button"
> >
Next Next
@@ -840,7 +1026,7 @@ function App() {
)} )}
</div> </div>
<div className="activity-card-grid dungeon-choice-grid"> <div className="activity-card-grid dungeon-choice-grid">
{pagedActivityOptions.map((candidate) => { {pagedActivityOptions.map((candidate, index) => {
const difficulty = candidate.difficulties.find( const difficulty = candidate.difficulties.find(
(option) => option.droppedItemLevel === selectedDifficulty.droppedItemLevel, (option) => option.droppedItemLevel === selectedDifficulty.droppedItemLevel,
) ?? candidate.difficulties[0] ) ?? candidate.difficulties[0]
@@ -848,7 +1034,8 @@ function App() {
const selected = candidate.id === activity.id const selected = candidate.id === activity.id
return ( return (
<button <button
className={`activity-card ${selected ? 'selected' : ''} ${locked ? 'locked' : ''}`} className={`activity-card ${selected ? 'selected' : ''} ${locked ? 'locked' : ''} ${dungeonEntrySelected('activity', index) ? 'game-selected' : ''}`}
data-controller-nav="skip"
disabled={locked} disabled={locked}
key={candidate.id} key={candidate.id}
onClick={() => { onClick={() => {
@@ -856,6 +1043,7 @@ function App() {
else setSelectedDungeonId(candidate.id) else setSelectedDungeonId(candidate.id)
setSelectedDifficultyId(difficulty.id) setSelectedDifficultyId(difficulty.id)
}} }}
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'activity' && 'index' in entry && entry.index === index))}
type="button" type="button"
> >
<span className={`dungeon-art ${candidate.contentType === 'raid' ? 'raid-art' : ''}`}> <span className={`dungeon-art ${candidate.contentType === 'raid' ? 'raid-art' : ''}`}>
@@ -883,12 +1071,13 @@ function App() {
<small>{screen === 'raids' ? 'Raid' : 'Dungeon'} tiers unlock by level.</small> <small>{screen === 'raids' ? 'Raid' : 'Dungeon'} tiers unlock by level.</small>
</div> </div>
<div className="tier-grid"> <div className="tier-grid">
{tierOptions.map((difficulty) => { {tierOptions.map((difficulty, index) => {
const locked = profile.character.level < difficulty.unlockLevel const locked = profile.character.level < difficulty.unlockLevel
const selected = difficulty.droppedItemLevel === selectedDifficulty.droppedItemLevel const selected = difficulty.droppedItemLevel === selectedDifficulty.droppedItemLevel
return ( return (
<button <button
className={`${selected ? 'selected' : ''} ${locked ? 'locked' : ''}`} className={`${selected ? 'selected' : ''} ${locked ? 'locked' : ''} ${dungeonEntrySelected('tier', index) ? 'game-selected' : ''}`}
data-controller-nav="skip"
disabled={locked} disabled={locked}
key={difficulty.id} key={difficulty.id}
onClick={() => { onClick={() => {
@@ -909,6 +1098,7 @@ function App() {
if (nextDifficulty) setSelectedDifficultyId(nextDifficulty.id) if (nextDifficulty) setSelectedDifficultyId(nextDifficulty.id)
} }
}} }}
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'tier' && 'index' in entry && entry.index === index))}
type="button" type="button"
> >
<strong>iLvl {difficulty.droppedItemLevel}</strong> <strong>iLvl {difficulty.droppedItemLevel}</strong>
@@ -933,7 +1123,8 @@ function App() {
</div> </div>
<div className="part-picker"> <div className="part-picker">
<button <button
className="primary-button selected-part" className={`primary-button selected-part ${dungeonEntrySelected('start') ? 'game-selected' : ''}`}
data-controller-nav="skip"
disabled={difficultyLocked} disabled={difficultyLocked}
onClick={() => { onClick={() => {
setSelectedMarathonMode(false) setSelectedMarathonMode(false)
@@ -941,12 +1132,14 @@ function App() {
setSelectedDifficultyId(selectedDifficulty.id) setSelectedDifficultyId(selectedDifficulty.id)
setScreen('combat') setScreen('combat')
}} }}
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'start'))}
type="button" type="button"
> >
Start Hunt Start Hunt
</button> </button>
<button <button
className={`primary-button ${selectedMarathonMode ? 'selected-part' : ''}`} className={`primary-button ${selectedMarathonMode ? 'selected-part' : ''} ${dungeonEntrySelected('marathon') ? 'game-selected' : ''}`}
data-controller-nav="skip"
disabled={difficultyLocked} disabled={difficultyLocked}
onClick={() => { onClick={() => {
setSelectedMarathonMode(true) setSelectedMarathonMode(true)
@@ -954,6 +1147,7 @@ function App() {
setSelectedDifficultyId(selectedDifficulty.id) setSelectedDifficultyId(selectedDifficulty.id)
setScreen('combat') setScreen('combat')
}} }}
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'marathon'))}
type="button" type="button"
> >
Marathon Marathon
@@ -983,8 +1177,10 @@ function App() {
<h2>{selectedDifficulty.name} Loot Tables</h2> <h2>{selectedDifficulty.name} Loot Tables</h2>
</div> </div>
<button <button
className="text-button" className={`text-button ${dungeonEntrySelected('loot') ? 'game-selected' : ''}`}
data-controller-nav="skip"
onClick={() => setShowLoot((current) => !current)} onClick={() => setShowLoot((current) => !current)}
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'loot'))}
type="button" type="button"
> >
{showLoot ? 'Hide Loot' : 'View Loot'} {showLoot ? 'Hide Loot' : 'View Loot'}
@@ -993,9 +1189,14 @@ function App() {
{showLoot && ( {showLoot && (
<> <>
<div className="loot-toolbar"> <div className="loot-toolbar">
<label> <label className={dungeonEntrySelected('lootSort') ? 'game-selected' : ''}>
<span>Sort</span> <span>Sort</span>
<select value={lootSort} onChange={(event) => setLootSort(event.target.value as 'sequence' | 'boss')}> <select
data-controller-nav="skip"
onChange={(event) => setLootSort(event.target.value as 'sequence' | 'boss')}
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'lootSort'))}
value={lootSort}
>
<option value="sequence">Encounter order</option> <option value="sequence">Encounter order</option>
<option value="boss">Boss name</option> <option value="boss">Boss name</option>
</select> </select>