dsh-agent-office 4.2.0 → 4.6.0
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/README.md +8 -3
- package/lib/client.js +52 -8
- package/lib/panel.html +305 -241
- package/package.json +2 -1
- package/panel-src/assets/exp-slits.png +0 -0
- package/panel-src/index.html +118 -54
- package/panel-src/main.js +527 -20
- package/screenshot-day.png +0 -0
- package/screenshot-focus.png +0 -0
- package/screenshot.png +0 -0
package/panel-src/main.js
CHANGED
|
@@ -13,6 +13,7 @@ import faceDizzy from './assets/exp-dizzy.png'
|
|
|
13
13
|
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
|
+
import faceSlits from './assets/exp-slits.png'
|
|
16
17
|
|
|
17
18
|
const API = '/agent-office/api/state'
|
|
18
19
|
const FLOOR_GAP = 4.0
|
|
@@ -285,7 +286,7 @@ function applyThemeFrame() {
|
|
|
285
286
|
for (const material of cityMats) material.color.copy(NIGHT_THEME.city).lerp(DAY_THEME.city, mix)
|
|
286
287
|
const neonMaterial = neonSign?.material
|
|
287
288
|
if (neonMaterial) neonMaterial.opacity = NIGHT_THEME.neon + (DAY_THEME.neon - NIGHT_THEME.neon) * mix
|
|
288
|
-
for (const item of themeLights) item.light.intensity = item.night + (item.day - item.night) * mix
|
|
289
|
+
for (const item of [...themeLights, ...annexLights]) item.light.intensity = item.night + (item.day - item.night) * mix
|
|
289
290
|
}
|
|
290
291
|
|
|
291
292
|
function syncThemeByClock(immediate = false) {
|
|
@@ -350,6 +351,7 @@ const FACE_SRC = {
|
|
|
350
351
|
surprised: faceSurprised,
|
|
351
352
|
thinking: faceThinking,
|
|
352
353
|
loved: faceLoved,
|
|
354
|
+
slits: faceSlits,
|
|
353
355
|
}
|
|
354
356
|
const faceTextures = {}
|
|
355
357
|
const faceLoader = new THREE.TextureLoader()
|
|
@@ -359,13 +361,15 @@ for (const [name, src] of Object.entries(FACE_SRC)) {
|
|
|
359
361
|
faceTextures[name] = texture
|
|
360
362
|
}
|
|
361
363
|
const CUSTOM_FACE_KEY = 'agent-office-custom-face'
|
|
364
|
+
const GLASS_KEY = 'agent-office-ui-opacity'
|
|
362
365
|
let customFaceTexture = null
|
|
366
|
+
let customFaceData = ''
|
|
363
367
|
const FACE_FOR_STATUS = {
|
|
364
368
|
working: 'happy',
|
|
365
369
|
need_me: 'surprised',
|
|
366
370
|
stuck: 'dizzy',
|
|
367
371
|
done: 'loved',
|
|
368
|
-
resting: '
|
|
372
|
+
resting: 'slits',
|
|
369
373
|
}
|
|
370
374
|
const zTexture = (() => {
|
|
371
375
|
const canvas = document.createElement('canvas')
|
|
@@ -384,6 +388,63 @@ const zTexture = (() => {
|
|
|
384
388
|
return texture
|
|
385
389
|
})()
|
|
386
390
|
|
|
391
|
+
|
|
392
|
+
const sweatTexture = (() => {
|
|
393
|
+
const canvas = document.createElement('canvas')
|
|
394
|
+
canvas.width = 64
|
|
395
|
+
canvas.height = 96
|
|
396
|
+
const ctx = canvas.getContext('2d')
|
|
397
|
+
ctx.clearRect(0, 0, 64, 96)
|
|
398
|
+
ctx.fillStyle = '#bfeaff'
|
|
399
|
+
ctx.beginPath()
|
|
400
|
+
ctx.moveTo(32, 8)
|
|
401
|
+
ctx.bezierCurveTo(46, 30, 55, 48, 55, 62)
|
|
402
|
+
ctx.bezierCurveTo(55, 79, 45, 89, 32, 89)
|
|
403
|
+
ctx.bezierCurveTo(19, 89, 9, 79, 9, 62)
|
|
404
|
+
ctx.bezierCurveTo(9, 48, 18, 30, 32, 8)
|
|
405
|
+
ctx.fill()
|
|
406
|
+
ctx.fillStyle = 'rgba(255,255,255,.85)'
|
|
407
|
+
ctx.beginPath()
|
|
408
|
+
ctx.ellipse(23, 58, 5, 9, -0.35, 0, Math.PI * 2)
|
|
409
|
+
ctx.fill()
|
|
410
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
411
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
412
|
+
return texture
|
|
413
|
+
})()
|
|
414
|
+
|
|
415
|
+
const scribbleTexture = (() => {
|
|
416
|
+
const canvas = document.createElement('canvas')
|
|
417
|
+
canvas.width = 256
|
|
418
|
+
canvas.height = 160
|
|
419
|
+
const ctx = canvas.getContext('2d')
|
|
420
|
+
ctx.clearRect(0, 0, 256, 160)
|
|
421
|
+
const strokes = [
|
|
422
|
+
[[34, 92], [64, 34], [92, 112], [124, 40], [154, 108], [186, 48], [224, 96]],
|
|
423
|
+
[[48, 54], [92, 78], [60, 126], [126, 92], [172, 124], [208, 66]],
|
|
424
|
+
[[78, 24], [104, 66], [144, 26], [168, 74], [208, 32]],
|
|
425
|
+
]
|
|
426
|
+
ctx.lineCap = 'round'
|
|
427
|
+
ctx.lineJoin = 'round'
|
|
428
|
+
for (const pass of [{ color: 'rgba(255,255,255,.18)', width: 13 }, { color: '#0b0d10', width: 8 }]) {
|
|
429
|
+
ctx.strokeStyle = pass.color
|
|
430
|
+
ctx.lineWidth = pass.width
|
|
431
|
+
for (const points of strokes) {
|
|
432
|
+
ctx.beginPath()
|
|
433
|
+
ctx.moveTo(points[0][0], points[0][1])
|
|
434
|
+
for (let i = 1; i < points.length - 1; i += 2) {
|
|
435
|
+
const [cx, cy] = points[i]
|
|
436
|
+
const [x, y] = points[i + 1]
|
|
437
|
+
ctx.quadraticCurveTo(cx, cy, x, y)
|
|
438
|
+
}
|
|
439
|
+
ctx.stroke()
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
443
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
444
|
+
return texture
|
|
445
|
+
})()
|
|
446
|
+
const OVERLOAD_COLOR = new THREE.Color(0xff5a5a)
|
|
447
|
+
|
|
387
448
|
const floorTexture = (() => {
|
|
388
449
|
const canvas = document.createElement('canvas')
|
|
389
450
|
canvas.width = canvas.height = 256
|
|
@@ -414,7 +475,7 @@ function disposeGroup(group) {
|
|
|
414
475
|
if (object.material) {
|
|
415
476
|
const materials = Array.isArray(object.material) ? object.material : [object.material]
|
|
416
477
|
for (const material of materials) {
|
|
417
|
-
if (material.map && material.map !== glowTexture && material.map !== shadowTexture && material.map !== floorTexture) material.map.dispose()
|
|
478
|
+
if (material.map && material.map !== glowTexture && material.map !== shadowTexture && material.map !== floorTexture && material.map !== sweatTexture && material.map !== scribbleTexture) material.map.dispose()
|
|
418
479
|
material.dispose()
|
|
419
480
|
}
|
|
420
481
|
}
|
|
@@ -456,6 +517,25 @@ function robotFor(agent, index = 0, sitY = 0) {
|
|
|
456
517
|
face.position.y = baseY
|
|
457
518
|
group.add(face)
|
|
458
519
|
|
|
520
|
+
// 正常工作:大汗淋漓;长时间工作:红温 + 头顶乱线
|
|
521
|
+
const sweat = []
|
|
522
|
+
for (let i = 0; i < 4; i++) {
|
|
523
|
+
const mat = new THREE.SpriteMaterial({ map: sweatTexture, transparent: true, opacity: 0, depthWrite: false, toneMapped: false, color: 0xbfeaff })
|
|
524
|
+
const drop = new THREE.Sprite(mat)
|
|
525
|
+
drop.scale.set(0.095, 0.145, 1)
|
|
526
|
+
drop.visible = false
|
|
527
|
+
group.add(drop)
|
|
528
|
+
sweat.push({ sprite: drop, phase: i / 4, x: i % 2 === 0 ? -0.27 : 0.27, drift: i < 2 ? -0.04 : 0.04 })
|
|
529
|
+
}
|
|
530
|
+
const scribbleGlow = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTexture, color: 0xff4a4a, transparent: true, opacity: 0, depthWrite: false, blending: THREE.AdditiveBlending }))
|
|
531
|
+
scribbleGlow.scale.set(1.0, 0.62, 1)
|
|
532
|
+
scribbleGlow.position.set(0, baseY + 1.55, 0.03)
|
|
533
|
+
group.add(scribbleGlow)
|
|
534
|
+
const scribble = new THREE.Sprite(new THREE.SpriteMaterial({ map: scribbleTexture, transparent: true, opacity: 0, depthWrite: false, toneMapped: false }))
|
|
535
|
+
scribble.scale.set(0.84, 0.5, 1)
|
|
536
|
+
scribble.position.set(0, baseY + 1.55, 0.04)
|
|
537
|
+
group.add(scribble)
|
|
538
|
+
|
|
459
539
|
// 猫咪上下浮动的 GSAP 循环(用统一相位对象驱动,tick 里再乘幅度)
|
|
460
540
|
const motion = { bob: 0.5 }
|
|
461
541
|
if (!reduceMotion) {
|
|
@@ -567,7 +647,7 @@ function robotFor(agent, index = 0, sitY = 0) {
|
|
|
567
647
|
group.userData = {
|
|
568
648
|
agentId: agent.id,
|
|
569
649
|
index,
|
|
570
|
-
parts: { face, faceMat, ring, glow, badge, label, zzz, ripples, motion, seatShadow, shadow, tint, baseY },
|
|
650
|
+
parts: { face, faceMat, ring, glow, badge, label, zzz, ripples, motion, seatShadow, shadow, tint, baseY, sweat, scribble, scribbleGlow },
|
|
571
651
|
}
|
|
572
652
|
return group
|
|
573
653
|
}
|
|
@@ -1099,7 +1179,7 @@ function buildFloor(team, index, teamAgents) {
|
|
|
1099
1179
|
|
|
1100
1180
|
// 彩色货架(参考原版的彩色格子)
|
|
1101
1181
|
const shelf = new THREE.Group()
|
|
1102
|
-
shelf.position.set(-W / 2 + 0.
|
|
1182
|
+
shelf.position.set(-W / 2 + 0.38, 0, -0.4)
|
|
1103
1183
|
shelf.rotation.y = Math.PI / 2
|
|
1104
1184
|
const shelfMat = new THREE.MeshStandardMaterial({ color: 0x1b2430, roughness: 0.7, metalness: 0.3 })
|
|
1105
1185
|
for (const y of [0.5, 1.15, 1.8]) {
|
|
@@ -1215,6 +1295,10 @@ function applyCustomFaceToAll() {
|
|
|
1215
1295
|
parts.faceMat.color.set(customFaceTexture ? 0xffffff : parts.tint ?? 0xffffff)
|
|
1216
1296
|
parts.faceMat.needsUpdate = true
|
|
1217
1297
|
}
|
|
1298
|
+
if (selectedId !== null) {
|
|
1299
|
+
const entry = agentsById.get(selectedId)
|
|
1300
|
+
if (entry) renderCard(entry.agent)
|
|
1301
|
+
}
|
|
1218
1302
|
}
|
|
1219
1303
|
|
|
1220
1304
|
function loadCustomFace() {
|
|
@@ -1222,9 +1306,11 @@ function loadCustomFace() {
|
|
|
1222
1306
|
try { data = localStorage.getItem(CUSTOM_FACE_KEY) ?? '' } catch {}
|
|
1223
1307
|
if (!data) {
|
|
1224
1308
|
customFaceTexture = null
|
|
1309
|
+
customFaceData = ''
|
|
1225
1310
|
applyCustomFaceToAll()
|
|
1226
1311
|
return
|
|
1227
1312
|
}
|
|
1313
|
+
customFaceData = data
|
|
1228
1314
|
faceLoader.load(data, texture => {
|
|
1229
1315
|
texture.colorSpace = THREE.SRGBColorSpace
|
|
1230
1316
|
customFaceTexture = texture
|
|
@@ -1232,8 +1318,20 @@ function loadCustomFace() {
|
|
|
1232
1318
|
})
|
|
1233
1319
|
}
|
|
1234
1320
|
|
|
1321
|
+
function applyGlassSetting() {
|
|
1322
|
+
let raw = ''
|
|
1323
|
+
try { raw = localStorage.getItem(GLASS_KEY) ?? '' } catch {}
|
|
1324
|
+
const value = Number.parseFloat(raw)
|
|
1325
|
+
if (Number.isFinite(value)) {
|
|
1326
|
+
document.documentElement.style.setProperty('--glass-alpha', String(Math.min(0.98, Math.max(0.5, value / 100))))
|
|
1327
|
+
} else {
|
|
1328
|
+
document.documentElement.style.removeProperty('--glass-alpha')
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1235
1332
|
window.addEventListener('storage', event => {
|
|
1236
1333
|
if (event.key === CUSTOM_FACE_KEY) loadCustomFace()
|
|
1334
|
+
if (event.key === GLASS_KEY) applyGlassSetting()
|
|
1237
1335
|
})
|
|
1238
1336
|
let usage = { tokens_week: 0, tokens_today: 0 }
|
|
1239
1337
|
let selectedId = null
|
|
@@ -1258,8 +1356,8 @@ function globalPose(floorsCount) {
|
|
|
1258
1356
|
const height = Math.max(0, (floorsCount - 1) * FLOOR_GAP)
|
|
1259
1357
|
const centerX = ((floorsCount - 1) * OFFSET_X) / 2
|
|
1260
1358
|
const centerZ = ((floorsCount - 1) * OFFSET_Z) / 2
|
|
1261
|
-
const dist = (
|
|
1262
|
-
const target = new THREE.Vector3(centerX, height / 2 + 1.
|
|
1359
|
+
const dist = (19 + floorsCount * 3) * viewScale()
|
|
1360
|
+
const target = new THREE.Vector3(centerX + 3.6, height / 2 + 1.0, centerZ + 0.8)
|
|
1263
1361
|
return {
|
|
1264
1362
|
target,
|
|
1265
1363
|
position: new THREE.Vector3(centerX + dist * 0.42, target.y + dist * 0.82, centerZ + dist * 0.9),
|
|
@@ -1302,6 +1400,8 @@ function setFloorFocus(index) {
|
|
|
1302
1400
|
for (const child of floor.children) child.robot.userData.parts.label.visible = visible
|
|
1303
1401
|
}
|
|
1304
1402
|
neonSign.visible = !focused
|
|
1403
|
+
if (annex) annex.visible = !focused
|
|
1404
|
+
if (mcpGroup) mcpGroup.visible = !focused
|
|
1305
1405
|
const chip = $('#focusChip')
|
|
1306
1406
|
if (focused) {
|
|
1307
1407
|
const floor = floors.find(item => item.index === index)
|
|
@@ -1409,7 +1509,7 @@ function renderChecks() {
|
|
|
1409
1509
|
]
|
|
1410
1510
|
const done = checks.filter(item => item.ok).length
|
|
1411
1511
|
$('#checkScore').textContent = `${done}/${checks.length}`
|
|
1412
|
-
$('#checkBody').innerHTML = checks.map(item => `<div class="check ${item.ok ? 'ok' : ''}"><i
|
|
1512
|
+
$('#checkBody').innerHTML = checks.map(item => `<div class="check ${item.ok ? 'ok' : ''}"><i><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 8.5l3 3 6-6"/></svg></i><span>${item.text}</span></div>`).join('')
|
|
1413
1513
|
}
|
|
1414
1514
|
|
|
1415
1515
|
function renderMeter() {
|
|
@@ -1432,31 +1532,36 @@ function renderFeed() {
|
|
|
1432
1532
|
}).join('')
|
|
1433
1533
|
}
|
|
1434
1534
|
|
|
1535
|
+
let cardHideTimer = null
|
|
1536
|
+
|
|
1435
1537
|
function renderCard(agent) {
|
|
1436
1538
|
const card = $('#card')
|
|
1437
1539
|
if (agent === null) {
|
|
1438
|
-
card.classList.
|
|
1540
|
+
if (!card.classList.contains('show') || card.classList.contains('is-out')) return
|
|
1541
|
+
card.classList.add('is-out')
|
|
1542
|
+
clearTimeout(cardHideTimer)
|
|
1543
|
+
cardHideTimer = setTimeout(() => card.classList.remove('show', 'is-out'), 160)
|
|
1439
1544
|
return
|
|
1440
1545
|
}
|
|
1546
|
+
clearTimeout(cardHideTimer)
|
|
1547
|
+
card.classList.remove('is-out')
|
|
1441
1548
|
card.classList.add('show')
|
|
1442
1549
|
const status = STATUS[agent.status] ?? STATUS.resting
|
|
1443
1550
|
const floor = floors.find(item => item.team === agent.team)
|
|
1444
1551
|
$('#cardTeam').textContent = floor === undefined ? agent.team : `楼层 ${floor.index + 1} · ${agent.team}`
|
|
1445
1552
|
const glyphEl = $('#cardGlyph')
|
|
1446
1553
|
glyphEl.textContent = ''
|
|
1447
|
-
glyphEl.style.backgroundImage = `url(${faceDefault})`
|
|
1554
|
+
glyphEl.style.backgroundImage = `url(${customFaceData || faceDefault})`
|
|
1448
1555
|
glyphEl.style.backgroundSize = '82% 82%'
|
|
1449
1556
|
glyphEl.style.backgroundRepeat = 'no-repeat'
|
|
1450
1557
|
glyphEl.style.backgroundPosition = 'center'
|
|
1451
|
-
glyphEl.style.backgroundColor = `${agent.color}22`
|
|
1452
|
-
glyphEl.style.border = `1px solid ${agent.color}66`
|
|
1558
|
+
glyphEl.style.backgroundColor = customFaceData ? 'transparent' : `${agent.color}22`
|
|
1559
|
+
glyphEl.style.border = customFaceData ? '1px solid var(--line-2)' : `1px solid ${agent.color}66`
|
|
1453
1560
|
$('#cardName').textContent = agent.name
|
|
1454
1561
|
$('#cardSub').textContent = agent.schedule
|
|
1455
1562
|
const statusEl = $('#cardStatus')
|
|
1456
1563
|
statusEl.textContent = `● ${status.label}`
|
|
1457
|
-
statusEl.
|
|
1458
|
-
statusEl.style.borderColor = status.color + '66'
|
|
1459
|
-
statusEl.style.background = status.color + '18'
|
|
1564
|
+
statusEl.dataset.status = agent.status
|
|
1460
1565
|
$('#cardRuns').textContent = String(agent.runs_today ?? 0)
|
|
1461
1566
|
$('#cardShare').textContent = `${Math.round(agent.usage_pct || 0)}%`
|
|
1462
1567
|
$('#cardTokens').textContent = fmt(agent.tokens_today || 0)
|
|
@@ -1482,6 +1587,351 @@ function teamList(list) {
|
|
|
1482
1587
|
return teams.slice(0, 4)
|
|
1483
1588
|
}
|
|
1484
1589
|
|
|
1590
|
+
|
|
1591
|
+
/* ------------------------------------------------------------------ *
|
|
1592
|
+
* 右侧 Annex:教室布局(讲台 + 通用/备考/题库三席)
|
|
1593
|
+
* ------------------------------------------------------------------ */
|
|
1594
|
+
const ANNEX_X = 9.6
|
|
1595
|
+
const ANNEX_Z = 0.3
|
|
1596
|
+
const ANNEX_W = 7.2
|
|
1597
|
+
const ANNEX_D = 6.0
|
|
1598
|
+
let annex = null
|
|
1599
|
+
let annexLights = []
|
|
1600
|
+
let mcpCurve = null
|
|
1601
|
+
let mcpBeads = []
|
|
1602
|
+
let mcpDashes = []
|
|
1603
|
+
let mcpGroup = null
|
|
1604
|
+
let mcpSign = null
|
|
1605
|
+
let sciFiIcons = []
|
|
1606
|
+
|
|
1607
|
+
function buildAnnex() {
|
|
1608
|
+
annex = new THREE.Group()
|
|
1609
|
+
annex.position.set(ANNEX_X, 0, ANNEX_Z)
|
|
1610
|
+
annex.scale.setScalar(0.84)
|
|
1611
|
+
scene.add(annex)
|
|
1612
|
+
|
|
1613
|
+
const wood = new THREE.MeshStandardMaterial({ color: 0x2a2119, roughness: 0.72, metalness: 0.2 })
|
|
1614
|
+
const dark = new THREE.MeshStandardMaterial({ color: 0x17212b, roughness: 0.5, metalness: 0.5 })
|
|
1615
|
+
const metal = new THREE.MeshStandardMaterial({ color: 0x334654, roughness: 0.35, metalness: 0.75 })
|
|
1616
|
+
const glass = new THREE.MeshStandardMaterial({ color: 0x8fc8ff, transparent: true, opacity: 0.06, roughness: 0.08, metalness: 0.1, depthWrite: false, side: THREE.DoubleSide })
|
|
1617
|
+
const cyan = new THREE.MeshBasicMaterial({ color: 0x7fd7ff, toneMapped: false })
|
|
1618
|
+
const warm = new THREE.MeshBasicMaterial({ color: 0xffcf9a, toneMapped: false })
|
|
1619
|
+
const box = (w, h, d, mat, x, y, z, parent = annex) => {
|
|
1620
|
+
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat)
|
|
1621
|
+
mesh.position.set(x, y, z)
|
|
1622
|
+
parent.add(mesh)
|
|
1623
|
+
return mesh
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// 地板 / 边线
|
|
1627
|
+
box(ANNEX_W, 0.3, ANNEX_D, wood, 0, -0.15, 0)
|
|
1628
|
+
const top = new THREE.Mesh(new THREE.PlaneGeometry(ANNEX_W - 0.18, ANNEX_D - 0.18), new THREE.MeshStandardMaterial({ map: floorTexture, roughness: 0.8, metalness: 0.25 }))
|
|
1629
|
+
top.rotation.x = -Math.PI / 2
|
|
1630
|
+
top.position.y = 0.012
|
|
1631
|
+
annex.add(top)
|
|
1632
|
+
const edge = new THREE.LineSegments(
|
|
1633
|
+
new THREE.EdgesGeometry(new THREE.BoxGeometry(ANNEX_W + 0.04, 0.34, ANNEX_D + 0.04)),
|
|
1634
|
+
new THREE.LineBasicMaterial({ color: 0x8fd4ff, transparent: true, opacity: 0.75 }),
|
|
1635
|
+
)
|
|
1636
|
+
annex.add(edge)
|
|
1637
|
+
|
|
1638
|
+
// 玻璃外墙(正面留空)
|
|
1639
|
+
box(ANNEX_W, 2.35, 0.05, glass, 0, 1.18, -ANNEX_D / 2 + 0.08)
|
|
1640
|
+
box(0.05, 2.35, ANNEX_D, glass, -ANNEX_W / 2 + 0.08, 1.18, 0)
|
|
1641
|
+
box(0.05, 2.35, ANNEX_D, glass, ANNEX_W / 2 - 0.08, 1.18, 0)
|
|
1642
|
+
for (const [x, z] of [[-ANNEX_W / 2 + 0.08, -ANNEX_D / 2 + 0.08], [ANNEX_W / 2 - 0.08, -ANNEX_D / 2 + 0.08], [-ANNEX_W / 2 + 0.08, ANNEX_D / 2 - 0.08], [ANNEX_W / 2 - 0.08, ANNEX_D / 2 - 0.08]]) {
|
|
1643
|
+
box(0.09, 2.45, 0.09, dark, x, 1.22, z)
|
|
1644
|
+
}
|
|
1645
|
+
box(ANNEX_W, 0.06, 0.06, metal, 0, 2.38, -ANNEX_D / 2 + 0.08)
|
|
1646
|
+
box(0.06, 0.06, ANNEX_D, metal, -ANNEX_W / 2 + 0.08, 2.38, 0)
|
|
1647
|
+
box(0.06, 0.06, ANNEX_D, metal, ANNEX_W / 2 - 0.08, 2.38, 0)
|
|
1648
|
+
box(ANNEX_W - 1.0, 0.025, 0.02, cyan, 0, 2.31, -ANNEX_D / 2 + 0.16)
|
|
1649
|
+
|
|
1650
|
+
// 教室布局:讲台在后墙,三张工位面朝讲台
|
|
1651
|
+
// 1) 通用工位
|
|
1652
|
+
function addDesk(x, z, matTop = wood) {
|
|
1653
|
+
const g = new THREE.Group()
|
|
1654
|
+
g.position.set(x, 0, z)
|
|
1655
|
+
box(1.16, 0.055, 0.62, matTop, 0, 0.72, 0, g)
|
|
1656
|
+
for (const [lx, lz] of [[-0.5, -0.24], [0.5, -0.24], [-0.5, 0.24], [0.5, 0.24]]) {
|
|
1657
|
+
box(0.055, 0.7, 0.055, dark, lx, 0.36, lz, g)
|
|
1658
|
+
}
|
|
1659
|
+
box(0.62, 0.4, 0.045, dark, 0, 0.95, -0.18, g)
|
|
1660
|
+
const screen = new THREE.Mesh(new THREE.PlaneGeometry(0.54, 0.3), cyan)
|
|
1661
|
+
screen.position.set(0, 0.95, -0.155)
|
|
1662
|
+
g.add(screen)
|
|
1663
|
+
box(0.5, 0.055, 0.06, warm, 0, 0.76, 0.3, g)
|
|
1664
|
+
// 椅子(面向讲台 -z)
|
|
1665
|
+
box(0.42, 0.06, 0.4, dark, 0, 0.45, 0.55, g)
|
|
1666
|
+
box(0.42, 0.5, 0.06, dark, 0, 0.7, 0.75, g)
|
|
1667
|
+
const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.42, 8), metal)
|
|
1668
|
+
stem.position.set(0, 0.22, 0.55)
|
|
1669
|
+
g.add(stem)
|
|
1670
|
+
const base = new THREE.Mesh(new THREE.CylinderGeometry(0.23, 0.23, 0.03, 16), dark)
|
|
1671
|
+
base.position.set(0, 0.02, 0.55)
|
|
1672
|
+
g.add(base)
|
|
1673
|
+
annex.add(g)
|
|
1674
|
+
}
|
|
1675
|
+
addDesk(-2.35, 0.85)
|
|
1676
|
+
|
|
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)
|
|
1697
|
+
|
|
1698
|
+
// 3) 题库工位:科幻桌面 + 浮动 OCR / 数据库 / 试卷投影
|
|
1699
|
+
const sci = new THREE.Group()
|
|
1700
|
+
sci.position.set(2.35, 0, 0.85)
|
|
1701
|
+
box(1.16, 0.07, 0.62, metal, 0, 0.72, 0, sci)
|
|
1702
|
+
for (const [lx, lz] of [[-0.5, -0.24], [0.5, -0.24], [-0.5, 0.24], [0.5, 0.24]]) {
|
|
1703
|
+
box(0.05, 0.7, 0.05, dark, lx, 0.36, lz, sci)
|
|
1704
|
+
}
|
|
1705
|
+
// 全息桌屏
|
|
1706
|
+
const holoCanvas = document.createElement('canvas')
|
|
1707
|
+
holoCanvas.width = 256
|
|
1708
|
+
holoCanvas.height = 128
|
|
1709
|
+
const hctx = holoCanvas.getContext('2d')
|
|
1710
|
+
hctx.clearRect(0, 0, 256, 128)
|
|
1711
|
+
hctx.strokeStyle = 'rgba(110,220,255,.7)'
|
|
1712
|
+
hctx.lineWidth = 2
|
|
1713
|
+
for (let x = 0; x <= 256; x += 24) {
|
|
1714
|
+
hctx.beginPath(); hctx.moveTo(x, 0); hctx.lineTo(x, 128); hctx.stroke()
|
|
1715
|
+
}
|
|
1716
|
+
for (let y = 0; y <= 128; y += 24) {
|
|
1717
|
+
hctx.beginPath(); hctx.moveTo(0, y); hctx.lineTo(256, y); hctx.stroke()
|
|
1718
|
+
}
|
|
1719
|
+
hctx.fillStyle = 'rgba(120,230,255,.16)'
|
|
1720
|
+
hctx.fillRect(16, 22, 224, 20)
|
|
1721
|
+
hctx.fillRect(16, 62, 150, 10)
|
|
1722
|
+
hctx.fillRect(16, 86, 190, 10)
|
|
1723
|
+
const holoTexture = new THREE.CanvasTexture(holoCanvas)
|
|
1724
|
+
holoTexture.colorSpace = THREE.SRGBColorSpace
|
|
1725
|
+
const holo = new THREE.Mesh(new THREE.PlaneGeometry(0.86, 0.42), new THREE.MeshBasicMaterial({ map: holoTexture, transparent: true, opacity: 0.55, depthWrite: false, toneMapped: false, side: THREE.DoubleSide }))
|
|
1726
|
+
holo.position.set(0, 1.08, -0.08)
|
|
1727
|
+
holo.rotation.x = -0.18
|
|
1728
|
+
sci.add(holo)
|
|
1729
|
+
box(0.5, 0.055, 0.06, cyan, 0, 0.76, 0.3, sci)
|
|
1730
|
+
// 椅子
|
|
1731
|
+
box(0.42, 0.06, 0.4, dark, 0, 0.45, 0.55, sci)
|
|
1732
|
+
box(0.42, 0.5, 0.06, dark, 0, 0.7, 0.75, sci)
|
|
1733
|
+
const sciStem = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.42, 8), metal)
|
|
1734
|
+
sciStem.position.set(0, 0.22, 0.55)
|
|
1735
|
+
sci.add(sciStem)
|
|
1736
|
+
const sciBase = new THREE.Mesh(new THREE.CylinderGeometry(0.23, 0.23, 0.03, 16), dark)
|
|
1737
|
+
sciBase.position.set(0, 0.02, 0.55)
|
|
1738
|
+
sci.add(sciBase)
|
|
1739
|
+
annex.add(sci)
|
|
1740
|
+
const halo = new THREE.Mesh(
|
|
1741
|
+
new THREE.RingGeometry(0.4, 0.44, 48),
|
|
1742
|
+
new THREE.MeshBasicMaterial({ color: 0x6fdcff, transparent: true, opacity: 0.35, side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending }),
|
|
1743
|
+
)
|
|
1744
|
+
halo.rotation.x = -Math.PI / 2
|
|
1745
|
+
halo.position.set(2.35, 1.58, 0.98)
|
|
1746
|
+
annex.add(halo)
|
|
1747
|
+
|
|
1748
|
+
// 浮动图标:OCR 相机 / 数据库 / 试卷(billboard,远看也清楚)
|
|
1749
|
+
const iconCanvas = (draw) => {
|
|
1750
|
+
const canvas = document.createElement('canvas')
|
|
1751
|
+
canvas.width = canvas.height = 160
|
|
1752
|
+
const ctx = canvas.getContext('2d')
|
|
1753
|
+
ctx.clearRect(0, 0, 160, 160)
|
|
1754
|
+
ctx.strokeStyle = '#9fe7ff'
|
|
1755
|
+
ctx.fillStyle = '#9fe7ff'
|
|
1756
|
+
ctx.lineWidth = 7
|
|
1757
|
+
ctx.lineCap = 'round'
|
|
1758
|
+
ctx.lineJoin = 'round'
|
|
1759
|
+
ctx.shadowColor = 'rgba(80,200,255,.9)'
|
|
1760
|
+
ctx.shadowBlur = 10
|
|
1761
|
+
draw(ctx)
|
|
1762
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
1763
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
1764
|
+
return texture
|
|
1765
|
+
}
|
|
1766
|
+
const iconSprite = (texture, x, y, z, scale = 0.4) => {
|
|
1767
|
+
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false, toneMapped: false }))
|
|
1768
|
+
sprite.scale.set(scale, scale, 1)
|
|
1769
|
+
sprite.position.set(2.35 + x, y, 0.85 + z)
|
|
1770
|
+
annex.add(sprite)
|
|
1771
|
+
sprite.userData.baseY = y
|
|
1772
|
+
sprite.userData.phase = sciFiIcons.length * 0.9
|
|
1773
|
+
sciFiIcons.push(sprite)
|
|
1774
|
+
return sprite
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
const ocrTexture = iconCanvas(ctx => {
|
|
1778
|
+
ctx.beginPath(); ctx.roundRect(32, 44, 78, 58, 10); ctx.stroke()
|
|
1779
|
+
ctx.beginPath(); ctx.arc(73, 73, 17, 0, Math.PI * 2); ctx.stroke()
|
|
1780
|
+
ctx.beginPath(); ctx.arc(73, 73, 6, 0, Math.PI * 2); ctx.fill()
|
|
1781
|
+
ctx.fillRect(42, 34, 22, 10)
|
|
1782
|
+
ctx.font = '700 15px ui-monospace, Menlo, monospace'
|
|
1783
|
+
ctx.textAlign = 'center'
|
|
1784
|
+
ctx.fillText('OCR', 80, 128)
|
|
1785
|
+
})
|
|
1786
|
+
const dbTexture = iconCanvas(ctx => {
|
|
1787
|
+
for (let i = 0; i < 3; i++) {
|
|
1788
|
+
const y = 48 + i * 24
|
|
1789
|
+
ctx.beginPath(); ctx.ellipse(80, y, 28, 9, 0, 0, Math.PI * 2); ctx.stroke()
|
|
1790
|
+
ctx.beginPath(); ctx.moveTo(52, y); ctx.lineTo(52, y + 22); ctx.stroke()
|
|
1791
|
+
ctx.beginPath(); ctx.moveTo(108, y); ctx.lineTo(108, y + 22); ctx.stroke()
|
|
1792
|
+
ctx.beginPath(); ctx.ellipse(80, y + 22, 28, 9, 0, 0, Math.PI * 2); ctx.stroke()
|
|
1793
|
+
}
|
|
1794
|
+
ctx.font = '700 15px ui-monospace, Menlo, monospace'
|
|
1795
|
+
ctx.textAlign = 'center'
|
|
1796
|
+
ctx.fillText('DB', 80, 140)
|
|
1797
|
+
})
|
|
1798
|
+
const paperTexture = iconCanvas(ctx => {
|
|
1799
|
+
ctx.beginPath(); ctx.roundRect(46, 28, 68, 92, 5); ctx.stroke()
|
|
1800
|
+
ctx.beginPath(); ctx.moveTo(98, 28); ctx.lineTo(114, 44); ctx.lineTo(98, 44); ctx.closePath(); ctx.stroke()
|
|
1801
|
+
for (let i = 0; i < 5; i++) {
|
|
1802
|
+
ctx.beginPath(); ctx.moveTo(60, 54 + i * 13); ctx.lineTo(100, 54 + i * 13); ctx.stroke()
|
|
1803
|
+
}
|
|
1804
|
+
ctx.font = '700 15px ui-monospace, Menlo, monospace'
|
|
1805
|
+
ctx.textAlign = 'center'
|
|
1806
|
+
ctx.fillText('EXAM', 80, 142)
|
|
1807
|
+
})
|
|
1808
|
+
|
|
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)
|
|
1812
|
+
|
|
1813
|
+
// 讲台(教室最前面,背靠后墙,面向三张工位)
|
|
1814
|
+
const podium = new THREE.Group()
|
|
1815
|
+
podium.position.set(0, 0, -1.95)
|
|
1816
|
+
box(1.35, 0.14, 0.95, dark, 0, 0.07, 0, podium)
|
|
1817
|
+
const lectern = box(0.7, 0.98, 0.5, wood, 0, 0.56, 0, podium)
|
|
1818
|
+
lectern.rotation.x = -0.04
|
|
1819
|
+
const topPlate = box(0.86, 0.06, 0.58, metal, 0, 1.08, 0, podium)
|
|
1820
|
+
topPlate.rotation.x = -0.16
|
|
1821
|
+
const panel = new THREE.Mesh(new THREE.PlaneGeometry(0.46, 0.26), cyan)
|
|
1822
|
+
panel.position.set(0, 0.64, 0.256)
|
|
1823
|
+
podium.add(panel)
|
|
1824
|
+
const mic = new THREE.Mesh(new THREE.CylinderGeometry(0.014, 0.014, 0.36, 6), metal)
|
|
1825
|
+
mic.position.set(0.2, 1.22, 0.02)
|
|
1826
|
+
mic.rotation.z = -0.22
|
|
1827
|
+
podium.add(mic)
|
|
1828
|
+
const micHead = new THREE.Mesh(new THREE.SphereGeometry(0.04, 10, 10), cyan)
|
|
1829
|
+
micHead.position.set(0.24, 1.4, 0.02)
|
|
1830
|
+
podium.add(micHead)
|
|
1831
|
+
annex.add(podium)
|
|
1832
|
+
// 讲台后的黑板/示教板
|
|
1833
|
+
box(3.3, 1.25, 0.04, dark, 0, 1.55, -2.86)
|
|
1834
|
+
box(3.1, 0.03, 0.05, cyan, 0, 2.18, -2.83)
|
|
1835
|
+
|
|
1836
|
+
// 室内灯
|
|
1837
|
+
const lightWarm = new THREE.PointLight(0xffd8b0, 2.6, 11, 2)
|
|
1838
|
+
lightWarm.position.set(0, 2.15, 0.4)
|
|
1839
|
+
annex.add(lightWarm)
|
|
1840
|
+
annexLights.push({ light: lightWarm, night: 2.6, day: 1.1 })
|
|
1841
|
+
const lightCool = new THREE.PointLight(0x8fc8ff, 1.8, 10, 2)
|
|
1842
|
+
lightCool.position.set(0, 2.3, -1.8)
|
|
1843
|
+
annex.add(lightCool)
|
|
1844
|
+
annexLights.push({ light: lightCool, night: 1.8, day: 0.8 })
|
|
1845
|
+
|
|
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
|
+
// MCP 虚线 + 流光 + 暂未接通标牌
|
|
1861
|
+
mcpGroup = new THREE.Group()
|
|
1862
|
+
scene.add(mcpGroup)
|
|
1863
|
+
mcpCurve = new THREE.CatmullRomCurve3([
|
|
1864
|
+
new THREE.Vector3(4.55, 2.55, 0.35),
|
|
1865
|
+
new THREE.Vector3(5.15, 3.35, 0.55),
|
|
1866
|
+
new THREE.Vector3(5.8, 3.05, 0.8),
|
|
1867
|
+
new THREE.Vector3(6.58, 2.3, 1.14),
|
|
1868
|
+
])
|
|
1869
|
+
const pts = mcpCurve.getPoints(90)
|
|
1870
|
+
const cableGeo = new THREE.BufferGeometry().setFromPoints(pts)
|
|
1871
|
+
const cable = new THREE.Line(cableGeo, new THREE.LineDashedMaterial({
|
|
1872
|
+
color: 0x2f7fff, dashSize: 0.2, gapSize: 0.16, transparent: true, opacity: 0.9,
|
|
1873
|
+
}))
|
|
1874
|
+
cable.computeLineDistances()
|
|
1875
|
+
mcpGroup.add(cable)
|
|
1876
|
+
|
|
1877
|
+
// 沿曲线流动的短划,形成可见的“流光”
|
|
1878
|
+
const dashGeo = new THREE.BoxGeometry(0.18, 0.05, 0.05)
|
|
1879
|
+
const dashMat = new THREE.MeshBasicMaterial({ color: 0x2f7fff, transparent: true, opacity: 0.95, depthWrite: false, toneMapped: false })
|
|
1880
|
+
for (let i = 0; i < 8; i++) {
|
|
1881
|
+
const dash = new THREE.Mesh(dashGeo, dashMat)
|
|
1882
|
+
mcpGroup.add(dash)
|
|
1883
|
+
mcpDashes.push({ mesh: dash, phase: i / 8 })
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
const beadGeo = new THREE.SphereGeometry(0.042, 8, 8)
|
|
1887
|
+
const beadMat = new THREE.MeshBasicMaterial({ color: 0x66b8ff, transparent: true, opacity: 0.95, depthWrite: false, toneMapped: false })
|
|
1888
|
+
for (let i = 0; i < 9; i++) {
|
|
1889
|
+
const bead = new THREE.Mesh(beadGeo, beadMat)
|
|
1890
|
+
mcpGroup.add(bead)
|
|
1891
|
+
mcpBeads.push({ mesh: bead, phase: i / 9 })
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
const signCanvas = document.createElement('canvas')
|
|
1895
|
+
signCanvas.width = 512
|
|
1896
|
+
signCanvas.height = 160
|
|
1897
|
+
const sctx = signCanvas.getContext('2d')
|
|
1898
|
+
sctx.clearRect(0, 0, 512, 160)
|
|
1899
|
+
sctx.fillStyle = 'rgba(5,16,28,.88)'
|
|
1900
|
+
sctx.beginPath()
|
|
1901
|
+
sctx.roundRect(6, 6, 500, 148, 24)
|
|
1902
|
+
sctx.fill()
|
|
1903
|
+
sctx.strokeStyle = 'rgba(84,168,255,.55)'
|
|
1904
|
+
sctx.lineWidth = 2
|
|
1905
|
+
sctx.stroke()
|
|
1906
|
+
sctx.strokeStyle = '#54a8ff'
|
|
1907
|
+
sctx.lineWidth = 13
|
|
1908
|
+
sctx.lineCap = 'round'
|
|
1909
|
+
const drawX = (cx, cy, size) => {
|
|
1910
|
+
sctx.beginPath(); sctx.moveTo(cx - size, cy - size); sctx.lineTo(cx + size, cy + size); sctx.stroke()
|
|
1911
|
+
sctx.beginPath(); sctx.moveTo(cx + size, cy - size); sctx.lineTo(cx - size, cy + size); sctx.stroke()
|
|
1912
|
+
}
|
|
1913
|
+
drawX(58, 70, 19)
|
|
1914
|
+
drawX(454, 70, 19)
|
|
1915
|
+
sctx.fillStyle = '#d8efff'
|
|
1916
|
+
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)
|
|
1922
|
+
const signTex = new THREE.CanvasTexture(signCanvas)
|
|
1923
|
+
signTex.colorSpace = THREE.SRGBColorSpace
|
|
1924
|
+
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)
|
|
1927
|
+
mcpGroup.add(mcpSign)
|
|
1928
|
+
|
|
1929
|
+
const signGlow = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTexture, color: 0x3f9dff, transparent: true, opacity: 0.24, blending: THREE.AdditiveBlending, depthWrite: false }))
|
|
1930
|
+
signGlow.position.copy(mcpSign.position)
|
|
1931
|
+
signGlow.scale.set(4.4, 1.8, 1)
|
|
1932
|
+
mcpGroup.add(signGlow)
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1485
1935
|
function rebuild(list) {
|
|
1486
1936
|
for (const [, entry] of agentsById) {
|
|
1487
1937
|
const motion = entry.robot?.userData?.parts?.motion
|
|
@@ -1510,6 +1960,8 @@ function rebuild(list) {
|
|
|
1510
1960
|
neonSign.position.set((teams.length - 1) * OFFSET_X * 0.5, height + 5.2, (teams.length - 1) * OFFSET_Z * 0.5)
|
|
1511
1961
|
selectedId = null
|
|
1512
1962
|
focusFloor = null
|
|
1963
|
+
if (annex) annex.visible = true
|
|
1964
|
+
if (mcpGroup) mcpGroup.visible = true
|
|
1513
1965
|
$('#focusChip').hidden = true
|
|
1514
1966
|
const pose = globalPose(teams.length)
|
|
1515
1967
|
controls.target.copy(pose.target)
|
|
@@ -1615,6 +2067,28 @@ function tick() {
|
|
|
1615
2067
|
const t = clock.getElapsedTime()
|
|
1616
2068
|
requestAnimationFrame(tick)
|
|
1617
2069
|
applyThemeFrame()
|
|
2070
|
+
if (mcpCurve && mcpBeads.length > 0) {
|
|
2071
|
+
const flow = clock.getElapsedTime()
|
|
2072
|
+
for (const bead of mcpBeads) {
|
|
2073
|
+
const p = mcpCurve.getPointAt((flow * 0.11 + bead.phase) % 1)
|
|
2074
|
+
bead.mesh.position.copy(p)
|
|
2075
|
+
bead.mesh.scale.setScalar(0.75 + 0.35 * Math.sin(flow * 5 + bead.phase * 18))
|
|
2076
|
+
}
|
|
2077
|
+
const axis = new THREE.Vector3(1, 0, 0)
|
|
2078
|
+
const point = new THREE.Vector3()
|
|
2079
|
+
const tangent = new THREE.Vector3()
|
|
2080
|
+
for (const dash of mcpDashes) {
|
|
2081
|
+
const t = (flow * 0.075 + dash.phase) % 1
|
|
2082
|
+
mcpCurve.getPointAt(t, point)
|
|
2083
|
+
mcpCurve.getTangentAt(t, tangent)
|
|
2084
|
+
dash.mesh.position.copy(point)
|
|
2085
|
+
dash.mesh.quaternion.setFromUnitVectors(axis, tangent.normalize())
|
|
2086
|
+
}
|
|
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
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
1618
2092
|
|
|
1619
2093
|
const focusedTeam = focusFloor === null ? null : floors.find(floor => floor.index === focusFloor)?.team
|
|
1620
2094
|
for (const [id, entry] of agentsById) {
|
|
@@ -1628,6 +2102,13 @@ function tick() {
|
|
|
1628
2102
|
const working = status === 'working'
|
|
1629
2103
|
const sleeping = status === 'resting'
|
|
1630
2104
|
const phase = (id.charCodeAt(2) || 0) * 0.7
|
|
2105
|
+
const nowMs = Date.now()
|
|
2106
|
+
if (working) {
|
|
2107
|
+
if (!entry.workingSince) entry.workingSince = nowMs
|
|
2108
|
+
} else {
|
|
2109
|
+
entry.workingSince = 0
|
|
2110
|
+
}
|
|
2111
|
+
const overloaded = working && entry.workingSince > 0 && nowMs - entry.workingSince > LOAD_WARN_MS
|
|
1631
2112
|
const amp = working ? 0.09 : 0.055
|
|
1632
2113
|
const bob = reduceMotion ? 0 : (parts.motion.bob * 2 - 1) * amp
|
|
1633
2114
|
parts.face.position.y = parts.baseY + bob
|
|
@@ -1638,7 +2119,7 @@ function tick() {
|
|
|
1638
2119
|
}
|
|
1639
2120
|
|
|
1640
2121
|
// 表情只随状态切换;有自定义猫咪图片时优先使用
|
|
1641
|
-
const expr = FACE_FOR_STATUS[status] ?? 'default'
|
|
2122
|
+
const expr = overloaded ? 'dizzy' : (FACE_FOR_STATUS[status] ?? 'default')
|
|
1642
2123
|
if (customFaceTexture) {
|
|
1643
2124
|
if (parts.faceMat.map !== customFaceTexture) {
|
|
1644
2125
|
parts.faceMat.map = customFaceTexture
|
|
@@ -1650,18 +2131,41 @@ function tick() {
|
|
|
1650
2131
|
parts.faceMat.map = faceTextures[expr]
|
|
1651
2132
|
parts.faceMat.needsUpdate = true
|
|
1652
2133
|
}
|
|
1653
|
-
parts.faceMat.color.copy(parts.tint)
|
|
2134
|
+
parts.faceMat.color.copy(parts.tint).lerp(OVERLOAD_COLOR, overloaded ? 0.55 : 0)
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// 正常工作:汗滴;长时间工作:红温乱线
|
|
2138
|
+
const sweaty = working && !overloaded
|
|
2139
|
+
for (const drop of parts.sweat) {
|
|
2140
|
+
drop.sprite.visible = sweaty
|
|
2141
|
+
if (!sweaty) continue
|
|
2142
|
+
const p = (t * 0.55 + drop.phase) % 1
|
|
2143
|
+
drop.sprite.position.set(drop.x + Math.sin(t * 2 + drop.phase * 8) * drop.drift, parts.baseY + 0.26 - p * 0.34, 0.12)
|
|
2144
|
+
drop.sprite.material.opacity = Math.sin(p * Math.PI) * 0.92
|
|
2145
|
+
drop.sprite.scale.set(0.085 + p * 0.03, 0.13 + p * 0.05, 1)
|
|
2146
|
+
}
|
|
2147
|
+
parts.scribble.visible = overloaded
|
|
2148
|
+
parts.scribbleGlow.visible = overloaded
|
|
2149
|
+
if (overloaded) {
|
|
2150
|
+
const jitter = Math.sin(t * 17 + phase) * 0.012
|
|
2151
|
+
parts.scribble.position.set(jitter, parts.baseY + 1.55 + Math.sin(t * 3.2 + phase) * 0.03, 0.04)
|
|
2152
|
+
parts.scribble.rotation = Math.sin(t * 6 + phase) * 0.08
|
|
2153
|
+
parts.scribble.material.opacity = 0.96
|
|
2154
|
+
parts.scribbleGlow.position.copy(parts.scribble.position)
|
|
2155
|
+
parts.scribbleGlow.position.z = 0.02
|
|
2156
|
+
parts.scribbleGlow.material.opacity = 0.26 + Math.sin(t * 8 + phase) * 0.08
|
|
1654
2157
|
}
|
|
1655
2158
|
|
|
1656
2159
|
// 呼吸/浮动
|
|
1657
2160
|
const breathe = sleeping ? 1 + Math.sin(t * 1.5 + phase) * 0.02 : 1 + Math.sin(t * 2.2 + phase) * 0.012
|
|
1658
2161
|
parts.face.scale.set(0.6 * breathe, 0.6 * breathe, 1)
|
|
1659
2162
|
|
|
1660
|
-
const statusColor = STATUS[status]?.color ?? STATUS.resting.color
|
|
2163
|
+
const statusColor = overloaded ? '#ff5a5a' : (STATUS[status]?.color ?? STATUS.resting.color)
|
|
1661
2164
|
parts.ring.material.color.set(statusColor)
|
|
1662
2165
|
parts.glow.material.color.set(statusColor)
|
|
1663
2166
|
parts.ring.rotation.z = status === 'stuck' ? t * 2.6 : 0
|
|
1664
|
-
const ringPulse =
|
|
2167
|
+
const ringPulse = overloaded ? 1 + Math.sin(t * 6.5 + phase) * 0.12
|
|
2168
|
+
: working ? 1 + Math.sin(t * 3.4 + phase) * 0.08
|
|
1665
2169
|
: status === 'need_me' ? 1 + Math.sin(t * 5 + phase) * 0.1 : 1
|
|
1666
2170
|
parts.ring.scale.setScalar(ringPulse * (id === selectedId ? 1.24 : 1))
|
|
1667
2171
|
for (let k = 0; k < parts.ripples.length; k++) {
|
|
@@ -1673,7 +2177,8 @@ function tick() {
|
|
|
1673
2177
|
ripple.scale.setScalar(1 + p * 1.1)
|
|
1674
2178
|
ripple.material.opacity = (1 - p) * 0.3
|
|
1675
2179
|
}
|
|
1676
|
-
parts.glow.material.opacity =
|
|
2180
|
+
parts.glow.material.opacity = overloaded ? 0.38 + Math.sin(t * 6.5 + phase) * 0.08
|
|
2181
|
+
: working ? 0.34 + Math.sin(t * 3.4 + phase) * 0.1
|
|
1677
2182
|
: status === 'need_me' ? 0.32 : status === 'stuck' ? 0.32 : 0.14
|
|
1678
2183
|
|
|
1679
2184
|
// 空闲睡觉:飘 Z
|
|
@@ -1855,6 +2360,8 @@ window.__NS = {
|
|
|
1855
2360
|
rebuild([])
|
|
1856
2361
|
$('#offline').classList.add('show')
|
|
1857
2362
|
$('#offline').textContent = '正在读取本机会话…'
|
|
2363
|
+
buildAnnex()
|
|
2364
|
+
applyGlassSetting()
|
|
1858
2365
|
loadCustomFace()
|
|
1859
2366
|
poll()
|
|
1860
2367
|
setInterval(poll, 2000)
|