Android build v1.1.2
This commit is contained in:
+34
-75
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user