Compare commits

...

3 Commits

Author SHA1 Message Date
Warren H fc7c6488ea Android build v1.0.28 2026-06-19 21:35:17 -04:00
Warren H ba6d3b614e Android build v1.0.27 2026-06-19 21:29:44 -04:00
Warren H 88874933c3 Android build v1.0.26 2026-06-19 20:55:23 -04:00
13 changed files with 1941 additions and 956 deletions
+2
View File
@@ -2,5 +2,7 @@
- AYN Thor main display: 6-inch AMOLED, 1920 x 1080, 120Hz. - AYN Thor main display: 6-inch AMOLED, 1920 x 1080, 120Hz.
- AYN Thor secondary display: 3.92-inch AMOLED, 1240 x 1080, 60Hz. - AYN Thor secondary display: 3.92-inch AMOLED, 1240 x 1080, 60Hz.
- AYN Thor UI sizing must be designed against Android CSS/layout viewport, not physical framebuffer pixels.
- Approximate Thor CSS viewports: main display 960 x 540, secondary display 620 x 540.
- User rebuilds app; do not rebuild APK unless explicitly requested. - User rebuilds app; do not rebuild APK unless explicitly requested.
- Apply game changes to both web version and mobile app version. - Apply game changes to both web version and mobile app version.
Binary file not shown.
Binary file not shown.
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 43 versionCode 46
versionName "1.0.27" versionName "1.0.28"
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.
+13 -8
View File
@@ -479,9 +479,7 @@ WHERE id BETWEEN 901 AND 1409;
UPDATE items UPDATE items
SET rarity = CASE item_level SET rarity = CASE item_level
WHEN 1 THEN 'common' WHEN 1 THEN 'common'
WHEN 5 THEN 'common'
WHEN 10 THEN 'uncommon' WHEN 10 THEN 'uncommon'
WHEN 15 THEN 'rare'
WHEN 20 THEN 'epic' WHEN 20 THEN 'epic'
WHEN 25 THEN 'legendary' WHEN 25 THEN 'legendary'
ELSE rarity ELSE rarity
@@ -493,9 +491,7 @@ SET name = (
SELECT SELECT
CASE items.item_level CASE items.item_level
WHEN 1 THEN 'Raw ' WHEN 1 THEN 'Raw '
WHEN 5 THEN 'Honed '
WHEN 10 THEN 'Green ' WHEN 10 THEN 'Green '
WHEN 15 THEN 'Blue '
WHEN 20 THEN 'Purple ' WHEN 20 THEN 'Purple '
WHEN 25 THEN 'Orange ' WHEN 25 THEN 'Orange '
ELSE '' ELSE ''
@@ -1276,12 +1272,23 @@ SET difficulty_id = CASE
END END
WHERE id BETWEEN 901 AND 1409; WHERE id BETWEEN 901 AND 1409;
DELETE FROM crafting_recipe_components
WHERE recipe_id IN (
SELECT crafting_recipes.id
FROM crafting_recipes
JOIN items ON items.id = crafting_recipes.item_id
WHERE items.item_level NOT IN (1, 10, 20, 25)
);
DELETE FROM crafting_recipes
WHERE item_id IN (
SELECT id FROM items WHERE item_level NOT IN (1, 10, 20, 25)
);
UPDATE items UPDATE items
SET rarity = CASE item_level SET rarity = CASE item_level
WHEN 1 THEN 'common' WHEN 1 THEN 'common'
WHEN 5 THEN 'common'
WHEN 10 THEN 'uncommon' WHEN 10 THEN 'uncommon'
WHEN 15 THEN 'rare'
WHEN 20 THEN 'epic' WHEN 20 THEN 'epic'
WHEN 25 THEN 'legendary' WHEN 25 THEN 'legendary'
ELSE rarity ELSE rarity
@@ -1293,9 +1300,7 @@ SET name = (
SELECT SELECT
CASE items.item_level CASE items.item_level
WHEN 1 THEN 'Raw ' WHEN 1 THEN 'Raw '
WHEN 5 THEN 'Honed '
WHEN 10 THEN 'Green ' WHEN 10 THEN 'Green '
WHEN 15 THEN 'Blue '
WHEN 20 THEN 'Purple ' WHEN 20 THEN 'Purple '
WHEN 25 THEN 'Orange ' WHEN 25 THEN 'Orange '
ELSE '' ELSE ''
+1591 -8
View File
File diff suppressed because it is too large Load Diff
+79 -61
View File
@@ -328,7 +328,7 @@ function App() {
: a.sequence - b.sequence) : a.sequence - b.sequence)
return ( return (
<main className="game-shell"> <main className={`game-shell ${screen === 'dungeons' || screen === 'raids' ? 'dungeon-shell' : ''} ${screen === 'customize' ? 'workshop-shell' : ''}`}>
<header className="topbar app-header"> <header className="topbar app-header">
<button <button
className="brand-button" className="brand-button"
@@ -586,19 +586,82 @@ function App() {
)} )}
{(screen === 'dungeons' || screen === 'raids') && ( {(screen === 'dungeons' || screen === 'raids') && (
<section className="content-screen"> <section className="content-screen dungeon-run-screen">
<ScreenHeading <ScreenHeading
eyebrow="Adventure" eyebrow="Adventure"
title={activity.contentType === 'raid' ? 'Raids' : 'Dungeons'} title={activity.contentType === 'raid' ? 'Raids' : 'Dungeons'}
onBack={() => setScreen('menu')} onBack={() => setScreen('menu')}
/> />
<section className="run-setup-panel"> <div className="dungeon-run-board">
<div className="dungeon-run-main">
<article className="run-summary-card dungeon-focus-card">
<div className={`dungeon-art ${activity.contentType === 'raid' ? 'raid-art' : ''}`}>
{activityInitials(activity.name)}
</div>
<div className="run-summary-copy">
<p className="eyebrow">Selected Run</p>
<h2>{activity.name}</h2>
<p>{activity.description}</p>
<div className="tag-row">
<span>Level {activity.recommendedLevel}</span>
<span>{activity.partySize} Players</span>
<span>{selectedDifficulty.name}</span>
<span>iLvl {selectedDifficulty.droppedItemLevel}</span>
<span>{Math.round(activity.experienceReward * selectedDifficulty.experienceMultiplier)} XP</span>
</div>
</div>
</article>
<section className="run-setup-panel dungeon-choice-panel">
<div className="run-setup-heading"> <div className="run-setup-heading">
<div> <div>
<p className="eyebrow">Step 1</p> <p className="eyebrow">Pick Run</p>
<h2>Item Level</h2> <h2>{screen === 'raids' ? 'Raid' : 'Dungeon'}</h2>
</div> </div>
<small>{screen === 'raids' ? 'Raid' : 'Dungeon'} tiers unlock by character level.</small> <small>{selectedDifficulty.name} rewards iLvl {selectedDifficulty.droppedItemLevel} components.</small>
</div>
<div className="activity-card-grid dungeon-choice-grid">
{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 (
<button
className={`activity-card ${selected ? 'selected' : ''} ${locked ? 'locked' : ''}`}
disabled={locked}
key={candidate.id}
onClick={() => {
if (screen === 'raids') setSelectedRaidId(candidate.id)
else setSelectedDungeonId(candidate.id)
setSelectedDifficultyId(difficulty.id)
}}
type="button"
>
<span className={`dungeon-art ${candidate.contentType === 'raid' ? 'raid-art' : ''}`}>
{activityInitials(candidate.name)}
</span>
<strong>{candidate.name}</strong>
<small>{candidate.locationName}</small>
<i>
Level {candidate.recommendedLevel} | {candidate.partySize} Players
</i>
</button>
)
})}
</div>
</section>
</div>
<aside className="dungeon-setup-rail">
<section className="run-setup-panel tier-setup-panel">
<div className="run-setup-heading">
<div>
<p className="eyebrow">Item Level</p>
<h2>Tier</h2>
</div>
<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) => {
@@ -636,62 +699,13 @@ function App() {
</div> </div>
</section> </section>
<section className="run-setup-panel"> <section className="run-setup-panel part-setup-panel">
<div className="run-setup-heading"> <div className="run-setup-heading">
<div> <div>
<p className="eyebrow">Step 2</p> <p className="eyebrow">Start</p>
<h2>{screen === 'raids' ? 'Pick Raid' : 'Pick Dungeon'}</h2> <h2>{sectionName}</h2>
</div>
<small>{selectedDifficulty.name} rewards iLvl {selectedDifficulty.droppedItemLevel} components.</small>
</div>
<div className="activity-card-grid">
{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 (
<button
className={`activity-card ${selected ? 'selected' : ''} ${locked ? 'locked' : ''}`}
disabled={locked}
key={candidate.id}
onClick={() => {
if (screen === 'raids') setSelectedRaidId(candidate.id)
else setSelectedDungeonId(candidate.id)
setSelectedDifficultyId(difficulty.id)
}}
type="button"
>
<span className={`dungeon-art ${candidate.contentType === 'raid' ? 'raid-art' : ''}`}>
{activityInitials(candidate.name)}
</span>
<strong>{candidate.name}</strong>
<small>{candidate.locationName}</small>
<i>
Level {candidate.recommendedLevel} | {candidate.partySize} Players
</i>
</button>
)
})}
</div>
</section>
<article className="run-summary-card">
<div className={`dungeon-art ${activity.contentType === 'raid' ? 'raid-art' : ''}`}>
{activityInitials(activity.name)}
</div>
<div className="run-summary-copy">
<p className="eyebrow">Step 3</p>
<h2>{activity.name}</h2>
<p>{activity.description}</p>
<div className="tag-row">
<span>Level {activity.recommendedLevel}</span>
<span>{activity.partySize} Players</span>
<span>{selectedDifficulty.name}</span>
<span>iLvl {selectedDifficulty.droppedItemLevel}</span>
<span>{Math.round(activity.experienceReward * selectedDifficulty.experienceMultiplier)} XP</span>
</div> </div>
<small>{difficultyLocked ? `Unlocks at level ${selectedDifficulty.unlockLevel}` : 'Choose a section to launch.'}</small>
</div> </div>
<div className="part-picker"> <div className="part-picker">
{parts.map((p) => ( {parts.map((p) => (
@@ -711,7 +725,8 @@ function App() {
</button> </button>
))} ))}
</div> </div>
</article> </section>
<div className="difficulty-section compact-difficulty-section"> <div className="difficulty-section compact-difficulty-section">
<div className={`difficulty-summary ${difficultyLocked ? 'locked' : ''}`}> <div className={`difficulty-summary ${difficultyLocked ? 'locked' : ''}`}>
<div> <div>
@@ -722,10 +737,11 @@ function App() {
<div><dt>Health</dt><dd>{selectedDifficulty.healthMultiplier.toFixed(2)}x</dd></div> <div><dt>Health</dt><dd>{selectedDifficulty.healthMultiplier.toFixed(2)}x</dd></div>
<div><dt>Damage</dt><dd>{selectedDifficulty.damageMultiplier.toFixed(2)}x</dd></div> <div><dt>Damage</dt><dd>{selectedDifficulty.damageMultiplier.toFixed(2)}x</dd></div>
<div><dt>XP</dt><dd>{selectedDifficulty.experienceMultiplier.toFixed(1)}x</dd></div> <div><dt>XP</dt><dd>{selectedDifficulty.experienceMultiplier.toFixed(1)}x</dd></div>
<div><dt>Components</dt><dd>iLvl {selectedDifficulty.droppedItemLevel}</dd></div> <div><dt>Loot</dt><dd>iLvl {selectedDifficulty.droppedItemLevel}</dd></div>
</dl> </dl>
</div> </div>
</div> </div>
<div className="loot-preview-section"> <div className="loot-preview-section">
<div className="equipment-heading toggle-heading"> <div className="equipment-heading toggle-heading">
<div> <div>
@@ -874,6 +890,8 @@ function App() {
)} )}
</div> </div>
)} )}
</aside>
</div>
</section> </section>
)} )}
+58 -31
View File
@@ -35,7 +35,6 @@ import {
} from '../dualScreen' } from '../dualScreen'
const TICK_MS = 700 const TICK_MS = 700
const TARGET_RENDER_THROTTLE_MS = 180
type RoguelikeMode = 'dungeon' | 'raid' type RoguelikeMode = 'dungeon' | 'raid'
type RoguelikeUpgradeTiming = 'boss' | 'encounter' type RoguelikeUpgradeTiming = 'boss' | 'encounter'
@@ -387,8 +386,11 @@ export function CombatScreen({
const nextFloatingTextId = useRef(1) const nextFloatingTextId = useRef(1)
const combatRef = useRef(initialCombatState) const combatRef = useRef(initialCombatState)
const selectedIdRef = useRef(partyTemplate[0].id) const selectedIdRef = useRef(partyTemplate[0].id)
const selectedRenderTimeoutRef = useRef<number | null>(null) const runCombatTickRef = useRef<() => void>(() => {})
const lastSelectedRenderAtRef = useRef(0) const combatClockActiveRef = useRef(false)
const lastCombatTickAtRef = useRef(performance.now())
const statusRef = useRef(status)
const pausedRef = useRef(paused)
const { party, resource, enemyHealth, cooldowns, freeCastReady } = combatState const { party, resource, enemyHealth, cooldowns, freeCastReady } = combatState
const encounter = encounters[encounterIndex] const encounter = encounters[encounterIndex]
const currentPart = getCurrentPart(encounterIndex) const currentPart = getCurrentPart(encounterIndex)
@@ -418,6 +420,9 @@ export function CombatScreen({
enabled: dualScreenEnabled, enabled: dualScreenEnabled,
} = useDualScreen() } = useDualScreen()
statusRef.current = status
pausedRef.current = paused
useEffect(() => { useEffect(() => {
const now = Date.now() const now = Date.now()
runStartedAtRef.current = now runStartedAtRef.current = now
@@ -438,32 +443,27 @@ export function CombatScreen({
? nextState(combatRef.current) ? nextState(combatRef.current)
: nextState : nextState
combatRef.current = next combatRef.current = next
setSelectedId(selectedIdRef.current)
setCombatState(next) 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) => { const setSelectedTargetId = useCallback((id: string) => {
if (selectedIdRef.current === id) return if (selectedIdRef.current === id) return
selectedIdRef.current = id selectedIdRef.current = id
const now = performance.now() syncSelectedTargetDom(id)
const elapsed = now - lastSelectedRenderAtRef.current }, [syncSelectedTargetDom])
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)
}, [])
useEffect(() => () => { useEffect(() => {
if (selectedRenderTimeoutRef.current !== null) { syncSelectedTargetDom(selectedIdRef.current)
window.clearTimeout(selectedRenderTimeoutRef.current) }, [combatState, syncSelectedTargetDom])
}
}, [])
const addLog = useCallback((text: string, tone: CombatLogEntry['tone']) => { const addLog = useCallback((text: string, tone: CombatLogEntry['tone']) => {
const entry = { id: nextLogId.current++, text, tone } const entry = { id: nextLogId.current++, text, tone }
@@ -854,9 +854,7 @@ export function CombatScreen({
if (spell) castSpell(spell) if (spell) castSpell(spell)
}) })
useEffect(() => { const runCombatTick = useCallback(() => {
if (status !== 'playing' || paused) return
const timer = window.setInterval(() => {
const current = combatRef.current const current = combatRef.current
const nextElapsedTicks = current.elapsedTicks + 1 const nextElapsedTicks = current.elapsedTicks + 1
const nextCooldowns = Object.fromEntries( const nextCooldowns = Object.fromEntries(
@@ -1050,8 +1048,6 @@ export function CombatScreen({
enemyHealth: nextEncounter.maxHealth, enemyHealth: nextEncounter.maxHealth,
}) })
addLog(`${encounter.enemyName} defeated. ${nextEncounter.enemyName} approaches.`, 'system') addLog(`${encounter.enemyName} defeated. ${nextEncounter.enemyName} approaches.`, 'system')
}, TICK_MS)
return () => window.clearInterval(timer)
}, [ }, [
addLog, addLog,
addFloatingHeal, addFloatingHeal,
@@ -1073,11 +1069,43 @@ export function CombatScreen({
profile.character.name, profile.character.name,
setCombat, setCombat,
startPart, startPart,
status,
currentPart, currentPart,
paused,
]) ])
useEffect(() => {
runCombatTickRef.current = runCombatTick
}, [runCombatTick])
useEffect(() => {
if (status === 'playing' && !paused) {
if (!combatClockActiveRef.current) {
lastCombatTickAtRef.current = performance.now()
combatClockActiveRef.current = true
}
return
}
combatClockActiveRef.current = false
}, [paused, status])
useEffect(() => {
const timer = window.setInterval(() => {
if (
!combatClockActiveRef.current
|| statusRef.current !== 'playing'
|| pausedRef.current
) return
const now = performance.now()
const dueTicks = Math.min(4, Math.floor((now - lastCombatTickAtRef.current) / TICK_MS))
if (dueTicks <= 0) return
lastCombatTickAtRef.current += dueTicks * TICK_MS
for (let index = 0; index < dueTicks; index += 1) {
if (statusRef.current !== 'playing' || pausedRef.current) return
runCombatTickRef.current()
}
}, 50)
return () => window.clearInterval(timer)
}, [])
useEffect(() => { useEffect(() => {
if ( if (
!reward !reward
@@ -1218,17 +1246,16 @@ export function CombatScreen({
{party.map((member) => ( {party.map((member) => (
<button <button
className={`party-member ${selectedId === member.id ? 'selected' : ''} ${member.health <= 0 ? 'dead' : ''}`} className={`party-member ${selectedId === member.id ? 'selected' : ''} ${member.health <= 0 ? 'dead' : ''}`}
data-party-member-id={member.id}
key={member.id} key={member.id}
onClick={() => setSelectedTargetId(member.id)} onClick={() => setSelectedTargetId(member.id)}
aria-pressed={selectedId === member.id} aria-pressed={selectedId === member.id}
type="button" type="button"
> >
{selectedId === member.id && (
<span className="target-marker" aria-hidden="true"> <span className="target-marker" aria-hidden="true">
<i /> <i />
Target Target
</span> </span>
)}
<div className="member-header"> <div className="member-header">
<span className={`role role-${member.role.toLowerCase()}`}>{member.role[0]}</span> <span className={`role role-${member.role.toLowerCase()}`}>{member.role[0]}</span>
<strong>{member.name}</strong> <strong>{member.name}</strong>
+112 -41
View File
@@ -126,6 +126,16 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
}, },
[profile.craftingRecipes, slotFilter, levelFilter], [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( const recipePageCount = Math.max(
1, 1,
Math.ceil(filteredRecipes.length / CRAFTING_LIST_PAGE_SIZE), 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)) setRecipePage((current) => Math.min(current, recipePageCount - 1))
}, [recipePageCount]) }, [recipePageCount])
useEffect(() => {
if (filteredRecipes.length === 0) {
setSelectedRecipeId(null)
return
}
if (!filteredRecipes.some((recipe) => recipe.id === selectedRecipeId)) {
setSelectedRecipeId(filteredRecipes[0].id)
}
}, [filteredRecipes, selectedRecipeId])
useEffect(() => { useEffect(() => {
if (equipmentTab === 'crafting') { if (equipmentTab === 'crafting') {
loadProfile().then((fresh) => onUpdated(fresh)).catch(() => {}) loadProfile().then((fresh) => onUpdated(fresh)).catch(() => {})
@@ -430,42 +450,81 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
<section className="crafting-panel"> <section className="crafting-panel">
<EquipmentHeading <EquipmentHeading
eyebrow="Crafting" eyebrow="Crafting"
title="Recipes" title="Workbench"
detail={`${filteredRecipes.filter((recipe) => recipe.canCraft).length} ready`} 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-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"> <div className="crafting-list">
{recipePageItems.map((recipe) => ( {recipePageItems.map((recipe) => (
<button <button
@@ -482,9 +541,13 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
{recipe.item.setName ? ` - ${recipe.item.setName}` : ''} {recipe.item.setName ? ` - ${recipe.item.setName}` : ''}
</small> </small>
</div> </div>
<i>{recipe.canCraft ? 'Ready' : 'Needs materials'}</i> <i className={recipe.canCraft ? 'ready' : 'missing'}>
{recipe.canCraft ? 'Ready' : 'Needs materials'}
</i>
</button> </button>
))} ))}
</div>
)}
{filteredRecipes.length > CRAFTING_LIST_PAGE_SIZE && ( {filteredRecipes.length > CRAFTING_LIST_PAGE_SIZE && (
<ListPager <ListPager
label={`Page ${recipePage + 1} / ${recipePageCount}`} label={`Page ${recipePage + 1} / ${recipePageCount}`}
@@ -494,10 +557,16 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
previousDisabled={recipePage <= 0} previousDisabled={recipePage <= 0}
/> />
)} )}
</div> </section>
{selectedRecipe && (
<section className="crafting-detail-panel">
{selectedRecipe ? (
<div className={`crafting-detail rarity-${selectedRecipe.item.rarity}`}> <div className={`crafting-detail rarity-${selectedRecipe.item.rarity}`}>
<ItemDetail title="Craft Output" item={{ ...selectedRecipe.item, quantity: 1, equipped: false }} /> <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"> <div className="crafting-components">
{selectedRecipe.components.map((component) => ( {selectedRecipe.components.map((component) => (
<div <div
@@ -519,13 +588,15 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
{crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'} {crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'}
</button> </button>
</div> </div>
) : (
<p className="inventory-empty">Select a recipe.</p>
)} )}
</section>
</div> </div>
)}
</section> </section>
)} )}
{profile.setBonuses.length > 0 && ( {equipmentTab === 'equipment' && profile.setBonuses.length > 0 && (
<section className="set-bonus-panel"> <section className="set-bonus-panel">
<div className="equipment-heading toggle-heading"> <div className="equipment-heading toggle-heading">
<div> <div>
@@ -561,11 +632,11 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
) )
if (embedded) { 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 ( return (
<section className="content-screen equipment-screen"> <section className={`content-screen equipment-screen ${equipmentTab === 'crafting' ? 'crafting-active' : ''}`}>
{content} {content}
</section> </section>
) )
+1
View File
@@ -475,6 +475,7 @@ export function DualScreenTopCombat({
return ( return (
<button <button
className={`dual-top-member ${state.selectedId === member.id ? 'selected' : ''} ${member.health <= 0 ? 'dead' : ''}`} className={`dual-top-member ${state.selectedId === member.id ? 'selected' : ''} ${member.health <= 0 ? 'dead' : ''}`}
data-party-member-id={member.id}
key={member.id} key={member.id}
onClick={() => onSelectTarget(member.id)} onClick={() => onSelectTarget(member.id)}
type="button" type="button"
-2
View File
@@ -385,9 +385,7 @@ function scaledPvpBossExperience(
type ComponentTemplate = { id: number; slug: string; name: string; itemLevel: number; glyph: string; description: string } type ComponentTemplate = { id: number; slug: string; name: string; itemLevel: number; glyph: string; description: string }
const COMPONENT_ITEMS: Record<number, ComponentTemplate> = { const COMPONENT_ITEMS: Record<number, ComponentTemplate> = {
1: { id: 600, slug: 'minor-component', name: 'Minor Component', itemLevel: 1, glyph: '◆', description: 'A basic crafting component.' }, 1: { id: 600, slug: 'minor-component', name: 'Minor Component', itemLevel: 1, glyph: '◆', description: 'A basic crafting component.' },
5: { id: 601, slug: 'basic-component', name: 'Basic Component', itemLevel: 5, glyph: '◇', description: 'A standard crafting component.' },
10: { id: 602, slug: 'refined-component', name: 'Refined Component', itemLevel: 10, glyph: '◈', description: 'A refined crafting component.' }, 10: { id: 602, slug: 'refined-component', name: 'Refined Component', itemLevel: 10, glyph: '◈', description: 'A refined crafting component.' },
15: { id: 603, slug: 'advanced-component', name: 'Advanced Component', itemLevel: 15, glyph: '◉', description: 'An advanced crafting component.' },
20: { id: 604, slug: 'superior-component', name: 'Superior Component', itemLevel: 20, glyph: '◎', description: 'A superior crafting component.' }, 20: { id: 604, slug: 'superior-component', name: 'Superior Component', itemLevel: 20, glyph: '◎', description: 'A superior crafting component.' },
25: { id: 605, slug: 'primal-component', name: 'Primal Component', itemLevel: 25, glyph: '✦', description: 'A primal crafting component.' }, 25: { id: 605, slug: 'primal-component', name: 'Primal Component', itemLevel: 25, glyph: '✦', description: 'A primal crafting component.' },
} }
-720
View File
@@ -1211,366 +1211,6 @@
], ],
"canCraft": false "canCraft": false
}, },
{
"id": 1004,
"difficultyId": 1,
"sourceDungeonId": 1,
"sourceEncounterId": 12,
"item": {
"id": 4,
"slug": "cinderstep-boots",
"name": "Honed Yian Kut-Ku Boots",
"slot": "boots",
"rarity": "common",
"itemLevel": 5,
"healingPower": 3,
"maxResourceBonus": 0,
"glyph": "b",
"description": "Crafted with Yian Kut-Ku coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 281201,
"slug": "yian-kut-ku-coin-ilvl-1",
"name": "Raw Yian Kut-Ku Coin",
"slot": "component",
"rarity": "common",
"itemLevel": 1,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Yian Kut-Ku used for item level 1 crafting."
},
"quantity": 5,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1002,
"difficultyId": 1,
"sourceDungeonId": 1,
"sourceEncounterId": 3,
"item": {
"id": 2,
"slug": "wardens-cinderwrap",
"name": "Honed Bulldrome Chest",
"slot": "chest",
"rarity": "common",
"itemLevel": 5,
"healingPower": 3,
"maxResourceBonus": 0,
"glyph": "C",
"description": "Crafted with Bulldrome coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 280301,
"slug": "bulldrome-coin-ilvl-1",
"name": "Raw Bulldrome Coin",
"slot": "component",
"rarity": "common",
"itemLevel": 1,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Bulldrome used for item level 1 crafting."
},
"quantity": 5,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1003,
"difficultyId": 1,
"sourceDungeonId": 1,
"sourceEncounterId": 3,
"item": {
"id": 6,
"slug": "furnace-tenders-wraps",
"name": "Honed Bulldrome Gloves",
"slot": "gloves",
"rarity": "common",
"itemLevel": 5,
"healingPower": 3,
"maxResourceBonus": 2,
"glyph": "g",
"description": "Crafted with Bulldrome coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 280301,
"slug": "bulldrome-coin-ilvl-1",
"name": "Raw Bulldrome Coin",
"slot": "component",
"rarity": "common",
"itemLevel": 1,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Bulldrome used for item level 1 crafting."
},
"quantity": 5,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1001,
"difficultyId": 1,
"sourceDungeonId": 1,
"sourceEncounterId": 3,
"item": {
"id": 5,
"slug": "adepts-hood",
"name": "Honed Bulldrome Helmet",
"slot": "helmet",
"rarity": "common",
"itemLevel": 5,
"healingPower": 3,
"maxResourceBonus": 4,
"glyph": "^",
"description": "Crafted with Bulldrome coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 280301,
"slug": "bulldrome-coin-ilvl-1",
"name": "Raw Bulldrome Coin",
"slot": "component",
"rarity": "common",
"itemLevel": 1,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Bulldrome used for item level 1 crafting."
},
"quantity": 5,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1009,
"difficultyId": 1,
"sourceDungeonId": 1,
"sourceEncounterId": 22,
"item": {
"id": 9,
"slug": "sootglass-pendant",
"name": "Honed Rathian Necklace",
"slot": "necklace",
"rarity": "common",
"itemLevel": 5,
"healingPower": 4,
"maxResourceBonus": 4,
"glyph": "n",
"description": "Crafted with Rathian coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 282201,
"slug": "rathian-coin-ilvl-1",
"name": "Raw Rathian Coin",
"slot": "component",
"rarity": "common",
"itemLevel": 1,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Rathian used for item level 1 crafting."
},
"quantity": 5,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1008,
"difficultyId": 1,
"sourceDungeonId": 1,
"sourceEncounterId": 22,
"item": {
"id": 8,
"slug": "ashwalker-legwraps",
"name": "Honed Rathian Pants",
"slot": "pants",
"rarity": "common",
"itemLevel": 5,
"healingPower": 3,
"maxResourceBonus": 3,
"glyph": "P",
"description": "Crafted with Rathian coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 282201,
"slug": "rathian-coin-ilvl-1",
"name": "Raw Rathian Coin",
"slot": "component",
"rarity": "common",
"itemLevel": 1,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Rathian used for item level 1 crafting."
},
"quantity": 5,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1005,
"difficultyId": 1,
"sourceDungeonId": 1,
"sourceEncounterId": 12,
"item": {
"id": 1,
"slug": "emberglass-sigil",
"name": "Honed Yian Kut-Ku Ring",
"slot": "ring",
"rarity": "common",
"itemLevel": 5,
"healingPower": 4,
"maxResourceBonus": 5,
"glyph": "o",
"description": "Crafted with Yian Kut-Ku coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 281201,
"slug": "yian-kut-ku-coin-ilvl-1",
"name": "Raw Yian Kut-Ku Coin",
"slot": "component",
"rarity": "common",
"itemLevel": 1,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Yian Kut-Ku used for item level 1 crafting."
},
"quantity": 5,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1006,
"difficultyId": 1,
"sourceDungeonId": 1,
"sourceEncounterId": 12,
"item": {
"id": 7,
"slug": "warden-ember",
"name": "Honed Yian Kut-Ku Trinket",
"slot": "trinket",
"rarity": "common",
"itemLevel": 5,
"healingPower": 4,
"maxResourceBonus": 4,
"glyph": "*",
"description": "Crafted with Yian Kut-Ku coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 281201,
"slug": "yian-kut-ku-coin-ilvl-1",
"name": "Raw Yian Kut-Ku Coin",
"slot": "component",
"rarity": "common",
"itemLevel": 1,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Yian Kut-Ku used for item level 1 crafting."
},
"quantity": 5,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1007,
"difficultyId": 1,
"sourceDungeonId": 1,
"sourceEncounterId": 22,
"item": {
"id": 3,
"slug": "ashwood-crook",
"name": "Honed Rathian Weapon",
"slot": "weapon",
"rarity": "common",
"itemLevel": 5,
"healingPower": 5,
"maxResourceBonus": 0,
"glyph": "/",
"description": "Crafted with Rathian coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 282201,
"slug": "rathian-coin-ilvl-1",
"name": "Raw Rathian Coin",
"slot": "component",
"rarity": "common",
"itemLevel": 1,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Rathian used for item level 1 crafting."
},
"quantity": 5,
"owned": 0
}
],
"canCraft": false
},
{ {
"id": 1104, "id": 1104,
"difficultyId": 2, "difficultyId": 2,
@@ -1931,366 +1571,6 @@
], ],
"canCraft": false "canCraft": false
}, },
{
"id": 1204,
"difficultyId": 2,
"sourceDungeonId": 1,
"sourceEncounterId": 12,
"item": {
"id": 304,
"slug": "runed-cinderstep-boots",
"name": "Blue Yian Kut-Ku Boots",
"slot": "boots",
"rarity": "rare",
"itemLevel": 15,
"healingPower": 9,
"maxResourceBonus": 8,
"glyph": "b",
"description": "Crafted with Yian Kut-Ku coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 281210,
"slug": "yian-kut-ku-coin-ilvl-10",
"name": "Green Yian Kut-Ku Coin",
"slot": "component",
"rarity": "uncommon",
"itemLevel": 10,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Yian Kut-Ku used for item level 10 crafting."
},
"quantity": 15,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1202,
"difficultyId": 2,
"sourceDungeonId": 1,
"sourceEncounterId": 3,
"item": {
"id": 302,
"slug": "runed-cinderwrap",
"name": "Blue Bulldrome Chest",
"slot": "chest",
"rarity": "rare",
"itemLevel": 15,
"healingPower": 11,
"maxResourceBonus": 3,
"glyph": "C",
"description": "Crafted with Bulldrome coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 280310,
"slug": "bulldrome-coin-ilvl-10",
"name": "Green Bulldrome Coin",
"slot": "component",
"rarity": "uncommon",
"itemLevel": 10,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Bulldrome used for item level 10 crafting."
},
"quantity": 15,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1203,
"difficultyId": 2,
"sourceDungeonId": 1,
"sourceEncounterId": 3,
"item": {
"id": 306,
"slug": "runed-furnace-wraps",
"name": "Blue Bulldrome Gloves",
"slot": "gloves",
"rarity": "rare",
"itemLevel": 15,
"healingPower": 11,
"maxResourceBonus": 6,
"glyph": "g",
"description": "Crafted with Bulldrome coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 280310,
"slug": "bulldrome-coin-ilvl-10",
"name": "Green Bulldrome Coin",
"slot": "component",
"rarity": "uncommon",
"itemLevel": 10,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Bulldrome used for item level 10 crafting."
},
"quantity": 15,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1201,
"difficultyId": 2,
"sourceDungeonId": 1,
"sourceEncounterId": 3,
"item": {
"id": 305,
"slug": "runed-adepts-hood",
"name": "Blue Bulldrome Helmet",
"slot": "helmet",
"rarity": "rare",
"itemLevel": 15,
"healingPower": 9,
"maxResourceBonus": 9,
"glyph": "^",
"description": "Crafted with Bulldrome coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 280310,
"slug": "bulldrome-coin-ilvl-10",
"name": "Green Bulldrome Coin",
"slot": "component",
"rarity": "uncommon",
"itemLevel": 10,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Bulldrome used for item level 10 crafting."
},
"quantity": 15,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1209,
"difficultyId": 2,
"sourceDungeonId": 1,
"sourceEncounterId": 22,
"item": {
"id": 309,
"slug": "runed-sootglass-pendant",
"name": "Blue Rathian Necklace",
"slot": "necklace",
"rarity": "rare",
"itemLevel": 15,
"healingPower": 12,
"maxResourceBonus": 10,
"glyph": "n",
"description": "Crafted with Rathian coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 282210,
"slug": "rathian-coin-ilvl-10",
"name": "Green Rathian Coin",
"slot": "component",
"rarity": "uncommon",
"itemLevel": 10,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Rathian used for item level 10 crafting."
},
"quantity": 15,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1208,
"difficultyId": 2,
"sourceDungeonId": 1,
"sourceEncounterId": 22,
"item": {
"id": 308,
"slug": "runed-ashwalker-legwraps",
"name": "Blue Rathian Pants",
"slot": "pants",
"rarity": "rare",
"itemLevel": 15,
"healingPower": 9,
"maxResourceBonus": 9,
"glyph": "P",
"description": "Crafted with Rathian coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 282210,
"slug": "rathian-coin-ilvl-10",
"name": "Green Rathian Coin",
"slot": "component",
"rarity": "uncommon",
"itemLevel": 10,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Rathian used for item level 10 crafting."
},
"quantity": 15,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1205,
"difficultyId": 2,
"sourceDungeonId": 1,
"sourceEncounterId": 12,
"item": {
"id": 301,
"slug": "runed-emberglass-sigil",
"name": "Blue Yian Kut-Ku Ring",
"slot": "ring",
"rarity": "rare",
"itemLevel": 15,
"healingPower": 10,
"maxResourceBonus": 13,
"glyph": "o",
"description": "Crafted with Yian Kut-Ku coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 281210,
"slug": "yian-kut-ku-coin-ilvl-10",
"name": "Green Yian Kut-Ku Coin",
"slot": "component",
"rarity": "uncommon",
"itemLevel": 10,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Yian Kut-Ku used for item level 10 crafting."
},
"quantity": 15,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1206,
"difficultyId": 2,
"sourceDungeonId": 1,
"sourceEncounterId": 12,
"item": {
"id": 307,
"slug": "runed-warden-ember",
"name": "Blue Yian Kut-Ku Trinket",
"slot": "trinket",
"rarity": "rare",
"itemLevel": 15,
"healingPower": 12,
"maxResourceBonus": 10,
"glyph": "*",
"description": "Crafted with Yian Kut-Ku coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 281210,
"slug": "yian-kut-ku-coin-ilvl-10",
"name": "Green Yian Kut-Ku Coin",
"slot": "component",
"rarity": "uncommon",
"itemLevel": 10,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Yian Kut-Ku used for item level 10 crafting."
},
"quantity": 15,
"owned": 0
}
],
"canCraft": false
},
{
"id": 1207,
"difficultyId": 2,
"sourceDungeonId": 1,
"sourceEncounterId": 22,
"item": {
"id": 303,
"slug": "runed-ashwood-crook",
"name": "Blue Rathian Weapon",
"slot": "weapon",
"rarity": "rare",
"itemLevel": 15,
"healingPower": 15,
"maxResourceBonus": 3,
"glyph": "/",
"description": "Crafted with Rathian coins.",
"setId": null,
"setSlug": null,
"setName": null
},
"components": [
{
"item": {
"id": 282210,
"slug": "rathian-coin-ilvl-10",
"name": "Green Rathian Coin",
"slot": "component",
"rarity": "uncommon",
"itemLevel": 10,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Rathian used for item level 10 crafting."
},
"quantity": 15,
"owned": 0
}
],
"canCraft": false
},
{ {
"id": 1304, "id": 1304,
"difficultyId": 4, "difficultyId": 4,