diff --git a/src/app/App.tsx b/src/app/App.tsx index 13148781..b6ea5544 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -239,7 +239,6 @@ import { areProjectTimeSettingsEqual, cloneProjectTimeSettings, formatTimeOfDayHours, - HOURS_PER_DAY, normalizeTimeOfDayHours, type ProjectTimeSettings } from "../document/project-time-settings"; @@ -431,10 +430,8 @@ import { } from "../runtime-three/runtime-global-state"; import type { RuntimeSceneTransitionRequest } from "../runtime-three/runtime-host"; import { - advanceRuntimeClockState, areRuntimeClockStatesEqual, createRuntimeClockState, - reconfigureRuntimeClockState, resolveRuntimeTimeState, type RuntimeClockState } from "../runtime-three/runtime-project-time"; @@ -443,7 +440,10 @@ import { type RuntimeNavigationMode, type RuntimeSceneDefinition } from "../runtime-three/runtime-scene-build"; -import { applyResolvedControlStateToRuntimeScene } from "../runtime-three/runtime-scene-editor-simulation"; +import { + EditorSimulationController, + type EditorSimulationUiSnapshot +} from "../runtime-three/editor-simulation-controller"; import { validateRuntimeSceneBuild } from "../runtime-three/runtime-scene-validation"; import { EditorAutosaveController } from "../serialization/editor-autosave"; import { Panel } from "../shared-ui/Panel"; @@ -873,28 +873,26 @@ function createVec3Draft(vector: Vec3): Vec3Draft { }; } -function offsetRuntimeClockState( - state: RuntimeClockState, - deltaHours: number -): RuntimeClockState { - const totalHours = Math.max( - 0, - state.dayCount * HOURS_PER_DAY + state.timeOfDayHours + deltaHours - ); - - return { - timeOfDayHours: normalizeTimeOfDayHours(totalHours), - dayCount: Math.floor(totalHours / HOURS_PER_DAY), - dayLengthMinutes: state.dayLengthMinutes - }; -} - function formatRuntimeDayPhaseLabel( dayPhase: ReturnType["dayPhase"] ): string { return dayPhase.charAt(0).toUpperCase() + dayPhase.slice(1); } +function createInitialEditorSimulationUiSnapshot( + time: ProjectTimeSettings +): EditorSimulationUiSnapshot { + return { + playing: false, + overrideActive: false, + clock: createRuntimeClockState(time), + message: null, + sceneReady: false, + sceneVersion: 0, + frameVersion: 0 + }; +} + function createPlayerStartMovementTemplateNumberDraft( template: PlayerStartMovementTemplate ): PlayerStartMovementTemplateNumberDraft { @@ -3121,14 +3119,10 @@ export function App({ store, initialStatusMessage }: AppProps) { createDefaultRuntimeGlobalState(editorState.projectDocument.time) ); const [runtimeMessage, setRuntimeMessage] = useState(null); - const [editorSimulationClockOverride, setEditorSimulationClockOverride] = - useState(null); - const [editorSimulationPlaying, setEditorSimulationPlaying] = useState(false); - const [editorSimulationScene, setEditorSimulationScene] = - useState(null); - const [editorSimulationMessage, setEditorSimulationMessage] = useState< - string | null - >(null); + const [editorSimulationSnapshot, setEditorSimulationSnapshot] = + useState(() => + createInitialEditorSimulationUiSnapshot(editorState.projectDocument.time) + ); const [firstPersonTelemetry, setFirstPersonTelemetry] = useState(null); const [runtimeInteractionPrompt, setRuntimeInteractionPrompt] = @@ -3169,6 +3163,8 @@ export function App({ store, initialStatusMessage }: AppProps) { Record >({}); const autosaveControllerRef = useRef(null); + const editorSimulationControllerRef = + useRef(null); const lastAutosaveErrorRef = useRef(null); const viewportQuadSplitRef = useRef(editorState.viewportQuadSplit); const lastPointerPositionRef = useRef({ @@ -3187,6 +3183,10 @@ export function App({ store, initialStatusMessage }: AppProps) { } | null>(null); const [schedulePaneResizeActive, setSchedulePaneResizeActive] = useState(false); + if (editorSimulationControllerRef.current === null) { + editorSimulationControllerRef.current = new EditorSimulationController(); + } + const editorSimulationController = editorSimulationControllerRef.current; const documentValidation = validateSceneDocument(editorState.document); const projectValidation = validateProjectDocument( editorState.projectDocument diff --git a/src/runtime-three/runtime-host.ts b/src/runtime-three/runtime-host.ts index fdacfc74..c14e95ea 100644 --- a/src/runtime-three/runtime-host.ts +++ b/src/runtime-three/runtime-host.ts @@ -1045,7 +1045,6 @@ export class RuntimeHost { mount(container: HTMLElement) { this.container = container; container.appendChild(this.domElement); - this.domElement.addEventListener("click", this.handleRuntimeClick); this.domElement.addEventListener( "pointerdown", this.handleRuntimePointerDown @@ -1372,7 +1371,6 @@ export class RuntimeHost { this.worldBackgroundRenderer.dispose(); this.renderer?.forceContextLoss(); this.renderer?.dispose(); - this.domElement.removeEventListener("click", this.handleRuntimeClick); this.domElement.removeEventListener( "pointerdown", this.handleRuntimePointerDown @@ -5008,6 +5006,7 @@ export class RuntimeHost { const now = performance.now(); const dt = Math.min((now - this.previousFrameTime) / 1000, 1 / 20); this.previousFrameTime = now; + this.updateInteractInputState(); this.updatePauseInputState(); this.updateRuntimeTargetingInputState(); const simulationDt = this.isRuntimePaused() ? 0 : dt; @@ -6591,7 +6590,7 @@ export class RuntimeHost { } } - private handleRuntimeClick = () => { + private dispatchRuntimeInteract() { if ( !this.sceneReady || this.runtimeScene === null || @@ -6621,7 +6620,7 @@ export class RuntimeHost { this.runtimeScene, this.createInteractionDispatcher() ); - }; + } private handleRuntimePointerDown = (event: PointerEvent) => { if (!this.sceneReady) { @@ -6630,6 +6629,20 @@ export class RuntimeHost { this.audioSystem.handleUserGesture(); + if (this.runtimeScene !== null) { + const pointerBinding = getPlayerStartMouseBindingCodeForButton( + event.button + ); + + if ( + pointerBinding !== null && + this.runtimeScene.playerInputBindings.keyboard.interact === + pointerBinding + ) { + this.dispatchRuntimeInteract(); + } + } + if ( this.activeRuntimeCameraRig === null || !this.activeRuntimeCameraRig.lookAround.enabled || @@ -6664,6 +6677,18 @@ export class RuntimeHost { return; } + const interactKeyboardBinding = + this.runtimeScene.playerInputBindings.keyboard.interact; + if ( + !isPlayerStartMouseBindingCode(interactKeyboardBinding) && + event.code === interactKeyboardBinding + ) { + event.preventDefault(); + this.dispatchRuntimeInteract(); + this.previousInteractInputActive = true; + return; + } + if (event.code === "Tab") { event.preventDefault(); this.activateOrCycleRuntimeTarget(); @@ -6748,6 +6773,25 @@ export class RuntimeHost { this.cameraRigLookDragging = false; }; + private updateInteractInputState() { + if (this.runtimeScene === null || !this.sceneReady) { + this.previousInteractInputActive = false; + return; + } + + const interactInputActive = + resolvePlayerStartInteractInput( + this.pressedKeys, + this.runtimeScene.playerInputBindings + ) >= 0.5; + + if (interactInputActive && !this.previousInteractInputActive) { + this.dispatchRuntimeInteract(); + } + + this.previousInteractInputActive = interactInputActive; + } + private updatePauseInputState() { if (this.runtimeScene === null || !this.sceneReady) { this.previousPauseInputActive = false;