made some changes to the UI, removed leaderboards. updated gamesaves
This commit is contained in:
+80
-13
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user