Android build v1.1.12

This commit is contained in:
Warren H
2026-07-03 00:28:38 -04:00
parent c6e2c61d0a
commit b7cb572631
33 changed files with 1772 additions and 305 deletions
+123 -1
View File
@@ -97,14 +97,34 @@ export type DualScreenWorkshopState = {
}>
}
export type DualScreenSetupState = {
contentType: 'dungeon' | 'raid'
title: string
subtitle: string
description: string
initials: string
difficultyName: string
itemLevel: number
experience: number
lockedReason?: string
stats: {
health: string
damage: string
xp: string
loot: string
}
}
type DualScreenMessage =
| { type: 'combat-state'; state: DualScreenCombatState }
| { type: 'workshop-state'; state: DualScreenWorkshopState }
| { type: 'setup-state'; state: DualScreenSetupState }
| { type: 'companion-ready' }
| { type: 'companion-heartbeat' }
| { type: 'control-action'; action: InputAction }
| { type: 'combat-ended' }
| { type: 'workshop-ended' }
| { type: 'setup-ended' }
type DualScreenContextValue = {
enabled: boolean
@@ -160,6 +180,14 @@ function shouldRelayBottomAction(
return action === 'pause' || action === 'back'
}
function shouldRelaySetupAction(
action: InputAction,
state: DualScreenSetupState | null,
) {
if (!state) return false
return action.startsWith('navigate') || action === 'confirm' || action === 'back'
}
export function DualScreenProvider({ children }: { children: ReactNode }) {
const [enabled, setEnabledState] = useState(
() => localStorage.getItem(STORAGE_KEY) === 'true',
@@ -361,6 +389,9 @@ export function useDualScreenWorkshopPublisher(
}
channel.onmessage = (event: MessageEvent<DualScreenMessage>) => {
if (event.data.type === 'companion-ready') publish()
if (event.data.type === 'control-action') {
dispatchExternalGameAction(event.data.action, 'controller')
}
}
publish()
return () => {
@@ -377,9 +408,53 @@ export function useDualScreenWorkshopPublisher(
}, [enabled, state])
}
export function useDualScreenSetupPublisher(
state: DualScreenSetupState | 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: 'setup-state',
state: stateRef.current,
} satisfies DualScreenMessage)
}
}
channel.onmessage = (event: MessageEvent<DualScreenMessage>) => {
if (event.data.type === 'companion-ready') publish()
if (event.data.type === 'control-action') {
dispatchExternalGameAction(event.data.action, 'controller')
}
}
publish()
return () => {
channel.postMessage({ type: 'setup-ended' } satisfies DualScreenMessage)
channel.close()
}
}, [enabled, state])
useEffect(() => {
const channel = createChannel()
if (!enabled || !channel) return
if (state) channel.postMessage({ type: 'setup-state', state } satisfies DualScreenMessage)
else channel.postMessage({ type: 'setup-ended' } satisfies DualScreenMessage)
channel.close()
}, [enabled, state])
}
export function DualScreenBottomDisplay() {
const [state, setState] = useState<DualScreenCombatState | null>(loadRecentSnapshot)
const [workshopState, setWorkshopState] = useState<DualScreenWorkshopState | null>(null)
const [setupState, setSetupState] = useState<DualScreenSetupState | null>(null)
useEffect(() => {
const channel = createChannel()
@@ -389,13 +464,21 @@ export function DualScreenBottomDisplay() {
if (event.data.type === 'combat-state') {
setState(event.data.state)
setWorkshopState(null)
setSetupState(null)
}
if (event.data.type === 'workshop-state') {
setWorkshopState(event.data.state)
setState(null)
setSetupState(null)
}
if (event.data.type === 'setup-state') {
setSetupState(event.data.state)
setState(null)
setWorkshopState(null)
}
if (event.data.type === 'combat-ended') setState(null)
if (event.data.type === 'workshop-ended') setWorkshopState(null)
if (event.data.type === 'setup-ended') setSetupState(null)
}
announce()
const timer = window.setInterval(() => {
@@ -414,7 +497,10 @@ export function DualScreenBottomDisplay() {
}
useGameAction((action) => {
if (!shouldRelayBottomAction(action, state)) return
if (
!shouldRelayBottomAction(action, state)
&& !shouldRelaySetupAction(action, setupState)
) return
sendAction(action)
})
@@ -452,6 +538,42 @@ export function DualScreenBottomDisplay() {
)
}
if (!state && setupState) {
return (
<main className="dual-bottom-display setup-bottom-display">
<header className="dual-controls-header">
<div>
<p className="eyebrow">{setupState.contentType}</p>
<h1>{setupState.title}</h1>
<small>{setupState.subtitle}</small>
</div>
<div className="dual-controls-progress">
<span>{setupState.difficultyName}</span>
<span>iLvl {setupState.itemLevel}</span>
<span>{setupState.experience} XP</span>
</div>
</header>
<section className="setup-bottom-summary">
<span className={`dungeon-art ${setupState.contentType === 'raid' ? 'raid-art' : ''}`}>
{setupState.initials}
</span>
<div>
<p className="eyebrow">Selected Run</p>
<h2>{setupState.title}</h2>
<p>{setupState.description}</p>
{setupState.lockedReason && <small>{setupState.lockedReason}</small>}
</div>
</section>
<dl className="setup-bottom-stats">
<div><dt>Health</dt><dd>{setupState.stats.health}</dd></div>
<div><dt>Damage</dt><dd>{setupState.stats.damage}</dd></div>
<div><dt>XP</dt><dd>{setupState.stats.xp}</dd></div>
<div><dt>Loot</dt><dd>{setupState.stats.loot}</dd></div>
</dl>
</main>
)
}
if (!state) {
return (
<main className="dual-bottom-display dual-bottom-waiting">