diff --git a/IWantToHeal-Thor-v1.0.29.apk b/IWantToHeal-Thor-v1.0.29.apk new file mode 100644 index 0000000..54f248f Binary files /dev/null and b/IWantToHeal-Thor-v1.0.29.apk differ diff --git a/android/app/build.gradle b/android/app/build.gradle index 9702892..3fbc1d6 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 46 - versionName "1.0.28" + versionCode 47 + versionName "1.0.29" 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 9b69028..e3c08fe 100644 --- a/src/App.css +++ b/src/App.css @@ -6851,6 +6851,109 @@ h2 { outline-color: var(--gold); } +.workshop-bottom-display { + gap: 8px; +} + +.workshop-bottom-summary { + background: #1c1e25; + border: 2px solid #0a0b0e; + color: var(--muted); + font-size: 15px; + line-height: 1.15; + outline: 2px solid #41404a; + padding: 10px; +} + +.workshop-bottom-grid { + display: grid; + flex: 1; + gap: 8px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + min-height: 0; + overflow: hidden; +} + +.workshop-bottom-grid article { + align-items: center; + background: #1c1e25; + border: 2px solid #0a0b0e; + color: var(--ink); + display: grid; + gap: 8px; + grid-template-columns: 36px minmax(0, 1fr) auto; + min-height: 58px; + outline: 2px solid #41404a; + padding: 8px; +} + +.workshop-bottom-grid article > span { + align-items: center; + background: #15161c; + color: var(--gold); + display: flex; + font-family: 'Press Start 2P', monospace; + height: 34px; + justify-content: center; +} + +.workshop-bottom-grid strong, +.workshop-bottom-grid small { + display: block; +} + +.workshop-bottom-grid strong { + font-family: 'Press Start 2P', monospace; + font-size: 7px; + line-height: 1.25; +} + +.workshop-bottom-grid small, +.workshop-bottom-grid p { + color: var(--muted); + font-size: 12px; + line-height: 1.05; + margin-top: 3px; +} + +.workshop-bottom-grid i { + color: var(--gold); + font-size: 12px; + font-style: normal; + text-align: right; +} + +.equipment-action-strip, +.crafting-action-row { + align-items: center; + background: #1c1e25; + border: 2px solid #0a0b0e; + display: flex; + gap: 8px; + margin-top: 8px; + min-height: 42px; + outline: 2px solid #3e3d47; + padding: 7px; +} + +.equipment-action-strip .comparison-delta { + display: grid; + gap: 4px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + min-width: 190px; +} + +.equipment-action-strip > p { + color: var(--muted); + margin: 0; +} + +.crafting-action-row { + justify-content: flex-end; + margin-top: 6px; + padding: 0; +} + @media (max-height: 620px) { .game-shell.workshop-shell { height: 100dvh; @@ -6907,6 +7010,10 @@ h2 { padding: 5px 7px; } + .workshop-shell .customize-tabs { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + .workshop-shell .equipment-screen, .workshop-shell .talent-screen { gap: 0; @@ -6965,6 +7072,23 @@ h2 { padding: 5px; } + .workshop-shell .item-comparison, + .workshop-shell .crafting-detail-panel { + display: none; + } + + .workshop-shell .equipment-action-strip { + gap: 5px; + margin-top: 5px; + min-height: 36px; + padding: 5px; + } + + .workshop-shell .equipment-action-strip .comparison-delta { + grid-template-columns: repeat(2, minmax(0, 1fr)); + min-width: 140px; + } + .workshop-shell .comparison-arrow { font-size: 11px; padding: 0; @@ -7020,7 +7144,7 @@ h2 { .workshop-shell .equipment-layout { gap: 6px; - grid-template-columns: minmax(0, 1.05fr) minmax(178px, 0.95fr); + grid-template-columns: minmax(0, 1.15fr) minmax(210px, 0.85fr); margin-top: 5px; } @@ -7115,12 +7239,15 @@ h2 { .workshop-shell .crafting-layout { gap: 6px; - grid-template-columns: minmax(132px, 0.42fr) minmax(230px, 1fr) minmax(190px, 0.72fr); + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr); margin-top: 6px; } .workshop-shell .crafting-filters { + display: grid; gap: 6px; + grid-template-columns: minmax(0, 1fr) auto; } .workshop-shell .crafting-filter-grid, @@ -7128,17 +7255,21 @@ h2 { gap: 4px; } + .workshop-shell .crafting-filter-grid { + grid-template-columns: repeat(11, minmax(0, 1fr)); + } + .workshop-shell .crafting-filter-grid button { min-height: 28px; - padding: 4px; + padding: 3px 1px; } .workshop-shell .crafting-filter-grid strong { - font-size: 5px; + font-size: 4px; } .workshop-shell .crafting-filter-grid span { - font-size: 10px; + font-size: 8px; } .workshop-shell .crafting-level-row button { @@ -7313,15 +7444,253 @@ h2 { } .workshop-shell .crafting-list > button { - grid-template-columns: 24px minmax(0, 1fr); - min-height: 34px; + min-height: 52px; } - .workshop-shell .crafting-list > button i { + .workshop-shell .crafting-action-row { + background: transparent; + border: 0; + margin-top: 5px; + min-height: 28px; + outline: 0; + padding: 0; + } + + .workshop-shell .crafting-action-row .primary-button { + font-size: 8px; + min-height: 28px; + padding: 4px 8px; + } + + .workshop-shell .customize-layout { + gap: 8px; + grid-template-columns: 148px minmax(0, 1fr); + margin-top: 5px; + overflow: hidden; + } + + .workshop-shell .class-picker { + padding-right: 6px; + } + + .workshop-shell .class-picker > .eyebrow { display: none; } + .workshop-shell .class-picker > button { + gap: 6px; + margin-bottom: 5px; + min-height: 44px; + padding: 5px; + } + + .workshop-shell .class-picker > button > span { + flex-basis: 28px; + height: 28px; + } + + .workshop-shell .class-picker strong { + font-size: 7px; + } + + .workshop-shell .class-picker small { + font-size: 10px; + margin-top: 2px; + } + + .workshop-shell .loadout-editor { + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; + } + + .workshop-shell .class-detail { + gap: 8px; + padding: 6px; + } + + .workshop-shell .class-portrait { + flex-basis: 32px; + height: 32px; + } + + .workshop-shell .class-detail h2 { + font-size: 9px; + } + + .workshop-shell .class-detail p:last-child, + .workshop-shell .loadout-heading > span { + display: none; + } + + .workshop-shell .loadout-heading, + .workshop-shell .ability-library-heading, + .workshop-shell .save-row { + margin-top: 6px; + } + + .workshop-shell .ability-slots { + gap: 5px; + margin-top: 5px; + } + + .workshop-shell .ability-slots button { + min-height: 40px; + padding: 5px 2px; + } + + .workshop-shell .ability-slots span { + font-size: 12px; + margin-bottom: 4px; + } + + .workshop-shell .ability-slots strong { + font-size: 9px; + line-height: 1.1; + } + + .workshop-shell .ability-library { + flex: 1; + gap: 5px; + grid-template-columns: repeat(5, minmax(0, 1fr)); + margin-top: 5px; + max-height: none; + min-height: 0; + overflow: hidden; + } + + .workshop-shell .ability-library > button { + align-content: center; + gap: 5px; + grid-template-columns: 1fr; + min-height: 58px; + padding: 5px; + text-align: center; + } + + .workshop-shell .ability-library > button > span { + height: 26px; + } + + .workshop-shell .ability-library strong { + font-size: 8px; + line-height: 1.1; + } + + .workshop-shell .ability-library small, + .workshop-shell .ability-library i { + font-size: 9px; + line-height: 1; + } + + .workshop-shell .ability-library small { + display: none; + } + + .workshop-shell .ability-library i { + font-size: 8px; + } + + .workshop-shell .ability-library > button:nth-child(n+6) { + display: none; + } + + .workshop-shell .save-row .primary-button { + font-size: 8px; + min-height: 28px; + padding: 4px 8px; + } + + .workshop-shell .save-row > span { + font-size: 11px; + } + + .workshop-bottom-display { + padding: 6px; + } + + .workshop-bottom-grid { + gap: 6px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .workshop-bottom-grid article { + min-height: 50px; + padding: 6px; + } +} + +@media (max-width: 700px) and (max-height: 620px) { + .workshop-shell .equipment-layout { + grid-template-columns: minmax(0, 1fr) minmax(190px, 0.7fr); + } + + .workshop-shell .equipment-action-strip .comparison-delta { + display: none; + } + + .workshop-shell .crafting-layout { + grid-template-columns: minmax(0, 1fr); + } + + .workshop-shell .crafting-filters { + grid-template-columns: minmax(0, 1fr); + } + + .workshop-shell .crafting-filter-grid { + grid-template-columns: repeat(11, minmax(0, 1fr)); + } + + .workshop-shell .crafting-filter-grid button { + min-height: 30px; + padding: 3px 1px; + } + + .workshop-shell .crafting-filter-grid strong { + font-size: 4px; + } + + .workshop-shell .crafting-filter-grid span { + font-size: 8px; + } + + .workshop-shell .crafting-level-row { + flex-wrap: nowrap; + } + + .workshop-shell .crafting-list > button { + display: grid; + min-height: 43px; + } + .workshop-shell .crafting-list > button:nth-child(n+4) { + display: grid; + } + + .workshop-shell .customize-layout { + grid-template-columns: 118px minmax(0, 1fr); + } + + .workshop-shell .ability-library { + grid-template-columns: 1fr; + } + + .workshop-shell .ability-library > button:nth-child(n+6) { + display: none; + } + + .workshop-bottom-grid { + grid-template-columns: 1fr; + } +} + +@media (max-height: 620px) { + .workshop-shell .crafting-list > button { + grid-template-columns: 24px minmax(0, 1fr); + } + + .workshop-shell .crafting-list > button i { display: none; } @@ -7341,3 +7710,141 @@ h2 { grid-column: auto !important; } } + +@media (max-height: 620px) { + .workshop-shell .customize-screen > .customize-layout { + gap: 8px; + grid-template-columns: 148px minmax(0, 1fr); + margin-top: 5px; + overflow: hidden; + } + + .workshop-shell .class-picker { + padding-right: 6px; + } + + .workshop-shell .class-picker > .eyebrow { + display: none; + } + + .workshop-shell .class-picker > button { + gap: 6px; + margin-bottom: 5px; + min-height: 44px; + padding: 5px; + } + + .workshop-shell .class-picker > button > span { + flex-basis: 28px; + height: 28px; + } + + .workshop-shell .class-picker strong { + font-size: 7px; + } + + .workshop-shell .class-picker small { + font-size: 10px; + margin-top: 2px; + } + + .workshop-shell .loadout-editor { + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; + } + + .workshop-shell .class-detail { + gap: 8px; + padding: 6px; + } + + .workshop-shell .class-detail p:last-child, + .workshop-shell .loadout-heading > span, + .workshop-shell .ability-library small { + display: none; + } + + .workshop-shell .class-portrait { + flex-basis: 32px; + height: 32px; + } + + .workshop-shell .class-detail h2 { + font-size: 9px; + } + + .workshop-shell .loadout-heading, + .workshop-shell .ability-library-heading, + .workshop-shell .save-row { + margin-top: 6px; + } + + .workshop-shell .ability-slots { + gap: 5px; + margin-top: 5px; + } + + .workshop-shell .ability-slots button { + min-height: 40px; + padding: 5px 2px; + } + + .workshop-shell .ability-slots span { + font-size: 12px; + margin-bottom: 4px; + } + + .workshop-shell .ability-slots strong { + font-size: 9px; + line-height: 1.1; + } + + .workshop-shell .ability-library { + flex: 1; + gap: 5px; + grid-template-columns: repeat(5, minmax(0, 1fr)); + margin-top: 5px; + max-height: none; + min-height: 0; + overflow: hidden; + } + + .workshop-shell .ability-library > button { + align-content: center; + gap: 5px; + grid-template-columns: 1fr; + min-height: 58px; + padding: 5px; + text-align: center; + } + + .workshop-shell .ability-library > button > span { + height: 26px; + } + + .workshop-shell .ability-library strong { + font-size: 8px; + line-height: 1.1; + } + + .workshop-shell .ability-library i { + font-size: 8px; + line-height: 1; + } + + .workshop-shell .ability-library > button:nth-child(n+6) { + display: none; + } +} + +@media (max-width: 700px) and (max-height: 620px) { + .workshop-shell .customize-screen > .customize-layout { + grid-template-columns: 118px minmax(0, 1fr); + } + + .workshop-shell .ability-library { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } +} diff --git a/src/components/CustomizeScreen.tsx b/src/components/CustomizeScreen.tsx index 883356c..a80df03 100644 --- a/src/components/CustomizeScreen.tsx +++ b/src/components/CustomizeScreen.tsx @@ -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>(profile.abilitySlots) const [selectedSlot, setSelectedSlot] = useState(0) @@ -63,6 +65,29 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) { ) } + const classWorkshopState = useMemo(() => { + 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) @@ -91,6 +116,7 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) {
{([ { key: 'equipment', label: 'Equipment' }, + { key: 'crafting', label: 'Crafting' }, { key: 'talents', label: 'Talents' }, { key: 'class', label: 'Class' }, ] as const).map((tab) => ( @@ -110,6 +136,18 @@ export function CustomizeScreen({ profile, onBack, onSaved }: Props) { {activeTab === 'equipment' && ( + )} + + {activeTab === 'crafting' && ( + diff --git a/src/components/EquipmentScreen.tsx b/src/components/EquipmentScreen.tsx index c4a731b..3b3312e 100644 --- a/src/components/EquipmentScreen.tsx +++ b/src/components/EquipmentScreen.tsx @@ -9,6 +9,7 @@ import { type EquipmentSlot, type Item, } from '../profile' +import { useDualScreen, useDualScreenWorkshopPublisher, type DualScreenWorkshopState } from '../dualScreen' const SLOT_LABELS: Record = { weapon: 'Weapon', @@ -24,16 +25,26 @@ const SLOT_LABELS: Record = { } const EQUIPMENT_LIST_PAGE_SIZE = 3 -const CRAFTING_LIST_PAGE_SIZE = 6 +const CRAFTING_LIST_PAGE_SIZE = 4 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 +60,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 +184,10 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false } } }, [equipmentTab]) + useEffect(() => { + if (mode) setEquipmentTab(mode) + }, [mode]) + function saveScroll() { scrollRef.current = window.scrollY } @@ -247,6 +262,127 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false } } } + function renderEquipmentActions() { + if (!selectedItem) { + return

Select an item to inspect it.

+ } + if (selectedItem.slot === 'component') { + return

Used in crafting.

+ } + return ( + <> + + + {upgradeRecipe && ( + + )} + {(!selectedItem.equipped || selectedItem.quantity > 1) && ( + + )} + + ) + } + + const workshopState = useMemo(() => { + 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', + }]), + ], + } + }, [comparisonItem, equipmentTab, selectedItem, selectedRecipe]) + + useDualScreenWorkshopPublisher(workshopState, dualScreenEnabled) + const content = ( <> {!embedded && ( @@ -273,22 +409,24 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
- + {showModeTabs && ( + + )} {equipmentTab === 'equipment' ? ( <> @@ -297,9 +435,6 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false } selectedItem.slot === 'component' ? ( <> -
-

Used in crafting.

-
) : ( <> @@ -313,41 +448,6 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }

{selectedItem.equipped ? 'Already Equipped' : 'Empty Slot'}

)} -
- - - {upgradeRecipe && ( - - )} - {(!selectedItem.equipped || selectedItem.quantity > 1) && ( - - )} -
) ) : ( @@ -355,6 +455,10 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false } )} +
+ {renderEquipmentActions()} +
+
@@ -557,6 +661,16 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false } previousDisabled={recipePage <= 0} /> )} +
+ +
@@ -579,14 +693,6 @@ export function EquipmentScreen({ profile, onBack, onUpdated, embedded = false }
))} - ) : (

Select a recipe.

diff --git a/src/dualScreen.tsx b/src/dualScreen.tsx index 0f07c12..2bd1888 100644 --- a/src/dualScreen.tsx +++ b/src/dualScreen.tsx @@ -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) => { + 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(loadRecentSnapshot) + const [workshopState, setWorkshopState] = useState(null) useEffect(() => { const channel = createChannel() if (!channel) return const announce = () => channel.postMessage({ type: 'companion-ready' } satisfies DualScreenMessage) channel.onmessage = (event: MessageEvent) => { - 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 ( +
+
+
+

{workshopState.mode}

+

{workshopState.title}

+
+
+ {workshopState.subtitle} +
+
+ {workshopState.summary && ( +
+ {workshopState.summary} +
+ )} +
+ {workshopState.items.map((item, index) => ( +
+ {item.glyph && {item.glyph}} +
+ {item.title} + {item.meta && {item.meta}} + {item.detail &&

{item.detail}

} +
+ {item.status && {item.status}} +
+ ))} +
+
+ ) + } + if (!state) { return (