Android build v1.0.26

This commit is contained in:
Warren H
2026-06-19 20:55:23 -04:00
parent bf12aefeeb
commit 88874933c3
7 changed files with 724 additions and 219 deletions
+19 -28
View File
@@ -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<number | null>(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<HTMLButtonElement>('[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) => (
<button
className={`party-member ${selectedId === member.id ? 'selected' : ''} ${member.health <= 0 ? 'dead' : ''}`}
data-party-member-id={member.id}
key={member.id}
onClick={() => setSelectedTargetId(member.id)}
aria-pressed={selectedId === member.id}
type="button"
>
{selectedId === member.id && (
<span className="target-marker" aria-hidden="true">
<i />
Target
</span>
)}
<span className="target-marker" aria-hidden="true">
<i />
Target
</span>
<div className="member-header">
<span className={`role role-${member.role.toLowerCase()}`}>{member.role[0]}</span>
<strong>{member.name}</strong>
+115 -44
View File
@@ -126,6 +126,16 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
},
[profile.craftingRecipes, slotFilter, levelFilter],
)
const readyRecipeCount = filteredRecipes.filter((recipe) => recipe.canCraft).length
const slotRecipeCounts = useMemo(
() => new Map(
(Object.keys(SLOT_LABELS) as EquipmentSlot[]).map((slot) => [
slot,
profile.craftingRecipes.filter((recipe) => recipe.item.slot === slot).length,
]),
),
[profile.craftingRecipes],
)
const recipePageCount = Math.max(
1,
Math.ceil(filteredRecipes.length / CRAFTING_LIST_PAGE_SIZE),
@@ -147,6 +157,16 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
setRecipePage((current) => Math.min(current, recipePageCount - 1))
}, [recipePageCount])
useEffect(() => {
if (filteredRecipes.length === 0) {
setSelectedRecipeId(null)
return
}
if (!filteredRecipes.some((recipe) => recipe.id === selectedRecipeId)) {
setSelectedRecipeId(filteredRecipes[0].id)
}
}, [filteredRecipes, selectedRecipeId])
useEffect(() => {
if (equipmentTab === 'crafting') {
loadProfile().then((fresh) => onUpdated(fresh)).catch(() => {})
@@ -430,43 +450,82 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
<section className="crafting-panel">
<EquipmentHeading
eyebrow="Crafting"
title="Recipes"
detail={`${filteredRecipes.filter((recipe) => recipe.canCraft).length} ready`}
title="Workbench"
detail={`${readyRecipeCount} ready / ${filteredRecipes.length} shown`}
/>
<div className="crafting-filter-bar">
<select
className="filter-select"
value={slotFilter}
onChange={(e) => {
setSlotFilter(e.target.value as EquipmentSlot | 'all')
setRecipePage(0)
}}
>
<option value="all">All Slots</option>
{(Object.entries(SLOT_LABELS) as [EquipmentSlot, string][]).map(([slot, label]) => (
<option key={slot} value={slot}>{label}</option>
))}
</select>
<select
className="filter-select"
value={levelFilter ?? ''}
onChange={(e) => {
setLevelFilter(e.target.value === '' ? null : Number(e.target.value))
setRecipePage(0)
}}
>
<option value="">All Levels</option>
{availableLevels.map((level) => (
<option key={level} value={level}>Item Level {level}</option>
))}
</select>
</div>
{filteredRecipes.length === 0 && (
<p className="inventory-empty">No crafting recipes match filters.</p>
)}
{filteredRecipes.length > 0 && (
<div className="crafting-layout">
<div className="crafting-list">
<div className="crafting-layout">
<aside className="crafting-filters">
<div>
<p className="eyebrow">Slot</p>
<div className="crafting-filter-grid">
<button
className={slotFilter === 'all' ? 'active' : ''}
onClick={() => {
setSlotFilter('all')
setRecipePage(0)
}}
type="button"
>
<strong>All</strong>
<span>{profile.craftingRecipes.length}</span>
</button>
{(Object.entries(SLOT_LABELS) as [EquipmentSlot, string][]).map(([slot, label]) => (
<button
className={slotFilter === slot ? 'active' : ''}
disabled={(slotRecipeCounts.get(slot) ?? 0) === 0}
key={slot}
onClick={() => {
setSlotFilter(slot)
setRecipePage(0)
}}
type="button"
>
<strong>{label}</strong>
<span>{slotRecipeCounts.get(slot) ?? 0}</span>
</button>
))}
</div>
</div>
<div>
<p className="eyebrow">Item Level</p>
<div className="crafting-level-row">
<button
className={levelFilter === null ? 'active' : ''}
onClick={() => {
setLevelFilter(null)
setRecipePage(0)
}}
type="button"
>
All
</button>
{availableLevels.map((level) => (
<button
className={levelFilter === level ? 'active' : ''}
key={level}
onClick={() => {
setLevelFilter(level)
setRecipePage(0)
}}
type="button"
>
{level}
</button>
))}
</div>
</div>
</aside>
<section className="crafting-list-panel">
<EquipmentHeading
eyebrow="Recipes"
title={slotFilter === 'all' ? 'Available' : SLOT_LABELS[slotFilter]}
detail={`Page ${recipePage + 1}/${recipePageCount}`}
/>
{filteredRecipes.length === 0 ? (
<p className="inventory-empty">No recipes match filters.</p>
) : (
<div className="crafting-list">
{recipePageItems.map((recipe) => (
<button
className={`${selectedRecipeId === recipe.id ? 'selected' : ''} rarity-${recipe.item.rarity}`}
@@ -482,9 +541,13 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
{recipe.item.setName ? ` - ${recipe.item.setName}` : ''}
</small>
</div>
<i>{recipe.canCraft ? 'Ready' : 'Needs materials'}</i>
<i className={recipe.canCraft ? 'ready' : 'missing'}>
{recipe.canCraft ? 'Ready' : 'Needs materials'}
</i>
</button>
))}
</div>
)}
{filteredRecipes.length > CRAFTING_LIST_PAGE_SIZE && (
<ListPager
label={`Page ${recipePage + 1} / ${recipePageCount}`}
@@ -494,10 +557,16 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
previousDisabled={recipePage <= 0}
/>
)}
</div>
{selectedRecipe && (
</section>
<section className="crafting-detail-panel">
{selectedRecipe ? (
<div className={`crafting-detail rarity-${selectedRecipe.item.rarity}`}>
<ItemDetail title="Craft Output" item={{ ...selectedRecipe.item, quantity: 1, equipped: false }} />
<div className="crafting-detail-heading">
<p className="eyebrow">Materials</p>
<span>{selectedRecipe.canCraft ? 'Ready' : 'Missing components'}</span>
</div>
<div className="crafting-components">
{selectedRecipe.components.map((component) => (
<div
@@ -519,13 +588,15 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
{crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'}
</button>
</div>
) : (
<p className="inventory-empty">Select a recipe.</p>
)}
</div>
)}
</section>
</div>
</section>
)}
{profile.setBonuses.length > 0 && (
{equipmentTab === 'equipment' && profile.setBonuses.length > 0 && (
<section className="set-bonus-panel">
<div className="equipment-heading toggle-heading">
<div>
@@ -561,11 +632,11 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
)
if (embedded) {
return <div className="equipment-screen embedded-screen">{content}</div>
return <div className={`equipment-screen embedded-screen ${equipmentTab === 'crafting' ? 'crafting-active' : ''}`}>{content}</div>
}
return (
<section className="content-screen equipment-screen">
<section className={`content-screen equipment-screen ${equipmentTab === 'crafting' ? 'crafting-active' : ''}`}>
{content}
</section>
)