From b8b8efcf48d14359fdc5300cb8f51e89cc2cba8f Mon Sep 17 00:00:00 2001 From: DaniloNovakovic Date: Mon, 8 Jun 2026 06:15:49 +0200 Subject: [PATCH 1/5] feat: add Ridge Stage Authoring mode to debugger (#96) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let Danilo tune Walk Rail points, Stage Spots, and object offsets in a session draft with live preview, sidebar controls, and copyable source snippets—without replacing the typed composition source as canonical data. Co-authored-by: Cursor --- CONTEXT.md | 17 + .../RidgeRuntimePreview.tsx | 33 +- .../RidgeStageDebugger.test.tsx | 47 ++- .../ridgeStageDebugger/RidgeStageDebugger.tsx | 47 ++- .../components/AuthoringPanel.tsx | 152 +++++++++ .../components/ScalarFieldControl.tsx | 89 +++++ .../hooks/useStageAuthoring.ts | 227 +++++++++++++ .../ridge/bridge/BridgeTracerStageRuntime.ts | 58 +++- .../bridge/BridgeWalkRailPlayerRuntime.ts | 23 +- .../bridge/bridgeStageDebugOverlay.test.ts | 15 + .../ridge/bridge/bridgeStageDebugOverlay.ts | 86 ++++- .../ridge/bridge/stageAuthoring.test.ts | 68 ++++ .../scenes/ridge/bridge/stageAuthoring.ts | 308 ++++++++++++++++++ src/game/scenes/ridge/runtime/RidgeScene.ts | 77 ++++- .../scenes/ridge/runtime/ridgeDevControls.ts | 10 + src/game/scenes/ridge/sceneContext.ts | 3 +- 16 files changed, 1213 insertions(+), 47 deletions(-) create mode 100644 src/dev/ridgeStageDebugger/components/AuthoringPanel.tsx create mode 100644 src/dev/ridgeStageDebugger/components/ScalarFieldControl.tsx create mode 100644 src/dev/ridgeStageDebugger/hooks/useStageAuthoring.ts create mode 100644 src/game/scenes/ridge/bridge/stageAuthoring.test.ts create mode 100644 src/game/scenes/ridge/bridge/stageAuthoring.ts diff --git a/CONTEXT.md b/CONTEXT.md index 7d94f91..1896579 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -43,6 +43,23 @@ Ridge Stage Composition Source. It is not the legacy Ridge Blockout Viewer or a final art map editor. _Avoid_: Ridge Blockout Viewer, final art editor, generic map editor +**Stage Authoring Draft**: +Ephemeral in-debugger adjustments to Ridge Stage Composition Source spatial +data. A draft updates the live preview immediately, shows copyable typed-source +snippets for review, and is applied to git only when Danilo or an agent pastes +the change into the Ridge Stage Composition Source. It is not a second persisted +editor format or runtime data store. +_Avoid_: map editor save file, localStorage canonical source, auto-patched source file + +**Stage Authoring Mode**: +An explicit Ridge Stage Debugger state for spatial tweaking. While active, player +movement is frozen, all source-backed Walk Rail points, Stage Spots, and Stage +Object contact markers are shown regardless of route-beat visibility, preview +clicks select draft targets, and the sidebar exposes nudge controls plus +copyable typed-source replacement snippets. Outside this mode, the debugger +keeps its normal live-preview QA behavior. +_Avoid_: always-on pick mode, modifier-only pick mode, beat-filtered edit targets, separate map editor app + **Grid Cell**: A configurable unit in the typed Ridge Blockout Source that converts text-grid positions into world-space pixels. _Avoid_: hard-coded tile size, permanent pixel size diff --git a/src/dev/ridgeStageDebugger/RidgeRuntimePreview.tsx b/src/dev/ridgeStageDebugger/RidgeRuntimePreview.tsx index 2481dcc..5f29183 100644 --- a/src/dev/ridgeStageDebugger/RidgeRuntimePreview.tsx +++ b/src/dev/ridgeStageDebugger/RidgeRuntimePreview.tsx @@ -12,12 +12,14 @@ function ignorePreviewSceneClose(): void {} export type RidgePreviewInputOwner = 'game' | 'panel'; export function RidgeRuntimePreview({ + authoringActive = false, inputOwner, isActive, onClaimGameInput, previewZoom, ridgeDevControls }: { + authoringActive?: boolean; inputOwner: RidgePreviewInputOwner; isActive: boolean; onClaimGameInput: () => void; @@ -27,21 +29,32 @@ export function RidgeRuntimePreview({ const bridge = useBridgeState(); const previewRef = useRef(null); const isInputPaused = - !isActive || bridge.isPaused || bridge.loadingSceneId !== null || inputOwner === 'panel'; + !isActive || + bridge.isPaused || + bridge.loadingSceneId !== null || + (inputOwner === 'panel' && !authoringActive); const inputStatusLabel = !isActive ? 'Debugger inactive' - : inputOwner === 'panel' - ? 'Panel focus' - : bridge.isPaused || bridge.loadingSceneId !== null - ? 'Runtime paused' - : 'Game input'; + : authoringActive + ? 'Authoring mode' + : inputOwner === 'panel' + ? 'Panel focus' + : bridge.isPaused || bridge.loadingSceneId !== null + ? 'Runtime paused' + : 'Game input'; const pauseReason = !isActive ? 'Debugger inactive' - : inputOwner === 'panel' - ? 'Panel focus' - : 'Runtime paused'; + : authoringActive + ? 'Authoring mode' + : inputOwner === 'panel' + ? 'Panel focus' + : 'Runtime paused'; const showPausedOverlay = - isActive && isInputPaused && bridge.loadingSceneId === null && bridge.activeOverlayId === null; + isActive && + !authoringActive && + isInputPaused && + bridge.loadingSceneId === null && + bridge.activeOverlayId === null; const presentationMode = getPhaserScenePresentationMode(bridge.activeSceneId); const isRidgeSceneActive = bridge.activeSceneId === RIDGE_SCENE_ID; diff --git a/src/dev/ridgeStageDebugger/RidgeStageDebugger.test.tsx b/src/dev/ridgeStageDebugger/RidgeStageDebugger.test.tsx index 0fa863c..877d84b 100644 --- a/src/dev/ridgeStageDebugger/RidgeStageDebugger.test.tsx +++ b/src/dev/ridgeStageDebugger/RidgeStageDebugger.test.tsx @@ -1,6 +1,6 @@ // @vitest-environment jsdom -import { act, cleanup, render, screen, waitFor } from '@testing-library/react'; +import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { bridgeActions, bridgeStore } from '@/game/bridge/store'; @@ -148,6 +148,51 @@ describe('RidgeStageDebugger', () => { }); }); + it('exposes Stage Authoring controls and draft source wiring', async () => { + render(); + + expect(screen.getByLabelText('Stage Authoring Mode')).toHaveProperty('checked', false); + expect(lastRidgeDevControls?.resolveAuthoringState?.()).toEqual({ + active: false, + selection: null + }); + expect(lastRidgeDevControls?.resolveCompositionSource?.()).toBeUndefined(); + + await userEvent.click(screen.getByLabelText('Stage Authoring Mode')); + + expect(screen.getByLabelText('Stage Authoring Mode')).toHaveProperty('checked', true); + expect(lastRidgeDevControls?.resolveAuthoringState?.()).toEqual({ + active: true, + selection: null + }); + expect(lastRidgeDevControls?.resolveCompositionSource?.()?.id).toBe('bridge'); + expect(lastRidgeDevControls?.resolveDebugSettings?.()).toMatchObject({ + showTraversalAssists: true + }); + }); + + it('updates the authoring draft and snippet from sidebar field edits', async () => { + render(); + + await userEvent.click(screen.getByLabelText('Stage Authoring Mode')); + await userEvent.selectOptions( + screen.getByLabelText('Authoring target'), + 'Spot draftsperson' + ); + + expect(screen.getByText('Stage Spot draftsperson')).not.toBeNull(); + + const offsetYInput = screen.getByLabelText('offset.y'); + fireEvent.change(offsetYInput, { target: { value: '-6' } }); + + expect(lastRidgeDevControls?.resolveCompositionSource?.()?.spots.find((spot) => spot.id === 'draftsperson')?.offset) + .toEqual({ x: 0, y: -6 }); + expect(screen.getByLabelText('Copyable Ridge Stage Source replacement snippet').textContent) + .toContain("id: 'draftsperson'"); + expect(screen.getByLabelText('Copyable Ridge Stage Source replacement snippet').textContent) + .toContain('offset: { x: 0, y: -6 }'); + }); + it('renders the latest rail snapshot and copyable source snippet', () => { render(); diff --git a/src/dev/ridgeStageDebugger/RidgeStageDebugger.tsx b/src/dev/ridgeStageDebugger/RidgeStageDebugger.tsx index ff0af63..233b50a 100644 --- a/src/dev/ridgeStageDebugger/RidgeStageDebugger.tsx +++ b/src/dev/ridgeStageDebugger/RidgeStageDebugger.tsx @@ -1,5 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; -import type { ReactNode } from 'react'; +import { useCallback, useMemo, useState, type ReactNode } from 'react'; import { Bug, Crosshair, @@ -32,7 +31,9 @@ import { import { IconButton } from './components/IconButton'; import { Detail } from './components/Detail'; import { PREVIEW_ZOOM_OPTIONS } from './constants'; +import { AuthoringPanel } from './components/AuthoringPanel'; import { useRidgePreviewControls } from './hooks/useRidgePreviewControls'; +import { useStageAuthoring } from './hooks/useStageAuthoring'; const BRIDGE_BEAT_OPTIONS = [ { id: 'intro', label: 'Intro' }, @@ -61,6 +62,7 @@ export default function RidgeStageDebugger() { const bridge = useBridgeState(); const [previewInputOwner, setPreviewInputOwner] = useState('game'); const previewControls = useRidgePreviewControls(); + const authoring = useStageAuthoring(); const claimPanelInput = useCallback(() => setPreviewInputOwner('panel'), []); const claimGameInput = useCallback(() => setPreviewInputOwner('game'), []); const routeState = bridge.progress.ridge.firstPlayableRoute; @@ -74,6 +76,27 @@ export default function RidgeStageDebugger() { return next; }, [resolvedSpots]); + const ridgeDevControls = useMemo(() => ({ + ...previewControls.ridgeDevControls, + resolveCompositionSource: authoring.resolveCompositionSource, + resolveAuthoringState: () => ({ + active: authoring.active, + selection: authoring.selection + }), + publishAuthoringPick: authoring.handlePick, + resolveDebugSettings: () => ({ + ...previewControls.debugSettings, + showTraversalAssists: previewControls.debugSettings.showTraversalAssists || authoring.active + }) + }), [ + authoring.active, + authoring.handlePick, + authoring.resolveCompositionSource, + authoring.selection, + previewControls.debugSettings, + previewControls.ridgeDevControls + ]); + return (
@@ -101,11 +124,12 @@ export default function RidgeStageDebugger() {
@@ -114,6 +138,23 @@ export default function RidgeStageDebugger() { onFocusCapture={claimPanelInput} onPointerDownCapture={claimPanelInput} > + { + authoring.setAuthoringActive(nextActive); + if (nextActive) claimGameInput(); + }} + onUpdateField={authoring.updateField} + selection={authoring.selection} + selectionLabel={authoring.selectionLabel} + snippet={authoring.snippet} + targetOptions={authoring.targetOptions} + /> void; + onResetSelection: () => void; + onSelectTarget: (selection: StageAuthoringSelection) => void; + onToggleActive: (active: boolean) => void; + onUpdateField: (field: string, value: number) => void; + selection: StageAuthoringSelection | null; + selectionLabel: string; + snippet: string; + targetOptions: readonly StageAuthoringTargetOption[]; +}) { + return ( +
+
+

+ + Authoring +

+ +
+ +

+ {active + ? 'Pick a target below or click markers in the preview.' + : 'Turn on Authoring Mode to select and nudge source-backed markers.'} +

+ + {active ? ( +
+ + +

+ {selectionLabel} +

+ + {fields.length > 0 ? ( +
+ {fields.map((field) => ( + onUpdateField(field.field, value)} + step={field.step} + value={field.value} + /> + ))} +
+ ) : null} + + {snippet ? ( +