- Expand area positions from 3 to 8 per zone (breakroom/writing/error) in both game.js (AREA_POSITIONS) and index.html (getAreaPoint) - Replace global areaPositionCounters with per-render slot index (_slotIndex) to ensure consistent position assignment across poll cycles Previously, agents in the same area would overlap after the 3rd one because positions cycled via modulo. The counter-based approach also caused misalignment when existing agents skipped position increments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1035 lines
35 KiB
JavaScript
1035 lines
35 KiB
JavaScript
// Star Office UI - 游戏主逻辑
|
||
// 依赖: layout.js(必须在这个之前加载)
|
||
|
||
// 检测浏览器是否支持 WebP
|
||
let supportsWebP = false;
|
||
|
||
// 方法 1: 使用 canvas 检测
|
||
function checkWebPSupport() {
|
||
return new Promise((resolve) => {
|
||
const canvas = document.createElement('canvas');
|
||
if (canvas.getContext && canvas.getContext('2d')) {
|
||
resolve(canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0);
|
||
} else {
|
||
resolve(false);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 方法 2: 使用 image 检测(备用)
|
||
function checkWebPSupportFallback() {
|
||
return new Promise((resolve) => {
|
||
const img = new Image();
|
||
img.onload = () => resolve(true);
|
||
img.onerror = () => resolve(false);
|
||
img.src = 'data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAABBxAR/Q9ERP8DAABWUDggGAAAADABAJ0BKgEAAQADADQlpAADcAD++/1QAA==';
|
||
});
|
||
}
|
||
|
||
// 获取文件扩展名(根据 WebP 支持情况 + 布局配置的 forcePng)
|
||
function getExt(pngFile) {
|
||
// star-working-spritesheet.png 太宽了,WebP 不支持,始终用 PNG
|
||
if (pngFile === 'star-working-spritesheet.png') {
|
||
return '.png';
|
||
}
|
||
// 如果布局配置里强制用 PNG,就用 .png
|
||
if (LAYOUT.forcePng && LAYOUT.forcePng[pngFile.replace(/\.(png|webp)$/, '')]) {
|
||
return '.png';
|
||
}
|
||
return supportsWebP ? '.webp' : '.png';
|
||
}
|
||
|
||
const config = {
|
||
type: Phaser.AUTO,
|
||
width: LAYOUT.game.width,
|
||
height: LAYOUT.game.height,
|
||
parent: 'game-container',
|
||
pixelArt: true,
|
||
physics: { default: 'arcade', arcade: { gravity: { y: 0 }, debug: false } },
|
||
scene: { preload: preload, create: create, update: update }
|
||
};
|
||
|
||
let totalAssets = 0;
|
||
let loadedAssets = 0;
|
||
let loadingProgressBar, loadingProgressContainer, loadingOverlay, loadingText;
|
||
|
||
// Memo 相关函数
|
||
async function loadMemo() {
|
||
const memoDate = document.getElementById('memo-date');
|
||
const memoContent = document.getElementById('memo-content');
|
||
|
||
try {
|
||
const response = await fetch('/yesterday-memo?t=' + Date.now(), { cache: 'no-store' });
|
||
const data = await response.json();
|
||
|
||
if (data.success && data.memo) {
|
||
memoDate.textContent = data.date || '';
|
||
memoContent.innerHTML = data.memo.replace(/\n/g, '<br>');
|
||
} else {
|
||
memoContent.innerHTML = '<div id="memo-placeholder">暂无昨日日记</div>';
|
||
}
|
||
} catch (e) {
|
||
console.error('加载 memo 失败:', e);
|
||
memoContent.innerHTML = '<div id="memo-placeholder">加载失败</div>';
|
||
}
|
||
}
|
||
|
||
// 更新加载进度
|
||
function updateLoadingProgress() {
|
||
loadedAssets++;
|
||
const percent = Math.min(100, Math.round((loadedAssets / totalAssets) * 100));
|
||
if (loadingProgressBar) {
|
||
loadingProgressBar.style.width = percent + '%';
|
||
}
|
||
if (loadingText) {
|
||
loadingText.textContent = `正在加载 Star 的像素办公室... ${percent}%`;
|
||
}
|
||
}
|
||
|
||
// 隐藏加载界面
|
||
function hideLoadingOverlay() {
|
||
setTimeout(() => {
|
||
if (loadingOverlay) {
|
||
loadingOverlay.style.transition = 'opacity 0.5s ease';
|
||
loadingOverlay.style.opacity = '0';
|
||
setTimeout(() => {
|
||
loadingOverlay.style.display = 'none';
|
||
}, 500);
|
||
}
|
||
}, 300);
|
||
}
|
||
|
||
const STATES = {
|
||
idle: { name: '待命', area: 'breakroom' },
|
||
writing: { name: '整理文档', area: 'writing' },
|
||
researching: { name: '搜索信息', area: 'researching' },
|
||
executing: { name: '执行任务', area: 'writing' },
|
||
syncing: { name: '同步备份', area: 'writing' },
|
||
error: { name: '出错了', area: 'error' }
|
||
};
|
||
|
||
const BUBBLE_TEXTS = {
|
||
idle: [
|
||
'待命中:耳朵竖起来了',
|
||
'我在这儿,随时可以开工',
|
||
'先把桌面收拾干净再说',
|
||
'呼——给大脑放个风',
|
||
'今天也要优雅地高效',
|
||
'等待,是为了更准确的一击',
|
||
'咖啡还热,灵感也还在',
|
||
'我在后台给你加 Buff',
|
||
'状态:静心 / 充电',
|
||
'小猫说:慢一点也没关系'
|
||
],
|
||
writing: [
|
||
'进入专注模式:勿扰',
|
||
'先把关键路径跑通',
|
||
'我来把复杂变简单',
|
||
'把 bug 关进笼子里',
|
||
'写到一半,先保存',
|
||
'把每一步都做成可回滚',
|
||
'今天的进度,明天的底气',
|
||
'先收敛,再发散',
|
||
'让系统变得更可解释',
|
||
'稳住,我们能赢'
|
||
],
|
||
researching: [
|
||
'我在挖证据链',
|
||
'让我把信息熬成结论',
|
||
'找到了:关键在这里',
|
||
'先把变量控制住',
|
||
'我在查:它为什么会这样',
|
||
'把直觉写成验证',
|
||
'先定位,再优化',
|
||
'别急,先画因果图'
|
||
],
|
||
executing: [
|
||
'执行中:不要眨眼',
|
||
'把任务切成小块逐个击破',
|
||
'开始跑 pipeline',
|
||
'一键推进:走你',
|
||
'让结果自己说话',
|
||
'先做最小可行,再做最美版本'
|
||
],
|
||
syncing: [
|
||
'同步中:把今天锁进云里',
|
||
'备份不是仪式,是安全感',
|
||
'写入中…别断电',
|
||
'把变更交给时间戳',
|
||
'云端对齐:咔哒',
|
||
'同步完成前先别乱动',
|
||
'把未来的自己从灾难里救出来',
|
||
'多一份备份,少一份后悔'
|
||
],
|
||
error: [
|
||
'警报响了:先别慌',
|
||
'我闻到 bug 的味道了',
|
||
'先复现,再谈修复',
|
||
'把日志给我,我会说人话',
|
||
'错误不是敌人,是线索',
|
||
'把影响面圈起来',
|
||
'先止血,再手术',
|
||
'我在:马上定位根因',
|
||
'别怕,这种我见多了',
|
||
'报警中:让问题自己现形'
|
||
],
|
||
cat: [
|
||
'喵~',
|
||
'咕噜咕噜…',
|
||
'尾巴摇一摇',
|
||
'晒太阳最开心',
|
||
'有人来看我啦',
|
||
'我是这个办公室的吉祥物',
|
||
'伸个懒腰',
|
||
'今天的罐罐准备好了吗',
|
||
'呼噜呼噜',
|
||
'这个位置视野最好'
|
||
]
|
||
};
|
||
|
||
let game, star, sofa, serverroom, areas = {}, currentState = 'idle', pendingDesiredState = null, statusText, lastFetch = 0, lastBlink = 0, lastBubble = 0, targetX = 660, targetY = 170, bubble = null, typewriterText = '', typewriterTarget = '', typewriterIndex = 0, lastTypewriter = 0, syncAnimSprite = null, catBubble = null;
|
||
let isMoving = false;
|
||
let waypoints = [];
|
||
let lastWanderAt = 0;
|
||
let coordsOverlay, coordsDisplay, coordsToggle;
|
||
let showCoords = false;
|
||
const FETCH_INTERVAL = 2000;
|
||
const BLINK_INTERVAL = 2500;
|
||
const BUBBLE_INTERVAL = 8000;
|
||
const CAT_BUBBLE_INTERVAL = 18000;
|
||
let lastCatBubble = 0;
|
||
const TYPEWRITER_DELAY = 50;
|
||
let agents = {}; // agentId -> sprite/container
|
||
let lastAgentsFetch = 0;
|
||
const AGENTS_FETCH_INTERVAL = 2500;
|
||
|
||
// agent 颜色配置
|
||
const AGENT_COLORS = {
|
||
star: 0xffd700,
|
||
npc1: 0x00aaff,
|
||
agent_nika: 0xff69b4,
|
||
default: 0x94a3b8
|
||
};
|
||
|
||
// agent 名字颜色
|
||
const NAME_TAG_COLORS = {
|
||
approved: 0x22c55e,
|
||
pending: 0xf59e0b,
|
||
rejected: 0xef4444,
|
||
offline: 0x64748b,
|
||
default: 0x1f2937
|
||
};
|
||
|
||
// breakroom / writing / error 区域的 agent 分布位置(多 agent 时错开)
|
||
const AREA_POSITIONS = {
|
||
breakroom: [
|
||
{ x: 620, y: 180 },
|
||
{ x: 560, y: 220 },
|
||
{ x: 680, y: 210 },
|
||
{ x: 540, y: 170 },
|
||
{ x: 700, y: 240 },
|
||
{ x: 600, y: 250 },
|
||
{ x: 650, y: 160 },
|
||
{ x: 580, y: 200 }
|
||
],
|
||
writing: [
|
||
{ x: 760, y: 320 },
|
||
{ x: 830, y: 280 },
|
||
{ x: 690, y: 350 },
|
||
{ x: 770, y: 260 },
|
||
{ x: 850, y: 340 },
|
||
{ x: 720, y: 300 },
|
||
{ x: 800, y: 370 },
|
||
{ x: 750, y: 240 }
|
||
],
|
||
error: [
|
||
{ x: 180, y: 260 },
|
||
{ x: 120, y: 220 },
|
||
{ x: 240, y: 230 },
|
||
{ x: 160, y: 200 },
|
||
{ x: 220, y: 270 },
|
||
{ x: 140, y: 250 },
|
||
{ x: 200, y: 210 },
|
||
{ x: 260, y: 260 }
|
||
]
|
||
};
|
||
|
||
|
||
// 状态控制栏函数(用于测试)
|
||
function setState(state, detail) {
|
||
fetch('/set_state', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ state, detail })
|
||
}).then(() => fetchStatus());
|
||
}
|
||
|
||
// 初始化:先检测 WebP 支持,再启动游戏
|
||
async function initGame() {
|
||
try {
|
||
supportsWebP = await checkWebPSupport();
|
||
} catch (e) {
|
||
try {
|
||
supportsWebP = await checkWebPSupportFallback();
|
||
} catch (e2) {
|
||
supportsWebP = false;
|
||
}
|
||
}
|
||
|
||
console.log('WebP 支持:', supportsWebP);
|
||
new Phaser.Game(config);
|
||
}
|
||
|
||
function preload() {
|
||
loadingOverlay = document.getElementById('loading-overlay');
|
||
loadingProgressBar = document.getElementById('loading-progress-bar');
|
||
loadingText = document.getElementById('loading-text');
|
||
loadingProgressContainer = document.getElementById('loading-progress-container');
|
||
|
||
// 从 LAYOUT 读取总资源数量(避免 magic number)
|
||
totalAssets = LAYOUT.totalAssets || 15;
|
||
loadedAssets = 0;
|
||
|
||
this.load.on('filecomplete', () => {
|
||
updateLoadingProgress();
|
||
});
|
||
|
||
this.load.on('complete', () => {
|
||
hideLoadingOverlay();
|
||
});
|
||
|
||
this.load.image('office_bg', '/static/office_bg_small' + (supportsWebP ? '.webp' : '.png') + '?v={{VERSION_TIMESTAMP}}');
|
||
this.load.spritesheet('star_idle', '/static/star-idle-spritesheet' + getExt('star-idle-spritesheet.png'), { frameWidth: 128, frameHeight: 128 });
|
||
this.load.spritesheet('star_researching', '/static/star-researching-spritesheet' + getExt('star-researching-spritesheet.png'), { frameWidth: 128, frameHeight: 105 });
|
||
|
||
this.load.image('sofa_idle', '/static/sofa-idle' + getExt('sofa-idle.png'));
|
||
this.load.spritesheet('sofa_busy', '/static/sofa-busy-spritesheet' + getExt('sofa-busy-spritesheet.png'), { frameWidth: 256, frameHeight: 256 });
|
||
|
||
this.load.spritesheet('plants', '/static/plants-spritesheet' + getExt('plants-spritesheet.png'), { frameWidth: 160, frameHeight: 160 });
|
||
this.load.spritesheet('posters', '/static/posters-spritesheet' + getExt('posters-spritesheet.png'), { frameWidth: 160, frameHeight: 160 });
|
||
this.load.spritesheet('coffee_machine', '/static/coffee-machine-spritesheet' + getExt('coffee-machine-spritesheet.png'), { frameWidth: 230, frameHeight: 230 });
|
||
this.load.spritesheet('serverroom', '/static/serverroom-spritesheet' + getExt('serverroom-spritesheet.png'), { frameWidth: 180, frameHeight: 251 });
|
||
|
||
this.load.spritesheet('error_bug', '/static/error-bug-spritesheet-grid' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 180, frameHeight: 180 });
|
||
this.load.spritesheet('cats', '/static/cats-spritesheet' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 160, frameHeight: 160 });
|
||
this.load.image('desk', '/static/desk' + getExt('desk.png'));
|
||
this.load.spritesheet('star_working', '/static/star-working-spritesheet-grid' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 230, frameHeight: 144 });
|
||
this.load.spritesheet('sync_anim', '/static/sync-animation-spritesheet-grid' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 256, frameHeight: 256 });
|
||
this.load.image('memo_bg', '/static/memo-bg' + (supportsWebP ? '.webp' : '.png'));
|
||
|
||
// 新办公桌:强制 PNG(透明)
|
||
this.load.image('desk_v2', '/static/desk-v2.png');
|
||
this.load.spritesheet('flowers', '/static/flowers-spritesheet' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 65, frameHeight: 65 });
|
||
}
|
||
|
||
function create() {
|
||
game = this;
|
||
this.add.image(640, 360, 'office_bg');
|
||
|
||
// === 沙发(来自 LAYOUT)===
|
||
sofa = this.add.sprite(
|
||
LAYOUT.furniture.sofa.x,
|
||
LAYOUT.furniture.sofa.y,
|
||
'sofa_busy'
|
||
).setOrigin(LAYOUT.furniture.sofa.origin.x, LAYOUT.furniture.sofa.origin.y);
|
||
sofa.setDepth(LAYOUT.furniture.sofa.depth);
|
||
|
||
this.anims.create({
|
||
key: 'sofa_busy',
|
||
frames: this.anims.generateFrameNumbers('sofa_busy', { start: 0, end: 47 }),
|
||
frameRate: 12,
|
||
repeat: -1
|
||
});
|
||
|
||
areas = LAYOUT.areas;
|
||
|
||
this.anims.create({
|
||
key: 'star_idle',
|
||
frames: this.anims.generateFrameNumbers('star_idle', { start: 0, end: 29 }),
|
||
frameRate: 12,
|
||
repeat: -1
|
||
});
|
||
this.anims.create({
|
||
key: 'star_researching',
|
||
frames: this.anims.generateFrameNumbers('star_researching', { start: 0, end: 95 }),
|
||
frameRate: 12,
|
||
repeat: -1
|
||
});
|
||
|
||
star = game.physics.add.sprite(areas.breakroom.x, areas.breakroom.y, 'star_idle');
|
||
star.setOrigin(0.5);
|
||
star.setScale(1.4);
|
||
star.setAlpha(0.95);
|
||
star.setDepth(20);
|
||
star.setVisible(false);
|
||
star.anims.stop();
|
||
|
||
if (game.textures.exists('sofa_busy')) {
|
||
sofa.setTexture('sofa_busy');
|
||
sofa.anims.play('sofa_busy', true);
|
||
}
|
||
|
||
// === 牌匾(来自 LAYOUT)===
|
||
const plaqueX = LAYOUT.plaque.x;
|
||
const plaqueY = LAYOUT.plaque.y;
|
||
const plaqueBg = game.add.rectangle(plaqueX, plaqueY, LAYOUT.plaque.width, LAYOUT.plaque.height, 0x5d4037);
|
||
plaqueBg.setStrokeStyle(3, 0x3e2723);
|
||
const plaqueText = game.add.text(plaqueX, plaqueY, '海辛小龙虾的办公室', {
|
||
fontFamily: 'ArkPixel, monospace',
|
||
fontSize: '18px',
|
||
fill: '#ffd700',
|
||
fontWeight: 'bold',
|
||
stroke: '#000',
|
||
strokeThickness: 2
|
||
}).setOrigin(0.5);
|
||
game.add.text(plaqueX - 190, plaqueY, '⭐', { fontFamily: 'ArkPixel, monospace', fontSize: '20px' }).setOrigin(0.5);
|
||
game.add.text(plaqueX + 190, plaqueY, '⭐', { fontFamily: 'ArkPixel, monospace', fontSize: '20px' }).setOrigin(0.5);
|
||
|
||
// === 植物们(来自 LAYOUT)===
|
||
const plantFrameCount = 16;
|
||
for (let i = 0; i < LAYOUT.furniture.plants.length; i++) {
|
||
const p = LAYOUT.furniture.plants[i];
|
||
const randomPlantFrame = Math.floor(Math.random() * plantFrameCount);
|
||
const plant = game.add.sprite(p.x, p.y, 'plants', randomPlantFrame).setOrigin(0.5);
|
||
plant.setDepth(p.depth);
|
||
plant.setInteractive({ useHandCursor: true });
|
||
window[`plantSprite${i === 0 ? '' : i + 1}`] = plant;
|
||
plant.on('pointerdown', (() => {
|
||
const next = Math.floor(Math.random() * plantFrameCount);
|
||
plant.setFrame(next);
|
||
}));
|
||
}
|
||
|
||
// === 海报(来自 LAYOUT)===
|
||
const postersFrameCount = 32;
|
||
const randomPosterFrame = Math.floor(Math.random() * postersFrameCount);
|
||
const poster = game.add.sprite(LAYOUT.furniture.poster.x, LAYOUT.furniture.poster.y, 'posters', randomPosterFrame).setOrigin(0.5);
|
||
poster.setDepth(LAYOUT.furniture.poster.depth);
|
||
poster.setInteractive({ useHandCursor: true });
|
||
window.posterSprite = poster;
|
||
window.posterFrameCount = postersFrameCount;
|
||
poster.on('pointerdown', () => {
|
||
const next = Math.floor(Math.random() * window.posterFrameCount);
|
||
window.posterSprite.setFrame(next);
|
||
});
|
||
|
||
// === 小猫(来自 LAYOUT)===
|
||
const catsFrameCount = 16;
|
||
const randomCatFrame = Math.floor(Math.random() * catsFrameCount);
|
||
const cat = game.add.sprite(LAYOUT.furniture.cat.x, LAYOUT.furniture.cat.y, 'cats', randomCatFrame).setOrigin(LAYOUT.furniture.cat.origin.x, LAYOUT.furniture.cat.origin.y);
|
||
cat.setDepth(LAYOUT.furniture.cat.depth);
|
||
cat.setInteractive({ useHandCursor: true });
|
||
window.catSprite = cat;
|
||
window.catsFrameCount = catsFrameCount;
|
||
cat.on('pointerdown', () => {
|
||
const next = Math.floor(Math.random() * window.catsFrameCount);
|
||
window.catSprite.setFrame(next);
|
||
});
|
||
|
||
// === 咖啡机(来自 LAYOUT)===
|
||
this.anims.create({
|
||
key: 'coffee_machine',
|
||
frames: this.anims.generateFrameNumbers('coffee_machine', { start: 0, end: 95 }),
|
||
frameRate: 12.5,
|
||
repeat: -1
|
||
});
|
||
const coffeeMachine = this.add.sprite(
|
||
LAYOUT.furniture.coffeeMachine.x,
|
||
LAYOUT.furniture.coffeeMachine.y,
|
||
'coffee_machine'
|
||
).setOrigin(LAYOUT.furniture.coffeeMachine.origin.x, LAYOUT.furniture.coffeeMachine.origin.y);
|
||
coffeeMachine.setDepth(LAYOUT.furniture.coffeeMachine.depth);
|
||
coffeeMachine.anims.play('coffee_machine', true);
|
||
|
||
// === 服务器区(来自 LAYOUT)===
|
||
this.anims.create({
|
||
key: 'serverroom_on',
|
||
frames: this.anims.generateFrameNumbers('serverroom', { start: 0, end: 39 }),
|
||
frameRate: 6,
|
||
repeat: -1
|
||
});
|
||
serverroom = this.add.sprite(
|
||
LAYOUT.furniture.serverroom.x,
|
||
LAYOUT.furniture.serverroom.y,
|
||
'serverroom',
|
||
0
|
||
).setOrigin(LAYOUT.furniture.serverroom.origin.x, LAYOUT.furniture.serverroom.origin.y);
|
||
serverroom.setDepth(LAYOUT.furniture.serverroom.depth);
|
||
serverroom.anims.stop();
|
||
serverroom.setFrame(0);
|
||
|
||
// === 新办公桌(来自 LAYOUT,强制透明 PNG)===
|
||
const desk = this.add.image(
|
||
LAYOUT.furniture.desk.x,
|
||
LAYOUT.furniture.desk.y,
|
||
'desk_v2'
|
||
).setOrigin(LAYOUT.furniture.desk.origin.x, LAYOUT.furniture.desk.origin.y);
|
||
desk.setDepth(LAYOUT.furniture.desk.depth);
|
||
|
||
// === 花盆(来自 LAYOUT)===
|
||
const flowerFrameCount = 16;
|
||
const randomFlowerFrame = Math.floor(Math.random() * flowerFrameCount);
|
||
const flower = this.add.sprite(
|
||
LAYOUT.furniture.flower.x,
|
||
LAYOUT.furniture.flower.y,
|
||
'flowers',
|
||
randomFlowerFrame
|
||
).setOrigin(LAYOUT.furniture.flower.origin.x, LAYOUT.furniture.flower.origin.y);
|
||
flower.setScale(LAYOUT.furniture.flower.scale || 1);
|
||
flower.setDepth(LAYOUT.furniture.flower.depth);
|
||
flower.setInteractive({ useHandCursor: true });
|
||
window.flowerSprite = flower;
|
||
window.flowerFrameCount = flowerFrameCount;
|
||
flower.on('pointerdown', () => {
|
||
const next = Math.floor(Math.random() * window.flowerFrameCount);
|
||
window.flowerSprite.setFrame(next);
|
||
});
|
||
|
||
// === Star 在桌前工作(来自 LAYOUT)===
|
||
this.anims.create({
|
||
key: 'star_working',
|
||
frames: this.anims.generateFrameNumbers('star_working', { start: 0, end: 191 }),
|
||
frameRate: 12,
|
||
repeat: -1
|
||
});
|
||
this.anims.create({
|
||
key: 'error_bug',
|
||
frames: this.anims.generateFrameNumbers('error_bug', { start: 0, end: 95 }),
|
||
frameRate: 12,
|
||
repeat: -1
|
||
});
|
||
|
||
// === 错误 bug(来自 LAYOUT)===
|
||
const errorBug = this.add.sprite(
|
||
LAYOUT.furniture.errorBug.x,
|
||
LAYOUT.furniture.errorBug.y,
|
||
'error_bug',
|
||
0
|
||
).setOrigin(LAYOUT.furniture.errorBug.origin.x, LAYOUT.furniture.errorBug.origin.y);
|
||
errorBug.setDepth(LAYOUT.furniture.errorBug.depth);
|
||
errorBug.setVisible(false);
|
||
errorBug.setScale(LAYOUT.furniture.errorBug.scale);
|
||
errorBug.anims.play('error_bug', true);
|
||
window.errorBug = errorBug;
|
||
window.errorBugDir = 1;
|
||
|
||
const starWorking = this.add.sprite(
|
||
LAYOUT.furniture.starWorking.x,
|
||
LAYOUT.furniture.starWorking.y,
|
||
'star_working',
|
||
0
|
||
).setOrigin(LAYOUT.furniture.starWorking.origin.x, LAYOUT.furniture.starWorking.origin.y);
|
||
starWorking.setVisible(false);
|
||
starWorking.setScale(LAYOUT.furniture.starWorking.scale);
|
||
starWorking.setDepth(LAYOUT.furniture.starWorking.depth);
|
||
window.starWorking = starWorking;
|
||
|
||
// === 同步动画(来自 LAYOUT)===
|
||
this.anims.create({
|
||
key: 'sync_anim',
|
||
frames: this.anims.generateFrameNumbers('sync_anim', { start: 1, end: 52 }),
|
||
frameRate: 12,
|
||
repeat: -1
|
||
});
|
||
syncAnimSprite = this.add.sprite(
|
||
LAYOUT.furniture.syncAnim.x,
|
||
LAYOUT.furniture.syncAnim.y,
|
||
'sync_anim',
|
||
0
|
||
).setOrigin(LAYOUT.furniture.syncAnim.origin.x, LAYOUT.furniture.syncAnim.origin.y);
|
||
syncAnimSprite.setDepth(LAYOUT.furniture.syncAnim.depth);
|
||
syncAnimSprite.anims.stop();
|
||
syncAnimSprite.setFrame(0);
|
||
|
||
window.starSprite = star;
|
||
|
||
statusText = document.getElementById('status-text');
|
||
coordsOverlay = document.getElementById('coords-overlay');
|
||
coordsDisplay = document.getElementById('coords-display');
|
||
coordsToggle = document.getElementById('coords-toggle');
|
||
|
||
coordsToggle.addEventListener('click', () => {
|
||
showCoords = !showCoords;
|
||
coordsOverlay.style.display = showCoords ? 'block' : 'none';
|
||
coordsToggle.textContent = showCoords ? '隐藏坐标' : '显示坐标';
|
||
coordsToggle.style.background = showCoords ? '#e94560' : '#333';
|
||
});
|
||
|
||
game.input.on('pointermove', (pointer) => {
|
||
if (!showCoords) return;
|
||
const x = Math.max(0, Math.min(config.width - 1, Math.round(pointer.x)));
|
||
const y = Math.max(0, Math.min(config.height - 1, Math.round(pointer.y)));
|
||
coordsDisplay.textContent = `${x}, ${y}`;
|
||
coordsOverlay.style.left = (pointer.x + 18) + 'px';
|
||
coordsOverlay.style.top = (pointer.y + 18) + 'px';
|
||
});
|
||
|
||
loadMemo();
|
||
fetchStatus();
|
||
fetchAgents();
|
||
|
||
// 可选调试:仅在显式开启 debug 模式时渲染测试用尼卡 agent
|
||
let debugAgents = false;
|
||
try {
|
||
if (typeof window !== 'undefined') {
|
||
if (window.STAR_OFFICE_DEBUG_AGENTS === true) {
|
||
debugAgents = true;
|
||
} else if (window.location && window.location.search && typeof URLSearchParams !== 'undefined') {
|
||
const sp = new URLSearchParams(window.location.search);
|
||
if (sp.get('debugAgents') === '1') {
|
||
debugAgents = true;
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
debugAgents = false;
|
||
}
|
||
|
||
if (debugAgents) {
|
||
const testNika = {
|
||
agentId: 'agent_nika',
|
||
name: '尼卡',
|
||
isMain: false,
|
||
state: 'writing',
|
||
detail: '在画像素画...',
|
||
area: 'writing',
|
||
authStatus: 'approved',
|
||
updated_at: new Date().toISOString()
|
||
};
|
||
renderAgent(testNika);
|
||
|
||
window.testNikaState = 'writing';
|
||
window.testNikaTimer = setInterval(() => {
|
||
const states = ['idle', 'writing', 'researching', 'executing'];
|
||
const areas = { idle: 'breakroom', writing: 'writing', researching: 'writing', executing: 'writing' };
|
||
window.testNikaState = states[Math.floor(Math.random() * states.length)];
|
||
const testAgent = {
|
||
agentId: 'agent_nika',
|
||
name: '尼卡',
|
||
isMain: false,
|
||
state: window.testNikaState,
|
||
detail: '在画像素画...',
|
||
area: areas[window.testNikaState],
|
||
authStatus: 'approved',
|
||
updated_at: new Date().toISOString()
|
||
};
|
||
renderAgent(testAgent);
|
||
}, 5000);
|
||
}
|
||
}
|
||
|
||
function update(time) {
|
||
if (time - lastFetch > FETCH_INTERVAL) { fetchStatus(); lastFetch = time; }
|
||
if (time - lastAgentsFetch > AGENTS_FETCH_INTERVAL) { fetchAgents(); lastAgentsFetch = time; }
|
||
|
||
const effectiveStateForServer = pendingDesiredState || currentState;
|
||
if (serverroom) {
|
||
if (effectiveStateForServer === 'idle') {
|
||
if (serverroom.anims.isPlaying) {
|
||
serverroom.anims.stop();
|
||
serverroom.setFrame(0);
|
||
}
|
||
} else {
|
||
if (!serverroom.anims.isPlaying || serverroom.anims.currentAnim?.key !== 'serverroom_on') {
|
||
serverroom.anims.play('serverroom_on', true);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (window.errorBug) {
|
||
if (effectiveStateForServer === 'error') {
|
||
window.errorBug.setVisible(true);
|
||
if (!window.errorBug.anims.isPlaying || window.errorBug.anims.currentAnim?.key !== 'error_bug') {
|
||
window.errorBug.anims.play('error_bug', true);
|
||
}
|
||
const leftX = LAYOUT.furniture.errorBug.pingPong.leftX;
|
||
const rightX = LAYOUT.furniture.errorBug.pingPong.rightX;
|
||
const speed = LAYOUT.furniture.errorBug.pingPong.speed;
|
||
const dir = window.errorBugDir || 1;
|
||
window.errorBug.x += speed * dir;
|
||
window.errorBug.y = LAYOUT.furniture.errorBug.y;
|
||
if (window.errorBug.x >= rightX) {
|
||
window.errorBug.x = rightX;
|
||
window.errorBugDir = -1;
|
||
} else if (window.errorBug.x <= leftX) {
|
||
window.errorBug.x = leftX;
|
||
window.errorBugDir = 1;
|
||
}
|
||
} else {
|
||
window.errorBug.setVisible(false);
|
||
window.errorBug.anims.stop();
|
||
}
|
||
}
|
||
|
||
if (syncAnimSprite) {
|
||
if (effectiveStateForServer === 'syncing') {
|
||
if (!syncAnimSprite.anims.isPlaying || syncAnimSprite.anims.currentAnim?.key !== 'sync_anim') {
|
||
syncAnimSprite.anims.play('sync_anim', true);
|
||
}
|
||
} else {
|
||
if (syncAnimSprite.anims.isPlaying) syncAnimSprite.anims.stop();
|
||
syncAnimSprite.setFrame(0);
|
||
}
|
||
}
|
||
|
||
if (time - lastBubble > BUBBLE_INTERVAL) {
|
||
showBubble();
|
||
lastBubble = time;
|
||
}
|
||
if (time - lastCatBubble > CAT_BUBBLE_INTERVAL) {
|
||
showCatBubble();
|
||
lastCatBubble = time;
|
||
}
|
||
|
||
if (typewriterIndex < typewriterTarget.length && time - lastTypewriter > TYPEWRITER_DELAY) {
|
||
typewriterText += typewriterTarget[typewriterIndex];
|
||
statusText.textContent = typewriterText;
|
||
typewriterIndex++;
|
||
lastTypewriter = time;
|
||
}
|
||
|
||
moveStar(time);
|
||
}
|
||
|
||
function normalizeState(s) {
|
||
if (!s) return 'idle';
|
||
if (s === 'working') return 'writing';
|
||
if (s === 'run' || s === 'running') return 'executing';
|
||
if (s === 'sync') return 'syncing';
|
||
if (s === 'research') return 'researching';
|
||
return s;
|
||
}
|
||
|
||
function fetchStatus() {
|
||
fetch('/status')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const nextState = normalizeState(data.state);
|
||
const stateInfo = STATES[nextState] || STATES.idle;
|
||
const changed = (pendingDesiredState === null) && (nextState !== currentState);
|
||
const nextLine = '[' + stateInfo.name + '] ' + (data.detail || '...');
|
||
if (changed) {
|
||
typewriterTarget = nextLine;
|
||
typewriterText = '';
|
||
typewriterIndex = 0;
|
||
|
||
pendingDesiredState = null;
|
||
currentState = nextState;
|
||
|
||
if (nextState === 'idle') {
|
||
if (game.textures.exists('sofa_busy')) {
|
||
sofa.setTexture('sofa_busy');
|
||
sofa.anims.play('sofa_busy', true);
|
||
}
|
||
star.setVisible(false);
|
||
star.anims.stop();
|
||
if (window.starWorking) {
|
||
window.starWorking.setVisible(false);
|
||
window.starWorking.anims.stop();
|
||
}
|
||
} else if (nextState === 'error') {
|
||
sofa.anims.stop();
|
||
sofa.setTexture('sofa_idle');
|
||
star.setVisible(false);
|
||
star.anims.stop();
|
||
if (window.starWorking) {
|
||
window.starWorking.setVisible(false);
|
||
window.starWorking.anims.stop();
|
||
}
|
||
} else if (nextState === 'syncing') {
|
||
sofa.anims.stop();
|
||
sofa.setTexture('sofa_idle');
|
||
star.setVisible(false);
|
||
star.anims.stop();
|
||
if (window.starWorking) {
|
||
window.starWorking.setVisible(false);
|
||
window.starWorking.anims.stop();
|
||
}
|
||
} else {
|
||
sofa.anims.stop();
|
||
sofa.setTexture('sofa_idle');
|
||
star.setVisible(false);
|
||
star.anims.stop();
|
||
if (window.starWorking) {
|
||
window.starWorking.setVisible(true);
|
||
window.starWorking.anims.play('star_working', true);
|
||
}
|
||
}
|
||
|
||
if (serverroom) {
|
||
if (nextState === 'idle') {
|
||
serverroom.anims.stop();
|
||
serverroom.setFrame(0);
|
||
} else {
|
||
serverroom.anims.play('serverroom_on', true);
|
||
}
|
||
}
|
||
|
||
if (syncAnimSprite) {
|
||
if (nextState === 'syncing') {
|
||
if (!syncAnimSprite.anims.isPlaying || syncAnimSprite.anims.currentAnim?.key !== 'sync_anim') {
|
||
syncAnimSprite.anims.play('sync_anim', true);
|
||
}
|
||
} else {
|
||
if (syncAnimSprite.anims.isPlaying) syncAnimSprite.anims.stop();
|
||
syncAnimSprite.setFrame(0);
|
||
}
|
||
}
|
||
} else {
|
||
if (!typewriterTarget || typewriterTarget !== nextLine) {
|
||
typewriterTarget = nextLine;
|
||
typewriterText = '';
|
||
typewriterIndex = 0;
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
typewriterTarget = '连接失败,正在重试...';
|
||
typewriterText = '';
|
||
typewriterIndex = 0;
|
||
});
|
||
}
|
||
|
||
function moveStar(time) {
|
||
const effectiveState = pendingDesiredState || currentState;
|
||
const stateInfo = STATES[effectiveState] || STATES.idle;
|
||
const baseTarget = areas[stateInfo.area] || areas.breakroom;
|
||
|
||
const dx = targetX - star.x;
|
||
const dy = targetY - star.y;
|
||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||
const speed = 1.4;
|
||
const wobble = Math.sin(time / 200) * 0.8;
|
||
|
||
if (dist > 3) {
|
||
star.x += (dx / dist) * speed;
|
||
star.y += (dy / dist) * speed;
|
||
star.setY(star.y + wobble);
|
||
isMoving = true;
|
||
} else {
|
||
if (waypoints && waypoints.length > 0) {
|
||
waypoints.shift();
|
||
if (waypoints.length > 0) {
|
||
targetX = waypoints[0].x;
|
||
targetY = waypoints[0].y;
|
||
isMoving = true;
|
||
} else {
|
||
if (pendingDesiredState !== null) {
|
||
isMoving = false;
|
||
currentState = pendingDesiredState;
|
||
pendingDesiredState = null;
|
||
|
||
if (currentState === 'idle') {
|
||
star.setVisible(false);
|
||
star.anims.stop();
|
||
if (window.starWorking) {
|
||
window.starWorking.setVisible(false);
|
||
window.starWorking.anims.stop();
|
||
}
|
||
} else {
|
||
star.setVisible(false);
|
||
star.anims.stop();
|
||
if (window.starWorking) {
|
||
window.starWorking.setVisible(true);
|
||
window.starWorking.anims.play('star_working', true);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
if (pendingDesiredState !== null) {
|
||
isMoving = false;
|
||
currentState = pendingDesiredState;
|
||
pendingDesiredState = null;
|
||
|
||
if (currentState === 'idle') {
|
||
star.setVisible(false);
|
||
star.anims.stop();
|
||
if (window.starWorking) {
|
||
window.starWorking.setVisible(false);
|
||
window.starWorking.anims.stop();
|
||
}
|
||
if (game.textures.exists('sofa_busy')) {
|
||
sofa.setTexture('sofa_busy');
|
||
sofa.anims.play('sofa_busy', true);
|
||
}
|
||
} else {
|
||
star.setVisible(false);
|
||
star.anims.stop();
|
||
if (window.starWorking) {
|
||
window.starWorking.setVisible(true);
|
||
window.starWorking.anims.play('star_working', true);
|
||
}
|
||
sofa.anims.stop();
|
||
sofa.setTexture('sofa_idle');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function showBubble() {
|
||
if (bubble) { bubble.destroy(); bubble = null; }
|
||
const texts = BUBBLE_TEXTS[currentState] || BUBBLE_TEXTS.idle;
|
||
if (currentState === 'idle') return;
|
||
|
||
let anchorX = star.x;
|
||
let anchorY = star.y;
|
||
if (currentState === 'syncing' && syncAnimSprite && syncAnimSprite.visible) {
|
||
anchorX = syncAnimSprite.x;
|
||
anchorY = syncAnimSprite.y;
|
||
} else if (currentState === 'error' && window.errorBug && window.errorBug.visible) {
|
||
anchorX = window.errorBug.x;
|
||
anchorY = window.errorBug.y;
|
||
} else if (!star.visible && window.starWorking && window.starWorking.visible) {
|
||
anchorX = window.starWorking.x;
|
||
anchorY = window.starWorking.y;
|
||
}
|
||
|
||
const text = texts[Math.floor(Math.random() * texts.length)];
|
||
const bubbleY = anchorY - 70;
|
||
const bg = game.add.rectangle(anchorX, bubbleY, text.length * 10 + 20, 28, 0xffffff, 0.95);
|
||
bg.setStrokeStyle(2, 0x000000);
|
||
const txt = game.add.text(anchorX, bubbleY, text, { fontFamily: 'ArkPixel, monospace', fontSize: '12px', fill: '#000', align: 'center' }).setOrigin(0.5);
|
||
bubble = game.add.container(0, 0, [bg, txt]);
|
||
bubble.setDepth(1200);
|
||
setTimeout(() => { if (bubble) { bubble.destroy(); bubble = null; } }, 3000);
|
||
}
|
||
|
||
function showCatBubble() {
|
||
if (!window.catSprite) return;
|
||
if (window.catBubble) { window.catBubble.destroy(); window.catBubble = null; }
|
||
const texts = BUBBLE_TEXTS.cat || ['喵~', '咕噜咕噜…'];
|
||
const text = texts[Math.floor(Math.random() * texts.length)];
|
||
const anchorX = window.catSprite.x;
|
||
const anchorY = window.catSprite.y - 60;
|
||
const bg = game.add.rectangle(anchorX, anchorY, text.length * 10 + 20, 24, 0xfffbeb, 0.95);
|
||
bg.setStrokeStyle(2, 0xd4a574);
|
||
const txt = game.add.text(anchorX, anchorY, text, { fontFamily: 'ArkPixel, monospace', fontSize: '11px', fill: '#8b6914', align: 'center' }).setOrigin(0.5);
|
||
window.catBubble = game.add.container(0, 0, [bg, txt]);
|
||
window.catBubble.setDepth(2100);
|
||
setTimeout(() => { if (window.catBubble) { window.catBubble.destroy(); window.catBubble = null; } }, 4000);
|
||
}
|
||
|
||
function fetchAgents() {
|
||
fetch('/agents?t=' + Date.now(), { cache: 'no-store' })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (!Array.isArray(data)) return;
|
||
// 重置位置计数器
|
||
// 按区域分配不同位置索引,避免重叠
|
||
const areaSlots = { breakroom: 0, writing: 0, error: 0 };
|
||
for (let agent of data) {
|
||
const area = agent.area || 'breakroom';
|
||
agent._slotIndex = areaSlots[area] || 0;
|
||
areaSlots[area] = (areaSlots[area] || 0) + 1;
|
||
renderAgent(agent);
|
||
}
|
||
// 移除不再存在的 agent
|
||
const currentIds = new Set(data.map(a => a.agentId));
|
||
for (let id in agents) {
|
||
if (!currentIds.has(id)) {
|
||
if (agents[id]) {
|
||
agents[id].destroy();
|
||
delete agents[id];
|
||
}
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('拉取 agents 失败:', error);
|
||
});
|
||
}
|
||
|
||
function getAreaPosition(area, slotIndex) {
|
||
const positions = AREA_POSITIONS[area] || AREA_POSITIONS.breakroom;
|
||
const idx = (slotIndex || 0) % positions.length;
|
||
return positions[idx];
|
||
}
|
||
|
||
function renderAgent(agent) {
|
||
const agentId = agent.agentId;
|
||
const name = agent.name || 'Agent';
|
||
const area = agent.area || 'breakroom';
|
||
const authStatus = agent.authStatus || 'pending';
|
||
const isMain = !!agent.isMain;
|
||
|
||
// 获取这个 agent 在区域里的位置
|
||
const pos = getAreaPosition(area, agent._slotIndex || 0);
|
||
const baseX = pos.x;
|
||
const baseY = pos.y;
|
||
|
||
// 颜色
|
||
const bodyColor = AGENT_COLORS[agentId] || AGENT_COLORS.default;
|
||
const nameColor = NAME_TAG_COLORS[authStatus] || NAME_TAG_COLORS.default;
|
||
|
||
// 透明度(离线/待批准/拒绝时变半透明)
|
||
let alpha = 1;
|
||
if (authStatus === 'pending') alpha = 0.7;
|
||
if (authStatus === 'rejected') alpha = 0.4;
|
||
if (authStatus === 'offline') alpha = 0.5;
|
||
|
||
if (!agents[agentId]) {
|
||
// 新建 agent
|
||
const container = game.add.container(baseX, baseY);
|
||
container.setDepth(1200 + (isMain ? 100 : 0)); // 放到最顶层!
|
||
|
||
// 像素小人:用星星图标,更明显
|
||
const starIcon = game.add.text(0, 0, '⭐', {
|
||
fontFamily: 'ArkPixel, monospace',
|
||
fontSize: '32px'
|
||
}).setOrigin(0.5);
|
||
starIcon.name = 'starIcon';
|
||
|
||
// 名字标签(漂浮)
|
||
const nameTag = game.add.text(0, -36, name, {
|
||
fontFamily: 'ArkPixel, monospace',
|
||
fontSize: '14px',
|
||
fill: '#' + nameColor.toString(16).padStart(6, '0'),
|
||
stroke: '#000',
|
||
strokeThickness: 3,
|
||
backgroundColor: 'rgba(255,255,255,0.95)'
|
||
}).setOrigin(0.5);
|
||
nameTag.name = 'nameTag';
|
||
|
||
// 状态小点(绿色/黄色/红色)
|
||
let dotColor = 0x64748b;
|
||
if (authStatus === 'approved') dotColor = 0x22c55e;
|
||
if (authStatus === 'pending') dotColor = 0xf59e0b;
|
||
if (authStatus === 'rejected') dotColor = 0xef4444;
|
||
if (authStatus === 'offline') dotColor = 0x94a3b8;
|
||
const statusDot = game.add.circle(20, -20, 5, dotColor, alpha);
|
||
statusDot.setStrokeStyle(2, 0x000000, alpha);
|
||
statusDot.name = 'statusDot';
|
||
|
||
container.add([starIcon, statusDot, nameTag]);
|
||
agents[agentId] = container;
|
||
} else {
|
||
// 更新 agent
|
||
const container = agents[agentId];
|
||
container.setPosition(baseX, baseY);
|
||
container.setAlpha(alpha);
|
||
container.setDepth(1200 + (isMain ? 100 : 0));
|
||
|
||
// 更新名字和颜色(如果变化)
|
||
const nameTag = container.getAt(2);
|
||
if (nameTag && nameTag.name === 'nameTag') {
|
||
nameTag.setText(name);
|
||
nameTag.setFill('#' + (NAME_TAG_COLORS[authStatus] || NAME_TAG_COLORS.default).toString(16).padStart(6, '0'));
|
||
}
|
||
// 更新状态点颜色
|
||
const statusDot = container.getAt(1);
|
||
if (statusDot && statusDot.name === 'statusDot') {
|
||
let dotColor = 0x64748b;
|
||
if (authStatus === 'approved') dotColor = 0x22c55e;
|
||
if (authStatus === 'pending') dotColor = 0xf59e0b;
|
||
if (authStatus === 'rejected') dotColor = 0xef4444;
|
||
if (authStatus === 'offline') dotColor = 0x94a3b8;
|
||
statusDot.fillColor = dotColor;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 启动游戏
|
||
initGame();
|