PageSourceSearch

https://yxwan123.github.io/Inference_Engine/js/3d/room-street.js

js yxwan123.github.io collected 2026-10-03 10:14:34 UTC 14,646 bytes, 379 lines download raw bytes

1/**
2 * ROOM-STREET - Glover Street Exterior in 3D.
3 * Foggy Victorian street. Two houses facing each other. Gas lamp. Cobblestones.
4 */
5
6import { Room3D, createHotspotMesh, setLightBaseIntensity, THREE } from './engine.js';
7
8class GloverStreetRoom extends Room3D {
9  constructor() {
10    super('room3d-street-container');
11    // Wider bounds for the street
12    this.bounds = { minX: -2.5, maxX: 2.5, minZ: -5, maxZ: 5 };
13    this.raycastDistance = 8;
14    this.hotspotTime = 0;
15    this.fogParticles = null;
16    this.gasLampLight = null;
17  }
18
19  build() {
20    // ─── Atmosphere ───────────────────────────────────────────
21    this.scene.fog = new THREE.FogExp2(0x0c0e10, 0.04);
22    this.scene.background = new THREE.Color(0x0a0c10);
23
24    // Camera — standing in the middle of the street
25    this.camera.position.set(0, 1.6, 0);
26    this.camera.lookAt(3, 1.2, -1);
27
28    // ─── Lighting ─────────────────────────────────────────────
29    // Moonlight from above
30    const moonLight = new THREE.DirectionalLight(0x6688cc, 0.7);
31    moonLight.position.set(2, 8, -3);
32    this.scene.add(moonLight);
33
34    // Ambient
35    const ambient = new THREE.AmbientLight(0x334455, 0.8);
36    this.scene.add(ambient);
37
38    this._buildStreetLamp(new THREE.Vector3(1.55, 0, -0.7));
39
40    // Secondary lamp glow from house windows
41    const windowGlow1 = new THREE.PointLight(0xffcc66, 0.8, 8, 1.2);
42    windowGlow1.position.set(-3.5, 1.8, 0);
43    this.scene.add(windowGlow1);
44    const windowGlow2 = new THREE.PointLight(0xffcc66, 0.6, 8, 1.2);
45    windowGlow2.position.set(3.5, 1.8, 0);
46    this.scene.add(windowGlow2);
47
48    // ─── Ground (cobblestones) ────────────────────────────────
49    const groundGeo = new THREE.PlaneGeometry(18, 16);
50    const groundMat = new THREE.MeshStandardMaterial({ color: 0x1a1a18, roughness: 0.95 });
51    const ground = new THREE.Mesh(groundGeo, groundMat);
52    ground.rotation.x = -Math.PI / 2;
53    ground.receiveShadow = true;
54    this.scene.add(ground);
55
56    // Cobblestone texture via small displaced boxes
57    const cobbleMat = new THREE.MeshStandardMaterial({ color: 0x222220, roughness: 0.9 });
58    for (let x = -5; x < 5; x += 0.4) {
59      for (let z = -7; z < 7; z += 0.3) {
60        if (Math.random() > 0.7) continue; // sparse
61        const cobble = new THREE.Mesh(
62          new THREE.BoxGeometry(0.3 + Math.random() * 0.1, 0.02, 0.2 + Math.random() * 0.08),
63          cobbleMat
64        );
65        cobble.position.set(x + Math.random() * 0.1, 0.01, z + Math.random() * 0.05);
66        this.scene.add(cobble);
67      }
68    }
69
70    // ─── Grodman's House (left / -X side, No. 9) ─────────────
71    this._buildHouse(-3.8, Math.PI / 2, 'grodman');
72
73    // ─── No.11 Drabdump's (right / +X side) ──────────────────
74    this._buildHouse(3.8, -Math.PI / 2, 'drabdump');
75
76    // ─── Fog particles ────────────────────────────────────────
77    this._createFog();
78
79    // ─── HOTSPOTS ─────────────────────────────────────────────
80    // Grodman's house entrance
81    const hsGrodman = createHotspotMesh(new THREE.Vector3(-3.0, 1.5, 0));
82    hsGrodman.scale.setScalar(2.5);
83    this.scene.add(hsGrodman);
84    this.hotspots.push({
85      mesh: hsGrodman,
86      clueIds: [],
87      label: '进入邻街老侦探宅邸 (9号)',
88      room: null,
89      visited: false,
90      navigate: 'grodmans-study'
91    });
92
93    // No.11 entrance → goes to Mortlake's room (ground floor)
94    const hsNo11 = createHotspotMesh(new THREE.Vector3(3.0, 1.5, 0));
95    hsNo11.scale.setScalar(2.5);
96    this.scene.add(hsNo11);
97    this.hotspots.push({
98      mesh: hsNo11,
99      clueIds: [],
100      label: '进入11号公寓',
101      room: null,
102      visited: false,
103      navigate: 'mortlakes-room'
104    });
105  }
106
107  _buildHouse(xOffset, rotY, id) {
108    const house = new THREE.Group();
109    const width = 4.2;
110    const height = 5;
111    const depth = 3.4;
112    const frontZ = 0;
113    const backZ = -depth;
114    const wallColor = id === 'grodman' ? 0x4a3020 : 0x303040;
115    const sideColor = id === 'grodman' ? 0x3a2618 : 0x262636;
116    const brickMat = new THREE.MeshStandardMaterial({ color: wallColor, roughness: 0.92 });
117    const sideMat = new THREE.MeshStandardMaterial({ color: sideColor, roughness: 0.95 });
118
119    // Full building mass: front, back, and side walls instead of a single flat facade.
120    const body = new THREE.Mesh(new THREE.BoxGeometry(width, height, depth), brickMat);
121    body.position.set(0, height / 2, -depth / 2);
122    body.castShadow = true;
123    body.receiveShadow = true;
124    house.add(body);
125
126    const sideWallGeo = new THREE.BoxGeometry(0.04, height, depth + 0.03);
127    [-1, 1].forEach(side => {
128      const sideWall = new THREE.Mesh(sideWallGeo, sideMat);
129      sideWall.position.set(side * (width / 2 + 0.02), height / 2, -depth / 2);
130      sideWall.castShadow = true;
131      sideWall.receiveShadow = true;
132      house.add(sideWall);
133    });
134
135    const rearWall = new THREE.Mesh(
136      new THREE.BoxGeometry(width, height, 0.04),
137      new THREE.MeshStandardMaterial({ color: sideColor, roughness: 0.96 })
138    );
139    rearWall.position.set(0, height / 2, backZ - 0.02);
140    rearWall.castShadow = true;
141    rearWall.receiveShadow = true;
142    house.add(rearWall);
143
144    const frontTrimMat = new THREE.MeshStandardMaterial({ color: 0x2a2118, roughness: 0.8 });
145    const cornice = new THREE.Mesh(new THREE.BoxGeometry(width + 0.25, 0.16, 0.18), frontTrimMat);
146    cornice.position.set(0, height - 0.15, frontZ + 0.09);
147    cornice.castShadow = true;
148    house.add(cornice);
149
150    // Door
151    const doorMat = new THREE.MeshStandardMaterial({ color: 0x1a0f08, roughness: 0.8 });
152    const door = new THREE.Mesh(new THREE.BoxGeometry(0.9, 2.1, 0.08), doorMat);
153    door.position.set(0, 1.05, frontZ + 0.07);
154    door.castShadow = true;
155    house.add(door);
156
157    // Door frame
158    const frameMat = new THREE.MeshStandardMaterial({ color: 0x3a2a1a, roughness: 0.7 });
159    house.add(new THREE.Mesh(new THREE.BoxGeometry(1.1, 0.08, 0.12), frameMat).translateY(2.15).translateZ(frontZ + 0.11));
160    house.add(new THREE.Mesh(new THREE.BoxGeometry(0.08, 2.1, 0.12), frameMat).translateX(-0.52).translateY(1.05).translateZ(frontZ + 0.11));
161    house.add(new THREE.Mesh(new THREE.BoxGeometry(0.08, 2.1, 0.12), frameMat).translateX(0.52).translateY(1.05).translateZ(frontZ + 0.11));
162
163    // Windows (2 per floor, 2 floors above ground)
164    const glassMat = new THREE.MeshStandardMaterial({
165      color: 0x1a2030, metalness: 0.1, roughness: 0.2, transparent: true, opacity: 0.5
166    });
167    const warmGlassMat = new THREE.MeshStandardMaterial({
168      color: 0x3a2a10, emissive: 0xffaa22, emissiveIntensity: 0.15, transparent: true, opacity: 0.6
169    });
170
171    for (let floor = 0; floor < 2; floor++) {
172      for (let side of [-1, 1]) {
173        const wy = 2.5 + floor * 1.8;
174        const wx = side * 1.2;
175        // Window frame
176        house.add(new THREE.Mesh(new THREE.BoxGeometry(0.7, 1.0, 0.1), frameMat).translateX(wx).translateY(wy).translateZ(frontZ + 0.1));
177        // Glass
178        const glass = new THREE.Mesh(new THREE.PlaneGeometry(0.6, 0.85), floor === 0 ? warmGlassMat : glassMat);
179        glass.position.set(wx, wy, frontZ + 0.16);
180        house.add(glass);
181      }
182    }
183
184    // Smaller end-wall windows make the house read as a real volume when viewed down the street.
185    [-1, 1].forEach(side => {
186      const sideWindow = new THREE.Mesh(new THREE.PlaneGeometry(0.55, 0.75), glassMat);
187      sideWindow.position.set(side * (width / 2 + 0.03), 3.35, -depth * 0.45);
188      sideWindow.rotation.y = side > 0 ? Math.PI / 2 : -Math.PI / 2;
189      house.add(sideWindow);
190
191      const lintel = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.08, 0.72), frameMat);
192      lintel.position.set(side * (width / 2 + 0.06), 3.78, -depth * 0.45);
193      lintel.rotation.y = side > 0 ? Math.PI / 2 : -Math.PI / 2;
194      house.add(lintel);
195    });
196
197    // Chimney
198    const chimney = new THREE.Mesh(
199      new THREE.BoxGeometry(0.4, 1.2, 0.35),
200      brickMat
201    );
202    chimney.position.set(1.2, height + 0.55, -depth * 0.65);
203    chimney.castShadow = true;
204    house.add(chimney);
205
206    // Roof (pitched prism with real depth)
207    const roofGeo = new THREE.BufferGeometry();
208    const eaveX = width / 2 + 0.25;
209    const eaveFrontZ = frontZ + 0.25;
210    const eaveBackZ = backZ - 0.25;
211    const roofPeakY = height + 1.15;
212    const roofVerts = new Float32Array([
213      -eaveX, height, eaveFrontZ,
214       eaveX, height, eaveFrontZ,
215       0, roofPeakY, eaveFrontZ,
216      -eaveX, height, eaveBackZ,
217       eaveX, height, eaveBackZ,
218       0, roofPeakY, eaveBackZ
219    ]);
220    roofGeo.setIndex([
221      0, 1, 2,
222      4, 3, 5,
223      3, 0, 2, 3, 2, 5,
224      1, 4, 5, 1, 5, 2,
225      3, 4, 1, 3, 1, 0
226    ]);
227    roofGeo.setAttribute('position', new THREE.BufferAttribute(roofVerts, 3));
228    roofGeo.computeVertexNormals();
229    const roofMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.9, side: THREE.DoubleSide });
230    const roof = new THREE.Mesh(roofGeo, roofMat);
231    roof.castShadow = true;
232    roof.receiveShadow = true;
233    house.add(roof);
234
235    // Number plate
236    const plateMat = new THREE.MeshStandardMaterial({ color: 0x2a2a2a, roughness: 0.7 });
237    const plate = new THREE.Mesh(new THREE.PlaneGeometry(0.3, 0.2), plateMat);
238    plate.position.set(0.6, 2.3, frontZ + 0.17);
239    house.add(plate);
240
241    house.position.set(xOffset, 0, 0);
242    house.rotation.y = rotY;
243    this.scene.add(house);
244  }
245
246  _buildStreetLamp(position) {
247    const lampGroup = new THREE.Group();
248    const ironMat = new THREE.MeshStandardMaterial({ color: 0x111111, metalness: 0.45, roughness: 0.55 });
249    const glassMat = new THREE.MeshStandardMaterial({
250      color: 0xffcc66,
251      emissive: 0xffaa22,
252      emissiveIntensity: 0.9,
253      transparent: true,
254      opacity: 0.62
255    });
256
257    const base = new THREE.Mesh(new THREE.CylinderGeometry(0.22, 0.32, 
2570.24, 16), ironMat);
258    base.position.set(0, 0.12, 0);
259    base.castShadow = true;
260    lampGroup.add(base);
261
262    const post = new THREE.Mesh(new THREE.CylinderGeometry(0.045, 0.07, 1.8, 16), ironMat);
263    post.position.set(0, 1.05, 0);
264    post.castShadow = true;
265    lampGroup.add(post);
266
267    const crossbar = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.045, 0.045), ironMat);
268    crossbar.position.set(0, 1.95, 0);
269    crossbar.castShadow = true;
270    lampGroup.add(crossbar);
271
272    const lanternY = 2.15;
273    const lanternHalf = 0.18;
274    [-1, 1].forEach(xSide => {
275      [-1, 1].forEach(zSide => {
276        const bar = new THREE.Mesh(new THREE.BoxGeometry(0.035, 0.48, 0.035), ironMat);
277        bar.position.set(xSide * lanternHalf, lanternY, zSide * lanternHalf);
278        bar.castShadow = true;
279        lampGroup.add(bar);
280      });
281    });
282    [
283      { size: [0.42, 0.035, 0.035], y: lanternY + 0.24, z: lanternHalf },
284      { size: [0.42, 0.035, 0.035], y: lanternY + 0.24, z: -lanternHalf },
285      { size: [0.42, 0.035, 0.035], y: lanternY - 0.24, z: lanternHalf },
286      { size: [0.42, 0.035, 0.035], y: lanternY - 0.24, z: -lanternHalf },
287      { size: [0.035, 0.035, 0.42], y: lanternY + 0.24, x: lanternHalf },
288      { size: [0.035, 0.035, 0.42], y: lanternY + 0.24, x: -lanternHalf },
289      { size: [0.035, 0.035, 0.42], y: lanternY - 0.24, x: lanternHalf },
290      { size: [0.035, 0.035, 0.42], y: lanternY - 0.24, x: -lanternHalf }
291    ].forEach(part => {
292      const rail = new THREE.Mesh(new THREE.BoxGeometry(...part.size), ironMat);
293      rail.position.set(part.x || 0, part.y, part.z || 0);
294      rail.castShadow = true;
295      lampGroup.add(rail);
296    });
297
298    const lanternGlass = new THREE.Mesh(new THREE.BoxGeometry(0.28, 0.36, 0.28), glassMat);
299    lanternGlass.position.set(0, lanternY, 0);
300    lampGroup.add(lanternGlass);
301
302    const cap = new THREE.Mesh(new THREE.ConeGeometry(0.28, 0.22, 4), ironMat);
303    cap.position.set(0, 2.5, 0);
304    cap.rotation.y = Math.PI / 4;
305    cap.castShadow = true;
306    lampGroup.add(cap);
307
308    const lightPool = new THREE.Mesh(
309      new THREE.CircleGeometry(1.8, 32),
310      new THREE.MeshBasicMaterial({ color: 0xffaa44, transparent: true, opacity: 0.12, depthWrite: false })
311    );
312    lightPool.rotation.x = -Math.PI / 2;
313    lightPool.position.set(0, 0.025, 0);
314    lampGroup.add(lightPool);
315
316    lampGroup.position.copy(position);
317    this.scene.add(lampGroup);
318
319    this.gasLampLight = new THREE.PointLight(0xffaa44, 2.8, 18, 1.2);
320    this.gasLampLight.position.set(position.x, 2.15, position.z);
321    this.gasLampLight.castShadow = true;
322    this.scene.add(this.gasLampLight);
323  }
324
325  _createFog() {
326    const count = 200;
327    const positions = new Float32Array(count * 3);
328    for (let i = 0; i < count; i++) {
329      positions[i * 3] = (Math.random() - 0.5) * 12;
330      positions[i * 3 + 1] = Math.random() * 3;
331      positions[i * 3 + 2] = (Math.random() - 0.5) * 14;
332    }
333    const geo = new THREE.BufferGeometry();
334    geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
335    const mat = new THREE.PointsMaterial({
336      color: 0x667788,
337      size: 0.08,
338      transparent: true,
339      opacity: 0.15,
340      sizeAttenuation: true
341    });
342    this.fogParticles = new THREE.Points(geo, mat);
343    this.scene.add(this.fogParticles);
344  }
345
346  update(delta) {
347    this.hotspotTime += delta;
348
349    // Animate fog drifting
350    if (this.fogParticles) {
351      const pos = this.fogParticles.geometry.attributes.position;
352      for (let i = 0; i < pos.count; i++) {
353        pos.array[i * 3] += Math.sin(this.hotspotTime * 0.2 + i * 0.3) * 0.002;
354        pos.array[i * 3 + 1] += Math.cos(this.hotspotTime * 0.1 + i * 0.5) * 0.0005;
355      }
356      pos.needsUpdate = true;
357    }
358
359    // Gas lamp flicker
360    if (this.gasLampLight) {
361      setLightBaseIntensity(
362        this.gasLampLight,
363        2.4 + Math.sin(this.hotspotTime * 6) * 0.08 + Math.sin(this.hotspotTime * 11) * 0.05
364      );
365    }
366
367    // Animate hotspots
368    this.hotspots.forEach((h, i) => {
369      if (!h.visited) {
370        const s = 0.9 + Math.sin(this.hotspotTime * 2 + i) * 0.2;
371        h.mesh.scale.setScalar(s);
372        h.mesh.material.emissiveIntensity = 0.5 + Math.sin(this.hotspotTime * 3 + i) * 0.3;
373      }
374    });
375  }
376
377}
378
379export { GloverStreetRoom };

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.