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.