Android build v1.1.11
This commit is contained in:
Binary file not shown.
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "com.warren.iwanttoheal"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 89
|
||||
versionName "1.1.10"
|
||||
versionCode 90
|
||||
versionName "1.1.11"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -70,6 +70,11 @@ html[data-input-device='controller'] [data-game-nav-active='true'] [tabindex]:fo
|
||||
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 {
|
||||
align-items: end;
|
||||
border-bottom: 2px solid #34343d;
|
||||
|
||||
+236
-35
@@ -63,6 +63,19 @@ const LAST_DIFFICULTY_KEY = 'i-want-to-heal:last-difficulty'
|
||||
const SHOW_LEADERBOARDS = false
|
||||
const ACTIVITY_PAGE_SIZE = 4
|
||||
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) {
|
||||
return name
|
||||
@@ -117,6 +130,7 @@ function App() {
|
||||
const [syncingCloud, setSyncingCloud] = useState(false)
|
||||
const [syncMessage, setSyncMessage] = useState('')
|
||||
const [homeSelectedIndex, setHomeSelectedIndex] = useState(0)
|
||||
const [dungeonSelectedIndex, setDungeonSelectedIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
loadAuthSession()
|
||||
@@ -356,29 +370,194 @@ function App() {
|
||||
setScreen('roguelike')
|
||||
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)
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (device !== 'controller' || screen !== 'menu') return
|
||||
if (action === 'confirm') {
|
||||
openHomeMenuIndex(homeActiveIndex)
|
||||
if (device !== 'controller') return
|
||||
if (screen === 'menu') {
|
||||
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
|
||||
})
|
||||
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 (screen === 'dungeons' || screen === 'raids') {
|
||||
if (action === 'back') {
|
||||
setScreen('menu')
|
||||
return
|
||||
}
|
||||
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 (action === 'confirm') {
|
||||
openDungeonEntry(activeDungeonEntry())
|
||||
return
|
||||
}
|
||||
if (action.startsWith('navigate')) moveDungeonSelection(action)
|
||||
}
|
||||
})
|
||||
|
||||
if (error) {
|
||||
@@ -563,14 +742,7 @@ function App() {
|
||||
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)
|
||||
}}
|
||||
onClick={() => openHomeMenuIndex(homeIndex)}
|
||||
onPointerDown={() => setHomeSelectedIndex(homeIndex)}
|
||||
type="button"
|
||||
>
|
||||
@@ -787,7 +959,7 @@ function App() {
|
||||
)}
|
||||
|
||||
{(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-main">
|
||||
<article className="run-summary-card dungeon-focus-card">
|
||||
@@ -798,7 +970,15 @@ function App() {
|
||||
<p className="eyebrow">Selected Run</p>
|
||||
<div className="run-title-row">
|
||||
<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>
|
||||
<p>{activity.description}</p>
|
||||
<div className="tag-row">
|
||||
@@ -820,16 +1000,22 @@ function App() {
|
||||
{activityPageCount > 1 ? (
|
||||
<div className="activity-pager" aria-label={`${screen === 'raids' ? 'Raid' : 'Dungeon'} pages`}>
|
||||
<button
|
||||
className={dungeonEntrySelected('pagePrev') ? 'game-selected' : ''}
|
||||
data-controller-nav="skip"
|
||||
disabled={currentActivityPage === 0}
|
||||
onClick={() => setActivityPage((page) => Math.max(0, page - 1))}
|
||||
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'pagePrev'))}
|
||||
type="button"
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<span>{activityPageStart}-{activityPageEnd} of {activityOptions.length}</span>
|
||||
<button
|
||||
className={dungeonEntrySelected('pageNext') ? 'game-selected' : ''}
|
||||
data-controller-nav="skip"
|
||||
disabled={currentActivityPage >= activityPageCount - 1}
|
||||
onClick={() => setActivityPage((page) => Math.min(activityPageCount - 1, page + 1))}
|
||||
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'pageNext'))}
|
||||
type="button"
|
||||
>
|
||||
Next
|
||||
@@ -840,7 +1026,7 @@ function App() {
|
||||
)}
|
||||
</div>
|
||||
<div className="activity-card-grid dungeon-choice-grid">
|
||||
{pagedActivityOptions.map((candidate) => {
|
||||
{pagedActivityOptions.map((candidate, index) => {
|
||||
const difficulty = candidate.difficulties.find(
|
||||
(option) => option.droppedItemLevel === selectedDifficulty.droppedItemLevel,
|
||||
) ?? candidate.difficulties[0]
|
||||
@@ -848,7 +1034,8 @@ function App() {
|
||||
const selected = candidate.id === activity.id
|
||||
return (
|
||||
<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}
|
||||
key={candidate.id}
|
||||
onClick={() => {
|
||||
@@ -856,6 +1043,7 @@ function App() {
|
||||
else setSelectedDungeonId(candidate.id)
|
||||
setSelectedDifficultyId(difficulty.id)
|
||||
}}
|
||||
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'activity' && 'index' in entry && entry.index === index))}
|
||||
type="button"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<div className="tier-grid">
|
||||
{tierOptions.map((difficulty) => {
|
||||
{tierOptions.map((difficulty, index) => {
|
||||
const locked = profile.character.level < difficulty.unlockLevel
|
||||
const selected = difficulty.droppedItemLevel === selectedDifficulty.droppedItemLevel
|
||||
return (
|
||||
<button
|
||||
className={`${selected ? 'selected' : ''} ${locked ? 'locked' : ''}`}
|
||||
className={`${selected ? 'selected' : ''} ${locked ? 'locked' : ''} ${dungeonEntrySelected('tier', index) ? 'game-selected' : ''}`}
|
||||
data-controller-nav="skip"
|
||||
disabled={locked}
|
||||
key={difficulty.id}
|
||||
onClick={() => {
|
||||
@@ -909,6 +1098,7 @@ function App() {
|
||||
if (nextDifficulty) setSelectedDifficultyId(nextDifficulty.id)
|
||||
}
|
||||
}}
|
||||
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'tier' && 'index' in entry && entry.index === index))}
|
||||
type="button"
|
||||
>
|
||||
<strong>iLvl {difficulty.droppedItemLevel}</strong>
|
||||
@@ -933,7 +1123,8 @@ function App() {
|
||||
</div>
|
||||
<div className="part-picker">
|
||||
<button
|
||||
className="primary-button selected-part"
|
||||
className={`primary-button selected-part ${dungeonEntrySelected('start') ? 'game-selected' : ''}`}
|
||||
data-controller-nav="skip"
|
||||
disabled={difficultyLocked}
|
||||
onClick={() => {
|
||||
setSelectedMarathonMode(false)
|
||||
@@ -941,12 +1132,14 @@ function App() {
|
||||
setSelectedDifficultyId(selectedDifficulty.id)
|
||||
setScreen('combat')
|
||||
}}
|
||||
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'start'))}
|
||||
type="button"
|
||||
>
|
||||
Start Hunt
|
||||
</button>
|
||||
<button
|
||||
className={`primary-button ${selectedMarathonMode ? 'selected-part' : ''}`}
|
||||
className={`primary-button ${selectedMarathonMode ? 'selected-part' : ''} ${dungeonEntrySelected('marathon') ? 'game-selected' : ''}`}
|
||||
data-controller-nav="skip"
|
||||
disabled={difficultyLocked}
|
||||
onClick={() => {
|
||||
setSelectedMarathonMode(true)
|
||||
@@ -954,6 +1147,7 @@ function App() {
|
||||
setSelectedDifficultyId(selectedDifficulty.id)
|
||||
setScreen('combat')
|
||||
}}
|
||||
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'marathon'))}
|
||||
type="button"
|
||||
>
|
||||
Marathon
|
||||
@@ -983,8 +1177,10 @@ function App() {
|
||||
<h2>{selectedDifficulty.name} Loot Tables</h2>
|
||||
</div>
|
||||
<button
|
||||
className="text-button"
|
||||
className={`text-button ${dungeonEntrySelected('loot') ? 'game-selected' : ''}`}
|
||||
data-controller-nav="skip"
|
||||
onClick={() => setShowLoot((current) => !current)}
|
||||
onPointerDown={() => setDungeonSelectedIndex(dungeonEntries().findIndex((entry) => entry.kind === 'loot'))}
|
||||
type="button"
|
||||
>
|
||||
{showLoot ? 'Hide Loot' : 'View Loot'}
|
||||
@@ -993,9 +1189,14 @@ function App() {
|
||||
{showLoot && (
|
||||
<>
|
||||
<div className="loot-toolbar">
|
||||
<label>
|
||||
<label className={dungeonEntrySelected('lootSort') ? 'game-selected' : ''}>
|
||||
<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="boss">Boss name</option>
|
||||
</select>
|
||||
|
||||
Reference in New Issue
Block a user