dsh-agent-office 4.6.0 → 4.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/panel-src/main.js CHANGED
@@ -14,6 +14,7 @@ import faceSurprised from './assets/exp-surprised.png'
14
14
  import faceThinking from './assets/exp-thinking.png'
15
15
  import faceLoved from './assets/exp-loved.png'
16
16
  import faceSlits from './assets/exp-slits.png'
17
+ import faceUneven from './assets/exp-uneven.png'
17
18
 
18
19
  const API = '/agent-office/api/state'
19
20
  const FLOOR_GAP = 4.0
@@ -287,6 +288,12 @@ function applyThemeFrame() {
287
288
  const neonMaterial = neonSign?.material
288
289
  if (neonMaterial) neonMaterial.opacity = NIGHT_THEME.neon + (DAY_THEME.neon - NIGHT_THEME.neon) * mix
289
290
  for (const item of [...themeLights, ...annexLights]) item.light.intensity = item.night + (item.day - item.night) * mix
291
+ const weatherDim = weatherMode === 'storm' ? 0.86 : weatherMode === 'rain' || weatherMode === 'snow' ? 0.92 : weatherMode === 'cloudy' ? 0.95 : weatherMode === 'wind' ? 0.98 : 1
292
+ hemi.intensity *= weatherDim
293
+ key.intensity *= weatherDim
294
+ rim.intensity *= weatherDim
295
+ fill.intensity *= weatherDim
296
+ scene.fog.density = 0.0085 * (weatherMode === 'rain' || weatherMode === 'snow' || weatherMode === 'storm' ? 1.06 : 1)
290
297
  }
291
298
 
292
299
  function syncThemeByClock(immediate = false) {
@@ -306,17 +313,17 @@ const neonSign = (() => {
306
313
  canvas.height = 192
307
314
  const ctx = canvas.getContext('2d')
308
315
  ctx.clearRect(0, 0, canvas.width, canvas.height)
309
- ctx.font = '900 88px Inter, "PingFang SC", system-ui, sans-serif'
316
+ ctx.font = '900 72px Inter, "PingFang SC", system-ui, sans-serif'
310
317
  ctx.textAlign = 'center'
311
318
  ctx.textBaseline = 'middle'
312
- ctx.shadowColor = 'rgba(255,106,61,.85)'
313
- ctx.shadowBlur = 36
314
- ctx.fillStyle = '#ffe9dd'
315
- ctx.fillText('CATORA', 512, 78)
319
+ ctx.shadowColor = 'rgba(77,107,254,.95)'
320
+ ctx.shadowBlur = 40
321
+ ctx.fillStyle = '#eaf1ff'
322
+ ctx.fillText('DEEPSEEK HARNESS', 512, 74)
316
323
  ctx.shadowBlur = 0
317
- ctx.font = '500 28px ui-monospace, Menlo, monospace'
318
- ctx.fillStyle = 'rgba(190,170,155,.85)'
319
- ctx.fillText('DEEPSEEK HARNESS — LIVE SESSION OFFICE', 512, 150)
324
+ ctx.font = '500 26px ui-monospace, Menlo, monospace'
325
+ ctx.fillStyle = 'rgba(160,190,255,.9)'
326
+ ctx.fillText('LIVE SESSION OFFICE', 512, 146)
320
327
  const texture = new THREE.CanvasTexture(canvas)
321
328
  texture.colorSpace = THREE.SRGBColorSpace
322
329
  const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false }))
@@ -352,6 +359,7 @@ const FACE_SRC = {
352
359
  thinking: faceThinking,
353
360
  loved: faceLoved,
354
361
  slits: faceSlits,
362
+ uneven: faceUneven,
355
363
  }
356
364
  const faceTextures = {}
357
365
  const faceLoader = new THREE.TextureLoader()
@@ -365,7 +373,7 @@ const GLASS_KEY = 'agent-office-ui-opacity'
365
373
  let customFaceTexture = null
366
374
  let customFaceData = ''
367
375
  const FACE_FOR_STATUS = {
368
- working: 'happy',
376
+ working: 'uneven',
369
377
  need_me: 'surprised',
370
378
  stuck: 'dizzy',
371
379
  done: 'loved',
@@ -445,6 +453,39 @@ const scribbleTexture = (() => {
445
453
  })()
446
454
  const OVERLOAD_COLOR = new THREE.Color(0xff5a5a)
447
455
 
456
+ const keyboardTexture = (() => {
457
+ const canvas = document.createElement('canvas')
458
+ canvas.width = 256
459
+ canvas.height = 96
460
+ const ctx = canvas.getContext('2d')
461
+ ctx.fillStyle = '#0a1017'
462
+ ctx.fillRect(0, 0, 256, 96)
463
+ const cols = 14
464
+ const rows = 4
465
+ const keyW = 14
466
+ const keyH = 14
467
+ const gap = 2
468
+ const startX = (256 - (cols * (keyW + gap) - gap)) / 2
469
+ for (let r = 0; r < rows; r++) {
470
+ for (let c = 0; c < cols; c++) {
471
+ const x = startX + c * (keyW + gap)
472
+ const y = 10 + r * (keyH + gap)
473
+ ctx.fillStyle = (r === 2 && c > 4 && c < 9) ? '#24445a' : '#1c2a36'
474
+ ctx.beginPath(); ctx.roundRect(x, y, keyW, keyH, 2); ctx.fill()
475
+ ctx.fillStyle = 'rgba(150,220,255,.22)'
476
+ ctx.fillRect(x + 2, y + 2, keyW - 4, 2)
477
+ }
478
+ }
479
+ // spacebar + underglow
480
+ ctx.fillStyle = '#1c2a36'
481
+ ctx.beginPath(); ctx.roundRect(62, 74, 132, 13, 3); ctx.fill()
482
+ ctx.fillStyle = 'rgba(80,220,255,.55)'
483
+ ctx.fillRect(4, 91, 248, 2)
484
+ const texture = new THREE.CanvasTexture(canvas)
485
+ texture.colorSpace = THREE.SRGBColorSpace
486
+ return texture
487
+ })()
488
+
448
489
  const floorTexture = (() => {
449
490
  const canvas = document.createElement('canvas')
450
491
  canvas.width = canvas.height = 256
@@ -464,6 +505,197 @@ const floorTexture = (() => {
464
505
  return texture
465
506
  })()
466
507
 
508
+ /* ------------------------------------------------------------------ *
509
+ * 天气:云层 / 雨 / 雪 / 风
510
+ * ------------------------------------------------------------------ */
511
+ const weatherGroup = new THREE.Group()
512
+ scene.add(weatherGroup)
513
+ const weatherCloudTexture = (() => {
514
+ const canvas = document.createElement('canvas')
515
+ canvas.width = 512
516
+ canvas.height = 192
517
+ const ctx = canvas.getContext('2d')
518
+ ctx.clearRect(0, 0, 512, 192)
519
+ for (const [x, y, rx, ry, alpha] of [[120, 108, 160, 26, .16], [260, 84, 210, 22, .14], [400, 112, 170, 20, .13], [190, 132, 130, 14, .11], [340, 64, 120, 16, .1]]) {
520
+ const grad = ctx.createRadialGradient(x, y, 0, x, y, rx)
521
+ grad.addColorStop(0, `rgba(230,242,250,${alpha})`)
522
+ grad.addColorStop(.5, `rgba(196,214,228,${alpha * .55})`)
523
+ grad.addColorStop(1, 'rgba(170,195,214,0)')
524
+ ctx.fillStyle = grad
525
+ ctx.beginPath(); ctx.ellipse(x, y, rx, ry, 0, 0, Math.PI * 2); ctx.fill()
526
+ }
527
+ const texture = new THREE.CanvasTexture(canvas)
528
+ texture.colorSpace = THREE.SRGBColorSpace
529
+ return texture
530
+ })()
531
+ const weatherClouds = []
532
+ for (let i = 0; i < 7; i++) {
533
+ const material = new THREE.SpriteMaterial({ map: weatherCloudTexture, color: 0x8d9dab, transparent: true, opacity: 0, depthWrite: false, toneMapped: false })
534
+ const cloud = new THREE.Sprite(material)
535
+ cloud.scale.set(17 + (i % 3) * 3, 2.8 + (i % 2) * .8, 1)
536
+ cloud.position.set(-16 + (i * 5.1) % 32, 6.2 + (i % 4) * .9, 4 + (i * 3.1) % 13)
537
+ cloud.userData = { baseX: cloud.position.x, baseY: cloud.position.y, baseZ: cloud.position.z, phase: i * 0.9 }
538
+ weatherGroup.add(cloud)
539
+ weatherClouds.push(cloud)
540
+ }
541
+
542
+ const WEATHER_COUNT = 560
543
+ const weatherPositions = new Float32Array(WEATHER_COUNT * 3)
544
+ const weatherGeometry = new THREE.BufferGeometry()
545
+ weatherGeometry.setAttribute('position', new THREE.BufferAttribute(weatherPositions, 3))
546
+ const weatherPointTexture = (() => {
547
+ const canvas = document.createElement('canvas')
548
+ canvas.width = canvas.height = 64
549
+ const ctx = canvas.getContext('2d')
550
+ const grad = ctx.createRadialGradient(32, 32, 0, 32, 32, 32)
551
+ grad.addColorStop(0, 'rgba(255,255,255,1)')
552
+ grad.addColorStop(0.5, 'rgba(235,250,255,.8)')
553
+ grad.addColorStop(1, 'rgba(235,250,255,0)')
554
+ ctx.fillStyle = grad
555
+ ctx.fillRect(0, 0, 64, 64)
556
+ const texture = new THREE.CanvasTexture(canvas)
557
+ texture.colorSpace = THREE.SRGBColorSpace
558
+ return texture
559
+ })()
560
+ const weatherMaterial = new THREE.PointsMaterial({ map: weatherPointTexture, alphaTest: 0.02, color: 0x9fd4ff, size: 0.055, transparent: true, opacity: 0, depthWrite: false, toneMapped: false, sizeAttenuation: true })
561
+ const weatherPoints = new THREE.Points(weatherGeometry, weatherMaterial)
562
+ weatherPoints.frustumCulled = false
563
+ weatherPoints.visible = false
564
+ weatherGroup.add(weatherPoints)
565
+ const rainPositions = new Float32Array(WEATHER_COUNT * 6)
566
+ const rainGeometry = new THREE.BufferGeometry()
567
+ rainGeometry.setAttribute('position', new THREE.BufferAttribute(rainPositions, 3))
568
+ const rainMaterial = new THREE.LineBasicMaterial({ color: 0x9fd4ff, transparent: true, opacity: 0, depthWrite: false, toneMapped: false })
569
+ const rainLines = new THREE.LineSegments(rainGeometry, rainMaterial)
570
+ rainLines.frustumCulled = false
571
+ rainLines.visible = false
572
+ weatherGroup.add(rainLines)
573
+ const RAIN_COUNT = 80
574
+ rainGeometry.setDrawRange(0, RAIN_COUNT * 2)
575
+ const weatherParticles = []
576
+ let weatherMode = 'clear'
577
+ let weatherWind = 0
578
+ let weatherCloudLevel = 0
579
+ let weatherPointLevel = 0
580
+
581
+ function seedWeatherParticles(mode) {
582
+ const isWind = mode === 'wind'
583
+ const isSnow = mode === 'snow'
584
+ for (let i = 0; i < WEATHER_COUNT; i++) {
585
+ const x = (Math.random() - 0.5) * (isWind ? 34 : 26)
586
+ const y = isWind ? 1.5 + Math.random() * 7 : Math.random() * 18
587
+ const z = (Math.random() - 0.5) * (isWind ? 22 : 24)
588
+ weatherParticles[i] = { x, y, z, speed: isSnow ? 0.035 + Math.random() * 0.035 : 0.16 + Math.random() * 0.2, drift: Math.random() * 2 - 1 }
589
+ weatherPositions[i * 3] = x
590
+ weatherPositions[i * 3 + 1] = y
591
+ weatherPositions[i * 3 + 2] = z
592
+ }
593
+ weatherGeometry.attributes.position.needsUpdate = true
594
+ }
595
+
596
+ function setWeather(mode, wind = 0) {
597
+ weatherMode = ['clear', 'cloudy', 'rain', 'snow', 'wind', 'storm'].includes(mode) ? mode : 'clear'
598
+ weatherWind = Number.isFinite(wind) ? wind : 0
599
+ const wet = weatherMode === 'rain' || weatherMode === 'storm'
600
+ const snow = weatherMode === 'snow'
601
+ const windy = weatherMode === 'wind'
602
+ weatherPoints.visible = snow || windy
603
+ rainLines.visible = wet
604
+ weatherMaterial.color.set(snow ? 0xe6f5ff : 0xcfe8ff)
605
+ weatherMaterial.size = snow ? 0.17 : 0.06
606
+ if (wet) rainGeometry.setDrawRange(0, RAIN_COUNT * 2)
607
+ if (wet || snow || windy) seedWeatherParticles(weatherMode)
608
+ }
609
+
610
+ function refreshWeather() {
611
+ let location = null
612
+ try {
613
+ const raw = localStorage.getItem('agent-office-weather-location')
614
+ if (raw) {
615
+ const parsed = JSON.parse(raw)
616
+ if (Number.isFinite(parsed?.lat) && Number.isFinite(parsed?.lon)) location = parsed
617
+ }
618
+ } catch {}
619
+ if (!location) {
620
+ setWeather('clear')
621
+ return
622
+ }
623
+ fetch(`/agent-office/api/weather?lat=${encodeURIComponent(location.lat)}&lon=${encodeURIComponent(location.lon)}`, { cache: 'no-store' })
624
+ .then(response => response.ok ? response.json() : null)
625
+ .then(data => {
626
+ if (!data?.ok) {
627
+ setWeather('clear')
628
+ return
629
+ }
630
+ const code = Number(data.code)
631
+ let mode = 'clear'
632
+ if (code >= 95) mode = 'storm'
633
+ else if (code >= 71 && code <= 86) mode = 'snow'
634
+ else if ((code >= 51 && code <= 67) || (code >= 80 && code <= 82)) mode = 'rain'
635
+ else if (code === 2 || code === 3 || code === 45 || code === 48) mode = 'cloudy'
636
+ else if (Number(data.wind) >= 28) mode = 'wind'
637
+ setWeather(mode, Number(data.wind))
638
+ })
639
+ .catch(() => setWeather('clear'))
640
+ }
641
+
642
+ function updateWeatherFrame(t) {
643
+ const cloudy = weatherMode === 'cloudy' || weatherMode === 'wind'
644
+ weatherCloudLevel += ((cloudy ? 1 : 0) - weatherCloudLevel) * 0.025
645
+ for (const cloud of weatherClouds) {
646
+ cloud.material.opacity = weatherCloudLevel * 0.22
647
+ cloud.position.x = cloud.userData.baseX + Math.sin(t * 0.035 + cloud.userData.phase) * 2.2
648
+ cloud.position.z = cloud.userData.baseZ + Math.cos(t * 0.03 + cloud.userData.phase) * 1.2
649
+ }
650
+ const active = weatherPoints.visible
651
+ const wet = weatherMode === 'rain' || weatherMode === 'storm'
652
+ weatherPointLevel += (((active || wet) ? 1 : 0) - weatherPointLevel) * 0.06
653
+ weatherMaterial.opacity = weatherPointLevel * (weatherMode === 'snow' ? 0.6 : 0.5)
654
+ rainMaterial.opacity = weatherPointLevel * 0.3
655
+ if (wet) {
656
+ for (let i = 0; i < RAIN_COUNT; i++) {
657
+ const p = weatherParticles[i]
658
+ if (!p) continue
659
+ p.y -= p.speed * 0.95
660
+ p.x += weatherWind * 0.0015 + p.drift * 0.0012
661
+ if (p.y < 0) { p.y = 17; p.x = (Math.random() - 0.5) * 26; p.z = (Math.random() - 0.5) * 24 }
662
+ rainPositions[i * 6] = p.x
663
+ rainPositions[i * 6 + 1] = p.y
664
+ rainPositions[i * 6 + 2] = p.z
665
+ rainPositions[i * 6 + 3] = p.x + weatherWind * 0.0025
666
+ rainPositions[i * 6 + 4] = p.y - 0.22
667
+ rainPositions[i * 6 + 5] = p.z
668
+ }
669
+ rainGeometry.attributes.position.needsUpdate = true
670
+ } else if (active) {
671
+ for (let i = 0; i < WEATHER_COUNT; i++) {
672
+ const p = weatherParticles[i]
673
+ if (!p) continue
674
+ if (weatherMode === 'wind') {
675
+ p.x += 0.11 + Math.abs(weatherWind) * 0.002
676
+ p.y += Math.sin(t * 1.3 + p.drift) * 0.004
677
+ if (p.x > 18) p.x = -18
678
+ } else if (weatherMode === 'snow') {
679
+ p.y -= p.speed * 0.55
680
+ p.x += Math.sin(t * 1.4 + p.drift * 5) * 0.006
681
+ if (p.y < 0) { p.y = 16; p.x = (Math.random() - 0.5) * 26 }
682
+ } else {
683
+ p.y -= p.speed
684
+ p.x += (weatherWind * 0.0015 + p.drift * 0.001)
685
+ if (p.y < 0) { p.y = 17; p.x = (Math.random() - 0.5) * 26; p.z = (Math.random() - 0.5) * 24 }
686
+ }
687
+ weatherPositions[i * 3] = p.x
688
+ weatherPositions[i * 3 + 1] = p.y
689
+ weatherPositions[i * 3 + 2] = p.z
690
+ }
691
+ weatherGeometry.attributes.position.needsUpdate = true
692
+ }
693
+ }
694
+
695
+ window.addEventListener('storage', event => {
696
+ if (event.key === 'agent-office-weather-location') refreshWeather()
697
+ })
698
+
467
699
  function disposeGroup(group) {
468
700
  group.traverse(object => {
469
701
  // CSS2D 标签是独立 DOM,scene 移除后渲染器不会回收,必须手动删掉,否则重建会累积重影
@@ -475,7 +707,7 @@ function disposeGroup(group) {
475
707
  if (object.material) {
476
708
  const materials = Array.isArray(object.material) ? object.material : [object.material]
477
709
  for (const material of materials) {
478
- if (material.map && material.map !== glowTexture && material.map !== shadowTexture && material.map !== floorTexture && material.map !== sweatTexture && material.map !== scribbleTexture) material.map.dispose()
710
+ if (material.map && material.map !== glowTexture && material.map !== shadowTexture && material.map !== floorTexture && material.map !== sweatTexture && material.map !== scribbleTexture && material.map !== keyboardTexture) material.map.dispose()
479
711
  material.dispose()
480
712
  }
481
713
  }
@@ -837,16 +1069,65 @@ function workstation(agent, spot) {
837
1069
  group.add(footrest)
838
1070
  }
839
1071
 
840
- const keyboard = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.02, 0.15), metalMat)
841
- keyboard.position.set(spot.x + 0.24, 0.6, spot.z + 0.82)
842
- group.add(keyboard)
1072
+ // 键盘:底座 + 键帽贴图 + 底部氛围灯
1073
+ const keyboardBase = new THREE.Mesh(new THREE.BoxGeometry(0.52, 0.03, 0.18), new THREE.MeshStandardMaterial({ color: 0x111923, roughness: 0.45, metalness: 0.4 }))
1074
+ keyboardBase.position.set(spot.x + 0.24, 0.575, spot.z + 0.8)
1075
+ group.add(keyboardBase)
1076
+ const keyboardTop = new THREE.Mesh(
1077
+ new THREE.PlaneGeometry(0.49, 0.16),
1078
+ new THREE.MeshBasicMaterial({ map: keyboardTexture, toneMapped: false }),
1079
+ )
1080
+ keyboardTop.rotation.x = -Math.PI / 2
1081
+ keyboardTop.position.set(spot.x + 0.24, 0.592, spot.z + 0.8)
1082
+ group.add(keyboardTop)
1083
+ const keyboardGlow = new THREE.Mesh(
1084
+ new THREE.BoxGeometry(0.48, 0.006, 0.012),
1085
+ new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.8, toneMapped: false }),
1086
+ )
1087
+ keyboardGlow.position.set(spot.x + 0.24, 0.58, spot.z + 0.9)
1088
+ group.add(keyboardGlow)
1089
+
1090
+ // 鼠标垫 + 鼠标 + 滚轮
1091
+ const mousePad = new THREE.Mesh(new THREE.BoxGeometry(0.26, 0.008, 0.22), new THREE.MeshStandardMaterial({ color: 0x101923, roughness: 0.92, metalness: 0.05 }))
1092
+ mousePad.position.set(spot.x + 0.63, 0.588, spot.z + 0.83)
1093
+ group.add(mousePad)
1094
+ const mouse = new THREE.Mesh(
1095
+ new RoundedBoxGeometry(0.09, 0.04, 0.13, 3, 0.018),
1096
+ new THREE.MeshStandardMaterial({ color: 0x1c2835, roughness: 0.5, metalness: 0.25 }),
1097
+ )
1098
+ mouse.position.set(spot.x + 0.63, 0.61, spot.z + 0.82)
1099
+ mouse.rotation.y = -0.2
1100
+ group.add(mouse)
1101
+ const wheel = new THREE.Mesh(new THREE.BoxGeometry(0.018, 0.018, 0.035), new THREE.MeshBasicMaterial({ color }))
1102
+ wheel.position.set(spot.x + 0.63, 0.64, spot.z + 0.79)
1103
+ group.add(wheel)
1104
+
1105
+ // 键盘 / 鼠标线缆
1106
+ const cableMat = new THREE.MeshStandardMaterial({ color: 0x607385, roughness: 0.38, metalness: 0.3 })
1107
+ const keyboardCable = new THREE.Mesh(
1108
+ new THREE.TubeGeometry(new THREE.CatmullRomCurve3([
1109
+ new THREE.Vector3(spot.x + 0.24, 0.592, spot.z + 0.71),
1110
+ new THREE.Vector3(spot.x + 0.18, 0.592, spot.z + 0.62),
1111
+ new THREE.Vector3(spot.x - 0.06, 0.592, spot.z + 0.54),
1112
+ new THREE.Vector3(spot.x - 0.34, 0.592, spot.z + 0.48),
1113
+ new THREE.Vector3(spot.x - 0.48, 0.6, spot.z + 0.42),
1114
+ ], false, 'centripetal'), 18, 0.008, 5, false),
1115
+ cableMat,
1116
+ )
1117
+ group.add(keyboardCable)
1118
+ const mouseCable = new THREE.Mesh(
1119
+ new THREE.TubeGeometry(new THREE.CatmullRomCurve3([
1120
+ new THREE.Vector3(spot.x + 0.63, 0.6, spot.z + 0.76),
1121
+ new THREE.Vector3(spot.x + 0.64, 0.592, spot.z + 0.62),
1122
+ new THREE.Vector3(spot.x + 0.34, 0.592, spot.z + 0.54),
1123
+ new THREE.Vector3(spot.x - 0.08, 0.592, spot.z + 0.48),
1124
+ new THREE.Vector3(spot.x - 0.46, 0.6, spot.z + 0.42),
1125
+ ], false, 'centripetal'), 18, 0.007, 5, false),
1126
+ cableMat,
1127
+ )
1128
+ group.add(mouseCable)
1129
+
843
1130
 
844
- const plate = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.16, 0.02), new THREE.MeshBasicMaterial({ color: 0x0e151d }))
845
- plate.position.set(spot.x + 0.6, 0.63, spot.z + 0.86)
846
- group.add(plate)
847
- const plateDot = new THREE.Mesh(new THREE.CircleGeometry(0.035, 12), new THREE.MeshBasicMaterial({ color }))
848
- plateDot.position.set(spot.x + 0.55, 0.63, spot.z + 0.872)
849
- group.add(plateDot)
850
1131
 
851
1132
  return group
852
1133
  }
@@ -1347,6 +1628,8 @@ let targetGoal = null
1347
1628
  let zoomGoal = null
1348
1629
  let tower = null
1349
1630
  let loading = true
1631
+ let introPlayed = false
1632
+ let introTl = null
1350
1633
 
1351
1634
  function viewScale() {
1352
1635
  return clamp(1638 / Math.max(900, window.innerWidth), 0.82, 1.5)
@@ -1603,6 +1886,68 @@ let mcpDashes = []
1603
1886
  let mcpGroup = null
1604
1887
  let mcpSign = null
1605
1888
  let sciFiIcons = []
1889
+ let studyClock = null
1890
+ let annexCats = []
1891
+
1892
+ function drawStudyClock(seconds, alarm) {
1893
+ if (!studyClock) return
1894
+ const ctx = studyClock.ctx
1895
+ const w = 512
1896
+ const h = 512
1897
+ const total = Math.max(0, Math.min(25 * 60, seconds))
1898
+ const mm = String(Math.floor(total / 60)).padStart(2, '0')
1899
+ const ss = String(total % 60).padStart(2, '0')
1900
+ ctx.clearRect(0, 0, w, h)
1901
+ ctx.save()
1902
+ ctx.translate(256, 256)
1903
+ // 背后柔光
1904
+ const glow = ctx.createRadialGradient(0, 0, 40, 0, 0, 240)
1905
+ glow.addColorStop(0, alarm ? 'rgba(255,92,92,.34)' : 'rgba(80,210,255,.24)')
1906
+ glow.addColorStop(1, 'rgba(80,210,255,0)')
1907
+ ctx.fillStyle = glow
1908
+ ctx.beginPath(); ctx.arc(0, 0, 240, 0, Math.PI * 2); ctx.fill()
1909
+ // 铃铛
1910
+ ctx.fillStyle = '#263744'
1911
+ ctx.strokeStyle = alarm ? '#ff6b6b' : '#9fe7ff'
1912
+ ctx.lineWidth = 10
1913
+ for (const x of [-104, 104]) {
1914
+ ctx.beginPath(); ctx.arc(x, -190, 54, Math.PI, 0); ctx.fill(); ctx.stroke()
1915
+ ctx.beginPath(); ctx.moveTo(x - 54, -190); ctx.lineTo(x + 54, -190); ctx.stroke()
1916
+ }
1917
+ // 支脚
1918
+ ctx.lineCap = 'round'
1919
+ ctx.beginPath(); ctx.moveTo(-110, 190); ctx.lineTo(-160, 248); ctx.stroke()
1920
+ ctx.beginPath(); ctx.moveTo(110, 190); ctx.lineTo(160, 248); ctx.stroke()
1921
+ // 表身
1922
+ ctx.fillStyle = '#e9e2d2'
1923
+ ctx.beginPath(); ctx.arc(0, 0, 170, 0, Math.PI * 2); ctx.fill()
1924
+ ctx.lineWidth = 22
1925
+ ctx.strokeStyle = alarm ? '#d84343' : '#294654'
1926
+ ctx.beginPath(); ctx.arc(0, 0, 170, 0, Math.PI * 2); ctx.stroke()
1927
+ // 内圈
1928
+ ctx.fillStyle = alarm ? '#3a1418' : '#112029'
1929
+ ctx.beginPath(); ctx.arc(0, 0, 140, 0, Math.PI * 2); ctx.fill()
1930
+ ctx.strokeStyle = alarm ? '#ff8f8f' : '#8fe7ff'
1931
+ ctx.lineWidth = 3
1932
+ ctx.beginPath(); ctx.arc(0, 0, 140, 0, Math.PI * 2); ctx.stroke()
1933
+ // 数字
1934
+ ctx.textAlign = 'center'
1935
+ ctx.textBaseline = 'middle'
1936
+ ctx.fillStyle = alarm ? '#ffb3b3' : '#9fe7ff'
1937
+ ctx.font = '600 26px ui-monospace, Menlo, monospace'
1938
+ ctx.fillText('FOCUS TIMER', 0, -88)
1939
+ ctx.fillStyle = alarm ? '#ffd0d0' : '#f2fbff'
1940
+ ctx.font = '800 98px ui-monospace, Menlo, monospace'
1941
+ ctx.fillText(mm + ':' + ss, 0, 8)
1942
+ ctx.fillStyle = alarm ? '#ff8f8f' : 'rgba(159,231,255,.75)'
1943
+ ctx.font = '600 22px "PingFang SC", system-ui, sans-serif'
1944
+ ctx.fillText(alarm ? '时间到' : '备考倒计时', 0, 72)
1945
+ // 顶部小灯
1946
+ ctx.fillStyle = alarm ? '#ff6b6b' : '#7dd3a0'
1947
+ ctx.beginPath(); ctx.arc(0, -118, 7, 0, Math.PI * 2); ctx.fill()
1948
+ ctx.restore()
1949
+ studyClock.texture.needsUpdate = true
1950
+ }
1606
1951
 
1607
1952
  function buildAnnex() {
1608
1953
  annex = new THREE.Group()
@@ -1649,6 +1994,12 @@ function buildAnnex() {
1649
1994
 
1650
1995
  // 教室布局:讲台在后墙,三张工位面朝讲台
1651
1996
  // 1) 通用工位
1997
+ const frosted = new THREE.MeshStandardMaterial({ color: 0xdcefff, transparent: true, opacity: 0.16, roughness: 0.86, metalness: 0.02, depthWrite: false, side: THREE.DoubleSide })
1998
+ function addFrostedHeader(parent, x = 0, width = 1.28, y = 1.16, z = -0.38) {
1999
+ box(width, 0.42, 0.035, frosted, x, y, z, parent)
2000
+ box(width, 0.025, 0.05, metal, x, y + 0.23, z, parent)
2001
+ box(width, 0.025, 0.05, metal, x, y - 0.23, z, parent)
2002
+ }
1652
2003
  function addDesk(x, z, matTop = wood) {
1653
2004
  const g = new THREE.Group()
1654
2005
  g.position.set(x, 0, z)
@@ -1671,29 +2022,38 @@ function buildAnnex() {
1671
2022
  base.position.set(0, 0.02, 0.55)
1672
2023
  g.add(base)
1673
2024
  annex.add(g)
2025
+ addFrostedHeader(g)
2026
+ return g
1674
2027
  }
1675
2028
  addDesk(-2.35, 0.85)
1676
2029
 
1677
- // 2) 备考隔间(产品里的学习空间)
1678
- const booth = new THREE.Group()
1679
- booth.position.set(0, 0, 0.85)
1680
- box(1.55, 1.78, 0.04, glass, 0, 0.9, -0.66, booth)
1681
- box(0.04, 1.78, 1.28, glass, -0.76, 0.9, 0, booth)
1682
- box(0.04, 1.78, 1.28, glass, 0.76, 0.9, 0, booth)
1683
- box(1.58, 0.06, 0.06, metal, 0, 1.79, -0.66, booth)
1684
- box(0.06, 0.06, 1.32, metal, -0.76, 1.79, 0, booth)
1685
- box(0.06, 0.06, 1.32, metal, 0.76, 1.79, 0, booth)
1686
- box(0.74, 0.055, 0.44, wood, 0, 0.7, -0.2, booth)
1687
- box(0.56, 0.34, 0.04, dark, 0, 0.93, -0.4, booth)
1688
- const boothScreen = new THREE.Mesh(new THREE.PlaneGeometry(0.48, 0.25), cyan)
1689
- boothScreen.position.set(0, 0.93, -0.375)
1690
- booth.add(boothScreen)
1691
- box(0.42, 0.06, 0.38, dark, 0, 0.44, 0.2, booth)
1692
- box(0.42, 0.5, 0.06, dark, 0, 0.69, 0.4, booth)
1693
- const boothLamp = new THREE.Mesh(new THREE.SphereGeometry(0.05, 10, 10), warm)
1694
- boothLamp.position.set(0.5, 1.34, -0.32)
1695
- booth.add(boothLamp)
1696
- annex.add(booth)
2030
+ // 2) 备考隔间:同一排的学习桌 + 透明磨砂隐私隔板
2031
+ const studyDesk = addDesk(0, 0.85)
2032
+ // 两侧磨砂挡板,形成半包围学习位
2033
+ box(0.035, 1.15, 1.15, frosted, -0.72, 0.85, 0.12, studyDesk)
2034
+ box(0.035, 1.15, 1.15, frosted, 0.72, 0.85, 0.12, studyDesk)
2035
+
2036
+ // 备考倒计时闹钟
2037
+ const clockCanvas = document.createElement('canvas')
2038
+ clockCanvas.width = clockCanvas.height = 512
2039
+ const clockCtx = clockCanvas.getContext('2d')
2040
+ const clockTexture = new THREE.CanvasTexture(clockCanvas)
2041
+ clockTexture.colorSpace = THREE.SRGBColorSpace
2042
+ const clockSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: clockTexture, transparent: true, depthWrite: false, toneMapped: false }))
2043
+ clockSprite.position.set(0, 2.45, 0.85)
2044
+ clockSprite.scale.set(0.98, 0.98, 1)
2045
+ annex.add(clockSprite)
2046
+ studyClock = {
2047
+ sprite: clockSprite,
2048
+ ctx: clockCtx,
2049
+ texture: clockTexture,
2050
+ endAt: Date.now() + 25 * 60 * 1000,
2051
+ lastSecond: -1,
2052
+ alarmAt: 0,
2053
+ }
2054
+ drawStudyClock(25 * 60, false)
2055
+ box(0.035, 0.025, 1.15, metal, -0.72, 1.51, 0.12, studyDesk)
2056
+ box(0.035, 0.025, 1.15, metal, 0.72, 1.51, 0.12, studyDesk)
1697
2057
 
1698
2058
  // 3) 题库工位:科幻桌面 + 浮动 OCR / 数据库 / 试卷投影
1699
2059
  const sci = new THREE.Group()
@@ -1737,6 +2097,7 @@ function buildAnnex() {
1737
2097
  sciBase.position.set(0, 0.02, 0.55)
1738
2098
  sci.add(sciBase)
1739
2099
  annex.add(sci)
2100
+ addFrostedHeader(sci)
1740
2101
  const halo = new THREE.Mesh(
1741
2102
  new THREE.RingGeometry(0.4, 0.44, 48),
1742
2103
  new THREE.MeshBasicMaterial({ color: 0x6fdcff, transparent: true, opacity: 0.35, side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending }),
@@ -1767,8 +2128,10 @@ function buildAnnex() {
1767
2128
  const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false, toneMapped: false }))
1768
2129
  sprite.scale.set(scale, scale, 1)
1769
2130
  sprite.position.set(2.35 + x, y, 0.85 + z)
2131
+ sprite.material.opacity = 0
1770
2132
  annex.add(sprite)
1771
2133
  sprite.userData.baseY = y
2134
+ sprite.userData.baseScale = scale
1772
2135
  sprite.userData.phase = sciFiIcons.length * 0.9
1773
2136
  sciFiIcons.push(sprite)
1774
2137
  return sprite
@@ -1781,7 +2144,6 @@ function buildAnnex() {
1781
2144
  ctx.fillRect(42, 34, 22, 10)
1782
2145
  ctx.font = '700 15px ui-monospace, Menlo, monospace'
1783
2146
  ctx.textAlign = 'center'
1784
- ctx.fillText('OCR', 80, 128)
1785
2147
  })
1786
2148
  const dbTexture = iconCanvas(ctx => {
1787
2149
  for (let i = 0; i < 3; i++) {
@@ -1793,7 +2155,6 @@ function buildAnnex() {
1793
2155
  }
1794
2156
  ctx.font = '700 15px ui-monospace, Menlo, monospace'
1795
2157
  ctx.textAlign = 'center'
1796
- ctx.fillText('DB', 80, 140)
1797
2158
  })
1798
2159
  const paperTexture = iconCanvas(ctx => {
1799
2160
  ctx.beginPath(); ctx.roundRect(46, 28, 68, 92, 5); ctx.stroke()
@@ -1803,12 +2164,11 @@ function buildAnnex() {
1803
2164
  }
1804
2165
  ctx.font = '700 15px ui-monospace, Menlo, monospace'
1805
2166
  ctx.textAlign = 'center'
1806
- ctx.fillText('EXAM', 80, 142)
1807
2167
  })
1808
2168
 
1809
- iconSprite(ocrTexture, -0.5, 2.08, 0.12, 0.48)
1810
- iconSprite(dbTexture, 0, 2.2, 0.12, 0.48)
1811
- iconSprite(paperTexture, 0.5, 2.08, 0.12, 0.48)
2169
+ iconSprite(ocrTexture, 0, 2.12, 0.12, 0.95)
2170
+ iconSprite(dbTexture, 0, 2.12, 0.12, 0.95)
2171
+ iconSprite(paperTexture, 0, 2.12, 0.12, 0.95)
1812
2172
 
1813
2173
  // 讲台(教室最前面,背靠后墙,面向三张工位)
1814
2174
  const podium = new THREE.Group()
@@ -1833,6 +2193,46 @@ function buildAnnex() {
1833
2193
  box(3.3, 1.25, 0.04, dark, 0, 1.55, -2.86)
1834
2194
  box(3.1, 0.03, 0.05, cyan, 0, 2.18, -2.83)
1835
2195
 
2196
+ // 讲台 + 三席的猫咪图片
2197
+ const addAnnexCat = (x, y, z, tint, scale = 0.48, shadowY = null) => {
2198
+ const material = new THREE.SpriteMaterial({ map: faceTextures.happy, color: tint, transparent: true, depthWrite: false, toneMapped: false })
2199
+ const sprite = new THREE.Sprite(material)
2200
+ sprite.scale.set(scale, scale, 1)
2201
+ sprite.position.set(x, y, z)
2202
+ annex.add(sprite)
2203
+ annexCats.push({ sprite, baseY: y, phase: annexCats.length * 1.3 })
2204
+ if (shadowY !== null) {
2205
+ const shadow = new THREE.Mesh(new THREE.PlaneGeometry(scale * 1.5, scale * 1.15), new THREE.MeshBasicMaterial({ map: shadowTexture, transparent: true, opacity: 0.34, depthWrite: false }))
2206
+ shadow.rotation.x = -Math.PI / 2
2207
+ shadow.position.set(x, shadowY, z)
2208
+ annex.add(shadow)
2209
+ }
2210
+ }
2211
+ addAnnexCat(-2.35, 0.74, 1.28, 0x7fa8ff, 0.46, 0.56)
2212
+ addAnnexCat(0, 0.74, 1.28, 0xff6347, 0.46, 0.56)
2213
+ addAnnexCat(2.35, 0.74, 1.28, 0x5ee7ff, 0.46, 0.56)
2214
+ addAnnexCat(0, 1.5, -1.85, 0xffffff, 0.6, null)
2215
+
2216
+ // Annex 上方 Catora 招牌
2217
+ const annexSignCanvas = document.createElement('canvas')
2218
+ annexSignCanvas.width = 640
2219
+ annexSignCanvas.height = 160
2220
+ const actx = annexSignCanvas.getContext('2d')
2221
+ actx.clearRect(0, 0, 640, 160)
2222
+ actx.font = '900 76px Inter, "PingFang SC", system-ui, sans-serif'
2223
+ actx.textAlign = 'center'
2224
+ actx.textBaseline = 'middle'
2225
+ actx.shadowColor = 'rgba(90,190,255,.85)'
2226
+ actx.shadowBlur = 32
2227
+ actx.fillStyle = '#e9f6ff'
2228
+ actx.fillText('CATORA', 320, 80)
2229
+ const annexSignTexture = new THREE.CanvasTexture(annexSignCanvas)
2230
+ annexSignTexture.colorSpace = THREE.SRGBColorSpace
2231
+ const annexSign = new THREE.Sprite(new THREE.SpriteMaterial({ map: annexSignTexture, transparent: true, depthWrite: false, toneMapped: false }))
2232
+ annexSign.scale.set(2.8, 0.7, 1)
2233
+ annexSign.position.set(0, 3.35, -0.9)
2234
+ annex.add(annexSign)
2235
+
1836
2236
  // 室内灯
1837
2237
  const lightWarm = new THREE.PointLight(0xffd8b0, 2.6, 11, 2)
1838
2238
  lightWarm.position.set(0, 2.15, 0.4)
@@ -1843,20 +2243,6 @@ function buildAnnex() {
1843
2243
  annex.add(lightCool)
1844
2244
  annexLights.push({ light: lightCool, night: 1.8, day: 0.8 })
1845
2245
 
1846
- // 区域标签
1847
- const tag = (text, x, y, z) => {
1848
- const el = document.createElement('div')
1849
- el.className = 'officeTag'
1850
- el.textContent = text
1851
- const obj = new CSS2DObject(el)
1852
- obj.position.set(x, y, z)
1853
- annex.add(obj)
1854
- }
1855
- tag('讲台', 0, 1.9, -1.95)
1856
- tag('通用', -2.35, 1.45, 0.85)
1857
- tag('备考', 0, 2.0, 0.85)
1858
- tag('题库', 2.35, 1.27, 0.85)
1859
-
1860
2246
  // MCP 虚线 + 流光 + 暂未接通标牌
1861
2247
  mcpGroup = new THREE.Group()
1862
2248
  scene.add(mcpGroup)
@@ -1910,26 +2296,33 @@ function buildAnnex() {
1910
2296
  sctx.beginPath(); sctx.moveTo(cx - size, cy - size); sctx.lineTo(cx + size, cy + size); sctx.stroke()
1911
2297
  sctx.beginPath(); sctx.moveTo(cx + size, cy - size); sctx.lineTo(cx - size, cy + size); sctx.stroke()
1912
2298
  }
1913
- drawX(58, 70, 19)
1914
- drawX(454, 70, 19)
2299
+ drawX(74, 74, 12)
2300
+ drawX(438, 74, 12)
1915
2301
  sctx.fillStyle = '#d8efff'
1916
2302
  sctx.textAlign = 'center'
1917
- sctx.font = '700 58px ui-monospace, Menlo, monospace'
1918
- sctx.fillText('MCP', 256, 89)
1919
- sctx.fillStyle = 'rgba(120,190,255,.82)'
1920
- sctx.font = '500 22px ui-monospace, Menlo, monospace'
1921
- sctx.fillText('暂未接通', 256, 128)
2303
+ sctx.font = '700 30px ui-monospace, Menlo, monospace'
2304
+ sctx.fillText('MCP', 256, 86)
1922
2305
  const signTex = new THREE.CanvasTexture(signCanvas)
1923
2306
  signTex.colorSpace = THREE.SRGBColorSpace
1924
2307
  mcpSign = new THREE.Sprite(new THREE.SpriteMaterial({ map: signTex, transparent: true, depthWrite: false, toneMapped: false }))
1925
- mcpSign.position.set(5.6, 4.55, 1.18)
1926
- mcpSign.scale.set(3.35, 1.05, 1)
2308
+ mcpSign.position.set(5.6, 4.3, 1.18)
2309
+ mcpSign.scale.set(1.8, 0.45, 1)
2310
+ mcpSign.visible = false
1927
2311
  mcpGroup.add(mcpSign)
1928
2312
 
1929
2313
  const signGlow = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTexture, color: 0x3f9dff, transparent: true, opacity: 0.24, blending: THREE.AdditiveBlending, depthWrite: false }))
1930
2314
  signGlow.position.copy(mcpSign.position)
1931
- signGlow.scale.set(4.4, 1.8, 1)
2315
+ signGlow.scale.set(2.5, 0.8, 1)
2316
+ signGlow.visible = false
1932
2317
  mcpGroup.add(signGlow)
2318
+
2319
+ // 小号 MCP 标签:固定屏幕字号,和小号说明文字一致
2320
+ const mcpTagEl = document.createElement('div')
2321
+ mcpTagEl.className = 'mcpTag'
2322
+ mcpTagEl.innerHTML = '<span class="x">✕</span><span>MCP</span><span class="x">✕</span>'
2323
+ const mcpTag = new CSS2DObject(mcpTagEl)
2324
+ mcpTag.position.set(5.55, 3.62, 1.02)
2325
+ mcpGroup.add(mcpTag)
1933
2326
  }
1934
2327
 
1935
2328
  function rebuild(list) {
@@ -2067,6 +2460,7 @@ function tick() {
2067
2460
  const t = clock.getElapsedTime()
2068
2461
  requestAnimationFrame(tick)
2069
2462
  applyThemeFrame()
2463
+ updateWeatherFrame(t)
2070
2464
  if (mcpCurve && mcpBeads.length > 0) {
2071
2465
  const flow = clock.getElapsedTime()
2072
2466
  for (const bead of mcpBeads) {
@@ -2084,9 +2478,40 @@ function tick() {
2084
2478
  dash.mesh.position.copy(point)
2085
2479
  dash.mesh.quaternion.setFromUnitVectors(axis, tangent.normalize())
2086
2480
  }
2087
- if (mcpSign) mcpSign.position.y = 4.55 + Math.sin(flow * 0.9) * 0.04
2088
- for (const icon of sciFiIcons) {
2089
- icon.position.y = icon.userData.baseY + Math.sin(flow * 2.2 + icon.userData.phase) * 0.04
2481
+ if (mcpSign) mcpSign.position.y = 4.3 + Math.sin(flow * 0.9) * 0.025
2482
+ if (studyClock) {
2483
+ const now = Date.now()
2484
+ if (now >= studyClock.endAt) {
2485
+ if (!studyClock.alarmAt) studyClock.alarmAt = now
2486
+ if (now - studyClock.alarmAt > 8000) {
2487
+ studyClock.endAt = now + 25 * 60 * 1000
2488
+ studyClock.alarmAt = 0
2489
+ }
2490
+ }
2491
+ const remainMs = Math.max(0, studyClock.endAt - now)
2492
+ const remainSec = Math.ceil(remainMs / 1000)
2493
+ if (remainSec !== studyClock.lastSecond) {
2494
+ studyClock.lastSecond = remainSec
2495
+ drawStudyClock(remainSec, remainMs === 0)
2496
+ }
2497
+ studyClock.sprite.position.y = 2.45 + Math.sin(flow * 1.1) * 0.03
2498
+ }
2499
+
2500
+ for (const cat of annexCats) {
2501
+ cat.sprite.position.y = cat.baseY + Math.sin(flow * 1.7 + cat.phase) * 0.065
2502
+ }
2503
+
2504
+ // 三个图标轮流展示:一次只亮一个,淡入淡出
2505
+ const iconCycle = 2.4
2506
+ const activeIcon = Math.floor(flow / iconCycle) % Math.max(1, sciFiIcons.length)
2507
+ for (let i = 0; i < sciFiIcons.length; i++) {
2508
+ const icon = sciFiIcons[i]
2509
+ const on = i === activeIcon
2510
+ icon.material.opacity += ((on ? 1 : 0) - icon.material.opacity) * 0.14
2511
+ const targetScale = icon.userData.baseScale * (on ? 1.12 : 0.86)
2512
+ icon.scale.x += (targetScale - icon.scale.x) * 0.14
2513
+ icon.scale.y += (targetScale - icon.scale.y) * 0.14
2514
+ icon.position.y = icon.userData.baseY + Math.sin(flow * 2.2 + icon.userData.phase) * 0.04 + (on ? 0.04 : 0)
2090
2515
  }
2091
2516
  }
2092
2517
 
@@ -2295,6 +2720,81 @@ async function refreshBalance() {
2295
2720
  }
2296
2721
  }
2297
2722
 
2723
+ function finishIntro() {
2724
+ if (introTl) {
2725
+ introTl.kill()
2726
+ introTl = null
2727
+ }
2728
+ for (const floor of floors) {
2729
+ floor.group.scale.set(1, 1, 1)
2730
+ floor.group.position.y = floor.offset.y
2731
+ }
2732
+ if (annex) {
2733
+ annex.visible = true
2734
+ annex.scale.setScalar(0.84)
2735
+ annex.position.y = 0
2736
+ }
2737
+ if (mcpGroup) {
2738
+ mcpGroup.visible = true
2739
+ mcpGroup.scale.set(1, 1, 1)
2740
+ }
2741
+ const pose = globalPose(floors.length)
2742
+ controls.target.copy(pose.target)
2743
+ camera.position.copy(pose.position)
2744
+ controls.update()
2745
+ document.body.classList.remove('intro')
2746
+ const intro = $('#intro')
2747
+ if (intro) {
2748
+ intro.classList.add('done')
2749
+ setTimeout(() => intro.remove(), 650)
2750
+ }
2751
+ introPlayed = true
2752
+ }
2753
+
2754
+ function playIntro() {
2755
+ if (introPlayed) return
2756
+ const intro = $('#intro')
2757
+ if (!intro) return
2758
+ introPlayed = true
2759
+ if (reduceMotion) {
2760
+ finishIntro()
2761
+ return
2762
+ }
2763
+ if (annex) {
2764
+ annex.visible = true
2765
+ annex.scale.setScalar(0.02)
2766
+ annex.position.y = -2.4
2767
+ }
2768
+ if (mcpGroup) {
2769
+ mcpGroup.visible = true
2770
+ mcpGroup.scale.setScalar(0.01)
2771
+ }
2772
+ for (const floor of floors) {
2773
+ floor.group.scale.set(0.02, 0.02, 0.02)
2774
+ floor.group.position.y = floor.offset.y - 5.5
2775
+ }
2776
+ const pose = globalPose(floors.length)
2777
+ camera.position.set(pose.position.x + 9, pose.position.y + 11, pose.position.z + 9)
2778
+ controls.target.copy(pose.target)
2779
+ controls.update()
2780
+
2781
+ introTl = gsap.timeline()
2782
+ introTl.to({ value: 0 }, { value: 1, duration: 2.4, ease: 'none' }, 0)
2783
+ introTl.to(camera.position, { x: pose.position.x, y: pose.position.y, z: pose.position.z, duration: 2.2, ease: 'power2.inOut' }, 0.15)
2784
+ introTl.to(controls.target, { x: pose.target.x, y: pose.target.y, z: pose.target.z, duration: 2.2, ease: 'power2.inOut' }, 0.15)
2785
+ floors.forEach((floor, i) => {
2786
+ introTl.to(floor.group.scale, { x: 1, y: 1, z: 1, duration: 0.85, ease: 'back.out(1.35)' }, 0.25 + i * 0.3)
2787
+ introTl.to(floor.group.position, { y: floor.offset.y, duration: 0.85, ease: 'power2.out' }, 0.25 + i * 0.3)
2788
+ })
2789
+ if (annex) {
2790
+ introTl.to(annex.scale, { x: 0.84, y: 0.84, z: 0.84, duration: 0.95, ease: 'back.out(1.25)' }, 1.05)
2791
+ introTl.to(annex.position, { y: 0, duration: 0.95, ease: 'power2.out' }, 1.05)
2792
+ }
2793
+ if (mcpGroup) introTl.to(mcpGroup.scale, { x: 1, y: 1, z: 1, duration: 0.8, ease: 'power2.out' }, 1.45)
2794
+ introTl.call(() => document.body.classList.remove('intro'), null, 2.25)
2795
+ introTl.to(intro, { opacity: 0, duration: 0.5, onComplete: () => intro.remove() }, 2.3)
2796
+ }
2797
+
2298
2798
  async function poll() {
2299
2799
  try {
2300
2800
  const response = await fetch(API, { cache: 'no-store' })
@@ -2312,6 +2812,7 @@ async function poll() {
2312
2812
  if (sig !== structureSig) {
2313
2813
  structureSig = sig
2314
2814
  rebuild(list)
2815
+ if (!introPlayed) playIntro()
2315
2816
  } else {
2316
2817
  for (const agent of list) {
2317
2818
  const entry = agentsById.get(agent.id)
@@ -2356,6 +2857,7 @@ window.__NS = {
2356
2857
  get focusFloor() { return focusFloor },
2357
2858
  setFloorFocus,
2358
2859
  toggleFloorFocus,
2860
+ setWeather,
2359
2861
  }
2360
2862
  rebuild([])
2361
2863
  $('#offline').classList.add('show')
@@ -2363,9 +2865,11 @@ $('#offline').textContent = '正在读取本机会话…'
2363
2865
  buildAnnex()
2364
2866
  applyGlassSetting()
2365
2867
  loadCustomFace()
2868
+ refreshWeather()
2366
2869
  poll()
2367
2870
  setInterval(poll, 2000)
2368
2871
  refreshBalance()
2369
2872
  setInterval(refreshBalance, 60_000)
2370
2873
  tick()
2371
2874
  setTimeout(() => $('#hint').classList.add('hide'), 9000)
2875
+ setTimeout(() => { if (!introPlayed) finishIntro() }, 9000)