Update Healer Man server
This commit is contained in:
1 parent
a868401ebc
commit
48a678dcc5
295 files changed
+7644
-467
No files matched your search
@@ -82,7 +82,7 @@ function CompanionDungeonSelection({
|
||||
|
||||
return (
|
||||
<FrontSurface bottom className="companion-panel companion-roster companion-dungeons" ariaLabel="Dungeon and party role selection companion screen">
|
||||
<header><BrandMark compact /><b>CHOOSE DUNGEON / ROLE</b></header>
|
||||
<header><b>CHOOSE DUNGEON / ROLE</b></header>
|
||||
<main>
|
||||
<div className="companion-dungeon-browser">
|
||||
<div className="companion-category-tabs" role="group" aria-label="Dungeon categories">
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { lazy, Suspense, useEffect, useRef, useState, type CSSProperties } from "react";
|
||||
import { BOSS_CARD_RARITIES, BOSS_CARD_RARITY_INFO } from "../game/bossCards";
|
||||
import type { DungeonBossCard as BossCard } from "../game/dungeonBossCards";
|
||||
import "./dungeonBossCards.css";
|
||||
|
||||
const BossCardPortrait = lazy(() => import("./BossCardPortrait"));
|
||||
|
||||
function DungeonBossModel({ card }: { readonly card: BossCard }) {
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const element = container.current;
|
||||
if (!element || !card.model) return;
|
||||
if (typeof IntersectionObserver === "undefined") {
|
||||
setVisible(true);
|
||||
return;
|
||||
}
|
||||
const observer = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting), {
|
||||
rootMargin: "40px",
|
||||
threshold: 0.01,
|
||||
});
|
||||
observer.observe(element);
|
||||
return () => observer.disconnect();
|
||||
}, [card.model]);
|
||||
|
||||
return <div ref={container} className="dungeon-boss-card__portrait">
|
||||
{!card.model ? <span className="dungeon-boss-card__model-status">Model unavailable</span>
|
||||
: visible ? <Suspense fallback={<span className="dungeon-boss-card__model-status">Loading model…</span>}>
|
||||
<BossCardPortrait model={card.model} name={card.name} installed />
|
||||
</Suspense> : <span className="dungeon-boss-card__model-status">Loading model…</span>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function DungeonBossCards({ cards, compact = false }: {
|
||||
readonly cards: readonly BossCard[];
|
||||
readonly compact?: boolean;
|
||||
}) {
|
||||
if (!cards.length) return <p className="dungeon-boss-cards__empty">No boss cards are listed for this dungeon.</p>;
|
||||
return <div className={`dungeon-boss-cards ${compact ? "dungeon-boss-cards--compact" : ""}`}>
|
||||
{cards.map((card, index) => {
|
||||
const highestRarity = card.ownedRarities.at(-1);
|
||||
const accent = highestRarity ? BOSS_CARD_RARITY_INFO[highestRarity].color : "#9a947f";
|
||||
return <article key={card.key} className={`dungeon-boss-card ${card.copies ? "is-owned" : ""}`}
|
||||
style={{ "--boss-card-accent": accent } as CSSProperties}
|
||||
aria-label={`${card.name}, ${card.copies ? `${card.copies} ${card.copies === 1 ? "card" : "cards"} owned` : "not collected"}`}>
|
||||
<header><span>Boss {String(index + 1).padStart(2, "0")}</span><strong>{card.copies ? "Collected" : "Uncollected"}</strong></header>
|
||||
<DungeonBossModel card={card} />
|
||||
<div className="dungeon-boss-card__body">
|
||||
<h3>{card.name}</h3>
|
||||
<p>{card.copies ? `${card.copies} ${card.copies === 1 ? "copy" : "copies"} owned` : "Card not collected"}</p>
|
||||
</div>
|
||||
<footer aria-label="Card rarities">
|
||||
{BOSS_CARD_RARITIES.map((rarity) => <span key={rarity}
|
||||
className={card.ownedRarities.includes(rarity) ? "is-owned" : ""}
|
||||
style={{ "--rarity-color": BOSS_CARD_RARITY_INFO[rarity].color } as CSSProperties}
|
||||
title={`${BOSS_CARD_RARITY_INFO[rarity].label}: ${card.ownedRarities.includes(rarity) ? "owned" : "not owned"}`}>
|
||||
{rarity.charAt(0).toUpperCase()}
|
||||
</span>)}
|
||||
</footer>
|
||||
</article>;
|
||||
})}
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useShellStore } from "../app/shellStore";
|
||||
import { dungeonBossCards } from "../game/dungeonBossCards";
|
||||
import { requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { DungeonBossCards } from "./DungeonBossCards";
|
||||
|
||||
const INTRO_DURATION_MS = 5_000;
|
||||
|
||||
export function DungeonBossIntro() {
|
||||
const active = useGameStore((state) => state.dungeonIntroActive);
|
||||
const assetStatus = useGameStore((state) => state.assetStatus);
|
||||
const dungeonId = useGameStore((state) => state.activeDungeonId);
|
||||
const difficultyId = useGameStore((state) => state.activeDifficultyId);
|
||||
const sessionRevision = useGameStore((state) => state.sessionRevision);
|
||||
const collection = useShellStore((state) => state.bossCards);
|
||||
const [seconds, setSeconds] = useState(5);
|
||||
const ready = assetStatus === "ready" || assetStatus === "proxy";
|
||||
const dungeon = useMemo(() => requireDungeonDefinition(dungeonId), [dungeonId, sessionRevision]);
|
||||
const cards = useMemo(() => dungeonBossCards(dungeon, difficultyId, collection), [dungeon, difficultyId, collection]);
|
||||
const collected = cards.filter((card) => card.copies > 0).length;
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || !ready) return;
|
||||
let remaining = INTRO_DURATION_MS;
|
||||
let last = performance.now();
|
||||
setSeconds(5);
|
||||
const timer = window.setInterval(() => {
|
||||
const now = performance.now();
|
||||
const elapsed = Math.min(250, Math.max(0, now - last));
|
||||
last = now;
|
||||
const game = useGameStore.getState();
|
||||
if (document.hidden || game.overlay || game.companionOpen) return;
|
||||
remaining = Math.max(0, remaining - elapsed);
|
||||
setSeconds(Math.max(1, Math.ceil(remaining / 1_000)));
|
||||
if (remaining === 0) game.completeDungeonIntro(sessionRevision);
|
||||
}, 50);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [active, ready, sessionRevision]);
|
||||
|
||||
if (!active || !ready) return null;
|
||||
return <div className="dungeon-boss-intro" role="dialog" aria-modal="true" aria-labelledby="dungeon-boss-intro-title"
|
||||
onPointerDown={(event) => event.stopPropagation()}>
|
||||
<section className="dungeon-boss-intro__panel">
|
||||
<header className="dungeon-boss-intro__header">
|
||||
<div><h2 id="dungeon-boss-intro-title">{dungeon.title}</h2>
|
||||
<span>{cards.length} {cards.length === 1 ? "boss" : "bosses"} ahead · {collected} collected</span></div>
|
||||
<div className="dungeon-boss-intro__timer" role="status" aria-live="polite">
|
||||
<span>Entering in</span><strong>{String(seconds).padStart(2, "0")}</strong>
|
||||
</div>
|
||||
</header>
|
||||
<div className="dungeon-boss-intro__grid" aria-label="Boss cards in this dungeon">
|
||||
<DungeonBossCards cards={cards} />
|
||||
</div>
|
||||
</section>
|
||||
</div>;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import { DungeonLeaderboard } from "./DungeonLeaderboard";
|
||||
import { useDungeonLeaderboard } from "./useDungeonLeaderboard";
|
||||
import type { DungeonClearFilter } from "../game/dungeonRunTypes";
|
||||
import { DUNGEON_LEADERBOARD_PAGE_SIZE } from "../app/dungeonLeaderboardClient";
|
||||
import { BrandMark, ControllerLegend, FrontSurface } from "./FrontSurface";
|
||||
import { ControllerLegend, FrontSurface } from "./FrontSurface";
|
||||
import { OnlineGroupPanel } from "./OnlineGroupPanel";
|
||||
import { useOnlineGroupStore } from "../app/onlineGroupStore";
|
||||
import { specializationById } from "../game/specializationCatalog";
|
||||
@@ -91,6 +91,7 @@ export function DungeonSelectScreen() {
|
||||
const [bossDifficulty, setBossDifficulty] = useState(1);
|
||||
const includeBetaSpecs = useCompanionPartyPreferences((state) => state.includeBetaSpecs);
|
||||
const [expandedRuns, setExpandedRuns] = useState<ReadonlySet<string>>(new Set());
|
||||
const [leaderboardOpen, setLeaderboardOpen] = useState(false);
|
||||
const [readinessOpen, setReadinessOpen] = useState(false);
|
||||
const leaderboardDifficulty = selectedDefinition?.difficultyVariants.find((variant) => variant.id === difficultyId)
|
||||
?? selectedDefinition?.difficultyVariants.find((variant) => variant.id === selectedDefinition.defaultDifficultyId);
|
||||
@@ -205,19 +206,24 @@ export function DungeonSelectScreen() {
|
||||
id: `dungeon-${dungeon.id}`,
|
||||
run: () => selectDungeon(dungeon.id),
|
||||
enabled: true,
|
||||
neighbors: { ...(index === 0 ? { up: `dungeon-category-${selectedCategory}` } : {}), right: "leaderboard-final" },
|
||||
neighbors: { ...(index === 0 ? { up: `dungeon-category-${selectedCategory}` } : {}), right: "leaderboard-toggle" },
|
||||
})),
|
||||
{ id: "leaderboard-final", run: () => setClearFilter("final") },
|
||||
{ id: "leaderboard-full", run: () => setClearFilter("full") },
|
||||
{ id: "leaderboard-difficulty", run: cycleDifficulty, enabled: (selectedDefinition?.difficultyVariants.length ?? 0) > 1 },
|
||||
{ id: "leaderboard-refresh", run: leaderboard.refresh },
|
||||
{ id: "leaderboard-toggle", run: () => setLeaderboardOpen((open) => !open) },
|
||||
...(leaderboardOpen ? [
|
||||
{ id: "leaderboard-final", run: () => setClearFilter("final") },
|
||||
{ id: "leaderboard-full", run: () => setClearFilter("full") },
|
||||
{ id: "leaderboard-difficulty", run: cycleDifficulty, enabled: (selectedDefinition?.difficultyVariants.length ?? 0) > 1 },
|
||||
{ id: "leaderboard-refresh", run: leaderboard.refresh },
|
||||
] : []),
|
||||
{ id: "boss-difficulty-down", run: () => setBossDifficulty((value) => Math.max(1, value - 1)), enabled: bossDifficulty > 1 },
|
||||
{ id: "boss-difficulty-up", run: () => setBossDifficulty((value) => Math.min(10, value + 1)), enabled: bossDifficulty < 10 },
|
||||
...leaderboard.data.entries.map((entry) => ({ id: `leaderboard-run-${entry.id}`, run: () => toggleRun(entry.id) })),
|
||||
{ id: "leaderboard-prev", run: () => leaderboard.setOffset(Math.max(0, leaderboard.offset - DUNGEON_LEADERBOARD_PAGE_SIZE)),
|
||||
enabled: !leaderboard.loading && leaderboard.offset > 0 },
|
||||
{ id: "leaderboard-next", run: () => leaderboard.setOffset(leaderboard.offset + DUNGEON_LEADERBOARD_PAGE_SIZE),
|
||||
enabled: !leaderboard.loading && leaderboard.offset + DUNGEON_LEADERBOARD_PAGE_SIZE < leaderboard.data.total },
|
||||
...(leaderboardOpen ? [
|
||||
...leaderboard.data.entries.map((entry) => ({ id: `leaderboard-run-${entry.id}`, run: () => toggleRun(entry.id) })),
|
||||
{ id: "leaderboard-prev", run: () => leaderboard.setOffset(Math.max(0, leaderboard.offset - DUNGEON_LEADERBOARD_PAGE_SIZE)),
|
||||
enabled: !leaderboard.loading && leaderboard.offset > 0 },
|
||||
{ id: "leaderboard-next", run: () => leaderboard.setOffset(leaderboard.offset + DUNGEON_LEADERBOARD_PAGE_SIZE),
|
||||
enabled: !leaderboard.loading && leaderboard.offset + DUNGEON_LEADERBOARD_PAGE_SIZE < leaderboard.data.total },
|
||||
] : []),
|
||||
...(rolePickerOpen
|
||||
? availableRoles.map((role) => ({
|
||||
id: `dungeon-role-${role}`,
|
||||
@@ -233,7 +239,7 @@ export function DungeonSelectScreen() {
|
||||
run: () => setRolePickerOpen(false),
|
||||
}] : []),
|
||||
{ id: "dungeon-back", run: returnToMainMenu },
|
||||
], [availableRoles, categoryDungeons, downloading, firstCategoryDungeon, pendingDownload, returnToMainMenu, rolePickerOpen, selectedCategory, selectedDungeon, selectedDefinition, leaderboard, expandedRuns, leaderboardDifficulty, bossDifficulty]);
|
||||
], [availableRoles, categoryDungeons, downloading, firstCategoryDungeon, pendingDownload, returnToMainMenu, rolePickerOpen, selectedCategory, selectedDungeon, selectedDefinition, leaderboard, leaderboardOpen, expandedRuns, leaderboardDifficulty, bossDifficulty]);
|
||||
const controller = useMenuController(actions, {
|
||||
initialId: firstAvailable ? `dungeon-${firstAvailable.id}` : "dungeon-back",
|
||||
onBack: pendingDownload
|
||||
@@ -250,10 +256,8 @@ export function DungeonSelectScreen() {
|
||||
top={
|
||||
<FrontSurface className="dungeon-select-screen" ariaLabel="Dungeon selection">
|
||||
<header className="dungeon-select-header">
|
||||
<BrandMark compact />
|
||||
<div>
|
||||
<div className="dungeon-select-header__breadcrumb">
|
||||
<span>Main menu / Dungeons</span>
|
||||
<strong>Choose an expedition</strong>
|
||||
</div>
|
||||
{character && race && characterClass && (
|
||||
<p>
|
||||
@@ -327,20 +331,26 @@ export function DungeonSelectScreen() {
|
||||
</ControllerButton>
|
||||
</aside>
|
||||
|
||||
<main className="dungeon-select-detail" aria-live="polite">
|
||||
<main className={`dungeon-select-detail ${leaderboardOpen ? "dungeon-select-detail--leaderboard-open" : ""}`} aria-live="polite">
|
||||
{selectedDungeon ? (
|
||||
<>
|
||||
<DungeonLeaderboard dungeonTitle={selectedDungeon.title} data={leaderboard.data} scope={leaderboard.scope}
|
||||
loading={leaderboard.loading} filter={clearFilter} onFilter={setClearFilter}
|
||||
expanded={expandedRuns} onExpand={toggleRun} offset={leaderboard.offset} onPage={leaderboard.setOffset}
|
||||
onRefresh={leaderboard.refresh} difficultyLabel={`${bossDifficultyLabel(bossDifficulty)} · ${leaderboardDifficulty?.difficulty === "heroic" ? "Heroic" : "Normal"}`}
|
||||
onDifficulty={(selectedDefinition?.difficultyVariants.length ?? 0) > 1 ? cycleDifficulty : undefined}
|
||||
selectedId={controller.selectedId} select={controller.select} />
|
||||
<section className="dungeon-select-copy">
|
||||
<p className="front-kicker">{canStart ? "Playable expedition" : selectedDungeon.available ? "Content required" : "In development"}</p>
|
||||
<h2>{selectedDungeon.title}</h2>
|
||||
<span className="dungeon-release-stage">{DUNGEON_RELEASE_STAGE_LABELS[selectedDungeon.releaseStage]}</span>
|
||||
<p>{selectedDungeon.summary}</p>
|
||||
<ControllerButton
|
||||
controlId="leaderboard-toggle"
|
||||
selectedId={controller.selectedId}
|
||||
select={controller.select}
|
||||
className="front-button dungeon-select-leaderboard-toggle"
|
||||
type="button"
|
||||
aria-expanded={leaderboardOpen}
|
||||
aria-controls={leaderboardOpen ? "dungeon-select-leaderboard" : undefined}
|
||||
onClick={() => setLeaderboardOpen((open) => !open)}
|
||||
>
|
||||
{leaderboardOpen ? "Hide leaderboard" : "View leaderboard"}
|
||||
</ControllerButton>
|
||||
<DungeonDifficultySlider value={bossDifficulty} onChange={setBossDifficulty}
|
||||
selectedId={controller.selectedId} select={controller.select} />
|
||||
{!canStart && selectedDungeon.availabilityReason && (
|
||||
@@ -391,11 +401,6 @@ export function DungeonSelectScreen() {
|
||||
) : (
|
||||
<>
|
||||
<dl>
|
||||
<div><dt>Status</dt><dd>{canStart ? "Available offline" : selectedDungeon.available ? "Download required" : "Coming soon"}</dd></div>
|
||||
<div><dt>Category</dt><dd>{DUNGEON_CATEGORIES.find((category) => category.id === selectedCategory)?.fullName}</dd></div>
|
||||
<div><dt>Location</dt><dd>{selectedDungeon.location}</dd></div>
|
||||
<div><dt>Theme</dt><dd>{selectedDungeon.theme}</dd></div>
|
||||
<div><dt>Map</dt><dd>{selectedDungeon.mapId}</dd></div>
|
||||
<div><dt>Starting point</dt><dd>{selectedDungeon.entranceName}</dd></div>
|
||||
</dl>
|
||||
<ControllerButton
|
||||
@@ -414,6 +419,16 @@ export function DungeonSelectScreen() {
|
||||
)}
|
||||
{notice && <p className="front-notice" role="status">{notice}</p>}
|
||||
</section>
|
||||
{leaderboardOpen && (
|
||||
<div id="dungeon-select-leaderboard" className="dungeon-select-leaderboard-column">
|
||||
<DungeonLeaderboard dungeonTitle={selectedDungeon.title} data={leaderboard.data} scope={leaderboard.scope}
|
||||
loading={leaderboard.loading} filter={clearFilter} onFilter={setClearFilter}
|
||||
expanded={expandedRuns} onExpand={toggleRun} offset={leaderboard.offset} onPage={leaderboard.setOffset}
|
||||
onRefresh={leaderboard.refresh} difficultyLabel={`${bossDifficultyLabel(bossDifficulty)} · ${leaderboardDifficulty?.difficulty === "heroic" ? "Heroic" : "Normal"}`}
|
||||
onDifficulty={(selectedDefinition?.difficultyVariants.length ?? 0) > 1 ? cycleDifficulty : undefined}
|
||||
selectedId={controller.selectedId} select={controller.select} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<section className="dungeon-select-empty">
|
||||
|
||||
@@ -8,13 +8,20 @@ import { DungeonIssueReportPanel } from "./DungeonIssueReportPanel";
|
||||
import { BossTestingReviewPanel } from "./BossTestingReviewPanel";
|
||||
import { classById } from "../app/characterCatalog";
|
||||
import { specializationById, specializationIconUrl } from "../game/specializationCatalog";
|
||||
import { dungeonBossCards } from "../game/dungeonBossCards";
|
||||
import { requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import { DungeonBossCards } from "./DungeonBossCards";
|
||||
|
||||
function PauseDialog() {
|
||||
const closeOverlay = useGameStore((state) => state.closeOverlay);
|
||||
const openOverlay = useGameStore((state) => state.openOverlay);
|
||||
const reset = useGameStore((state) => state.resetAtEntrance);
|
||||
const gameMode = useGameStore((state) => state.gameMode);
|
||||
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
|
||||
const activeDifficultyId = useGameStore((state) => state.activeDifficultyId);
|
||||
const sessionRevision = useGameStore((state) => state.sessionRevision);
|
||||
const bossTest = useGameStore((state) => state.bossTest);
|
||||
const bossCards = useShellStore((state) => state.bossCards);
|
||||
const activeCharacter = useShellStore((state) => state.activeCharacter);
|
||||
const session = useShellStore((state) => state.session);
|
||||
const returnToMainMenu = useShellStore((state) => state.returnToMainMenu);
|
||||
@@ -41,6 +48,9 @@ function PauseDialog() {
|
||||
const classDetail = activeCharacter?.secondaryClassId
|
||||
? classById(activeCharacter.secondaryClassId).name
|
||||
: specialization && specialization.classId === activeCharacter?.classId ? specialization.displayName : null;
|
||||
const dungeonCards = useMemo(() => gameMode === "dungeon"
|
||||
? dungeonBossCards(requireDungeonDefinition(activeDungeonId), activeDifficultyId, bossCards)
|
||||
: [], [activeDungeonId, activeDifficultyId, bossCards, gameMode, sessionRevision]);
|
||||
|
||||
const actions = useMemo<MenuAction[]>(() => [
|
||||
{ id: "pause-resume", run: resume, neighbors: { down: "pause-party", right: "pause-party", up: "pause-main-menu", left: "pause-main-menu" } },
|
||||
@@ -95,6 +105,11 @@ function PauseDialog() {
|
||||
</div>
|
||||
{bossTest && <p className="pause-menu__context">Boss Testing · {bossTest.name}</p>}
|
||||
</header>
|
||||
{gameMode === "dungeon" && <section className="pause-menu__bosses" aria-label="Boss cards in this dungeon">
|
||||
<div className="pause-menu__bosses-heading"><strong>Boss cards in this dungeon</strong>
|
||||
<span>{dungeonCards.filter((card) => card.copies > 0).length} / {dungeonCards.length} collected</span></div>
|
||||
<DungeonBossCards cards={dungeonCards} compact />
|
||||
</section>}
|
||||
<div className="pause-menu__resume">{menuButton("pause-resume", "Resume game", resume, true)}</div>
|
||||
<div className="pause-menu__groups">
|
||||
<section className="pause-menu__group" aria-labelledby="pause-adventure-title">
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
.dungeon-boss-intro, .pause-menu__bosses {
|
||||
--boss-intro-ink: #f3ead1;
|
||||
--boss-intro-muted: #b8b5a7;
|
||||
--boss-intro-gold: #d5b875;
|
||||
--boss-intro-line: rgba(214, 192, 132, .36);
|
||||
}
|
||||
.dungeon-boss-intro {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 86;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: clamp(12px, 3vw, 32px);
|
||||
background: rgba(2, 7, 7, .82);
|
||||
backdrop-filter: blur(9px);
|
||||
color: var(--boss-intro-ink);
|
||||
}
|
||||
.dungeon-boss-intro__panel {
|
||||
display: grid;
|
||||
width: min(1080px, 100%);
|
||||
max-height: min(94dvh, 820px);
|
||||
min-height: 0;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
padding: clamp(16px, 2.5vw, 28px);
|
||||
border: 1px solid var(--boss-intro-line);
|
||||
border-radius: 13px;
|
||||
background: linear-gradient(145deg, #202523, #101716 70%);
|
||||
box-shadow: 0 24px 80px #000b, inset 0 0 45px rgba(142, 156, 110, .07);
|
||||
}
|
||||
.dungeon-boss-intro__header { display: flex; justify-content: space-between; align-items: center; gap: 16px; }
|
||||
.dungeon-boss-intro__header h2 { margin: 0 0 4px; color: var(--boss-intro-ink); font: clamp(22px, 3vw, 34px) Georgia, serif; }
|
||||
.dungeon-boss-intro__header span { color: var(--boss-intro-muted); font-size: 12px; }
|
||||
.dungeon-boss-intro__timer { display: grid; min-width: 100px; justify-items: center; padding: 6px 14px; border-left: 1px solid var(--boss-intro-line); }
|
||||
.dungeon-boss-intro__timer span { font-size: 10px; letter-spacing: .12em; text-transform: uppercase; white-space: nowrap; }
|
||||
.dungeon-boss-intro__timer strong { color: var(--boss-intro-gold); font: 48px Georgia, serif; font-variant-numeric: tabular-nums; line-height: 1; }
|
||||
.dungeon-boss-intro__grid { min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 2px 4px 4px 2px; }
|
||||
.dungeon-boss-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
||||
.dungeon-boss-cards__empty { margin: 0; padding: 20px; color: var(--boss-intro-muted); }
|
||||
.dungeon-boss-card {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, var(--boss-card-accent) 50%, #373d39);
|
||||
border-radius: 8px;
|
||||
background: radial-gradient(ellipse at 50% 35%, #37413d, #161d1b 75%);
|
||||
box-shadow: inset 0 0 0 3px #111a18, 0 7px 16px #0005;
|
||||
color: var(--boss-intro-ink);
|
||||
}
|
||||
.dungeon-boss-card.is-owned { border-color: var(--boss-card-accent); }
|
||||
.dungeon-boss-card > header { display: flex; justify-content: space-between; gap: 5px; padding: 8px 10px; border-bottom: 1px solid #ffffff18; background: #07100dbb; font-size: 10px; letter-spacing: .08em; text-transform: uppercase; }
|
||||
.dungeon-boss-card > header strong { color: var(--boss-card-accent); font-size: 9px; }
|
||||
.dungeon-boss-card__portrait { position: relative; height: 120px; overflow: hidden; background: radial-gradient(circle, color-mix(in srgb, var(--boss-card-accent) 15%, transparent), transparent 70%); }
|
||||
.dungeon-boss-card__portrait .boss-card__portrait { width: 100%; height: 100%; }
|
||||
.dungeon-boss-card__portrait canvas { display: block; width: 100%; height: 100%; }
|
||||
.dungeon-boss-card__model-status, .dungeon-boss-card__portrait .boss-card__model-status { display: grid; width: 100%; height: 100%; place-items: center; padding: 10px; color: var(--boss-intro-muted); font-size: 10px; text-align: center; }
|
||||
.dungeon-boss-card__body { flex: 1; padding: 9px 10px; }
|
||||
.dungeon-boss-card__body h3 { margin: 0 0 4px; color: var(--boss-intro-ink); font: 16px Georgia, serif; line-height: 1.15; }
|
||||
.dungeon-boss-card__body p { margin: 0; color: var(--boss-intro-muted); font-size: 10px; }
|
||||
.dungeon-boss-card > footer { display: flex; gap: 5px; padding: 8px 10px; border-top: 1px solid #ffffff18; background: #07100dbb; }
|
||||
.dungeon-boss-card > footer span { display: grid; width: 20px; height: 20px; place-items: center; border: 1px solid #56605a; border-radius: 3px; color: #8c968e; font-size: 10px; font-weight: 700; }
|
||||
.dungeon-boss-card > footer span.is-owned { border-color: var(--rarity-color); background: color-mix(in srgb, var(--rarity-color) 20%, #101819); color: var(--rarity-color); }
|
||||
.pause-menu__bosses { min-width: 0; margin-top: 12px; padding: 10px 0; border-block: 1px solid var(--boss-intro-line); }
|
||||
.pause-menu__bosses-heading { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 8px; color: var(--boss-intro-gold); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; }
|
||||
.pause-menu__bosses-heading span { color: var(--boss-intro-muted); white-space: nowrap; }
|
||||
.dungeon-boss-cards--compact { display: flex; gap: 8px; overflow-x: auto; padding: 2px 2px 5px; scroll-snap-type: x proximity; }
|
||||
.dungeon-boss-cards--compact .dungeon-boss-card { flex: 0 0 150px; scroll-snap-align: start; }
|
||||
.dungeon-boss-cards--compact .dungeon-boss-card > header { padding: 5px 7px; font-size: 8px; }
|
||||
.dungeon-boss-cards--compact .dungeon-boss-card > header strong { font-size: 8px; }
|
||||
.dungeon-boss-cards--compact .dungeon-boss-card__portrait { height: 86px; }
|
||||
.dungeon-boss-cards--compact .dungeon-boss-card__body { min-height: 48px; padding: 7px; }
|
||||
.dungeon-boss-cards--compact .dungeon-boss-card__body h3 { overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.dungeon-boss-cards--compact .dungeon-boss-card > footer { padding: 5px 7px; }
|
||||
.dungeon-boss-cards--compact .dungeon-boss-card > footer span { width: 16px; height: 16px; font-size: 8px; }
|
||||
@media (max-width: 620px) {
|
||||
.dungeon-boss-intro__panel { gap: 9px; padding: 13px; }
|
||||
.dungeon-boss-intro__header h2 { font-size: 22px; }
|
||||
.dungeon-boss-intro__timer { min-width: 66px; padding: 3px 5px; }
|
||||
.dungeon-boss-intro__timer strong { font-size: 36px; }
|
||||
.dungeon-boss-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
||||
.dungeon-boss-card__portrait { height: 80px; }
|
||||
.dungeon-boss-card__body h3 { font-size: 13px; }
|
||||
.dungeon-boss-cards--compact { display: flex; }
|
||||
}
|
||||
@media (max-height: 620px) {
|
||||
.dungeon-boss-intro__panel { gap: 9px; padding: 14px; }
|
||||
.dungeon-boss-intro__header h2 { font-size: 24px; }
|
||||
.dungeon-boss-intro__timer strong { font-size: 36px; }
|
||||
.dungeon-boss-card__portrait { height: 72px; }
|
||||
.dungeon-boss-card__body { padding: 7px 9px; }
|
||||
.dungeon-boss-card__body h3 { font-size: 14px; }
|
||||
.dungeon-boss-card > header, .dungeon-boss-card > footer { padding: 5px 9px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dungeon-boss-cards--compact { scroll-behavior: auto; }
|
||||
}
|
||||
@@ -100,13 +100,15 @@
|
||||
.dungeon-results > footer { display: flex; margin-top: 16px; flex: 0 0 auto; flex-wrap: wrap; gap: 8px; }
|
||||
.dungeon-results > footer button { min-height: 38px; flex: 1; white-space: nowrap; }
|
||||
@media (max-width: 960px) {
|
||||
.dungeon-select-detail { grid-template-rows: minmax(145px, 1fr) minmax(120px, .8fr); gap: 9px; }
|
||||
.dungeon-leaderboard > header { padding: 9px; }
|
||||
.dungeon-leaderboard h3 { margin: 4px 0 8px; font-size: 19px; }
|
||||
.dungeon-leaderboard__filters button { min-height: 39px; font-size: 10px; }
|
||||
.dungeon-leaderboard__meta { margin-top: 5px; }
|
||||
.dungeon-leaderboard__run { min-height: 52px; }
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.dungeon-select-detail--leaderboard-open { grid-template-rows: minmax(120px, .8fr) minmax(145px, 1fr); gap: 9px; }
|
||||
}
|
||||
@media (max-height: 520px) {
|
||||
.dungeon-results { padding: 13px; }
|
||||
.dungeon-results h2 { font-size: 23px; }
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
.creator-footer { gap: 8px; }
|
||||
.dungeon-select-screen { grid-template-columns: 260px minmax(0, 1fr); gap: 10px; }
|
||||
.dungeon-select-detail { grid-template-columns: 1fr; gap: 10px; }
|
||||
.dungeon-select-detail--leaderboard-open { grid-template-columns: 1fr; grid-template-rows: minmax(120px, .8fr) minmax(145px, 1fr); }
|
||||
.dungeon-select-map { display: none; }
|
||||
.dungeon-select-copy { padding: 8px; }
|
||||
.dungeon-role-option { min-height: 48px; }
|
||||
|
||||
Reference in new issue
Block a user