sov-kernel-monster / bobs game /src /world /SovereignInterior.js
SNAPKITTYWEST's picture
chore: push full sov-kernel-monster content from local build
9425aed verified
Raw
History Blame Contribute Delete
16.4 kB
import * as THREE from "three";
import { RoundedBoxGeometry } from "three/addons/geometries/RoundedBoxGeometry.js";
import { TextureFactory } from "../rendering/TextureFactory.js";
export class SovereignInterior {
constructor({ scene, renderer, physics, store, bus, commands, animations, audio }) {
this.scene = scene;
this.renderer = renderer;
this.physics = physics;
this.store = store;
this.bus = bus;
this.commands = commands;
this.animations = animations;
this.audio = audio;
this.interactables = [];
this.textureFactory = new TextureFactory(renderer.renderer);
this.materials = this.textureFactory.materials();
this.clock = new THREE.Clock();
this.doorOpen = false;
this.lampState = null;
this.buildArchitecture();
this.buildLighting();
this.buildArt();
this.buildCouch();
this.buildTables();
this.buildDiningChairs();
this.buildFloorLamp();
this.buildDocument();
this.buildTerminalAndDoor();
this.physics.addTrigger({ id: "trigger.exit", position: [3.9, 1.1, 5.45], size: [1.35, 2.2, 0.7] });
this.unsubscribe = [
bus.on("STATE_CHANGED", ({ state, event }) => this.applyState(state, event)),
bus.on("STATE_HYDRATED", ({ state }) => this.applyState(state, { type: "STATE_HYDRATED" })),
bus.on("TRIGGER_ENTERED", ({ id }) => {
if (id === "trigger.exit" && this.store.state.world.entities["door.exit"].open) {
this.commands.execute("MISSION_COMPLETE");
}
}),
];
this.applyState(store.state, { type: "BOOT" });
}
piece(parent, geometry, material, position, rotation = [0, 0, 0], shadows = true) {
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(...position);
mesh.rotation.set(...rotation);
mesh.castShadow = shadows;
mesh.receiveShadow = true;
parent.add(mesh);
return mesh;
}
box(parent, size, material, position, rotationY = 0, shadows = true) {
return this.piece(parent, new THREE.BoxGeometry(...size), material, position, [0, rotationY, 0], shadows);
}
round(parent, size, radius, material, position, rotation = [0, 0, 0]) {
return this.piece(parent, new RoundedBoxGeometry(...size, 5, radius), material, position, rotation);
}
cylinder(parent, top, bottom, height, material, position, segments = 32) {
return this.piece(parent, new THREE.CylinderGeometry(top, bottom, height, segments), material, position);
}
register(object, metadata) {
object.name = metadata.name;
object.traverse(child => {
if (child.isMesh) child.userData.entityId = metadata.id;
});
this.interactables.push({ object, ...metadata });
return object;
}
buildArchitecture() {
const M = this.materials;
const floor = this.box(this.scene, [12, 0.18, 10], M.floor, [0, -0.09, 0]);
floor.name = "Wood floor";
this.physics.addFixedBox({ position: [0, -0.09, 0], size: [12, 0.18, 10], friction: 0.9 });
const walls = [
{ position: [0, 2.5, -5], size: [12, 5, 0.18] },
{ position: [-6, 2.5, 0], size: [0.18, 5, 10] },
{ position: [6, 2.5, 0], size: [0.18, 5, 10] },
{ position: [-1.45, 2.5, 5], size: [9.1, 5, 0.18] },
{ position: [5.35, 2.5, 5], size: [1.3, 5, 0.18] },
{ position: [3.9, 3.85, 5], size: [1.6, 2.3, 0.18] },
];
for (const wall of walls) {
this.box(this.scene, wall.size, M.wall, wall.position, 0, false);
this.physics.addFixedBox(wall);
}
const ceiling = this.box(this.scene, [12, 0.12, 10], M.wall, [0, 5.02, 0], 0, false);
ceiling.name = "Ceiling";
this.physics.addFixedBox({ position: [0, 5.02, 0], size: [12, 0.12, 10] });
this.box(this.scene, [11.8, 0.18, 0.12], M.dark, [0, 0.16, -4.84]);
this.box(this.scene, [0.12, 0.18, 9.7], M.dark, [-5.84, 0.16, 0]);
this.box(this.scene, [0.12, 0.18, 9.7], M.dark, [5.84, 0.16, 0]);
this.box(this.scene, [9, 0.18, 0.12], M.dark, [-1.5, 0.16, 4.84]);
this.box(this.scene, [1.2, 0.18, 0.12], M.dark, [5.35, 0.16, 4.84]);
this.box(this.scene, [5.2, 0.06, 3.25], M.rug, [-2.1, 0.08, -1.35]);
}
buildLighting() {
this.scene.add(new THREE.AmbientLight(0xfff6e8, 1.25));
this.mainLight = new THREE.PointLight(0xffd19a, 105, 16, 2);
this.mainLight.position.set(0, 4.35, 0.65);
this.mainLight.castShadow = true;
const mapSize = this.store.state.settings.shadows === "high" ? 2048 : 1024;
this.mainLight.shadow.mapSize.set(mapSize, mapSize);
this.mainLight.shadow.camera.near = 0.2;
this.mainLight.shadow.camera.far = 15;
this.mainLight.shadow.bias = -0.0004;
this.mainLight.shadow.normalBias = 0.025;
this.scene.add(this.mainLight);
const pendant = this.cylinder(this.scene, 0.045, 0.045, 0.5, this.materials.dark, [0, 4.76, 0.65], 16);
pendant.castShadow = false;
const bulbMaterial = new THREE.MeshStandardMaterial({ color: 0xffd7a0, emissive: 0xffa94c, emissiveIntensity: 2 });
const bulb = this.piece(this.scene, new THREE.SphereGeometry(0.24, 24, 16), bulbMaterial, [0, 4.43, 0.65]);
bulb.castShadow = false;
}
buildArt() {
const group = new THREE.Group();
group.position.set(-2.15, 3.42, -4.77);
this.scene.add(group);
this.box(group, [3.2, 1.78, 0.09], this.materials.dark, [0, 0, 0]);
this.box(group, [2.94, 1.52, 0.06], this.materials.art, [0, 0, 0.09]);
this.register(group, {
id: "artwork.seal",
name: "Sealed Artwork",
interactionType: "inspect",
interactionDistance: 2.8,
prompt: state => state.world.entities["artwork.seal"].inspected ? "Review sealed artwork" : "Inspect sealed artwork",
onInteract: () => this.commands.execute("INSPECT_ARTWORK"),
});
}
buildCouch() {
const M = this.materials;
const couch = new THREE.Group();
couch.position.set(-2.2, 0, -3.72);
this.scene.add(couch);
this.round(couch, [4.7, 0.52, 1.45], 0.14, M.sofa, [0, 0.48, 0]);
this.round(couch, [4.68, 0.72, 0.4], 0.14, M.sofa, [0, 1.35, -0.58], [-0.09, 0, 0]);
this.round(couch, [0.5, 1.05, 1.5], 0.16, M.sofa, [-2.18, 0.82, 0.02]);
this.round(couch, [0.5, 1.05, 1.5], 0.16, M.sofa, [2.18, 0.82, 0.02]);
for (const x of [-1.43, 0, 1.43]) {
this.round(couch, [1.32, 0.28, 1.17], 0.11, M.sofa, [x, 0.82, 0.15]);
this.round(couch, [1.3, 1.03, 0.27], 0.12, M.sofa, [x, 1.43, -0.47], [-0.08, 0, 0]);
}
for (const x of [-1.9, 1.9]) {
for (const z of [-0.45, 0.45]) this.cylinder(couch, 0.08, 0.1, 0.28, M.dark, [x, 0.13, z], 12);
}
this.round(couch, [0.72, 0.72, 0.18], 0.1, M.chair, [-1.55, 1.46, -0.24], [0, 0, -0.15]);
this.round(couch, [0.72, 0.72, 0.18], 0.1, M.chair, [1.5, 1.45, -0.22], [0, 0, 0.12]);
this.physics.addFixedBox({ position: [-2.2, 0.62, -3.72], size: [4.7, 1.24, 1.45] });
this.register(couch, {
id: "couch.main",
name: "Couch",
interactionType: "sit",
interactionDistance: 2.5,
prompt: () => "Sit on couch",
onInteract: () => this.commands.execute("USE_COUCH", {
anchor: {
position: [-2.2, 0.79, -3.12],
standPosition: [0.65, 0.79, -2.7],
yaw: Math.PI,
pitch: 0,
},
}),
});
}
buildTables() {
const M = this.materials;
const coffee = new THREE.Group();
coffee.position.set(-2.1, 0, -1.28);
this.scene.add(coffee);
this.cylinder(coffee, 1.18, 1.18, 0.16, M.wood, [0, 0.72, 0], 48);
for (const x of [-0.65, 0.65]) {
for (const z of [-0.42, 0.42]) this.cylinder(coffee, 0.09, 0.12, 0.67, M.dark, [x, 0.35, z], 12);
}
this.physics.addFixedBox({ position: [-2.1, 0.72, -1.28], size: [2.35, 0.16, 2.35] });
const dining = new THREE.Group();
dining.position.set(3.45, 0, -1.62);
this.scene.add(dining);
this.box(dining, [2.55, 0.18, 1.45], M.wood, [0, 1.42, 0]);
for (const x of [-1, 1]) {
for (const z of [-0.51, 0.51]) this.box(dining, [0.12, 1.35, 0.12], M.dark, [x, 0.7, z]);
}
this.physics.addFixedBox({ position: [3.45, 1.42, -1.62], size: [2.55, 0.18, 1.45] });
}
buildDiningChairs() {
this.createChair([2.75, 0.02, -0.12], Math.PI, "chair.dining.01");
this.createChair([4.15, 0.02, -3.1], 0, "chair.dining.02");
}
createChair(position, rotationY, id) {
const chair = new THREE.Group();
this.round(chair, [1.02, 0.2, 0.96], 0.08, this.materials.chair, [0, 0.88, 0]);
this.round(chair, [1.02, 0.95, 0.2], 0.08, this.materials.chair, [0, 1.45, 0.42], [-0.08, 0, 0]);
for (const x of [-0.38, 0.38]) {
for (const z of [-0.34, 0.34]) this.box(chair, [0.1, 0.82, 0.1], this.materials.dark, [x, 0.43, z]);
}
chair.rotation.y = rotationY;
this.scene.add(chair);
const body = this.physics.addDynamicChair(position, chair);
body.setRotation({ x: 0, y: Math.sin(rotationY / 2), z: 0, w: Math.cos(rotationY / 2) }, true);
this.register(chair, {
id,
name: "Dining Chair",
interactionType: "push",
interactionDistance: 2.2,
prompt: () => "Move chair",
onInteract: ({ direction }) => this.commands.execute("PUSH_CHAIR", {
body,
impulse: { x: direction.x * 2.8, y: 0.18, z: direction.z * 2.8 },
}),
});
}
buildFloorLamp() {
const lamp = new THREE.Group();
lamp.position.set(-5.03, 0, -3.75);
this.scene.add(lamp);
this.cylinder(lamp, 0.38, 0.46, 0.12, this.materials.brass, [0, 0.08, 0]);
this.cylinder(lamp, 0.045, 0.045, 2.75, this.materials.brass, [0, 1.47, 0], 16);
this.shadeMaterial = new THREE.MeshPhysicalMaterial({
color: 0xe8c994,
roughness: 0.6,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.88,
emissive: 0xffb35c,
emissiveIntensity: 0.13,
});
this.piece(lamp, new THREE.CylinderGeometry(0.35, 0.62, 0.72, 40, 1, true), this.shadeMaterial, [0, 2.76, 0]);
const glow = new THREE.MeshStandardMaterial({ color: 0xffd49a, emissive: 0xffa43a, emissiveIntensity: 3 });
this.piece(lamp, new THREE.SphereGeometry(0.1, 20, 12), glow, [0, 2.68, 0]);
this.floorLight = new THREE.PointLight(0xffb66e, 38, 6, 2);
this.floorLight.position.set(-5.03, 2.7, -3.75);
this.scene.add(this.floorLight);
this.physics.addFixedBox({ position: [-5.03, 1.4, -3.75], size: [0.75, 2.8, 0.75] });
this.register(lamp, {
id: "lamp.floor",
name: "Floor Lamp",
interactionType: "toggle",
interactionDistance: 2.5,
prompt: state => state.world.entities["lamp.floor"].on ? "Turn lamp off" : "Turn lamp on",
onInteract: () => this.commands.execute("TOGGLE_LAMP"),
});
}
buildDocument() {
const document = new THREE.Group();
document.position.set(-2.1, 0.84, -1.28);
document.rotation.y = -0.2;
this.scene.add(document);
this.box(document, [0.72, 0.035, 0.92], this.materials.paper, [0, 0, 0]);
this.cylinder(document, 0.09, 0.09, 0.018, this.materials.brass, [0.22, 0.03, 0.27], 24);
this.documentObject = document;
this.register(document, {
id: "document.covenant",
name: "Covenant Fragment",
interactionType: "collect",
interactionDistance: 2.25,
prompt: () => "Collect covenant document",
onInteract: () => this.commands.execute("COLLECT_DOCUMENT"),
});
}
buildTerminalAndDoor() {
const terminal = new THREE.Group();
terminal.position.set(2.58, 1.42, 4.78);
terminal.rotation.y = Math.PI;
this.scene.add(terminal);
this.box(terminal, [0.74, 0.92, 0.18], this.materials.dark, [0, 0, 0]);
this.terminalScreen = this.box(terminal, [0.58, 0.46, 0.04], this.materials.screen, [0, 0.12, 0.11]);
this.box(terminal, [0.48, 0.08, 0.07], this.materials.brass, [0, -0.32, 0.12]);
this.register(terminal, {
id: "terminal.sovereign",
name: "Sovereign Terminal",
interactionType: "activate",
interactionDistance: 2.45,
enabled: state => state.world.entities["document.covenant"].verified,
prompt: state => state.world.entities["terminal.sovereign"].activated ? "Trust chain accepted" : "Activate sovereign terminal",
disabledPrompt: () => "Terminal awaits verified evidence",
onInteract: () => this.commands.execute("ACTIVATE_TERMINAL"),
});
this.doorPivot = new THREE.Group();
this.doorPivot.position.set(3.1, 0, 4.84);
this.scene.add(this.doorPivot);
const panel = this.box(this.doorPivot, [1.55, 2.68, 0.12], this.materials.door, [0.775, 1.34, 0]);
this.box(this.doorPivot, [0.09, 0.09, 0.16], this.materials.brass, [1.38, 1.35, -0.1]);
this.register(this.doorPivot, {
id: "door.exit",
name: "Exit Door",
interactionType: "door",
interactionDistance: 2.2,
enabled: state => !state.world.entities["door.exit"].locked && !state.world.entities["door.exit"].open,
prompt: () => "Open exit door",
disabledPrompt: state => state.world.entities["door.exit"].open ? "Exit is open" : "Door requires verified covenant",
onInteract: () => {
if (!this.store.state.world.entities["door.exit"].open) {
this.store.dispatch("DOOR_OPENED", { automatic: false }, { entityId: "door.exit" });
}
},
});
this.doorCollider = this.physics.addFixedBox({ position: [3.875, 1.34, 4.84], size: [1.55, 2.68, 0.18] });
}
applyState(state, event) {
const entities = state.world.entities;
const lampOn = entities["lamp.floor"].on;
if (this.floorLight && this.lampState !== lampOn) {
this.lampState = lampOn;
const start = this.floorLight.intensity;
const target = lampOn ? 38 : 0;
this.animations.to({
duration: 0.28,
update: progress => {
this.floorLight.intensity = THREE.MathUtils.lerp(start, target, progress);
this.shadeMaterial.emissiveIntensity = THREE.MathUtils.lerp(lampOn ? 0.02 : 0.13, lampOn ? 0.13 : 0.02, progress);
},
});
this.audio.setLamp(lampOn);
}
if (this.documentObject) this.documentObject.visible = entities["document.covenant"].present;
if (this.terminalScreen) {
this.terminalScreen.material.emissive.setHex(entities["terminal.sovereign"].activated ? 0x5ee6ba : 0x214f45);
this.terminalScreen.material.emissiveIntensity = entities["terminal.sovereign"].activated ? 2.2 : 1.2;
}
if (entities["door.exit"].open && !this.doorOpen) this.openDoor(event.type === "STATE_HYDRATED");
if (!entities["door.exit"].open && this.doorOpen) this.closeDoor();
}
openDoor(instant = false) {
this.doorOpen = true;
this.physics.removeCollider(this.doorCollider);
if (instant) {
this.doorPivot.rotation.y = Math.PI / 2;
return;
}
const start = this.doorPivot.rotation.y;
this.cancelDoorAnimation?.();
this.cancelDoorAnimation = this.animations.to({
duration: this.store.state.settings.reducedMotion ? 0.01 : 1.05,
ease: "outCubic",
update: progress => {
this.doorPivot.rotation.y = THREE.MathUtils.lerp(start, Math.PI / 2, progress);
},
complete: () => this.audio.playTone(520, 0.12, 0.035),
});
}
closeDoor() {
this.cancelDoorAnimation?.();
this.cancelDoorAnimation = null;
this.doorOpen = false;
this.doorPivot.rotation.y = 0;
this.doorCollider = this.physics.addFixedBox({ position: [3.875, 1.34, 4.84], size: [1.55, 2.68, 0.18] });
}
setShadowQuality(quality) {
this.mainLight.castShadow = quality !== "off";
const size = quality === "high" ? 2048 : 1024;
this.mainLight.shadow.mapSize.set(size, size);
this.mainLight.shadow.map?.dispose();
this.mainLight.shadow.map = null;
}
update() {
const time = this.clock.getElapsedTime();
if (this.store.state.world.entities["lamp.floor"].on) this.floorLight.intensity = 38 + Math.sin(time * 5.2) * 0.35;
this.mainLight.intensity = 105 + Math.sin(time * 0.9) * 1.2;
}
dispose() {
this.unsubscribe.forEach(dispose => dispose());
this.textureFactory.dispose();
}
}