Release v0.1.7 2026-07-12
This commit is contained in:
+37
-20
@@ -37,6 +37,7 @@ import {
|
||||
import { requestDisplaySurface } from "../platform/displayRouting";
|
||||
import { onlineRepository, type LeaderboardResult } from "../frontend/onlineRepository";
|
||||
import { DualDisplayFrame } from "./DualDisplayFrame";
|
||||
import { DEFAULT_CONTROLLER_GLYPHS } from "../input/controllerGlyphs";
|
||||
|
||||
const BossTrophyPortrait = lazy(() => import("./BossTrophyPortrait").then((module) => ({ default: module.BossTrophyPortrait })));
|
||||
|
||||
@@ -76,7 +77,7 @@ function BrandMark({ compact = false }: { compact?: boolean }) {
|
||||
}
|
||||
|
||||
function ControllerLegend({ back = false }: { back?: boolean }) {
|
||||
return <div className="controller-legend"><span><b>A</b> Select</span>{back && <span><b>B</b> Back</span>}<span><b>+</b> Navigate</span></div>;
|
||||
return <div className="controller-legend"><span><b>{DEFAULT_CONTROLLER_GLYPHS.confirm}</b> Select</span>{back && <span><b>{DEFAULT_CONTROLLER_GLYPHS.back}</b> Back</span>}<span><b>+</b> Navigate</span></div>;
|
||||
}
|
||||
|
||||
function LoginScreen() {
|
||||
@@ -335,7 +336,7 @@ function SaveScreen() {
|
||||
{selected.online && <div className="online-record"><span><b>ONLINE</b>{selected.online.hunterName}</span><time>{formatSaveTimestamp(selected.online.updatedAt)}</time></div>}
|
||||
<div className="save-actions">
|
||||
<FocusButton id={hasLocal ? "play" : "create"} focusedId={controller.focusedId} focus={controller.focus} className="front-primary" onClick={() => hasLocal ? playSlot(selectedSlotId) : openCreation()}>
|
||||
{hasLocal ? "Continue offline save" : "Create new hunter"}<small>A</small>
|
||||
{hasLocal ? "Continue offline save" : "Create new hunter"}<small>{DEFAULT_CONTROLLER_GLYPHS.confirm}</small>
|
||||
</FocusButton>
|
||||
<div className="sync-actions">
|
||||
<FocusButton id="upload" focusedId={controller.focusedId} focus={controller.focus} disabled={!hasLocal || !accountId} onClick={() => uploadSlot(selectedSlotId)}>↑ Sync offline to server</FocusButton>
|
||||
@@ -399,7 +400,6 @@ function HomeScreen() {
|
||||
top={
|
||||
<FrontSurface className="home-surface" ariaLabel="Main menu">
|
||||
<header className="home-header"><BrandMark compact /><span>Welcome back, <b>{hunter.hunterName}</b></span><i>{accountId ? "● SYNC READY" : "○ OFFLINE"}</i></header>
|
||||
<div className="home-title"><span>Choose your hunt</span><h1>Where are you needed?</h1></div>
|
||||
<div className="mode-grid">
|
||||
{HOME_MODES.map((mode) => (
|
||||
<FocusButton key={mode.id} id={mode.id} focusedId={controller.focusedId} focus={controller.focus} className="mode-card" onClick={() => selectMode(mode.id)}>
|
||||
@@ -441,6 +441,8 @@ function HomeScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
type ProfileStatId = BossId | "roguelike" | "rogue-trials-endless";
|
||||
|
||||
function ProfileScreen() {
|
||||
const hunter = useActiveHunter();
|
||||
const accountId = useFrontendStore((state) => state.accountId);
|
||||
@@ -449,11 +451,11 @@ function ProfileScreen() {
|
||||
const [groupId, setGroupId] = useState(collections[0]?.groupId ?? "");
|
||||
const [collectionView, setCollectionView] = useState<"loot" | "trophies" | "stats">("trophies");
|
||||
const collection = collections.find((group) => group.groupId === groupId) ?? collections[0];
|
||||
const [selectedStat, setSelectedStat] = useState<BossId | "roguelike">("roguelike");
|
||||
const [selectedStat, setSelectedStat] = useState<ProfileStatId>("roguelike");
|
||||
const [leaderboard, setLeaderboard] = useState<LeaderboardResult | null>(null);
|
||||
const [leaderboardStatus, setLeaderboardStatus] = useState("");
|
||||
useEffect(() => {
|
||||
if (selectedStat === "roguelike" || collection?.bosses.some((boss) => boss.bossId === selectedStat)) return;
|
||||
if (selectedStat === "roguelike" || selectedStat === "rogue-trials-endless" || collection?.bosses.some((boss) => boss.bossId === selectedStat)) return;
|
||||
setSelectedStat(collection?.bosses[0]?.bossId ?? "roguelike");
|
||||
}, [collection, selectedStat]);
|
||||
useEffect(() => {
|
||||
@@ -467,7 +469,9 @@ function ProfileScreen() {
|
||||
setLeaderboardStatus("Loading overall rankings…");
|
||||
const request = selectedStat === "roguelike"
|
||||
? onlineRepository.roguelikeLeaderboard(hunter.slotId)
|
||||
: onlineRepository.bossLeaderboard(selectedStat, hunter.slotId);
|
||||
: selectedStat === "rogue-trials-endless"
|
||||
? onlineRepository.rogueTrialsEndlessLeaderboard(hunter.slotId)
|
||||
: onlineRepository.bossLeaderboard(selectedStat, hunter.slotId);
|
||||
void request.then((result) => {
|
||||
if (cancelled) return;
|
||||
setLeaderboard(result);
|
||||
@@ -484,12 +488,13 @@ function ProfileScreen() {
|
||||
{ id: "view-stats", run: () => setCollectionView("stats"), neighbors: { left: "view-trophies", right: "view-loot", down: collectionView === "stats" ? "stat-roguelike" : undefined } },
|
||||
{ id: "view-loot", run: () => setCollectionView("loot"), neighbors: { left: "view-stats" } },
|
||||
...(collectionView === "stats" ? [
|
||||
{ id: "stat-roguelike", run: () => setSelectedStat("roguelike"), neighbors: { up: "view-stats", down: `stat-${collection.bosses[0].bossId}` } },
|
||||
{ id: "stat-roguelike", run: () => setSelectedStat("roguelike"), neighbors: { up: "view-stats", down: "stat-rogue-trials-endless" } },
|
||||
{ id: "stat-rogue-trials-endless", run: () => setSelectedStat("rogue-trials-endless"), neighbors: { up: "stat-roguelike", down: `stat-${collection.bosses[0].bossId}` } },
|
||||
...collection.bosses.map((boss, index) => ({
|
||||
id: `stat-${boss.bossId}`,
|
||||
run: () => setSelectedStat(boss.bossId),
|
||||
neighbors: {
|
||||
up: index === 0 ? "stat-roguelike" : `stat-${collection.bosses[index - 1].bossId}`,
|
||||
up: index === 0 ? "stat-rogue-trials-endless" : `stat-${collection.bosses[index - 1].bossId}`,
|
||||
down: index === collection.bosses.length - 1 ? `group-${collection.groupId}` : `stat-${collection.bosses[index + 1].bossId}`,
|
||||
},
|
||||
})),
|
||||
@@ -503,6 +508,16 @@ function ProfileScreen() {
|
||||
const activeProgress = hunter.healers[hunter.activeClassId];
|
||||
const earned = collection.drops.filter((drop) => drop.count > 0).length;
|
||||
const trophiesEarned = collection.bosses.filter((boss) => boss.pet.count > 0).length;
|
||||
const selectedStatValue = selectedStat === "roguelike"
|
||||
? hunter.stats.highestRoguelikeRound
|
||||
: selectedStat === "rogue-trials-endless"
|
||||
? hunter.stats.highestRogueTrialsEndlessKills
|
||||
: hunter.stats.bossKills[selectedStat] ?? 0;
|
||||
const selectedStatLabel = selectedStat === "roguelike"
|
||||
? "Roguelike rounds"
|
||||
: selectedStat === "rogue-trials-endless"
|
||||
? "Rogue Trials endless kills"
|
||||
: BOSS_DEFINITIONS[selectedStat].name;
|
||||
|
||||
return (
|
||||
<DualDisplayFrame
|
||||
@@ -512,7 +527,7 @@ function ProfileScreen() {
|
||||
<FocusButton id="view-trophies" focusedId={controller.focusedId} focus={controller.focus} className={collectionView === "trophies" ? "is-selected" : ""} role="tab" aria-selected={collectionView === "trophies"} onClick={() => setCollectionView("trophies")}>Trophy Case</FocusButton>
|
||||
<FocusButton id="view-stats" focusedId={controller.focusedId} focus={controller.focus} className={collectionView === "stats" ? "is-selected" : ""} role="tab" aria-selected={collectionView === "stats"} onClick={() => setCollectionView("stats")}>Boss Stats</FocusButton>
|
||||
<FocusButton id="view-loot" focusedId={controller.focusedId} focus={controller.focus} className={collectionView === "loot" ? "is-selected" : ""} role="tab" aria-selected={collectionView === "loot"} onClick={() => setCollectionView("loot")}>Group Loot</FocusButton>
|
||||
</div><FocusButton id="back" focusedId={controller.focusedId} focus={controller.focus} className="header-back" onClick={() => navigate("home")}>B · Back</FocusButton></header>
|
||||
</div><FocusButton id="back" focusedId={controller.focusedId} focus={controller.focus} className="header-back" onClick={() => navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back</FocusButton></header>
|
||||
{collectionView === "loot" ? <>
|
||||
<div className="collection-heading"><span><small>Shared group drops · Core: {collection.coreMechanic}</small><h2>Group {collection.groupLetter} · {collection.groupName}</h2></span><b>{earned} / {collection.drops.length} discovered</b></div>
|
||||
<div className="collection-grid">
|
||||
@@ -540,18 +555,19 @@ function ProfileScreen() {
|
||||
</div>
|
||||
<div className="collection-note trophy-note"><i>♛</i><span><strong>Each guardian keeps its own trophy.</strong><small>Defeat that boss for a 1 in 500 pet roll.</small></span></div>
|
||||
</> : <>
|
||||
<div className="collection-heading boss-stats-heading"><span><small>Lifetime records · Overall leaderboards</small><h2>Boss Stats</h2></span><b>Highest roguelike round {hunter.stats.highestRoguelikeRound}</b></div>
|
||||
<div className="collection-heading boss-stats-heading"><span><small>Lifetime records · Overall leaderboards</small><h2>Boss Stats</h2></span><b>Endless best {hunter.stats.highestRogueTrialsEndlessKills} kills</b></div>
|
||||
<div className="boss-stats-layout">
|
||||
<section className="boss-stat-selector" aria-label="Boss statistic selection">
|
||||
<FocusButton id="stat-roguelike" focusedId={controller.focusedId} focus={controller.focus} className={selectedStat === "roguelike" ? "is-selected" : ""} onClick={() => setSelectedStat("roguelike")}><i>∞</i><span><strong>Roguelike</strong><small>Highest round before defeat</small></span><b>{hunter.stats.highestRoguelikeRound}</b></FocusButton>
|
||||
<FocusButton id="stat-rogue-trials-endless" focusedId={controller.focusedId} focus={controller.focus} className={selectedStat === "rogue-trials-endless" ? "is-selected" : ""} onClick={() => setSelectedStat("rogue-trials-endless")}><i>Ⅲ</i><span><strong>Trials Endless</strong><small>Most bosses in one run</small></span><b>{hunter.stats.highestRogueTrialsEndlessKills}</b></FocusButton>
|
||||
{collection.bosses.map((boss) => <FocusButton key={boss.bossId} id={`stat-${boss.bossId}`} focusedId={controller.focusedId} focus={controller.focus} className={selectedStat === boss.bossId ? "is-selected" : ""} onClick={() => setSelectedStat(boss.bossId)}><i>{BOSS_DEFINITIONS[boss.bossId].icon}</i><span><strong>{boss.bossName}</strong><small>Lifetime boss kills</small></span><b>{boss.kills}</b></FocusButton>)}
|
||||
</section>
|
||||
<section className="leaderboard-panel" aria-label="Overall leaderboard">
|
||||
<header><span><small>Overall Top 5</small><strong>{selectedStat === "roguelike" ? "Roguelike rounds" : BOSS_DEFINITIONS[selectedStat].name}</strong></span><b>{selectedStat === "roguelike" ? `${hunter.stats.highestRoguelikeRound} best` : `${hunter.stats.bossKills[selectedStat] ?? 0} kills`}</b></header>
|
||||
<header><span><small>Overall Top 5</small><strong>{selectedStatLabel}</strong></span><b>{selectedStatValue} {selectedStat === "roguelike" ? "round" : "kills"}</b></header>
|
||||
{leaderboardStatus ? <div className="leaderboard-status">{leaderboardStatus}</div> : <div className="leaderboard-rows">
|
||||
{leaderboard?.top.length ? leaderboard.top.map((entry) => <div key={`${entry.username}-${entry.slotId}`} className={entry.username === accountId && entry.slotId === hunter.slotId ? "is-you" : ""}><b>#{entry.rank}</b><span><strong>{entry.hunterName}</strong><small>{entry.username}</small></span><em>{entry.value}</em></div>) : <div className="leaderboard-empty">No ranked hunters yet.</div>}
|
||||
</div>}
|
||||
<div className="leaderboard-self"><b>{leaderboard?.current ? `#${leaderboard.current.rank}` : "—"}</b><span><strong>Your rank · {hunter.hunterName}</strong><small>{accountId ?? "Offline hunter"}</small></span><em>{selectedStat === "roguelike" ? hunter.stats.highestRoguelikeRound : hunter.stats.bossKills[selectedStat] ?? 0}</em></div>
|
||||
<div className="leaderboard-self"><b>{leaderboard?.current ? `#${leaderboard.current.rank}` : "—"}</b><span><strong>Your rank · {hunter.hunterName}</strong><small>{accountId ?? "Offline hunter"}</small></span><em>{selectedStatValue}</em></div>
|
||||
</section>
|
||||
</div>
|
||||
<div className="collection-note trophy-note"><i>◆</i><span><strong>Rankings update with server saves.</strong><small>Top five always shown; your row stays visible at any rank.</small></span></div>
|
||||
@@ -567,6 +583,7 @@ function ProfileScreen() {
|
||||
<span><small>Allies saved</small><strong>{hunter.stats.alliesSaved}</strong></span>
|
||||
<span><small>Healing done</small><strong>{hunter.stats.healingDone.toLocaleString()}</strong></span>
|
||||
<span><small>Highest roguelike round</small><strong>{hunter.stats.highestRoguelikeRound}</strong></span>
|
||||
<span><small>Endless best</small><strong>{hunter.stats.highestRogueTrialsEndlessKills}</strong></span>
|
||||
</div>
|
||||
<div className="boss-log"><span>Mechanic groups</span>{collections.map((group) => (
|
||||
<FocusButton key={group.groupId} id={`group-${group.groupId}`} focusedId={controller.focusedId} focus={controller.focus} className={group.groupId === collection.groupId ? "is-selected" : ""} onClick={() => setGroupId(group.groupId)}>
|
||||
@@ -701,7 +718,7 @@ function GearScreen() {
|
||||
<DualDisplayFrame
|
||||
top={
|
||||
<FrontSurface className="gear-surface" ariaLabel="Gear upgrade workshop">
|
||||
<header className="front-screen-header"><BrandMark compact /><div><span>Group drop workshop</span><h1>Gear & Infusions</h1></div><div className="gear-mode-tabs"><FocusButton id="workshop-upgrade" focusedId={controller.focusedId} focus={controller.focus} className={workshopMode === "upgrade" ? "is-selected" : ""} onClick={() => selectWorkshopMode("upgrade")}>Upgrade</FocusButton><FocusButton id="workshop-infusion" focusedId={controller.focusedId} focus={controller.focus} className={workshopMode === "infusion" ? "is-selected" : ""} onClick={() => selectWorkshopMode("infusion")}>Infusion</FocusButton></div><FocusButton id="back" focusedId={controller.focusedId} focus={controller.focus} className="header-back" onClick={() => navigate("home")}>B · Back</FocusButton></header>
|
||||
<header className="front-screen-header"><BrandMark compact /><div><span>Group drop workshop</span><h1>Gear & Infusions</h1></div><div className="gear-mode-tabs"><FocusButton id="workshop-upgrade" focusedId={controller.focusedId} focus={controller.focus} className={workshopMode === "upgrade" ? "is-selected" : ""} onClick={() => selectWorkshopMode("upgrade")}>Upgrade</FocusButton><FocusButton id="workshop-infusion" focusedId={controller.focusedId} focus={controller.focus} className={workshopMode === "infusion" ? "is-selected" : ""} onClick={() => selectWorkshopMode("infusion")}>Infusion</FocusButton></div><FocusButton id="back" focusedId={controller.focusedId} focus={controller.focus} className="header-back" onClick={() => navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back</FocusButton></header>
|
||||
<div className="gear-workshop-layout">
|
||||
<section className="gear-owner-list" aria-label="Party gear owners">
|
||||
{GEAR_OWNER_ORDER.map((ownerId) => {
|
||||
@@ -764,7 +781,7 @@ function GearScreen() {
|
||||
return <article className={owned >= cost.quantity ? "is-met" : "is-missing"} key={cost.itemId}><i>{owned >= cost.quantity ? "✓" : "×"}</i><span><strong>{cost.itemName}</strong><small>{owned} owned · {cost.quantity} needed</small></span><b>{owned}/{cost.quantity}</b></article>;
|
||||
}) : <article className="is-met"><i>✓</i><span><strong>Maximum rank reached</strong><small>No more materials required.</small></span><b>+{MAX_GEAR_LEVEL}</b></article>}
|
||||
</div>
|
||||
{workshopMode === "upgrade" ? <FocusButton id="upgrade" focusedId={controller.focusedId} focus={controller.focus} className="gear-upgrade-action" disabled={!canUpgrade} onClick={upgrade}><span>{slot.level >= MAX_GEAR_LEVEL ? "Maximum rank" : `Upgrade to +${slot.level + 1}`}</span><small>{canUpgrade ? "Spend group drops · autosave" : "Collect required group drops"}</small></FocusButton> : passiveContext ? <div className="gear-passive-context-action"><span>{hunter.gearProgress[selectedOwnerId].passiveInfusionId === selectedPassive.id ? "Passive equipped" : "A · Equip selected passive"}</span><small>Applies at rank 1 next encounter.</small></div> : <FocusButton id="install-infusion" focusedId={controller.focusedId} focus={controller.focus} className="gear-upgrade-action" disabled={!canInstallInfusion} onClick={installInfusion}><span>{infusionEquipped ? "Infusion equipped" : `Install ${selectedInfusion.name}`}</span><small>{!activeUnlocked ? `Raise any ${GEAR_OWNER_LABELS[selectedOwnerId]} slot to +${ACTIVE_INFUSION_MIN_GEAR_LEVEL}` : !anchorUnlocked ? `Select a +${ACTIVE_INFUSION_MIN_GEAR_LEVEL} anchor slot` : canInstallInfusion ? "Spend group drops · autosave" : infusionEquipped ? "Applies next encounter" : "Collect required group drops"}</small></FocusButton>}
|
||||
{workshopMode === "upgrade" ? <FocusButton id="upgrade" focusedId={controller.focusedId} focus={controller.focus} className="gear-upgrade-action" disabled={!canUpgrade} onClick={upgrade}><span>{slot.level >= MAX_GEAR_LEVEL ? "Maximum rank" : `Upgrade to +${slot.level + 1}`}</span><small>{canUpgrade ? "Spend group drops · autosave" : "Collect required group drops"}</small></FocusButton> : passiveContext ? <div className="gear-passive-context-action"><span>{hunter.gearProgress[selectedOwnerId].passiveInfusionId === selectedPassive.id ? "Passive equipped" : `${DEFAULT_CONTROLLER_GLYPHS.confirm} · Equip selected passive`}</span><small>Applies at rank 1 next encounter.</small></div> : <FocusButton id="install-infusion" focusedId={controller.focusedId} focus={controller.focus} className="gear-upgrade-action" disabled={!canInstallInfusion} onClick={installInfusion}><span>{infusionEquipped ? "Infusion equipped" : `Install ${selectedInfusion.name}`}</span><small>{!activeUnlocked ? `Raise any ${GEAR_OWNER_LABELS[selectedOwnerId]} slot to +${ACTIVE_INFUSION_MIN_GEAR_LEVEL}` : !anchorUnlocked ? `Select a +${ACTIVE_INFUSION_MIN_GEAR_LEVEL} anchor slot` : canInstallInfusion ? "Spend group drops · autosave" : infusionEquipped ? "Applies next encounter" : "Collect required group drops"}</small></FocusButton>}
|
||||
<div className="front-notice is-lower">{notice || "Gear changes save locally and apply when next encounter starts."}</div>
|
||||
</FrontSurface>
|
||||
}
|
||||
@@ -795,7 +812,7 @@ function SettingsScreen() {
|
||||
<DualDisplayFrame
|
||||
top={
|
||||
<FrontSurface className="settings-surface" ariaLabel="Settings">
|
||||
<header className="front-screen-header"><BrandMark compact /><div><span>Field configuration</span><h1>Settings</h1></div><FocusButton id="back" focusedId={controller.focusedId} focus={controller.focus} className="header-back" onClick={() => navigate("home")}>B · Back</FocusButton></header>
|
||||
<header className="front-screen-header"><BrandMark compact /><div><span>Field configuration</span><h1>Settings</h1></div><FocusButton id="back" focusedId={controller.focusedId} focus={controller.focus} className="header-back" onClick={() => navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back</FocusButton></header>
|
||||
<div className="settings-layout">
|
||||
<section><span className="settings-section-title">Audio</span><div className="volume-setting"><span><strong>Master volume</strong><small>All music, effects, and voice</small></span><div><FocusButton id="volume-down" focusedId={controller.focusedId} focus={controller.focus} onClick={() => updateSetting("masterVolume", Math.max(0, settings.masterVolume - 10))}>−</FocusButton><b>{settings.masterVolume}%</b><FocusButton id="volume-up" focusedId={controller.focusedId} focus={controller.focus} onClick={() => updateSetting("masterVolume", Math.min(100, settings.masterVolume + 10))}>+</FocusButton></div><i><em style={{ width: `${settings.masterVolume}%` }} /></i></div></section>
|
||||
<section><span className="settings-section-title">Display & accessibility</span><SettingToggle id="motion" label="Reduced motion" copy="Limit non-essential UI movement" value={settings.reducedMotion} focusedId={controller.focusedId} focus={controller.focus} onClick={() => updateSetting("reducedMotion", !settings.reducedMotion)} /><SettingToggle id="numbers" label="Damage numbers" copy="Show combat values over units" value={settings.damageNumbers} focusedId={controller.focusedId} focus={controller.focus} onClick={() => updateSetting("damageNumbers", !settings.damageNumbers)} /><SettingToggle id="text" label="Large interface text" copy="Increase menu and tactical labels" value={settings.largeText} focusedId={controller.focusedId} focus={controller.focus} onClick={() => updateSetting("largeText", !settings.largeText)} /></section>
|
||||
@@ -808,9 +825,9 @@ function SettingsScreen() {
|
||||
<header className="context-header"><span>Controller</span><b>BUILT-IN THOR PAD</b></header>
|
||||
<div className="controller-map">
|
||||
<div className="pad-diagram"><i>↑</i><span>←<b>●</b>→</span><i>↓</i></div>
|
||||
<div className="face-diagram"><i className="y">Y</i><span><i className="x">X</i><b>●</b><i className="b">B</i></span><i className="a">A</i></div>
|
||||
<div className="face-diagram"><i className="triangle">{DEFAULT_CONTROLLER_GLYPHS.faceTop}</i><span><i className="square">{DEFAULT_CONTROLLER_GLYPHS.faceLeft}</i><b>●</b><i className="circle">{DEFAULT_CONTROLLER_GLYPHS.faceRight}</i></span><i className="cross">{DEFAULT_CONTROLLER_GLYPHS.faceBottom}</i></div>
|
||||
</div>
|
||||
<div className="mapping-list"><span><b>A</b> Confirm / cast Purify</span><span><b>B</b> Back / cast Shield</span><span><b>D-Pad</b> Navigate / target party</span><span><b>Right stick</b> Rotate camera</span><span><b>Start</b> Pause / menu</span></div>
|
||||
<div className="mapping-list"><span><b>{DEFAULT_CONTROLLER_GLYPHS.confirm}</b> Confirm / cast Purify</span><span><b>{DEFAULT_CONTROLLER_GLYPHS.back}</b> Back / cast Shield</span><span><b>D-Pad</b> Navigate / target party</span><span><b>Right stick</b> Rotate camera</span><span><b>{DEFAULT_CONTROLLER_GLYPHS.start}</b> Pause / menu</span></div>
|
||||
<div className="control-assurance"><i>✓</i><span><strong>No click-to-focus required</strong><small>Controller input routes through app-level actions.</small></span></div>
|
||||
</FrontSurface>
|
||||
}
|
||||
@@ -908,7 +925,7 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi
|
||||
? [
|
||||
["Four dual rounds", "Clear four randomized pairs while drafting one stacking buff after each win."],
|
||||
["Unseen trio finale", "Round 5 selects three bosses that have not appeared earlier in that run."],
|
||||
["Trial victory", "Defeat all three final bosses together to complete Rogue Trials."],
|
||||
["Endless choice", "After the trio falls, quit with the clear or continue while every dead boss is replaced."],
|
||||
]
|
||||
: isPve
|
||||
? [
|
||||
@@ -925,7 +942,7 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi
|
||||
<DualDisplayFrame
|
||||
top={
|
||||
<FrontSurface className={`mode-surface mode-${modeId}`} ariaLabel={`${mode.title} details`}>
|
||||
<header className="front-screen-header"><BrandMark compact /><div><span>Game mode</span><h1>{mode.title}</h1></div><FocusButton id="back" focusedId={controller.focusedId} focus={controller.focus} className="header-back" onClick={() => navigate("home")}>B · Back</FocusButton></header>
|
||||
<header className="front-screen-header"><BrandMark compact /><div><span>Game mode</span><h1>{mode.title}</h1></div><FocusButton id="back" focusedId={controller.focusedId} focus={controller.focus} className="header-back" onClick={() => navigate("home")}>{DEFAULT_CONTROLLER_GLYPHS.back} · Back</FocusButton></header>
|
||||
{!isDungeon && <div className="mode-hero"><span>{mode.eyebrow}</span><h2>{mode.title}</h2><p>{mode.description}</p><b>{mode.detail}</b></div>}
|
||||
{isDungeon && (
|
||||
<div className="boss-picker" aria-label="Choose boss encounter">
|
||||
@@ -973,7 +990,7 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi
|
||||
{DIFFICULTIES.map((difficulty) => <FocusButton key={difficulty.slug} id={`difficulty-${difficulty.slug}`} focusedId={controller.focusedId} focus={controller.focus} className={difficulty.slug === selectedDifficultySlug ? "is-selected" : ""} onClick={() => selectDifficulty(difficulty.slug)}><strong>{difficulty.name}</strong><small>iLvl {difficulty.itemLevel}</small></FocusButton>)}
|
||||
</div>
|
||||
)}
|
||||
<FocusButton id="launch" focusedId={controller.focusedId} focus={controller.focus} className="mode-launch" onClick={launch}><span>{launchLabel}</span><small>{mode.status} · A</small></FocusButton>
|
||||
<FocusButton id="launch" focusedId={controller.focusedId} focus={controller.focus} className="mode-launch" onClick={launch}><span>{launchLabel}</span><small>{mode.status} · {DEFAULT_CONTROLLER_GLYPHS.confirm}</small></FocusButton>
|
||||
{message && <div className="front-notice">{message}</div>}
|
||||
</FrontSurface>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user