Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,30 @@ 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

**Stage Authoring Camera**:
The preview camera behavior used during Stage Authoring Mode. It stops following
the frozen player, scrolls to center the selected spatial target when selection
changes, and allows free panning across the authored stage canvas so distant
rail points, spots, and objects remain easy to inspect.
_Avoid_: player-follow camera, locked-on-target camera with no pan, separate editor viewport

**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
Expand Down
4 changes: 0 additions & 4 deletions fallow-baselines/dupes.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@
"src/game/scenes/basement/runtime/BasementScene.ts:175-188|src/game/scenes/hobbies/runtime/HobbiesScene.ts:169-182",
"src/game/scenes/potassiumSlip/runtime/PotassiumSlipScene.ts:507-513|src/game/scenes/potassiumSlip/runtime/PotassiumSlipScene.ts:536-542",
"src/game/scenes/potassiumSlip/runtime/waves.test.ts:175-182|src/game/scenes/potassiumSlip/runtime/waves.test.ts:55-62",
"src/game/scenes/ridge/bridge/BridgeTracerStageRuntime.ts:400-409|src/game/scenes/ridge/bridge/BridgeTracerStageRuntime.ts:440-449",
"src/game/scenes/ridge/bridge/BridgeWalkRailPlayerRuntime.ts:81-97|src/game/sharedSceneRuntime/player/SideViewPlayerRuntime.ts:78-94",
"src/game/scenes/ridge/bridge/BridgeWalkRailPlayerRuntime.ts:112-122|src/game/sharedSceneRuntime/player/SideViewPlayerRuntime.ts:112-122",
"src/game/scenes/ridge/bridge/BridgeWalkRailPlayerRuntime.ts:152-162|src/game/sharedSceneRuntime/player/SideViewPlayerRuntime.ts:143-155",
"src/game/scenes/stampedeSketch/runtime/rewards.test.ts:16-22|src/game/scenes/stampedeSketch/runtime/rewards.test.ts:24-30",
"src/game/scenes/stampedeSketch/runtime/runFlow.test.ts:121-129|src/game/scenes/stampedeSketch/runtime/runFlow.test.ts:94-102",
"src/game/scenes/stampedeSketch/runtime/runFlow.test.ts:261-271|src/game/scenes/stampedeSketch/runtime/runFlow.test.ts:97-103",
Expand Down
27 changes: 19 additions & 8 deletions fallow-baselines/health.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
}
},
"src/dev/ridgeStageDebugger/RidgeRuntimePreview.tsx": {
"complexity_moderate": {
"complexity_high": {
"count": 1
}
},
Expand All @@ -23,6 +23,11 @@
"count": 1
}
},
"src/dev/ridgeStageDebugger/hooks/useStageAuthoring.ts": {
"crap_high": {
"count": 1
}
},
"src/game/bridge/store.ts": {
"crap_moderate": {
"count": 2
Expand Down Expand Up @@ -90,12 +95,23 @@
"count": 1
}
},
"src/game/scenes/ridge/bridge/BridgeWalkRailPlayerRuntime.ts": {
"crap_moderate": {
"src/game/scenes/ridge/bridge/bridgeStageDebugOverlay.ts": {
"complexity_moderate": {
"count": 1
}
},
"src/game/scenes/ridge/bridge/stageAuthoring.ts": {
"complexity_high": {
"count": 1
},
"crap_high": {
"count": 1
}
},
"src/game/scenes/ridge/runtime/RidgeScene.ts": {
"crap_high": {
"count": 2
},
"crap_moderate": {
"count": 3
}
Expand All @@ -118,11 +134,6 @@
"count": 1
}
},
"src/game/sharedSceneRuntime/player/SideViewPlayerRuntime.ts": {
"crap_high": {
"count": 1
}
},
"src/game/shell/usePhaserGameBoot.ts": {
"crap_moderate": {
"count": 1
Expand Down
41 changes: 24 additions & 17 deletions src/dev/ridgeStageDebugger/RidgeRuntimePreview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { resolveRidgePreviewStatus } from './resolveRidgePreviewStatus';
import Game from '@/game/shell/Game';
import { bridgeActions, useBridgeState } from '@/game/bridge/store';
import { OverlayHost } from '@/game/overlays/OverlayHost';
Expand All @@ -12,12 +13,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;
Expand All @@ -26,22 +29,25 @@ export function RidgeRuntimePreview({
}) {
const bridge = useBridgeState();
const previewRef = useRef<HTMLElement | null>(null);
const isInputPaused =
!isActive || bridge.isPaused || bridge.loadingSceneId !== null || inputOwner === 'panel';
const inputStatusLabel = !isActive
? 'Debugger inactive'
: inputOwner === 'panel'
? 'Panel focus'
: bridge.isPaused || bridge.loadingSceneId !== null
? 'Runtime paused'
: 'Game input';
const pauseReason = !isActive
? 'Debugger inactive'
: inputOwner === 'panel'
? 'Panel focus'
: 'Runtime paused';
const showPausedOverlay =
isActive && isInputPaused && bridge.loadingSceneId === null && bridge.activeOverlayId === null;
const previewStatus = useMemo(
() => resolveRidgePreviewStatus({
activeOverlayId: bridge.activeOverlayId,
authoringActive,
inputOwner,
isActive,
isPaused: bridge.isPaused,
loadingSceneId: bridge.loadingSceneId
}),
[
authoringActive,
bridge.activeOverlayId,
bridge.isPaused,
bridge.loadingSceneId,
inputOwner,
isActive
]
);
const { inputStatusLabel, isInputPaused, pauseReason, showPausedOverlay } = previewStatus;
const presentationMode = getPhaserScenePresentationMode(bridge.activeSceneId);
const isRidgeSceneActive = bridge.activeSceneId === RIDGE_SCENE_ID;

Expand Down Expand Up @@ -84,6 +90,7 @@ export function RidgeRuntimePreview({
aria-hidden={!isActive}
className={[
'relative h-full min-h-0 min-w-0 overflow-hidden border-4 border-[#1a1a1a] bg-[#fbfbf9] shadow-[7px_7px_0_rgba(26,26,26,1)]',
authoringActive ? 'cursor-grab active:cursor-grabbing' : '',
isActive ? '' : 'invisible pointer-events-none'
].join(' ')}
data-testid="ridge-runtime-preview"
Expand Down
47 changes: 46 additions & 1 deletion src/dev/ridgeStageDebugger/RidgeStageDebugger.test.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -148,6 +148,51 @@ describe('RidgeStageDebugger', () => {
});
});

it('exposes Stage Authoring controls and draft source wiring', async () => {
render(<RidgeStageDebugger />);

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(<RidgeStageDebugger />);

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(<RidgeStageDebugger />);

Expand Down
49 changes: 46 additions & 3 deletions src/dev/ridgeStageDebugger/RidgeStageDebugger.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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' },
Expand Down Expand Up @@ -61,6 +62,7 @@ export default function RidgeStageDebugger() {
const bridge = useBridgeState();
const [previewInputOwner, setPreviewInputOwner] = useState<RidgePreviewInputOwner>('game');
const previewControls = useRidgePreviewControls();
const authoring = useStageAuthoring();
const claimPanelInput = useCallback(() => setPreviewInputOwner('panel'), []);
const claimGameInput = useCallback(() => setPreviewInputOwner('game'), []);
const routeState = bridge.progress.ridge.firstPlayableRoute;
Expand All @@ -74,6 +76,29 @@ 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,
publishAuthoringDrag: authoring.handleDrag,
resolveDebugSettings: () => ({
...previewControls.debugSettings,
showTraversalAssists: previewControls.debugSettings.showTraversalAssists || authoring.active
})
}), [
authoring.active,
authoring.handleDrag,
authoring.handlePick,
authoring.resolveCompositionSource,
authoring.selection,
previewControls.debugSettings,
previewControls.ridgeDevControls
]);

return (
<div className="h-[100dvh] max-h-[100dvh] overflow-hidden bg-[#f4f1ea] text-[#1a1a1a]">
<div className="grid h-full min-h-0 min-w-0 grid-rows-[auto_minmax(0,1fr)] gap-2 p-2 lg:p-3">
Expand Down Expand Up @@ -101,11 +126,12 @@ export default function RidgeStageDebugger() {
<main className="grid min-h-0 min-w-0 grid-rows-[minmax(0,1fr)_minmax(260px,38dvh)] gap-2 lg:grid-cols-[minmax(0,1fr)_340px] lg:grid-rows-none">
<div className="relative h-full min-h-0 min-w-0">
<RidgeRuntimePreview
authoringActive={authoring.active}
inputOwner={previewInputOwner}
isActive
onClaimGameInput={claimGameInput}
previewZoom={previewControls.previewZoom}
ridgeDevControls={previewControls.ridgeDevControls}
ridgeDevControls={ridgeDevControls}
/>
</div>

Expand All @@ -114,6 +140,23 @@ export default function RidgeStageDebugger() {
onFocusCapture={claimPanelInput}
onPointerDownCapture={claimPanelInput}
>
<AuthoringPanel
active={authoring.active}
fields={authoring.fields}
isDirty={authoring.isDirty}
onDiscardDraft={authoring.discardDraft}
onResetSelection={authoring.resetSelectionDraft}
onSelectTarget={authoring.handlePick}
onToggleActive={(nextActive) => {
authoring.setAuthoringActive(nextActive);
if (nextActive) claimGameInput();
}}
onUpdateField={authoring.updateField}
selection={authoring.selection}
selectionLabel={authoring.selectionLabel}
snippet={authoring.snippet}
targetOptions={authoring.targetOptions}
/>
<StageSourcePanel spotCount={resolvedSpots.length} />
<PreviewPanel
lastCommandLabel={previewControls.lastCommandLabel}
Expand Down
Loading
Loading