Update NPC presence handling and collision world refresh in runtime-host.ts

This commit is contained in:
2026-04-13 23:50:32 +02:00
parent 1220df79e6
commit bb938d94dd

View File

@@ -90,6 +90,10 @@ import {
type RuntimeInteractionPrompt
} from "./runtime-interaction-system";
import { RuntimeAudioSystem } from "./runtime-audio-system";
import {
hasNpcPresenceActivityChanged,
resolveNpcPresenceActive
} from "./runtime-npc-presence";
import {
advanceRuntimeClockState,
areRuntimeClockStatesEqual,
@@ -104,6 +108,7 @@ import { ThirdPersonNavigationController } from "./third-person-navigation-contr
import { resolveUnderwaterFogState } from "./underwater-fog";
import { resolveWaterContact } from "./water-volume-utils";
import type {
RuntimeNpcDefinition,
RuntimeBoxBrushInstance,
RuntimeLocalLightCollection,
RuntimeNavigationMode,
@@ -111,6 +116,7 @@ import type {
RuntimeSceneDefinition,
RuntimeTeleportTarget
} from "./runtime-scene-build";
import { buildRuntimeNpcCollider } from "./runtime-scene-build";
interface CachedMaterialTexture {
signature: string;
@@ -143,6 +149,10 @@ function dampScalar(current: number, target: number, rate: number, dt: number) {
return current + (target - current) * Math.min(1, dt * rate);
}
function isNonNull<T>(value: T | null): value is T {
return value !== null;
}
export interface RuntimeSceneLoadState {
status: "loading" | "ready" | "error";
message: string | null;
@@ -429,6 +439,7 @@ export class RuntimeHost {
this.runtimeScene = runtimeScene;
this.currentWorld = runtimeScene.world;
this.syncRuntimeClockState(runtimeScene.time);
this.syncRuntimeNpcPresenceToCurrentClock();
this.activeController?.deactivate(this.controllerContext, {
releasePointerLock: !preservePointerLockDuringLoad
});
@@ -453,7 +464,7 @@ export class RuntimeHost {
this.rebuildBrushMeshes(runtimeScene.brushes);
this.rebuildModelRenderObjects(
runtimeScene.modelInstances,
runtimeScene.entities.npcs
runtimeScene.npcDefinitions
);
this.audioSystem.loadScene(runtimeScene);
void this.finalizeSceneLoad(
@@ -481,7 +492,7 @@ export class RuntimeHost {
if (this.runtimeScene !== null) {
this.rebuildModelRenderObjects(
this.runtimeScene.modelInstances,
this.runtimeScene.entities.npcs
this.runtimeScene.npcDefinitions
);
}
@@ -1248,7 +1259,7 @@ export class RuntimeHost {
private rebuildModelRenderObjects(
modelInstances: RuntimeSceneDefinition["modelInstances"],
npcs: RuntimeNpc[]
npcs: RuntimeNpcDefinition[]
) {
this.clearModelRenderObjects();
@@ -1338,7 +1349,7 @@ export class RuntimeHost {
this.loadedModelAssets[npc.modelAssetId],
false
);
renderGroup.visible = npc.visible;
renderGroup.visible = npc.visible && npc.active;
this.modelGroup.add(renderGroup);
this.modelRenderObjects.set(npc.entityId, renderGroup);
}
@@ -2030,10 +2041,15 @@ export class RuntimeHost {
}
if (this.currentClockState !== null) {
const previousTimeOfDayHours = this.currentClockState.timeOfDayHours;
this.currentClockState = advanceRuntimeClockState(
this.currentClockState,
dt
);
this.updateRuntimeNpcPresenceAfterClockAdvance(
previousTimeOfDayHours,
this.currentClockState.timeOfDayHours
);
this.applyDayNightLighting();
this.clockPublishAccumulator += dt;
@@ -2099,6 +2115,133 @@ export class RuntimeHost {
this.firstPersonController.teleportTo(target.position, target.yawDegrees);
}
private syncRuntimeNpcPresenceToCurrentClock() {
if (this.runtimeScene === null || this.currentClockState === null) {
return;
}
let changed = false;
for (const npc of this.runtimeScene.npcDefinitions) {
const nextActive = resolveNpcPresenceActive(
npc.presence,
this.currentClockState.timeOfDayHours
);
if (npc.active === nextActive) {
continue;
}
npc.active = nextActive;
changed = true;
}
if (changed) {
this.refreshRuntimeNpcCollections();
}
}
private updateRuntimeNpcPresenceAfterClockAdvance(
previousTimeOfDayHours: number,
currentTimeOfDayHours: number
) {
if (this.runtimeScene === null) {
return;
}
let changed = false;
for (const npc of this.runtimeScene.npcDefinitions) {
if (
!hasNpcPresenceActivityChanged(
npc.presence,
previousTimeOfDayHours,
currentTimeOfDayHours
)
) {
continue;
}
const nextActive = resolveNpcPresenceActive(
npc.presence,
currentTimeOfDayHours
);
if (npc.active === nextActive) {
continue;
}
npc.active = nextActive;
changed = true;
const renderGroup = this.modelRenderObjects.get(npc.entityId);
if (renderGroup !== undefined) {
renderGroup.visible = npc.visible && npc.active;
}
}
if (!changed) {
return;
}
this.refreshRuntimeNpcCollections();
this.refreshCollisionWorldForNpcPresence();
}
private refreshRuntimeNpcCollections() {
if (this.runtimeScene === null) {
return;
}
this.runtimeScene.entities.npcs = this.runtimeScene.npcDefinitions.filter(
(npc) => npc.active
);
this.runtimeScene.colliders = [
...this.runtimeScene.staticColliders,
...this.runtimeScene.entities.npcs
.map((npc) => buildRuntimeNpcCollider(npc))
.filter(isNonNull)
];
}
private refreshCollisionWorldForNpcPresence() {
if (this.runtimeScene === null) {
return;
}
const requestId = ++this.collisionWorldRequestId;
const previousCollisionWorld = this.collisionWorld;
void this.buildCollisionWorld(
requestId,
this.runtimeScene.colliders,
this.runtimeScene.playerCollider,
this.runtimeScene.playerMovement
)
.then((nextCollisionWorld) => {
if (requestId !== this.collisionWorldRequestId) {
nextCollisionWorld.dispose();
return;
}
this.collisionWorld = nextCollisionWorld;
previousCollisionWorld?.dispose();
})
.catch((error) => {
if (requestId !== this.collisionWorldRequestId) {
return;
}
const detail =
error instanceof Error && error.message.trim().length > 0
? error.message.trim()
: "Unknown error.";
const message = `Runner collision refresh failed: ${detail}`;
this.currentRuntimeMessage = message;
this.runtimeMessageHandler?.(message);
});
}
private applyToggleBrushVisibilityAction(
brushId: string,
visible: boolean | undefined