diff --git a/IWantToHeal-Thor-v1.0.26.apk b/IWantToHeal-Thor-v1.0.26.apk new file mode 100644 index 0000000..c7ac098 Binary files /dev/null and b/IWantToHeal-Thor-v1.0.26.apk differ diff --git a/android/app/build.gradle b/android/app/build.gradle index 89f62c2..72e84cc 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 43 - versionName "1.0.27" + versionCode 44 + versionName "1.0.26" 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/src/App.css b/src/App.css index 354e11a..7b93702 100644 --- a/src/App.css +++ b/src/App.css @@ -1052,6 +1052,10 @@ textarea:focus-visible, position: relative; } +.game-shell.dungeon-shell { + width: min(1400px, calc(100% - 28px)); +} + .auth-shell { align-items: center; display: flex; @@ -1368,6 +1372,10 @@ h2 { flex-direction: column; } +.dungeon-run-screen { + gap: 14px; +} + .menu-screen { align-items: center; display: flex; @@ -1377,6 +1385,31 @@ h2 { flex: 0 0 auto; } +.dungeon-run-board { + display: grid; + flex: 1; + gap: 14px; + grid-template-columns: minmax(0, 1fr) minmax(340px, 0.48fr); + min-height: 0; + overflow: hidden; +} + +.dungeon-run-main, +.dungeon-setup-rail { + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +.dungeon-run-main { + gap: 14px; +} + +.dungeon-setup-rail { + gap: 10px; +} + .message-panel { align-items: center; display: flex; @@ -1857,6 +1890,17 @@ h2 { grid-template-columns: 92px minmax(0, 1fr) minmax(190px, 260px); } +.dungeon-focus-card { + flex: 0 0 auto; + grid-template-columns: 108px minmax(0, 1fr); + margin-top: 0; + min-height: 178px; +} + +.dungeon-focus-card > .dungeon-art { + height: 108px; +} + .run-summary-copy p:not(.eyebrow) { color: var(--muted); font-size: 18px; @@ -1868,6 +1912,240 @@ h2 { gap: 8px; } +.part-setup-panel .part-picker { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.part-setup-panel .primary-button { + min-height: 54px; +} + +.dungeon-choice-panel { + display: flex; + flex: 1; + flex-direction: column; + margin-top: 0; + min-height: 0; +} + +.dungeon-choice-grid { + align-content: start; + flex: 1; + grid-auto-rows: minmax(86px, max-content); + grid-template-columns: repeat(2, minmax(0, 1fr)); + min-height: 0; + overflow: hidden; +} + +.dungeon-choice-grid .activity-card { + min-height: 86px; +} + +.tier-setup-panel, +.part-setup-panel, +.dungeon-setup-rail .difficulty-section, +.dungeon-setup-rail .loot-preview-section, +.dungeon-setup-rail .leaderboard-section { + margin-top: 0; +} + +.tier-setup-panel { + flex: 0 0 auto; +} + +.tier-setup-panel .tier-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.tier-setup-panel .tier-grid button { + min-height: 56px; + padding: 9px; +} + +.dungeon-setup-rail .run-setup-heading { + gap: 10px; +} + +.dungeon-setup-rail .run-setup-heading small { + font-size: 14px; + line-height: 1.05; +} + +.dungeon-setup-rail .difficulty-summary { + grid-template-columns: 1fr; +} + +.dungeon-setup-rail .difficulty-summary small { + line-height: 1.05; +} + +.dungeon-setup-rail .difficulty-summary dl { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.dungeon-setup-rail .loot-preview-section { + min-height: 0; +} + +.dungeon-setup-rail .loot-preview-grid { + grid-template-columns: 1fr; + max-height: 260px; + overflow-y: auto; +} + +@media (max-height: 1100px) { + .game-shell.dungeon-shell { + padding: 8px 0; + } + + .dungeon-shell .app-header { + min-height: 58px; + padding: 9px 14px; + } + + .dungeon-run-screen { + gap: 10px; + margin-top: 10px; + padding: 16px; + } + + .dungeon-run-screen .screen-heading { + padding-bottom: 10px; + } + + .dungeon-run-screen .screen-heading h1 { + font-size: 18px; + } + + .dungeon-run-board, + .dungeon-run-main { + gap: 10px; + } + + .dungeon-setup-rail { + gap: 8px; + } + + .dungeon-run-screen .run-setup-panel, + .dungeon-run-screen .run-summary-card, + .dungeon-run-screen .difficulty-section, + .dungeon-run-screen .loot-preview-section, + .dungeon-run-screen .leaderboard-section { + padding: 10px; + } + + .dungeon-focus-card { + grid-template-columns: 84px minmax(0, 1fr); + min-height: 132px; + } + + .dungeon-focus-card > .dungeon-art { + height: 84px; + } + + .dungeon-focus-card .run-summary-copy p:not(.eyebrow) { + font-size: 16px; + line-height: 1.05; + } + + .dungeon-run-screen .run-setup-heading h2, + .dungeon-focus-card .run-summary-copy h2 { + font-size: 12px; + } + + .dungeon-run-screen .eyebrow { + font-size: 7px; + margin-bottom: 5px; + } + + .dungeon-choice-grid, + .dungeon-run-screen .tier-grid { + gap: 8px; + margin-top: 9px; + } + + .dungeon-choice-grid .activity-card { + gap: 5px 9px; + grid-template-columns: 54px minmax(0, 1fr); + min-height: 82px; + padding: 8px; + } + + .dungeon-choice-grid .activity-card .dungeon-art { + height: 50px; + width: 50px; + } + + .dungeon-choice-grid .activity-card strong, + .dungeon-run-screen .tier-grid strong { + font-size: 7px; + } + + .dungeon-choice-grid .activity-card small, + .dungeon-choice-grid .activity-card i, + .dungeon-run-screen .tier-grid span { + font-size: 13px; + line-height: 1; + } + + .tier-setup-panel .tier-grid button { + min-height: 48px; + padding: 7px; + } + + .part-setup-panel .part-picker { + gap: 6px; + } + + .part-setup-panel .primary-button { + font-size: 8px; + min-height: 44px; + padding: 7px 8px; + } + + .dungeon-setup-rail .difficulty-summary { + gap: 8px; + padding: 8px; + } + + .dungeon-setup-rail .difficulty-summary small { + font-size: 13px; + } + + .dungeon-setup-rail .difficulty-summary dl { + gap: 6px; + } + + .dungeon-setup-rail .difficulty-summary dl > div { + padding: 5px 6px; + } + + .dungeon-setup-rail .difficulty-summary dt, + .dungeon-setup-rail .difficulty-summary dd { + font-size: 12px; + } + + .dungeon-setup-rail .equipment-heading h2 { + font-size: 11px; + } + + .dungeon-setup-rail .loot-preview-grid { + max-height: 210px; + } +} + +@media (max-width: 900px) { + .dungeon-run-board { + grid-template-columns: 1fr; + overflow-y: auto; + } + + .dungeon-run-main, + .dungeon-setup-rail { + overflow: visible; + } +} + .part-picker .primary-button.selected-part { background: #f0cb79; outline-color: #fff; @@ -2730,6 +3008,29 @@ h2 { margin-top: 18px; } +.equipment-screen.crafting-active .gear-summary { + gap: 10px; + margin-top: 12px; + padding: 10px 13px; +} + +.equipment-screen.crafting-active .gear-character > span { + flex-basis: 40px; + height: 40px; +} + +.equipment-screen.crafting-active .gear-stat strong { + font-size: 13px; +} + +.equipment-screen.crafting-active .gear-stat span { + font-size: 13px; +} + +.equipment-screen.crafting-active .equipment-tabs { + margin-top: 12px; +} + .equipment-tab { background: var(--panel-light); border: 2px solid #090a0d; @@ -2836,6 +3137,15 @@ h2 { margin-top: 18px; } +.equipment-screen.crafting-active .crafting-panel { + display: flex; + flex: 1; + flex-direction: column; + margin-top: 12px; + min-height: 0; + overflow: hidden; +} + .crafting-filter-bar { display: flex; gap: 8px; @@ -2861,14 +3171,100 @@ h2 { .crafting-layout { display: grid; gap: 12px; - grid-template-columns: minmax(0, 1fr) minmax(280px, 0.75fr); + flex: 1; + grid-template-columns: minmax(210px, 0.55fr) minmax(360px, 1fr) minmax(320px, 0.85fr); margin-top: 13px; + min-height: 0; + overflow: hidden; +} + +.crafting-filters, +.crafting-list-panel, +.crafting-detail-panel { + background: var(--panel-light); + border: 2px solid #090a0d; + display: flex; + flex-direction: column; + min-height: 0; + outline: 2px solid #41404a; + overflow: hidden; + padding: 10px; +} + +.crafting-filters { + gap: 14px; +} + +.crafting-filter-grid { + display: grid; + gap: 7px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.crafting-filter-grid button, +.crafting-level-row button { + background: #15161c; + border: 2px solid #090a0d; + color: var(--ink); + cursor: pointer; + outline: 2px solid #41404a; +} + +.crafting-filter-grid button { + display: grid; + gap: 4px; + min-height: 48px; + padding: 7px; + text-align: left; +} + +.crafting-filter-grid button:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.crafting-filter-grid button.active, +.crafting-level-row button.active { + background: #29291f; + outline-color: var(--gold); +} + +.crafting-filter-grid strong, +.crafting-filter-grid span { + display: block; +} + +.crafting-filter-grid strong { + font-family: 'Press Start 2P', monospace; + font-size: 6px; + line-height: 1.35; +} + +.crafting-filter-grid span { + color: var(--gold); + font-size: 14px; +} + +.crafting-level-row { + display: flex; + flex-wrap: wrap; + gap: 7px; +} + +.crafting-level-row button { + font-family: 'Press Start 2P', monospace; + font-size: 7px; + min-height: 34px; + min-width: 48px; + padding: 6px 9px; } .crafting-list { display: grid; + flex: 1; gap: 8px; - max-height: 360px; + margin-top: 10px; + min-height: 0; overflow: hidden; padding: 2px; } @@ -2953,23 +3349,47 @@ h2 { text-align: right; } +.crafting-list i.ready { + color: var(--green); +} + +.crafting-list i.missing { + color: #e36c79; +} + .crafting-detail { - background: var(--panel-light); - border: 2px solid #090a0d; + background: transparent; + border: 0; border-top-color: var(--rarity-color, #a8a3ad); display: grid; gap: 10px; - padding: 10px; + min-height: 0; + overflow: hidden; + padding: 0; } .crafting-detail .item-detail { - padding: 0; - border: 0; + flex: 0 0 auto; +} + +.crafting-detail-heading { + align-items: center; + display: flex; + justify-content: space-between; +} + +.crafting-detail-heading span { + color: var(--muted); + font-family: 'Press Start 2P', monospace; + font-size: 7px; } .crafting-components { display: grid; + flex: 1; gap: 6px; + min-height: 0; + overflow-y: auto; } .crafting-components > div { @@ -3709,7 +4129,7 @@ h2 { background: var(--gold); border: 2px solid #0a0b0e; color: #21180a; - display: flex; + display: none; font-family: 'Press Start 2P', monospace; font-size: 7px; gap: 5px; @@ -3721,6 +4141,10 @@ h2 { z-index: 2; } +.party-member.selected .target-marker { + display: flex; +} + .target-marker i { border-bottom: 4px solid transparent; border-left: 6px solid #21180a; diff --git a/src/App.tsx b/src/App.tsx index ffe01dd..ccdbce5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -328,7 +328,7 @@ function App() { : a.sequence - b.sequence) return ( -
+
- ) - })} - - +
+
+
+
+ {activityInitials(activity.name)} +
+
+

Selected Run

+

{activity.name}

+

{activity.description}

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

Step 2

-

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

-
- {selectedDifficulty.name} rewards iLvl {selectedDifficulty.droppedItemLevel} components. +
+
+
+

Pick Run

+

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

+
+ {selectedDifficulty.name} rewards iLvl {selectedDifficulty.droppedItemLevel} components. +
+
+ {tierActivityOptions.map((candidate) => { + const difficulty = candidate.difficulties.find( + (option) => option.droppedItemLevel === selectedDifficulty.droppedItemLevel, + ) ?? candidate.difficulties[0] + const locked = profile.character.level < difficulty.unlockLevel + const selected = candidate.id === activity.id + return ( + + ) + })} +
+
-
- {tierActivityOptions.map((candidate) => { - const difficulty = candidate.difficulties.find( - (option) => option.droppedItemLevel === selectedDifficulty.droppedItemLevel, - ) ?? candidate.difficulties[0] - const locked = profile.character.level < difficulty.unlockLevel - const selected = candidate.id === activity.id - return ( - - ) - })} -
-
-
-
- {activityInitials(activity.name)} -
-
-

Step 3

-

{activity.name}

-

{activity.description}

-
- Level {activity.recommendedLevel} - {activity.partySize} Players - {selectedDifficulty.name} - iLvl {selectedDifficulty.droppedItemLevel} - {Math.round(activity.experienceReward * selectedDifficulty.experienceMultiplier)} XP +
-
- {parts.map((p) => ( - - ))} -
-
-
-
-
- {selectedDifficulty.name} - {difficultyLocked ? `Unlocks at level ${selectedDifficulty.unlockLevel}` : selectedDifficulty.description} -
-
-
Health
{selectedDifficulty.healthMultiplier.toFixed(2)}x
-
Damage
{selectedDifficulty.damageMultiplier.toFixed(2)}x
-
XP
{selectedDifficulty.experienceMultiplier.toFixed(1)}x
-
Components
iLvl {selectedDifficulty.droppedItemLevel}
-
-
-
-
+ +

Encounter Rewards

@@ -793,9 +809,9 @@ function App() {
)} -
- {SHOW_LEADERBOARDS && ( -
+
+ {SHOW_LEADERBOARDS && ( +

Efficiency Rankings

@@ -872,8 +888,10 @@ function App() {
)} +
+ )} +
- )} )} diff --git a/src/components/CombatScreen.tsx b/src/components/CombatScreen.tsx index 08d9cb7..48e434d 100644 --- a/src/components/CombatScreen.tsx +++ b/src/components/CombatScreen.tsx @@ -35,7 +35,6 @@ import { } from '../dualScreen' const TICK_MS = 700 -const TARGET_RENDER_THROTTLE_MS = 180 type RoguelikeMode = 'dungeon' | 'raid' type RoguelikeUpgradeTiming = 'boss' | 'encounter' @@ -387,8 +386,6 @@ export function CombatScreen({ const nextFloatingTextId = useRef(1) const combatRef = useRef(initialCombatState) const selectedIdRef = useRef(partyTemplate[0].id) - const selectedRenderTimeoutRef = useRef(null) - const lastSelectedRenderAtRef = useRef(0) const { party, resource, enemyHealth, cooldowns, freeCastReady } = combatState const encounter = encounters[encounterIndex] const currentPart = getCurrentPart(encounterIndex) @@ -438,32 +435,27 @@ export function CombatScreen({ ? nextState(combatRef.current) : nextState combatRef.current = next + setSelectedId(selectedIdRef.current) setCombatState(next) }, []) + const syncSelectedTargetDom = useCallback((id: string) => { + document.querySelectorAll('[data-party-member-id]').forEach((button) => { + const selected = button.dataset.partyMemberId === id + button.classList.toggle('selected', selected) + button.setAttribute('aria-pressed', String(selected)) + }) + }, []) + const setSelectedTargetId = useCallback((id: string) => { if (selectedIdRef.current === id) return selectedIdRef.current = id - const now = performance.now() - const elapsed = now - lastSelectedRenderAtRef.current - if (elapsed >= TARGET_RENDER_THROTTLE_MS) { - lastSelectedRenderAtRef.current = now - setSelectedId(id) - return - } - if (selectedRenderTimeoutRef.current !== null) return - selectedRenderTimeoutRef.current = window.setTimeout(() => { - selectedRenderTimeoutRef.current = null - lastSelectedRenderAtRef.current = performance.now() - setSelectedId(selectedIdRef.current) - }, TARGET_RENDER_THROTTLE_MS - elapsed) - }, []) + syncSelectedTargetDom(id) + }, [syncSelectedTargetDom]) - useEffect(() => () => { - if (selectedRenderTimeoutRef.current !== null) { - window.clearTimeout(selectedRenderTimeoutRef.current) - } - }, []) + useEffect(() => { + syncSelectedTargetDom(selectedIdRef.current) + }, [combatState, syncSelectedTargetDom]) const addLog = useCallback((text: string, tone: CombatLogEntry['tone']) => { const entry = { id: nextLogId.current++, text, tone } @@ -1218,17 +1210,16 @@ export function CombatScreen({ {party.map((member) => ( + {(Object.entries(SLOT_LABELS) as [EquipmentSlot, string][]).map(([slot, label]) => ( + + ))} +
+
+
+

Item Level

+
+ + {availableLevels.map((level) => ( + + ))} +
+
+ + +
+ + {filteredRecipes.length === 0 ? ( +

No recipes match filters.

+ ) : ( +
{recipePageItems.map((recipe) => (
- {recipe.canCraft ? 'Ready' : 'Needs materials'} + + {recipe.canCraft ? 'Ready' : 'Needs materials'} + ))} +
+ )} {filteredRecipes.length > CRAFTING_LIST_PAGE_SIZE && ( )} -
- {selectedRecipe && ( + + +
+ {selectedRecipe ? (
+
+

Materials

+ {selectedRecipe.canCraft ? 'Ready' : 'Missing components'} +
{selectedRecipe.components.map((component) => (
+ ) : ( +

Select a recipe.

)} -
- )} +
+ )} - {profile.setBonuses.length > 0 && ( + {equipmentTab === 'equipment' && profile.setBonuses.length > 0 && (
@@ -561,11 +632,11 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false } ) if (embedded) { - return
{content}
+ return
{content}
} return ( -
+
{content}
) diff --git a/src/dualScreen.tsx b/src/dualScreen.tsx index 5ddb007..0f07c12 100644 --- a/src/dualScreen.tsx +++ b/src/dualScreen.tsx @@ -475,6 +475,7 @@ export function DualScreenTopCombat({ return (