made some changes to the UI, removed leaderboards. updated gamesaves

This commit is contained in:
Warren H
2026-06-18 13:00:29 -04:00
parent 3c90998a61
commit a604569a2f
44 changed files with 2301 additions and 435 deletions
+80 -13
View File
@@ -11,6 +11,7 @@ import {
} from 'react'
import type { CombatLogEntry, PartyMember, Spell } from './game'
import {
getNativeDisplays,
hasNativeDualScreenBridge,
openNativeTopDisplay,
} from './nativeDualScreen'
@@ -23,6 +24,7 @@ import { ControllerBindingLabel } from './components/ControllerIcons'
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'
export type DualScreenCombatState = {
@@ -51,7 +53,7 @@ export type DualScreenCombatState = {
controllerIconStyle: ControllerIconStyle
directPartyTargeting: boolean
paused: boolean
targetGroup: 0 | 1
targetGroup: 0 | 1 | 2
}
type DualScreenMessage =
@@ -172,6 +174,73 @@ export function useDualScreen() {
return context
}
export function DualScreenStartupPrompt() {
const { openTopDisplay, setEnabled } = useDualScreen()
const [visible, setVisible] = useState(false)
const [displayCount, setDisplayCount] = useState<number | null>(null)
const [message, setMessage] = useState('')
const autoOpenedRef = useRef(false)
useEffect(() => {
if (!hasNativeDualScreenBridge()) return
if (new URLSearchParams(window.location.search).has('display')) return
const choice = localStorage.getItem(STARTUP_CHOICE_KEY)
if (choice === 'yes') {
if (autoOpenedRef.current) return
autoOpenedRef.current = true
openTopDisplay().catch(() => {
// Settings can still launch the display manually if Android rejects startup launch.
})
return
}
if (choice === 'no') return
getNativeDisplays()
.then((result) => setDisplayCount(result.displays.length))
.catch(() => setDisplayCount(null))
.finally(() => setVisible(true))
}, [openTopDisplay])
async function enableDualScreen() {
localStorage.setItem(STARTUP_CHOICE_KEY, 'yes')
setMessage('Opening second display...')
const opened = await openTopDisplay()
if (opened) {
setVisible(false)
return
}
setMessage('No second display found. Check Thor display mode, then try again.')
}
function skipDualScreen() {
localStorage.setItem(STARTUP_CHOICE_KEY, 'no')
setEnabled(false)
setVisible(false)
}
if (!visible) return null
return (
<div className="dual-startup-prompt" role="dialog" aria-modal="true">
<section>
<p className="eyebrow">Display Setup</p>
<h2>Use Dual-Screen Mode?</h2>
<p>
Choose yes on AYN Thor. The game opens the combat view on the upper
display and keeps controls on the lower display.
</p>
{displayCount !== null && (
<small>{displayCount} Android display{displayCount === 1 ? '' : 's'} detected.</small>
)}
{message && <small>{message}</small>}
<div>
<button onClick={enableDualScreen} type="button">Yes, Enable</button>
<button onClick={skipDualScreen} type="button">No</button>
</div>
</section>
</div>
)
}
export function useDualScreenPublisher(
state: DualScreenCombatState,
enabled: boolean,
@@ -280,9 +349,9 @@ export function DualScreenBottomDisplay() {
<div className={`dual-controls-targets ${state.directPartyTargeting ? 'direct' : ''}`}>
{state.directPartyTargeting ? (
<>
{([1, 2, 3, 4, 5] as const).map((slot) => {
{([1, 2, 3, 4, 5, 6] as const).map((slot) => {
const action = `targetParty${slot}` as InputAction
const memberIndex = slot - 1 + (state.partySize === 10 ? state.targetGroup * 5 : 0)
const memberIndex = slot - 1 + (state.partySize > 6 ? state.targetGroup * 6 : 0)
return (
<button onClick={() => sendAction(action)} type="button" key={action}>
<ControllerBindingLabel
@@ -293,13 +362,13 @@ export function DualScreenBottomDisplay() {
</button>
)
})}
{state.partySize === 10 && (
{state.partySize > 6 && (
<button onClick={() => sendAction('toggleTargetGroup')} type="button">
<ControllerBindingLabel
binding={state.bindings.toggleTargetGroup}
iconStyle={state.controllerIconStyle}
/>{' '}
Party Group {state.targetGroup + 1}
Party Group {state.targetGroup + 1}/{Math.ceil(state.partySize / 6)}
</button>
)}
</>
@@ -396,11 +465,13 @@ export function DualScreenTopCombat({
</section>
<section className="dual-top-party">
<div className={`dual-top-party-grid ${state.partySize === 10 ? 'raid' : ''}`}>
<div className={`dual-top-party-grid ${state.partySize > 6 ? 'raid' : ''}`}>
{state.party.map((member, index) => {
const partySlot = index + 1 + (state.directPartyTargeting && state.partySize === 10 ? state.targetGroup * 5 : 0)
const partySlot = (index % 6) + 1
const targetAction = `targetParty${partySlot}` as InputAction
const targetBinding = state.directPartyTargeting ? state.bindings[targetAction] : null
const groupStart = state.partySize > 6 ? state.targetGroup * 6 : 0
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' : ''}`}
@@ -418,6 +489,7 @@ export function DualScreenTopCombat({
{member.shield > 0 && (
<i style={{ width: `${(member.shield / member.maxHealth) * 100}%` }} />
)}
<em className="health-text">{Math.ceil(member.health)} / {member.maxHealth}</em>
</div>
{state.directPartyTargeting && targetBinding && (
<div className="member-target-key">
@@ -437,11 +509,6 @@ export function DualScreenTopCombat({
</div>
</section>
<footer className="dual-top-log">
{state.log.slice(0, 3).map((entry) => (
<span className={entry.tone} key={entry.id}>{entry.text}</span>
))}
</footer>
</div>
)
}