diff --git a/src/lib/components/CenterStage.svelte b/src/lib/components/CenterStage.svelte index 401777f..5a316a1 100644 --- a/src/lib/components/CenterStage.svelte +++ b/src/lib/components/CenterStage.svelte @@ -5,6 +5,7 @@ import { onMount } from "svelte"; import { fly } from "svelte/transition"; import ConfigPanel from "$lib/components/ConfigPanel.svelte"; + import NeonBreakoutArena from "$lib/components/NeonBreakoutArena.svelte"; import PressureMatrixViewer from "$lib/components/PressureMatrixViewer.svelte"; import SignalChart from "$lib/components/SignalChart.svelte"; import SummaryCurve from "$lib/components/SummaryCurve.svelte"; @@ -12,12 +13,14 @@ HudColorMapOption, HudSignalPanel, HudSummary, + LocaleCode, PressureColorMapPreset, StageStatusTone } from "$lib/types/hud"; export let title = ""; export let hint = ""; + export let locale: LocaleCode = "zh-CN"; export let statusText = ""; export let statusTone: StageStatusTone = "idle"; export let leftPanels: HudSignalPanel[] = []; @@ -54,6 +57,7 @@ export let replayProgress = 0; export let replayFileName = ""; export let replayFrameInfo = ""; + export let showPrecisionTestPanel = false; let stagePlaneEl: HTMLDivElement | undefined; let topOverlayEl: HTMLDivElement | undefined; @@ -81,6 +85,8 @@ $: replaySide = summarySide === "left" ? "right" : "left"; $: replayToggleButtonText = replayIsPlaying ? replayPauseLabel : replayPlayLabel; $: replayProgressPercent = Math.round(Math.min(1, Math.max(0, replayProgress)) * 100); + $: splitMatrixTitle = locale === "zh-CN" ? "数字矩阵" : "Matrix"; + $: splitMatrixHint = locale === "zh-CN" ? "实时压力数据 / 数字矩阵" : "Live pressure matrix"; function toPxNumber(rawValue: string): number { const value = Number.parseFloat(rawValue); @@ -181,31 +187,70 @@ bind:this={stagePlaneEl} style="--panel-zone-top-dyn: {panelZoneTopPx}px; --rail-scale-left: {leftRailScale}; --rail-scale-right: {rightRailScale};" > -
-
-

WebGL2 Stage

-

{title}

-

{hint}

+ {#if !showPrecisionTestPanel} +
+
+

WebGL2 Stage

+

{title}

+

{hint}

+
+

+ {statusText} +

-

- {statusText} -

-
+ {/if} -
- {#key `${matrixRows}x${matrixCols}:${colorMapPreset}`} - - {/key} -
+ {#if showPrecisionTestPanel} +
+
+
+

{splitMatrixTitle}

+ {splitMatrixHint} +
+
+ {#key `${matrixRows}x${matrixCols}:${colorMapPreset}:split`} + + {/key} +
+
- {#if showConfigPanel} +
+ +
+
+ {:else} +
+ {#key `${matrixRows}x${matrixCols}:${colorMapPreset}`} + + {/key} +
+ {/if} + + {#if showConfigPanel && !showPrecisionTestPanel}
{/if} -
- -
+ +
+ {/if} - {#if replayHasData} + {#if replayHasData && !showPrecisionTestPanel} {/if} -
- -
+ {#if !showPrecisionTestPanel} +
+ +
+ {/if}
@@ -463,6 +512,70 @@ max-inline-size: min(24rem, 40vw); } + .split-game-wrap { + position: absolute; + inset: clamp(0.46rem, 1vw, 0.82rem); + z-index: 6; + display: grid; + grid-template-columns: minmax(0, 0.98fr) minmax(0, 1.02fr); + gap: clamp(0.45rem, 1vw, 0.9rem); + } + + .split-panel { + position: relative; + min-block-size: 0; + overflow: hidden; + border: 1px solid rgb(var(--hud-border-rgb) / 0.26); + border-radius: 0.58rem; + background: + linear-gradient(180deg, rgb(var(--hud-surface-rgb) / 0.84), rgb(var(--hud-surface-deep-rgb) / 0.9)), + radial-gradient(circle at 50% 0, rgb(var(--hud-glow-rgb) / 0.06), transparent 56%); + box-shadow: + inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.07), + 0 0 20px rgb(var(--hud-glow-rgb) / 0.08); + } + + .split-panel-head { + position: absolute; + top: 0.42rem; + left: 0.52rem; + z-index: 5; + display: grid; + gap: 0.1rem; + margin: 0; + pointer-events: none; + } + + .split-panel-head p { + margin: 0; + color: rgb(var(--hud-text-main-rgb) / 0.96); + font-size: 0.62rem; + letter-spacing: 0.12em; + text-transform: uppercase; + } + + .split-panel-head span { + color: rgb(var(--hud-text-dim-rgb) / 0.82); + font-size: 0.52rem; + letter-spacing: 0.05em; + text-transform: uppercase; + } + + .split-panel-body { + position: absolute; + inset: 0; + } + + .split-matrix-panel :global(.viewer-controls) { + left: clamp(0.7rem, 1.7vw, 1.15rem); + top: clamp(3.8rem, 8.8vh, 4.9rem); + max-inline-size: min(13.2rem, 65%); + } + + .split-matrix-panel :global(.stats-panel) { + padding: 0.62rem 0.68rem 0.72rem; + } + .panel-zone { position: absolute; top: var(--panel-zone-top-dyn, var(--panel-zone-top)); @@ -744,6 +857,10 @@ .replay-floating-panel { inline-size: min(var(--rail-width), 20.8rem); } + + .split-game-wrap { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + } } @media (max-height: 900px) { @@ -787,5 +904,10 @@ right: calc(var(--rail-edge-inset) + 0.1rem); inline-size: auto; } + + .split-game-wrap { + grid-template-columns: 1fr; + grid-template-rows: minmax(0, 0.92fr) minmax(0, 1.08fr); + } } diff --git a/src/lib/components/NeonBreakoutArena.svelte b/src/lib/components/NeonBreakoutArena.svelte new file mode 100644 index 0000000..09f5c57 --- /dev/null +++ b/src/lib/components/NeonBreakoutArena.svelte @@ -0,0 +1,1027 @@ + + +
+ + +
+
+
+

{ui.title}

+
+
+ + +
+
+ +
+
{ui.score}{Math.round(score)}
+
{ui.combo}{combo}
+
{ui.lives}{lives}
+
{ui.level}{level}
+
{ui.bricks}{Math.max(0, bricksLeft)}
+
+ +
+

{ui.chase} / {statusText}

+
+
+ + {#if gameState === "paused"} +
+
+

{ui.pausedOverlay}

+
+
+ {/if} + + {#if gameState === "over"} +
+
+

{ui.over}

+ +
+
+ {/if} +
+ + diff --git a/src/lib/components/PressureMatrixViewer.svelte b/src/lib/components/PressureMatrixViewer.svelte index 43e52b5..76478be 100644 --- a/src/lib/components/PressureMatrixViewer.svelte +++ b/src/lib/components/PressureMatrixViewer.svelte @@ -28,6 +28,7 @@ export let rangeMin = 0; export let rangeMax = 16000; export let colorMapPreset: PressureColorMapPreset = "emerald"; + export let showStatsPanel = true; let viewerEl: HTMLDivElement | undefined; let canvasEl: HTMLCanvasElement | undefined; @@ -608,26 +609,28 @@ -
-
-

Pressure Matrix

-
-
- Total Pressure - {stats.total.toFixed(0)} -
-
- Max - {stats.max.toFixed(0)} -
-
- Avg - {stats.avg.toFixed(0)} -
-
-

{statsNote}

-
-
+ {#if showStatsPanel} +
+
+

Pressure Matrix

+
+
+ Total Pressure + {stats.total.toFixed(0)} +
+
+ Max + {stats.max.toFixed(0)} +
+
+ Avg + {stats.avg.toFixed(0)} +
+
+

{statsNote}

+
+
+ {/if}