Android build v1.1.2

This commit is contained in:
Warren H
2026-06-29 22:35:49 -04:00
parent cbe42b6164
commit c6251167a5
52 changed files with 5554 additions and 3117 deletions
+34 -75
View File
@@ -9,7 +9,7 @@ import {
useState,
type ReactNode,
} from 'react'
import type { CombatLogEntry, PartyMember, Spell } from './game'
import type { PartyMember, Spell } from './game'
import {
getNativeDisplays,
hasNativeDualScreenBridge,
@@ -21,11 +21,15 @@ import {
type InputAction,
} from './input'
import { ControllerBindingLabel } from './components/ControllerIcons'
import { PartyMemberFrame } from './components/PartyFrames'
import { groupFloatingTextsByMember } from './combat/combatPresentation'
const STORAGE_KEY = 'ashen-halls-dual-screen-enabled'
const SNAPSHOT_KEY = 'ashen-halls-dual-screen-snapshot'
const STARTUP_CHOICE_KEY = 'ashen-halls-dual-screen-startup-choice'
const CHANNEL_NAME = 'ashen-halls-dual-screen'
const COMBAT_PUBLISH_INTERVAL_MS = 100
const COMBAT_SNAPSHOT_INTERVAL_MS = 1000
export type DualScreenCombatState = {
difficultyName: string
@@ -42,7 +46,6 @@ export type DualScreenCombatState = {
opponentName?: string
opponentClassName?: string
opponentParty?: PartyMember[]
opponentResource?: number
opponentEnemyHealth?: number
opponentBuffSummary?: string
opponentDebuffSummary?: string
@@ -53,14 +56,12 @@ export type DualScreenCombatState = {
}>
partySize: number
selectedId: string
log: CombatLogEntry[]
status: 'playing' | 'won' | 'lost' | 'part-complete' | 'marathon-choice' | 'upgrade-choice'
resource: number
maxResource: number
resourceName: string
playerIsAlive: boolean
spells: Array<(Spell & { slotIndex: number; remaining: number }) | null>
activeDevice: 'pc' | 'controller'
bindings: Record<InputAction, string>
controllerIconStyle: ControllerIconStyle
directPartyTargeting: boolean
@@ -139,13 +140,6 @@ function loadRecentSnapshot() {
}
}
function memberHotEffects(member: PartyMember) {
if (member.hotEffects?.length) return member.hotEffects
return member.hotTicks > 0
? [{ id: 'legacy-renew', spellId: 'legacy-renew', label: 'Renew', ticks: member.hotTicks, power: 6 }]
: []
}
function formatDualTime(seconds: number) {
const total = Math.max(0, Math.floor(seconds))
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`
@@ -314,19 +308,19 @@ export function useDualScreenPublisher(
}
}
publish()
saveSnapshot(stateRef.current)
const publishTimer = window.setInterval(publish, COMBAT_PUBLISH_INTERVAL_MS)
const snapshotTimer = window.setInterval(() => {
saveSnapshot(stateRef.current)
}, COMBAT_SNAPSHOT_INTERVAL_MS)
return () => {
window.clearInterval(publishTimer)
window.clearInterval(snapshotTimer)
saveSnapshot(stateRef.current)
channel.postMessage({ type: 'combat-ended' } satisfies DualScreenMessage)
channel.close()
}
}, [enabled])
useEffect(() => {
if (!enabled) return
saveSnapshot(state)
const channel = createChannel()
channel?.postMessage({ type: 'combat-state', state } satisfies DualScreenMessage)
channel?.close()
}, [enabled, state])
}
export function useDualScreenWorkshopPublisher(
@@ -490,25 +484,13 @@ export function DualScreenBottomDisplay() {
<section className={`dual-opponent-party-grid ${state.opponentParty.length > 6 ? 'raid' : ''}`}>
{state.opponentParty.map((member) => (
<article className={`dual-opponent-member ${member.health <= 0 ? 'dead' : ''}`} key={member.id}>
<div className="member-header">
<span className={`role role-${member.role.toLowerCase()}`}>{member.role[0]}</span>
<strong>{member.name}</strong>
<small>{Math.ceil(member.health)} / {member.maxHealth}</small>
</div>
<div className="bar member-health">
<span style={{ width: `${(member.health / member.maxHealth) * 100}%` }} />
{member.shield > 0 && (
<i style={{ width: `${(member.shield / member.maxHealth) * 100}%` }} />
)}
</div>
<div className="member-effects">
{memberHotEffects(member).map((effect) => (
<span className="buff" key={effect.id}>{effect.label}</span>
))}
{member.debuff && <span className="debuff">{member.debuff}</span>}
</div>
</article>
<PartyMemberFrame
as="article"
baseClassName="dual-opponent-member"
effectMode="compact"
key={member.id}
member={member}
/>
))}
</section>
@@ -639,6 +621,10 @@ export function DualScreenTopCombat({
0,
(state.encounterHealth / state.encounterMaxHealth) * 100,
)
const floatingTextsByMember = useMemo(
() => groupFloatingTextsByMember(state.floatingTexts),
[state.floatingTexts],
)
return (
<div className="dual-top-main">
@@ -667,44 +653,17 @@ export function DualScreenTopCombat({
const inCurrentTargetGroup = index >= groupStart && index < groupStart + 6
const targetBinding = state.directPartyTargeting && inCurrentTargetGroup ? state.bindings[targetAction] : null
return (
<button
className={`dual-top-member ${state.selectedId === member.id ? 'selected' : ''} ${member.health <= 0 ? 'dead' : ''}`}
data-party-member-id={member.id}
<PartyMemberFrame
baseClassName="dual-top-member"
controllerIconStyle={state.controllerIconStyle}
effectMode="compact"
floatingTexts={floatingTextsByMember.get(member.id) ?? []}
key={member.id}
onClick={() => onSelectTarget(member.id)}
type="button"
>
<div className="member-header">
<span className={`role role-${member.role.toLowerCase()}`}>{member.role[0]}</span>
<strong>{member.name}</strong>
<small>{Math.ceil(member.health)} / {member.maxHealth}</small>
</div>
<div className="bar member-health">
<span style={{ width: `${(member.health / member.maxHealth) * 100}%` }} />
{member.shield > 0 && (
<i style={{ width: `${(member.shield / member.maxHealth) * 100}%` }} />
)}
</div>
<div className="floating-combat-texts" aria-hidden="true">
{state.floatingTexts
.filter((entry) => entry.memberId === member.id)
.map((entry) => <span className="floating-heal" key={entry.id}>+{entry.value}</span>)}
</div>
{state.directPartyTargeting && targetBinding && (
<div className="member-target-key">
<ControllerBindingLabel
binding={targetBinding}
iconStyle={state.controllerIconStyle}
/>
</div>
)}
<div className="member-effects">
{memberHotEffects(member).map((effect) => (
<span className="buff" key={effect.id}>{effect.label}</span>
))}
{member.debuff && <span className="debuff">{member.debuff}</span>}
</div>
</button>
member={member}
onSelect={onSelectTarget}
selected={state.selectedId === member.id}
targetBinding={targetBinding}
/>
)
})}
</div>