Compare commits

..

4 Commits

Author SHA1 Message Date
Warren H fd6a1ce3c7 Android build v1.0.30 2026-06-19 22:10:14 -04:00
Warren H f8b98e6b23 Android build v1.0.29 2026-06-19 21:58:23 -04:00
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
15 changed files with 2087 additions and 830 deletions
+2
View File
@@ -2,5 +2,7 @@
- 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 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.
- Apply game changes to both web version and mobile app version.
Binary file not shown.
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"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 44
versionName "1.0.26"
versionCode 48
versionName "1.0.30"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// 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
SET rarity = CASE item_level
WHEN 1 THEN 'common'
WHEN 5 THEN 'common'
WHEN 10 THEN 'uncommon'
WHEN 15 THEN 'rare'
WHEN 20 THEN 'epic'
WHEN 25 THEN 'legendary'
ELSE rarity
@@ -493,9 +491,7 @@ SET name = (
SELECT
CASE items.item_level
WHEN 1 THEN 'Raw '
WHEN 5 THEN 'Honed '
WHEN 10 THEN 'Green '
WHEN 15 THEN 'Blue '
WHEN 20 THEN 'Purple '
WHEN 25 THEN 'Orange '
ELSE ''
@@ -1276,12 +1272,23 @@ SET difficulty_id = CASE
END
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
SET rarity = CASE item_level
WHEN 1 THEN 'common'
WHEN 5 THEN 'common'
WHEN 10 THEN 'uncommon'
WHEN 15 THEN 'rare'
WHEN 20 THEN 'epic'
WHEN 25 THEN 'legendary'
ELSE rarity
@@ -1293,9 +1300,7 @@ SET name = (
SELECT
CASE items.item_level
WHEN 1 THEN 'Raw '
WHEN 5 THEN 'Honed '
WHEN 10 THEN 'Green '
WHEN 15 THEN 'Blue '
WHEN 20 THEN 'Purple '
WHEN 25 THEN 'Orange '
ELSE ''
+1695 -20
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -328,7 +328,7 @@ function App() {
: a.sequence - b.sequence)
return (
<main className={`game-shell ${screen === 'dungeons' || screen === 'raids' ? 'dungeon-shell' : ''}`}>
<main className={`game-shell ${screen === 'dungeons' || screen === 'raids' ? 'dungeon-shell' : ''} ${screen === 'customize' ? 'workshop-shell' : ''}`}>
<header className="topbar app-header">
<button
className="brand-button"
+43 -7
View File
@@ -386,6 +386,11 @@ export function CombatScreen({
const nextFloatingTextId = useRef(1)
const combatRef = useRef(initialCombatState)
const selectedIdRef = useRef(partyTemplate[0].id)
const runCombatTickRef = useRef<() => void>(() => {})
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 encounter = encounters[encounterIndex]
const currentPart = getCurrentPart(encounterIndex)
@@ -415,6 +420,9 @@ export function CombatScreen({
enabled: dualScreenEnabled,
} = useDualScreen()
statusRef.current = status
pausedRef.current = paused
useEffect(() => {
const now = Date.now()
runStartedAtRef.current = now
@@ -846,9 +854,7 @@ export function CombatScreen({
if (spell) castSpell(spell)
})
useEffect(() => {
if (status !== 'playing' || paused) return
const timer = window.setInterval(() => {
const runCombatTick = useCallback(() => {
const current = combatRef.current
const nextElapsedTicks = current.elapsedTicks + 1
const nextCooldowns = Object.fromEntries(
@@ -1042,8 +1048,6 @@ export function CombatScreen({
enemyHealth: nextEncounter.maxHealth,
})
addLog(`${encounter.enemyName} defeated. ${nextEncounter.enemyName} approaches.`, 'system')
}, TICK_MS)
return () => window.clearInterval(timer)
}, [
addLog,
addFloatingHeal,
@@ -1065,11 +1069,43 @@ export function CombatScreen({
profile.character.name,
setCombat,
startPart,
status,
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(() => {
if (
!reward
+41 -2
View File
@@ -4,6 +4,7 @@ import {
type CharacterProfile,
type GameClass,
} from '../profile'
import { useDualScreen, useDualScreenWorkshopPublisher, type DualScreenWorkshopState } from '../dualScreen'
import { EquipmentScreen } from './EquipmentScreen'
import { TalentScreen } from './TalentScreen'
@@ -14,7 +15,8 @@ type Props = {
}
export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
const [activeTab, setActiveTab] = useState<'equipment' | 'talents' | 'class'>('class')
const [activeTab, setActiveTab] = useState<'equipment' | 'crafting' | 'talents' | 'class'>('class')
const { enabled: dualScreenEnabled } = useDualScreen()
const [classId, setClassId] = useState(profile.character.classId)
const [slots, setSlots] = useState<Array<number | null>>(profile.abilitySlots)
const [selectedSlot, setSelectedSlot] = useState(0)
@@ -63,6 +65,29 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
)
}
const classWorkshopState = useMemo<DualScreenWorkshopState | null>(() => {
if (activeTab !== 'class') return null
return {
mode: 'class',
title: 'Ability Library',
subtitle: gameClass.name,
summary: `Selected slot ${selectedSlot + 1}. ${message || 'Choose an ability for the active loadout.'}`,
items: gameClass.spells.map((ability) => {
const locked = ability.unlockLevel > profile.character.level
const equipped = slots.includes(ability.id)
return {
glyph: locked ? 'L' : ability.glyph,
title: ability.name,
meta: locked ? `Level ${ability.unlockLevel}` : `${ability.cost} ${gameClass.resourceName}`,
detail: ability.description,
status: equipped ? 'Equipped' : locked ? 'Locked' : '',
}
}),
}
}, [activeTab, gameClass, message, profile.character.level, selectedSlot, slots])
useDualScreenWorkshopPublisher(classWorkshopState, dualScreenEnabled)
async function persistChanges() {
saveScroll()
setSaving(true)
@@ -80,7 +105,7 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
return (
<section className="content-screen customize-screen">
<div className="screen-heading">
<div className="screen-heading customize-heading">
<div>
<p className="eyebrow">Character Workshop</p>
<h1>Customize Character</h1>
@@ -89,8 +114,10 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
</div>
<div className="customize-tabs" role="tablist" aria-label="Customize character sections">
<button className="back-button customize-tab-back" onClick={onBack} type="button">Back</button>
{([
{ key: 'equipment', label: 'Equipment' },
{ key: 'crafting', label: 'Crafting' },
{ key: 'talents', label: 'Talents' },
{ key: 'class', label: 'Class' },
] as const).map((tab) => (
@@ -110,6 +137,18 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
{activeTab === 'equipment' && (
<EquipmentScreen
embedded
mode="equipment"
showModeTabs={false}
profile={profile}
onUpdated={onSaved}
/>
)}
{activeTab === 'crafting' && (
<EquipmentScreen
embedded
mode="crafting"
showModeTabs={false}
profile={profile}
onUpdated={onSaved}
/>
+191 -67
View File
@@ -9,6 +9,7 @@ import {
type EquipmentSlot,
type Item,
} from '../profile'
import { useDualScreen, useDualScreenWorkshopPublisher, type DualScreenWorkshopState } from '../dualScreen'
const SLOT_LABELS: Record<EquipmentSlot, string> = {
weapon: 'Weapon',
@@ -24,16 +25,28 @@ const SLOT_LABELS: Record<EquipmentSlot, string> = {
}
const EQUIPMENT_LIST_PAGE_SIZE = 3
const CRAFTING_LIST_PAGE_SIZE = 6
const CRAFTING_LIST_PAGE_SIZE = 3
const CRAFTING_FILTER_SLOTS = (Object.keys(SLOT_LABELS) as EquipmentSlot[])
.filter((slot) => slot !== 'component')
type Props = {
profile: CharacterProfile
onBack?: () => void
onUpdated: (profile: CharacterProfile) => void
embedded?: boolean
mode?: 'equipment' | 'crafting'
showModeTabs?: boolean
}
export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }: Props) {
export function EquipmentScreen({
profile,
onBack,
onUpdated,
embedded = false,
mode,
showModeTabs = true,
}: Props) {
const { enabled: dualScreenEnabled } = useDualScreen()
const totalItemCount = profile.inventory.reduce(
(total, item) => total + item.quantity,
0,
@@ -49,7 +62,7 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
const [crafting, setCrafting] = useState(false)
const [upgrading, setUpgrading] = useState(false)
const [showSetBonuses, setShowSetBonuses] = useState(false)
const [equipmentTab, setEquipmentTab] = useState<'equipment' | 'crafting'>('equipment')
const [equipmentTab, setEquipmentTab] = useState<'equipment' | 'crafting'>(mode ?? 'equipment')
const [inventoryPage, setInventoryPage] = useState(0)
const [recipePage, setRecipePage] = useState(0)
const [message, setMessage] = useState('')
@@ -173,6 +186,10 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
}
}, [equipmentTab])
useEffect(() => {
if (mode) setEquipmentTab(mode)
}, [mode])
function saveScroll() {
scrollRef.current = window.scrollY
}
@@ -247,6 +264,143 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
}
}
function renderEquipmentActions() {
if (!selectedItem) {
return <p>Select an item to inspect it.</p>
}
if (selectedItem.slot === 'component') {
return <p className="component-note">Used in crafting.</p>
}
return (
<>
<ComparisonDelta selected={selectedItem} equipped={comparisonItem} />
<button
className="primary-button"
disabled={selectedItem.equipped || equipping || breakingDown || upgrading}
onClick={equipSelected}
type="button"
>
{selectedItem.equipped ? 'Equipped' : equipping ? 'Equipping...' : 'Equip Item'}
</button>
{upgradeRecipe && (
<button
className="primary-button"
disabled={!upgradeRecipe.canCraft || equipping || breakingDown || upgrading}
onClick={upgradeSelected}
type="button"
>
{upgrading ? 'Upgrading...' : `Upgrade to iLvl ${upgradeRecipe.item.itemLevel}`}
</button>
)}
{(!selectedItem.equipped || selectedItem.quantity > 1) && (
<button
className="breakdown-button"
disabled={equipping || breakingDown || upgrading}
onClick={breakdownSelected}
type="button"
>
{breakingDown
? 'Breaking Down...'
: selectedItem.quantity > 1
? 'Break Down Duplicate'
: 'Break Down'}
</button>
)}
</>
)
}
const workshopState = useMemo<DualScreenWorkshopState>(() => {
if (equipmentTab === 'crafting') {
if (!selectedRecipe) {
return {
mode: 'crafting',
title: 'Craft Output',
subtitle: 'No recipe selected',
items: [],
}
}
return {
mode: 'crafting',
title: selectedRecipe.item.name,
subtitle: `${SLOT_LABELS[selectedRecipe.item.slot]} - Item Level ${selectedRecipe.item.itemLevel}`,
summary: selectedRecipe.item.description,
items: [
{
glyph: selectedRecipe.item.glyph,
title: 'Craft Output',
meta: `+${selectedRecipe.item.healingPower} Healing Power / +${selectedRecipe.item.maxResourceBonus} Max Resource`,
status: selectedRecipe.canCraft ? 'Ready' : 'Missing components',
},
...selectedRecipe.components.map((component) => ({
glyph: component.item.glyph,
title: component.item.name,
meta: `Item Level ${component.item.itemLevel}`,
status: `${component.owned}/${component.quantity}`,
})),
],
}
}
if (!selectedItem) {
return {
mode: 'equipment',
title: 'Equipment Detail',
subtitle: 'No item selected',
items: [],
}
}
return {
mode: 'equipment',
title: selectedItem.slot === 'component' ? 'Crafting Component' : selectedItem.name,
subtitle: `${SLOT_LABELS[selectedItem.slot]} - Item Level ${selectedItem.itemLevel}`,
summary: selectedItem.description,
items: selectedItem.slot === 'component'
? [{
glyph: selectedItem.glyph,
title: selectedItem.name,
meta: `Owned: ${selectedItem.quantity}`,
status: 'Component',
}]
: [
{
glyph: selectedItem.glyph,
title: selectedItem.name,
meta: `+${selectedItem.healingPower} Healing Power / +${selectedItem.maxResourceBonus} Max Resource`,
status: selectedItem.equipped ? 'Equipped' : 'Inventory',
},
...(comparisonItem && comparisonItem.id !== selectedItem.id
? [{
glyph: comparisonItem.glyph,
title: comparisonItem.name,
meta: `+${comparisonItem.healingPower} Healing Power / +${comparisonItem.maxResourceBonus} Max Resource`,
status: 'Currently Equipped',
}]
: [{
title: selectedItem.equipped ? 'Already Equipped' : 'Empty Slot',
status: 'Comparison',
}]),
...(upgradeRecipe
? [
{
glyph: upgradeRecipe.item.glyph,
title: `Upgrade to ${upgradeRecipe.item.name}`,
meta: `Item Level ${upgradeRecipe.item.itemLevel}`,
status: upgradeRecipe.canCraft ? 'Ready' : 'Missing materials',
},
...upgradeRecipe.components.map((component) => ({
glyph: component.item.glyph,
title: component.item.name,
meta: `Required for upgrade`,
status: `${component.owned}/${component.quantity}`,
})),
]
: []),
],
}
}, [comparisonItem, equipmentTab, selectedItem, selectedRecipe, upgradeRecipe])
useDualScreenWorkshopPublisher(workshopState, dualScreenEnabled)
const content = (
<>
{!embedded && (
@@ -273,22 +427,24 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
<GearStat value={`+${profile.gearStats.maxResourceBonus}`} label={`Max ${profile.character.resourceName}`} />
</div>
<nav className="equipment-tabs">
<button
className={`equipment-tab ${equipmentTab === 'equipment' ? 'active' : ''}`}
onClick={() => setEquipmentTab('equipment')}
type="button"
>
Equipment
</button>
<button
className={`equipment-tab ${equipmentTab === 'crafting' ? 'active' : ''}`}
onClick={() => setEquipmentTab('crafting')}
type="button"
>
Crafting
</button>
</nav>
{showModeTabs && (
<nav className="equipment-tabs">
<button
className={`equipment-tab ${equipmentTab === 'equipment' ? 'active' : ''}`}
onClick={() => setEquipmentTab('equipment')}
type="button"
>
Equipment
</button>
<button
className={`equipment-tab ${equipmentTab === 'crafting' ? 'active' : ''}`}
onClick={() => setEquipmentTab('crafting')}
type="button"
>
Crafting
</button>
</nav>
)}
{equipmentTab === 'equipment' ? (
<>
@@ -297,9 +453,6 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
selectedItem.slot === 'component' ? (
<>
<ItemDetail title="Crafting Component" item={selectedItem} />
<div className="equip-action">
<p className="component-note">Used in crafting.</p>
</div>
</>
) : (
<>
@@ -313,41 +466,6 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
<h2>{selectedItem.equipped ? 'Already Equipped' : 'Empty Slot'}</h2>
</div>
)}
<div className="equip-action">
<ComparisonDelta selected={selectedItem} equipped={comparisonItem} />
<button
className="primary-button"
disabled={selectedItem.equipped || equipping || breakingDown || upgrading}
onClick={equipSelected}
type="button"
>
{selectedItem.equipped ? 'Equipped' : equipping ? 'Equipping...' : 'Equip Item'}
</button>
{upgradeRecipe && (
<button
className="primary-button"
disabled={!upgradeRecipe.canCraft || equipping || breakingDown || upgrading}
onClick={upgradeSelected}
type="button"
>
{upgrading ? 'Upgrading...' : `Upgrade to iLvl ${upgradeRecipe.item.itemLevel}`}
</button>
)}
{(!selectedItem.equipped || selectedItem.quantity > 1) && (
<button
className="breakdown-button"
disabled={equipping || breakingDown || upgrading}
onClick={breakdownSelected}
type="button"
>
{breakingDown
? 'Breaking Down...'
: selectedItem.quantity > 1
? 'Break Down Duplicate'
: 'Break Down'}
</button>
)}
</div>
</>
)
) : (
@@ -355,6 +473,10 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
)}
</section>
<section className="equipment-action-strip">
{renderEquipmentActions()}
</section>
<div className="equipment-layout">
<section className="equipped-panel">
<EquipmentHeading eyebrow="Currently Worn" title="Equipment Slots" />
@@ -469,7 +591,7 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
<strong>All</strong>
<span>{profile.craftingRecipes.length}</span>
</button>
{(Object.entries(SLOT_LABELS) as [EquipmentSlot, string][]).map(([slot, label]) => (
{CRAFTING_FILTER_SLOTS.map((slot) => (
<button
className={slotFilter === slot ? 'active' : ''}
disabled={(slotRecipeCounts.get(slot) ?? 0) === 0}
@@ -480,7 +602,7 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
}}
type="button"
>
<strong>{label}</strong>
<strong>{SLOT_LABELS[slot]}</strong>
<span>{slotRecipeCounts.get(slot) ?? 0}</span>
</button>
))}
@@ -557,6 +679,16 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
previousDisabled={recipePage <= 0}
/>
)}
<div className="crafting-action-row">
<button
className="primary-button"
disabled={selectedRecipeRequiresUpgrade || !selectedRecipe?.canCraft || crafting}
onClick={craftSelected}
type="button"
>
{crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'}
</button>
</div>
</section>
<section className="crafting-detail-panel">
@@ -579,14 +711,6 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
</div>
))}
</div>
<button
className="primary-button"
disabled={selectedRecipeRequiresUpgrade || !selectedRecipe.canCraft || crafting}
onClick={craftSelected}
type="button"
>
{crafting ? 'Crafting...' : selectedRecipeRequiresUpgrade ? 'Upgrade Existing Item' : 'Craft Item'}
</button>
</div>
) : (
<p className="inventory-empty">Select a recipe.</p>
+99 -1
View File
@@ -56,12 +56,28 @@ export type DualScreenCombatState = {
targetGroup: 0 | 1 | 2
}
export type DualScreenWorkshopState = {
mode: 'class' | 'equipment' | 'crafting' | 'talents'
title: string
subtitle: string
summary?: string
items: Array<{
glyph?: string
title: string
meta?: string
detail?: string
status?: string
}>
}
type DualScreenMessage =
| { type: 'combat-state'; state: DualScreenCombatState }
| { type: 'workshop-state'; state: DualScreenWorkshopState }
| { type: 'companion-ready' }
| { type: 'companion-heartbeat' }
| { type: 'control-action'; action: InputAction }
| { type: 'combat-ended' }
| { type: 'workshop-ended' }
type DualScreenContextValue = {
enabled: boolean
@@ -280,16 +296,64 @@ export function useDualScreenPublisher(
}, [enabled, state])
}
export function useDualScreenWorkshopPublisher(
state: DualScreenWorkshopState | null,
enabled: boolean,
) {
const stateRef = useRef(state)
useEffect(() => {
stateRef.current = state
}, [state])
useEffect(() => {
if (!enabled || !state) return
const channel = createChannel()
if (!channel) return
const publish = () => {
if (stateRef.current) {
channel.postMessage({
type: 'workshop-state',
state: stateRef.current,
} satisfies DualScreenMessage)
}
}
channel.onmessage = (event: MessageEvent<DualScreenMessage>) => {
if (event.data.type === 'companion-ready') publish()
}
publish()
return () => {
channel.postMessage({ type: 'workshop-ended' } satisfies DualScreenMessage)
channel.close()
}
}, [enabled, state])
useEffect(() => {
if (!enabled || !state) return
const channel = createChannel()
channel?.postMessage({ type: 'workshop-state', state } satisfies DualScreenMessage)
channel?.close()
}, [enabled, state])
}
export function DualScreenBottomDisplay() {
const [state, setState] = useState<DualScreenCombatState | null>(loadRecentSnapshot)
const [workshopState, setWorkshopState] = useState<DualScreenWorkshopState | null>(null)
useEffect(() => {
const channel = createChannel()
if (!channel) return
const announce = () => channel.postMessage({ type: 'companion-ready' } satisfies DualScreenMessage)
channel.onmessage = (event: MessageEvent<DualScreenMessage>) => {
if (event.data.type === 'combat-state') setState(event.data.state)
if (event.data.type === 'combat-state') {
setState(event.data.state)
setWorkshopState(null)
}
if (event.data.type === 'workshop-state') {
setWorkshopState(event.data.state)
setState(null)
}
if (event.data.type === 'combat-ended') setState(null)
if (event.data.type === 'workshop-ended') setWorkshopState(null)
}
announce()
const timer = window.setInterval(() => {
@@ -307,6 +371,40 @@ export function DualScreenBottomDisplay() {
channel?.close()
}
if (!state && workshopState) {
return (
<main className="dual-bottom-display workshop-bottom-display">
<header className="dual-controls-header">
<div>
<p className="eyebrow">{workshopState.mode}</p>
<h1>{workshopState.title}</h1>
</div>
<div className="dual-controls-progress">
<span>{workshopState.subtitle}</span>
</div>
</header>
{workshopState.summary && (
<section className="workshop-bottom-summary">
{workshopState.summary}
</section>
)}
<section className="workshop-bottom-grid">
{workshopState.items.map((item, index) => (
<article key={`${item.title}-${index}`}>
{item.glyph && <span>{item.glyph}</span>}
<div>
<strong>{item.title}</strong>
{item.meta && <small>{item.meta}</small>}
{item.detail && <p>{item.detail}</p>}
</div>
{item.status && <i>{item.status}</i>}
</article>
))}
</section>
</main>
)
}
if (!state) {
return (
<main className="dual-bottom-display dual-bottom-waiting">
-2
View File
@@ -385,9 +385,7 @@ function scaledPvpBossExperience(
type ComponentTemplate = { id: number; slug: string; name: string; itemLevel: number; glyph: string; description: string }
const COMPONENT_ITEMS: Record<number, ComponentTemplate> = {
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.' },
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.' },
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
},
{
"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,
"difficultyId": 2,
@@ -1931,366 +1571,6 @@
],
"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,
"difficultyId": 4,