diff --git a/IWantToHeal-Thor-v1.1.12.apk b/IWantToHeal-Thor-v1.1.12.apk new file mode 100644 index 0000000..1d375ec Binary files /dev/null and b/IWantToHeal-Thor-v1.1.12.apk differ diff --git a/android/app/build.gradle b/android/app/build.gradle index d3e3dcc..0c5d571 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.warren.iwanttoheal" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 90 - versionName "1.1.11" + versionCode 91 + versionName "1.1.12" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/screenshots/current-pvp-top.png b/screenshots/current-pvp-top.png new file mode 100644 index 0000000..512cdbe Binary files /dev/null and b/screenshots/current-pvp-top.png differ diff --git a/screenshots/hunter-profile-thor-collection-selected.png b/screenshots/hunter-profile-thor-collection-selected.png new file mode 100644 index 0000000..377bd93 Binary files /dev/null and b/screenshots/hunter-profile-thor-collection-selected.png differ diff --git a/screenshots/hunter-profile-thor-collection.png b/screenshots/hunter-profile-thor-collection.png new file mode 100644 index 0000000..acb69d0 Binary files /dev/null and b/screenshots/hunter-profile-thor-collection.png differ diff --git a/screenshots/thor-customize-character.png b/screenshots/thor-customize-character.png new file mode 100644 index 0000000..1315e68 Binary files /dev/null and b/screenshots/thor-customize-character.png differ diff --git a/screenshots/thor-dungeon-bottom.png b/screenshots/thor-dungeon-bottom.png new file mode 100644 index 0000000..7473d29 Binary files /dev/null and b/screenshots/thor-dungeon-bottom.png differ diff --git a/screenshots/thor-dungeon-top.png b/screenshots/thor-dungeon-top.png new file mode 100644 index 0000000..7ac47ae Binary files /dev/null and b/screenshots/thor-dungeon-top.png differ diff --git a/screenshots/thor-pvp-dual-bottom.png b/screenshots/thor-pvp-dual-bottom.png new file mode 100644 index 0000000..cc146cc Binary files /dev/null and b/screenshots/thor-pvp-dual-bottom.png differ diff --git a/screenshots/thor-pvp-dual-top.png b/screenshots/thor-pvp-dual-top.png new file mode 100644 index 0000000..fcedeef Binary files /dev/null and b/screenshots/thor-pvp-dual-top.png differ diff --git a/screenshots/thor-pvp-top-pause-leave-selected.png b/screenshots/thor-pvp-top-pause-leave-selected.png new file mode 100644 index 0000000..1ef06ff Binary files /dev/null and b/screenshots/thor-pvp-top-pause-leave-selected.png differ diff --git a/screenshots/thor-pvp-top-pause.png b/screenshots/thor-pvp-top-pause.png new file mode 100644 index 0000000..96007cd Binary files /dev/null and b/screenshots/thor-pvp-top-pause.png differ diff --git a/screenshots/thor-pvp-top-playing.png b/screenshots/thor-pvp-top-playing.png new file mode 100644 index 0000000..97c0f79 Binary files /dev/null and b/screenshots/thor-pvp-top-playing.png differ diff --git a/screenshots/thor-pvp-top-upgrade-debuff-selected.png b/screenshots/thor-pvp-top-upgrade-debuff-selected.png new file mode 100644 index 0000000..e894a91 Binary files /dev/null and b/screenshots/thor-pvp-top-upgrade-debuff-selected.png differ diff --git a/screenshots/thor-pvp-top-upgrade-ready.png b/screenshots/thor-pvp-top-upgrade-ready.png new file mode 100644 index 0000000..75348ed Binary files /dev/null and b/screenshots/thor-pvp-top-upgrade-ready.png differ diff --git a/screenshots/thor-pvp-top-upgrade.png b/screenshots/thor-pvp-top-upgrade.png new file mode 100644 index 0000000..f71b25e Binary files /dev/null and b/screenshots/thor-pvp-top-upgrade.png differ diff --git a/screenshots/thor-raid-bottom.png b/screenshots/thor-raid-bottom.png new file mode 100644 index 0000000..b540fd7 Binary files /dev/null and b/screenshots/thor-raid-bottom.png differ diff --git a/screenshots/thor-raid-top.png b/screenshots/thor-raid-top.png new file mode 100644 index 0000000..e462e58 Binary files /dev/null and b/screenshots/thor-raid-top.png differ diff --git a/screenshots/thor-roguelike-top-pve.png b/screenshots/thor-roguelike-top-pve.png new file mode 100644 index 0000000..bab7828 Binary files /dev/null and b/screenshots/thor-roguelike-top-pve.png differ diff --git a/screenshots/thor-roguelike-top-pvp.png b/screenshots/thor-roguelike-top-pvp.png new file mode 100644 index 0000000..c78a3bc Binary files /dev/null and b/screenshots/thor-roguelike-top-pvp.png differ diff --git a/screenshots/thor-settings-960x540.png b/screenshots/thor-settings-960x540.png new file mode 100644 index 0000000..888aaf9 Binary files /dev/null and b/screenshots/thor-settings-960x540.png differ diff --git a/screenshots/thor-settings-bindings.png b/screenshots/thor-settings-bindings.png new file mode 100644 index 0000000..52d9663 Binary files /dev/null and b/screenshots/thor-settings-bindings.png differ diff --git a/screenshots/thor-settings-controller-nav-960x540.png b/screenshots/thor-settings-controller-nav-960x540.png new file mode 100644 index 0000000..5e6463a Binary files /dev/null and b/screenshots/thor-settings-controller-nav-960x540.png differ diff --git a/screenshots/thor-settings-display.png b/screenshots/thor-settings-display.png new file mode 100644 index 0000000..c50549b Binary files /dev/null and b/screenshots/thor-settings-display.png differ diff --git a/screenshots/thor-settings-input.png b/screenshots/thor-settings-input.png new file mode 100644 index 0000000..4cf7ac8 Binary files /dev/null and b/screenshots/thor-settings-input.png differ diff --git a/src/App.css b/src/App.css index ff70e74..c836e65 100644 --- a/src/App.css +++ b/src/App.css @@ -1042,6 +1042,8 @@ html[data-input-device='controller'] [data-game-nav-active='true'] [tabindex]:fo .dual-controls-resource, .dual-controls-targets, .dual-controls-spells, +.setup-bottom-summary, +.setup-bottom-stats, .dual-bottom-waiting > section { background: var(--panel); border: 3px solid #0c0d11; @@ -1166,8 +1168,69 @@ html[data-input-device='controller'] [data-game-nav-active='true'] [tabindex]:fo .dual-controls-progress { color: var(--muted); + display: grid; + gap: 4px; font-family: var(--pixel-font); font-size: 7px; + text-align: right; +} + +.setup-bottom-display { + grid-template-rows: auto minmax(0, 1fr) auto; +} + +.setup-bottom-summary { + align-items: start; + display: grid; + gap: 12px; + grid-template-columns: 82px minmax(0, 1fr); + min-height: 0; + padding: 12px; +} + +.setup-bottom-summary .dungeon-art { + height: 78px; + width: 78px; +} + +.setup-bottom-summary h2 { + font-size: 14px; +} + +.setup-bottom-summary p:not(.eyebrow), +.setup-bottom-summary small { + color: var(--muted); + display: block; + font-size: 16px; + line-height: 1.15; + margin-top: 6px; +} + +.setup-bottom-stats { + display: grid; + gap: 8px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin: 0; + padding: 10px; +} + +.setup-bottom-stats > div { + background: #111217; + border: 2px solid #090a0d; + padding: 8px; + text-align: center; +} + +.setup-bottom-stats dt { + color: var(--muted); + font-size: 13px; +} + +.setup-bottom-stats dd { + color: var(--ink); + font-family: var(--pixel-font); + font-size: 9px; + margin: 4px 0 0; } .dual-controls-resource { @@ -1256,7 +1319,9 @@ html[data-input-device='controller'] [data-game-nav-active='true'] [tabindex]:fo .dual-controls-header, .dual-controls-resource, .dual-controls-targets, - .dual-controls-spells { + .dual-controls-spells, + .setup-bottom-summary, + .setup-bottom-stats { border-width: 2px; width: 100%; } @@ -1284,6 +1349,47 @@ html[data-input-device='controller'] [data-game-nav-active='true'] [tabindex]:fo font-size: 6px; } + .setup-bottom-summary { + gap: 8px; + grid-template-columns: 58px minmax(0, 1fr); + padding: 8px; + } + + .setup-bottom-summary .dungeon-art { + font-size: 13px; + height: 54px; + width: 54px; + } + + .setup-bottom-summary h2 { + font-size: 11px; + line-height: 1.2; + } + + .setup-bottom-summary p:not(.eyebrow), + .setup-bottom-summary small { + font-size: 13px; + line-height: 1.1; + margin-top: 4px; + } + + .setup-bottom-stats { + gap: 5px; + padding: 5px; + } + + .setup-bottom-stats > div { + padding: 5px 4px; + } + + .setup-bottom-stats dt { + font-size: 10px; + } + + .setup-bottom-stats dd { + font-size: 7px; + } + .dual-controls-resource { gap: 8px; grid-template-columns: minmax(110px, 0.8fr) minmax(150px, 1.2fr); @@ -1813,13 +1919,19 @@ h2 { } .dungeon-run-main { - gap: 14px; + gap: 10px; } .dungeon-setup-rail { gap: 10px; } +.dungeon-heading-actions { + align-items: center; + display: flex; + gap: 10px; +} + .message-panel { align-items: center; display: flex; @@ -1891,11 +2003,31 @@ h2 { margin-bottom: 16px; } +.roguelike-screen { + gap: 12px; +} + +.roguelike-screen .screen-heading { + flex: 0 0 auto; +} + +.roguelike-screen > .roguelike-variant-row, +.roguelike-screen > .roguelike-option-panel, +.roguelike-screen > .pvp-queue-panel { + flex: 0 0 auto; + margin-bottom: 0; + margin-top: 0; +} + .roguelike-option-panel { align-items: center; + background: #111319; + border: 2px solid #090a0d; display: flex; gap: 16px; justify-content: space-between; + outline: 2px solid #41404a; + padding: 14px; } .roguelike-timing-row { @@ -1917,6 +2049,14 @@ h2 { white-space: nowrap; } +.roguelike-screen .pvp-queue-panel { + min-height: 104px; +} + +.roguelike-screen .text-button { + min-height: 38px; +} + .menu-card { align-items: center; background: var(--panel-light); @@ -2077,6 +2217,17 @@ h2 { outline-color: var(--gold); } +.settings-back-button.game-selected, +.settings-tabs button.game-selected, +.dual-screen-actions button.game-selected, +.controller-preferences button.game-selected, +.binding-tabs button.game-selected, +.binding-list button.game-selected, +.settings-footer .text-button.game-selected, +.binding-capture button.game-selected { + color: var(--ink); +} + .settings-screen, .equipment-screen, .talent-screen, @@ -2701,7 +2852,7 @@ h2 { } .dungeon-run-board { - grid-template-columns: minmax(0, 1fr) minmax(214px, 0.45fr); + grid-template-columns: minmax(0, 1fr) 292px; } .dungeon-run-screen .run-setup-panel, @@ -2766,20 +2917,20 @@ h2 { } .dungeon-choice-grid { - grid-auto-rows: minmax(52px, max-content); + grid-auto-rows: minmax(72px, max-content); } .dungeon-choice-grid .activity-card { - gap: 4px 6px; - grid-template-columns: 34px minmax(0, 1fr); - min-height: 52px; - padding: 5px; + gap: 5px 7px; + grid-template-columns: 40px minmax(0, 1fr); + min-height: 72px; + padding: 6px; } .dungeon-choice-grid .activity-card .dungeon-art { font-size: 10px; - height: 32px; - width: 32px; + height: 38px; + width: 38px; } .dungeon-choice-grid .activity-card small, @@ -2790,7 +2941,7 @@ h2 { } .tier-setup-panel .tier-grid button { - min-height: 32px; + min-height: 34px; padding: 4px 5px; } @@ -2800,7 +2951,7 @@ h2 { .part-setup-panel .primary-button { font-size: 7px; - min-height: 32px; + min-height: 36px; padding: 4px 5px; } @@ -2835,11 +2986,46 @@ h2 { font-size: 12px; padding: 5px 7px; } + + .dungeon-heading-actions { + gap: 6px; + } + + .roguelike-screen { + gap: 7px; + padding: 8px; + } + + .roguelike-screen .screen-heading { + padding-bottom: 6px; + } + + .roguelike-screen .roguelike-option-panel { + padding: 8px; + } + + .roguelike-screen .pvp-queue-panel { + min-height: 76px; + } + + .roguelike-screen .menu-card > span { + flex-basis: 38px; + height: 38px; + } + + .roguelike-screen .menu-card small { + font-size: 13px; + } + + .roguelike-screen .text-button { + min-height: 30px; + padding: 5px 8px; + } } @media (max-width: 700px) and (max-height: 620px) { .dungeon-run-board { - grid-template-columns: minmax(0, 1fr) 214px; + grid-template-columns: minmax(0, 1fr) 292px; overflow: hidden; } @@ -5170,6 +5356,15 @@ h2 { outline-color: var(--gold); } +.customize-tabs button.game-selected, +.class-picker > button.game-selected, +.ability-slots button.game-selected, +.ability-library-actions button.game-selected, +.ability-library > button.game-selected, +.save-row .primary-button.game-selected { + color: var(--ink); +} + .customize-tab-back { display: none; } @@ -5265,6 +5460,24 @@ h2 { margin-top: 20px; } +.ability-library-actions { + align-items: center; + display: flex; + gap: 8px; +} + +.ability-library-pager { + align-items: center; + display: flex; + gap: 6px; +} + +.ability-library-pager > span { + color: var(--muted); + font-family: var(--pixel-font); + font-size: 8px; +} + .loadout-heading > span, .save-row > span { color: var(--muted); @@ -6322,6 +6535,15 @@ h2 { padding: 8px; } +.pvp-match-queue-panel { + gap: 14px; +} + +.pvp-match-queue-panel .secondary-result-button { + margin: 0 auto; + max-width: 220px; +} + .pvp-screen-tools { align-items: center; display: flex; @@ -6596,10 +6818,77 @@ h2 { box-shadow: inset 0 0 0 2px #6e5727; } +.pvp-upgrade-dialog .upgrade-choice-grid button.game-selected, +.pvp-upgrade-dialog .secondary-result-button.game-selected, +.pause-screen button.game-selected, +.pvp-match-queue-panel button.game-selected { + box-shadow: 0 0 0 3px rgba(245, 196, 83, 0.45), inset 0 0 0 2px var(--gold); +} + +.pvp-upgrade-dialog > .secondary-result-button { + display: block; + margin-left: auto; + margin-top: 10px; + width: fit-content; +} + .pvp-upgrade-dialog button:focus { outline-color: var(--gold); } +@media (max-height: 620px) { + .pvp-upgrade-dialog { + max-height: calc(100dvh - 32px); + overflow: hidden; + padding: 10px !important; + } + + .pvp-upgrade-header { + margin-bottom: 8px; + } + + .pvp-upgrade-header h2 { + font-size: 14px; + } + + .pvp-upgrade-header > strong { + font-size: 12px; + } + + .pvp-upgrade-dialog .pvp-choice-columns { + gap: 8px; + } + + .pvp-choice-columns > div > strong { + margin-bottom: 6px; + } + + .pvp-choice-columns .upgrade-choice-grid { + gap: 7px; + } + + .pvp-choice-columns .upgrade-choice-grid button { + min-height: 54px; + padding: 6px; + } + + .pvp-upgrade-dialog .upgrade-choice-grid strong { + font-size: 8px; + } + + .pvp-upgrade-dialog .upgrade-choice-grid small { + font-size: 10px; + line-height: 1.1; + } + + .pvp-upgrade-dialog .secondary-result-button { + margin-left: auto; + margin-top: 8px; + min-height: 34px; + padding: 6px 10px; + } +} + .stadium-screen { min-height: 100dvh; } @@ -6775,6 +7064,13 @@ h2 { margin-top: 12px; } +.result-screen .pvp-upgrade-dialog > .secondary-result-button { + display: block; + margin-left: auto; + margin-top: 10px; + width: fit-content; +} + .pause-screen button:focus-visible { outline: 3px solid #fff4a8 !important; box-shadow: 0 0 0 5px #8b6726, 0 0 18px rgba(229, 185, 95, 0.65); @@ -7118,6 +7414,7 @@ h2 { } .settings-screen { + gap: 8px; padding: 10px; } @@ -7145,21 +7442,26 @@ h2 { scrollbar-width: thin; } + .settings-screen .settings-tab-panel { + overflow: hidden; + } + .dual-screen-settings, .controller-preferences { - gap: 10px; - margin: 8px 0 0; - padding: 10px; + gap: 16px; + margin: 10px 0 0; + padding: 16px; } .dual-screen-settings.settings-tab-panel, .controller-preferences.settings-tab-panel { - flex: 0 0 auto; + flex: 1 1 auto; } .dual-screen-settings { align-items: start; - grid-template-columns: minmax(0, 1fr) minmax(214px, auto); + align-content: start; + grid-template-columns: minmax(0, 1fr) minmax(238px, auto); } .dual-screen-settings h2, @@ -7181,20 +7483,20 @@ h2 { .dual-screen-actions { align-content: start; align-self: start; - gap: 7px; - min-width: 214px; + gap: 10px; + min-width: 238px; } .dual-screen-actions button, .controller-preferences button { - min-height: 32px; - padding: 5px 8px; + min-height: 46px; + padding: 8px 12px; } .controller-preferences { align-items: stretch; align-content: start; - grid-template-columns: minmax(0, 1fr) repeat(2, minmax(158px, auto)); + grid-template-columns: minmax(0, 1fr) repeat(2, minmax(176px, auto)); } .controller-preferences h3 { @@ -7206,9 +7508,9 @@ h2 { } .controller-icon-options { - gap: 6px; - grid-template-columns: minmax(112px, 1fr) repeat(3, minmax(104px, auto)); - padding: 6px; + gap: 8px; + grid-template-columns: minmax(130px, 1fr) repeat(3, minmax(120px, auto)); + padding: 10px; } .controller-icon-options > span { @@ -7218,6 +7520,7 @@ h2 { .controller-icon-options button { font-size: 6px; gap: 5px; + min-height: 38px; } .controller-style-preview .controller-face-icon { @@ -7253,6 +7556,7 @@ h2 { .binding-list { gap: 4px; padding-right: 4px; + overflow-y: auto; } .binding-list button { @@ -9402,6 +9706,25 @@ h2 { margin-top: 6px; } + .workshop-shell .ability-library-heading { + gap: 8px; + } + + .workshop-shell .ability-library-actions, + .workshop-shell .ability-library-pager { + gap: 5px; + } + + .workshop-shell .ability-library-actions .text-button { + font-size: 7px; + min-height: 28px; + padding: 4px 7px; + } + + .workshop-shell .ability-library-pager > span { + font-size: 7px; + } + .workshop-shell .ability-slots { gap: 5px; margin-top: 5px; @@ -9713,6 +10036,15 @@ h2 { gap: 14px; } +.hunter-profile-screen .back-button.game-selected, +.hunter-profile-screen .equipment-tab.game-selected, +.hunter-stat-tile.game-selected, +.collection-boss-button.game-selected, +.collection-drop-row.game-selected { + color: var(--ink); + outline-color: #fff4a8; +} + .hunter-profile-heading { align-items: center; } @@ -9755,6 +10087,7 @@ h2 { .collection-log-layout { display: grid; + flex: 1; grid-template-columns: 250px minmax(0, 1fr); gap: 14px; min-height: 0; @@ -9772,6 +10105,8 @@ h2 { grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 6px; min-height: 0; + overflow: auto; + padding: 2px; } .collection-boss-list { @@ -9833,6 +10168,7 @@ h2 { min-height: 0; flex-direction: column; gap: 10px; + overflow: hidden; padding: 12px; border: 1px solid rgba(229, 185, 95, 0.2); border-radius: 8px; @@ -9910,6 +10246,10 @@ h2 { opacity: 0.46; } +.collection-drop-row.missing.game-selected { + opacity: 1; +} + .collection-drop-row.missing > span { background: rgba(238, 232, 210, 0.08); color: rgba(238, 232, 210, 0.54); @@ -10005,3 +10345,80 @@ h2 { max-height: 190px; } } + +@media (max-width: 1000px) and (max-height: 620px) { + .hunter-profile-screen { + gap: 10px; + margin-top: 0; + padding: 12px; + } + + .hunter-profile-heading { + padding-bottom: 10px; + } + + .hunter-profile-tabs { + gap: 6px; + margin-top: 0; + } + + .hunter-profile-screen .equipment-tab, + .hunter-profile-screen .back-button { + min-height: 34px; + padding: 6px 10px; + } + + .hunter-stat-grid { + gap: 8px; + } + + .hunter-stat-tile { + min-height: 92px; + padding: 10px; + } + + .hunter-stat-tile strong { + font-size: 1.05rem; + } + + .collection-log-layout { + gap: 10px; + grid-template-columns: 198px minmax(0, 1fr); + } + + .collection-boss-list { + gap: 6px; + max-height: none; + } + + .collection-boss-button { + grid-template-columns: 32px minmax(0, 1fr); + min-height: 46px; + padding: 6px; + } + + .collection-boss-button > span { + width: 32px; + height: 32px; + font-size: 0.68rem; + } + + .collection-boss-detail { + padding: 8px; + } + + .collection-drop-list { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + + .collection-drop-row { + grid-template-rows: 20px minmax(24px, 1fr) auto auto; + min-height: 72px; + padding: 5px; + } + + .collection-drop-row > span { + width: 20px; + height: 20px; + } +} diff --git a/src/App.tsx b/src/App.tsx index 7c14a04..36e299d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,11 @@ import { type GameMode, } from './gameRepository' import { focusFirstControl, useGameAction } from './input.tsx' +import { + useDualScreen, + useDualScreenSetupPublisher, + type DualScreenSetupState, +} from './dualScreen' import { barFillStyle } from './components/barStyles' const CombatScreen = lazy(() => import('./components/CombatScreen').then((module) => ({ default: module.CombatScreen }))) @@ -77,6 +82,23 @@ type DungeonNavEntry = | { kind: 'loot'; disabled?: boolean } | { kind: 'lootSort'; disabled?: boolean } +type RoguelikeNavEntry = + | { kind: 'back' } + | { kind: 'variantPve' } + | { kind: 'variantPvp' } + | { kind: 'pveDungeon' } + | { kind: 'pveRaid' } + | { kind: 'pveStart' } + | { kind: 'pvpDungeon' } + | { kind: 'pvpRaid' } + | { kind: 'pvpStadium' } + | { kind: 'pvpStart' } + +type RoguelikeNavPosition = { + row: number + column: number +} + function activityInitials(name: string) { return name .split(/\s+/) @@ -97,11 +119,10 @@ function ScreenLoading() { ) } -type RoguelikeUpgradeTiming = 'boss' | 'encounter' type RoguelikeVariant = 'pve' | 'pvp' -type RoguelikeAbilityLabelMode = 'ability' | 'slot' function App() { + const { enabled: dualScreenEnabled } = useDualScreen() const [screen, setScreen] = useState('menu') const [account, setAccount] = useState(null) const [profile, setProfile] = useState(null) @@ -116,8 +137,6 @@ function App() { const [selectedRaidId, setSelectedRaidId] = useState(20) const [roguelikeKind, setRoguelikeKind] = useState<'dungeon' | 'raid'>('dungeon') const [roguelikeVariant, setRoguelikeVariant] = useState('pve') - const [roguelikeUpgradeTiming, setRoguelikeUpgradeTiming] = useState('encounter') - const [roguelikeAbilityLabelMode, setRoguelikeAbilityLabelMode] = useState('ability') const [pvpContentType, setPvpContentType] = useState('dungeon') const [selectedMarathonMode, setSelectedMarathonMode] = useState(false) const [activityPage, setActivityPage] = useState(0) @@ -131,6 +150,7 @@ function App() { const [syncMessage, setSyncMessage] = useState('') const [homeSelectedIndex, setHomeSelectedIndex] = useState(0) const [dungeonSelectedIndex, setDungeonSelectedIndex] = useState(0) + const [roguelikeSelectedIndex, setRoguelikeSelectedIndex] = useState(1) useEffect(() => { loadAuthSession() @@ -367,9 +387,13 @@ function App() { if (!item) return if (item.screen === 'pvp') { setRoguelikeVariant('pvp') + setRoguelikeSelectedIndex(2) setScreen('roguelike') return } + if (item.screen === 'roguelike') { + setRoguelikeSelectedIndex(1) + } if (item.screen === 'dungeons' || item.screen === 'raids') { const nextOptions = item.screen === 'raids' ? raidOptions : dungeonOptions setActivityPage(0) @@ -407,6 +431,10 @@ function App() { disabled: !profile || profile.character.level < candidateDifficulty.unlockLevel, }) }) + entries.push( + { kind: 'start', disabled: locked }, + { kind: 'marathon', disabled: locked }, + ) tierOptions.forEach((difficultyOption, index) => { entries.push({ kind: 'tier', @@ -414,11 +442,7 @@ function App() { disabled: !profile || profile.character.level < difficultyOption.unlockLevel, }) }) - entries.push( - { kind: 'start', disabled: locked }, - { kind: 'marathon', disabled: locked }, - { kind: 'loot' }, - ) + entries.push({ kind: 'loot' }) if (showLoot) entries.push({ kind: 'lootSort' }) return entries } @@ -524,6 +548,109 @@ function App() { else if (entry.kind === 'lootSort') setLootSort((current) => current === 'sequence' ? 'boss' : 'sequence') } + function startPveRoguelike() { + const baseDungeon = dungeonOptions[0] + const baseRaid = raidOptions[0] + if (roguelikeKind === 'raid') { + setCombatContentId(-2) + setSelectedDifficultyId(baseRaid?.difficulties[0]?.id ?? 101) + } else { + setCombatContentId(-1) + setSelectedDifficultyId(baseDungeon?.difficulties[0]?.id ?? 1) + } + setSelectedMarathonMode(false) + setScreen('combat') + } + + function roguelikeEntries() { + const entries: RoguelikeNavEntry[] = [ + { kind: 'back' }, + { kind: 'variantPve' }, + { kind: 'variantPvp' }, + ] + if (roguelikeVariant === 'pve') { + entries.push( + { kind: 'pveDungeon' }, + { kind: 'pveRaid' }, + { kind: 'pveStart' }, + ) + } else { + entries.push( + { kind: 'pvpDungeon' }, + { kind: 'pvpRaid' }, + { kind: 'pvpStadium' }, + { kind: 'pvpStart' }, + ) + } + return entries + } + + function activeRoguelikeEntry(entries = roguelikeEntries()) { + return entries[Math.min(roguelikeSelectedIndex, entries.length - 1)] ?? entries[0] + } + + function roguelikeEntrySelected(kind: RoguelikeNavEntry['kind']) { + return activeRoguelikeEntry()?.kind === kind + } + + function roguelikeEntryPosition(entry: RoguelikeNavEntry): RoguelikeNavPosition { + if (entry.kind === 'back') return { row: 0, column: 0 } + if (entry.kind === 'variantPve') return { row: 1, column: 0 } + if (entry.kind === 'variantPvp') return { row: 1, column: 1 } + if (entry.kind === 'pveDungeon' || entry.kind === 'pvpDungeon') return { row: 2, column: 0 } + if (entry.kind === 'pveRaid' || entry.kind === 'pvpRaid') return { row: 2, column: 1 } + if (entry.kind === 'pvpStart') return { row: 3, column: 1 } + return { row: 3, column: 0 } + } + + function moveRoguelikeSelection(action: string) { + const entries = roguelikeEntries() + setRoguelikeSelectedIndex((current) => { + const bounded = Math.min(current, entries.length - 1) + const active = entries[bounded] + if (!active) return bounded + const activePosition = roguelikeEntryPosition(active) + const candidates = entries + .map((entry, index) => ({ entry, index, position: roguelikeEntryPosition(entry) })) + .filter(({ index, position }) => { + if (index === bounded) return false + if (action === 'navigateLeft') return position.row === activePosition.row && position.column < activePosition.column + if (action === 'navigateRight') return position.row === activePosition.row && position.column > activePosition.column + if (action === 'navigateUp') return position.row < activePosition.row + return position.row > activePosition.row + }) + if (candidates.length === 0) return bounded + candidates.sort((a, b) => { + const aPrimary = Math.abs(a.position.row - activePosition.row) + + Math.abs(a.position.column - activePosition.column) + const bPrimary = Math.abs(b.position.row - activePosition.row) + + Math.abs(b.position.column - activePosition.column) + const aSecondary = action === 'navigateLeft' || action === 'navigateRight' + ? Math.abs(a.position.column - activePosition.column) + : Math.abs(a.position.row - activePosition.row) + const bSecondary = action === 'navigateLeft' || action === 'navigateRight' + ? Math.abs(b.position.column - activePosition.column) + : Math.abs(b.position.row - activePosition.row) + return aPrimary - bPrimary || aSecondary - bSecondary || a.index - b.index + }) + return candidates[0]?.index ?? bounded + }) + } + + function openRoguelikeEntry(entry: RoguelikeNavEntry | undefined) { + if (!entry) return + if (entry.kind === 'back') setScreen('menu') + else if (entry.kind === 'variantPve') setRoguelikeVariant('pve') + else if (entry.kind === 'variantPvp') setRoguelikeVariant('pvp') + else if (entry.kind === 'pveDungeon') setRoguelikeKind('dungeon') + else if (entry.kind === 'pveRaid') setRoguelikeKind('raid') + else if (entry.kind === 'pveStart') startPveRoguelike() + else if (entry.kind === 'pvpDungeon') setPvpContentType('dungeon') + else if (entry.kind === 'pvpRaid') setPvpContentType('raid') + else if (entry.kind === 'pvpStadium') setPvpContentType('stadium') + else if (entry.kind === 'pvpStart') setScreen('pvp') + } + useGameAction((action, device) => { if (device !== 'controller') return if (screen === 'menu') { @@ -558,8 +685,48 @@ function App() { } if (action.startsWith('navigate')) moveDungeonSelection(action) } + if (screen === 'roguelike') { + if (action === 'back') { + setScreen('menu') + return + } + if (action === 'confirm') { + openRoguelikeEntry(activeRoguelikeEntry()) + return + } + if (action.startsWith('navigate')) moveRoguelikeSelection(action) + } }) + const setupDualScreenState = useMemo(() => { + if ( + !(screen === 'dungeons' || screen === 'raids') + || !profile + || !selectedActivityOption + || !selectedDifficultyOption + ) return null + const locked = profile.character.level < selectedDifficultyOption.unlockLevel + return { + contentType: selectedActivityOption.contentType, + title: selectedActivityOption.name, + subtitle: `${selectedActivityOption.locationName} | Level ${selectedActivityOption.recommendedLevel} | ${selectedActivityOption.partySize} Players`, + description: selectedActivityOption.description, + initials: activityInitials(selectedActivityOption.name), + difficultyName: selectedDifficultyOption.name, + itemLevel: selectedDifficultyOption.droppedItemLevel, + experience: Math.round(selectedActivityOption.experienceReward * selectedDifficultyOption.experienceMultiplier), + lockedReason: locked ? `Unlocks at level ${selectedDifficultyOption.unlockLevel}` : undefined, + stats: { + health: `${selectedDifficultyOption.healthMultiplier.toFixed(2)}x`, + damage: `${selectedDifficultyOption.damageMultiplier.toFixed(2)}x`, + xp: `${selectedDifficultyOption.experienceMultiplier.toFixed(1)}x`, + loot: `iLvl ${selectedDifficultyOption.droppedItemLevel}`, + }, + } + }, [profile, screen, selectedActivityOption, selectedDifficultyOption]) + + useDualScreenSetupPublisher(setupDualScreenState, dualScreenEnabled) + if (error) { return (
@@ -604,8 +771,8 @@ function App() { marathonMode={selectedMarathonMode && combatContentId > 0} profile={profile} roguelikeMode={combatContentId < 0 ? roguelikeKind : undefined} - roguelikeUpgradeTiming={combatContentId < 0 ? roguelikeUpgradeTiming : undefined} - roguelikeAbilityLabelMode={combatContentId < 0 ? roguelikeAbilityLabelMode : undefined} + roguelikeUpgradeTiming={combatContentId < 0 ? 'encounter' : undefined} + roguelikeAbilityLabelMode={combatContentId < 0 ? 'ability' : undefined} roguelikeEncounterPool={combatContentId < 0 ? roguelikePool : undefined} startPart={1} onExit={() => { @@ -659,19 +826,6 @@ function App() { ? 100 : Math.min(100, (experienceIntoLevel / experienceForLevel) * 100) const dungeon = selectedDungeonOption ?? profile.dungeons[0]! - const startPveRoguelike = () => { - const baseDungeon = dungeonOptions[0] - const baseRaid = raidOptions[0] - if (roguelikeKind === 'raid') { - setCombatContentId(-2) - setSelectedDifficultyId(baseRaid?.difficulties[0]?.id ?? 101) - } else { - setCombatContentId(-1) - setSelectedDifficultyId(baseDungeon?.difficulties[0]?.id ?? 1) - } - setSelectedMarathonMode(false) - setScreen('combat') - } const activityPageStart = activityOptions.length === 0 ? 0 : currentActivityPage * ACTIVITY_PAGE_SIZE + 1 @@ -759,23 +913,30 @@ function App() { )} {screen === 'roguelike' && ( -
+
setScreen('menu')} + onBackPointerDown={() => setRoguelikeSelectedIndex(roguelikeEntries().findIndex((entry) => entry.kind === 'back'))} />
-
-
-

Upgrade Timing

-

Buff Drafts

-
-
- - -
-
-
-
-

Upgrade Labels

-

Display Mode

-
-
- - -
-
{roguelikeKind === 'raid' ? 'R' : 'D'}
@@ -860,8 +981,10 @@ function App() {
- -

{activity.description}

-
- Level {activity.recommendedLevel} - {activity.partySize} Players - {selectedDifficulty.name} - iLvl {selectedDifficulty.droppedItemLevel} - {Math.round(activity.experienceReward * selectedDifficulty.experienceMultiplier)} XP -
- - - -
-
-
-

Pick Run

-

{screen === 'raids' ? 'Raid' : 'Dungeon'}

-
- {activityPageCount > 1 ? ( -
+ {activityPageCount > 1 ? ( +
-
- ) : ( - {selectedDifficulty.name} rewards iLvl {selectedDifficulty.droppedItemLevel} components. - )} +
+ ) : ( + {selectedDifficulty.name} rewards iLvl {selectedDifficulty.droppedItemLevel} components. + )} +
{pagedActivityOptions.map((candidate, index) => { @@ -1062,6 +1175,52 @@ function App() {