Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
496 lines
17 KiB
JavaScript
496 lines
17 KiB
JavaScript
(function () {
|
|
const ASSET_BASE = './assets'
|
|
const API_BASE = '/api/office'
|
|
const GAME_WIDTH = 1280
|
|
const GAME_HEIGHT = 720
|
|
const POLL_MS = 2500
|
|
|
|
const STATUS_META = {
|
|
idle: { label: 'IDLE', detail: 'Awaiting commands', area: 'breakroom', bubble: ['Standing by.', 'Quiet office. Ready when you are.'] },
|
|
writing: { label: 'WRITING', detail: 'Working at the command desk', area: 'writing', bubble: ['Drafting the next move.', 'Keeping the mainline clean.'] },
|
|
researching: { label: 'RESEARCH', detail: 'Collecting evidence and context', area: 'writing', bubble: ['Tracing the signal.', 'Checking the facts before acting.'] },
|
|
executing: { label: 'EXECUTING', detail: 'Pushing tasks through the pipeline', area: 'writing', bubble: ['Executing the plan.', 'Reducing the task to concrete steps.'] },
|
|
syncing: { label: 'SYNCING', detail: 'Aligning state in the server room', area: 'syncing', bubble: ['Sync in progress.', 'Writing the latest changes to the shared state.'] },
|
|
error: { label: 'ERROR', detail: 'Investigating a failure path', area: 'error', bubble: ['Something broke. Looking at it now.', 'The bug bay is active.'] },
|
|
}
|
|
|
|
const AREA_POSITIONS = {
|
|
breakroom: { x: 798, y: 272 },
|
|
writing: { x: 217, y: 343 },
|
|
syncing: { x: 1157, y: 592 },
|
|
error: { x: 1007, y: 221 },
|
|
}
|
|
|
|
const VISITOR_SLOTS = {
|
|
breakroom: [
|
|
{ x: 700, y: 350 },
|
|
{ x: 855, y: 322 },
|
|
{ x: 926, y: 362 },
|
|
],
|
|
writing: [
|
|
{ x: 386, y: 356 },
|
|
{ x: 462, y: 319 },
|
|
{ x: 538, y: 356 },
|
|
],
|
|
syncing: [
|
|
{ x: 1035, y: 520 },
|
|
{ x: 1096, y: 560 },
|
|
{ x: 974, y: 560 },
|
|
],
|
|
error: [
|
|
{ x: 1080, y: 262 },
|
|
{ x: 1138, y: 250 },
|
|
{ x: 1078, y: 324 },
|
|
],
|
|
}
|
|
|
|
const DEMO_VISITORS = [
|
|
{ agentId: 'demo-nika', name: 'Nika', state: 'researching' },
|
|
{ agentId: 'demo-mercury', name: 'Mercury', state: 'writing' },
|
|
{ agentId: 'demo-echo', name: 'Echo', state: 'syncing' },
|
|
]
|
|
|
|
let game
|
|
let mainState = 'idle'
|
|
let mainDetail = STATUS_META.idle.detail
|
|
let lastPollAt = 0
|
|
let lastBubbleAt = 0
|
|
let lastVisitorShuffleAt = 0
|
|
let lastCatBubbleAt = 0
|
|
let lastAgentsCount = 1
|
|
let lastRemoteAgents = []
|
|
|
|
let officeBgSprite
|
|
let sofa
|
|
let sofaShadow
|
|
let starIdle
|
|
let starWorking
|
|
let serverRoom
|
|
let syncAnim
|
|
let errorBug
|
|
let mainBubble = null
|
|
let catBubble = null
|
|
let guestSprites = {}
|
|
|
|
const statusText = document.getElementById('status-text')
|
|
const stateChip = document.getElementById('live-state-chip')
|
|
const agentsCount = document.getElementById('agents-count')
|
|
const memoDate = document.getElementById('memo-date')
|
|
const memoContent = document.getElementById('memo-content')
|
|
const loadingText = document.getElementById('loading-text')
|
|
const loadingBar = document.getElementById('loading-progress-bar')
|
|
const loadingOverlay = document.getElementById('loading-overlay')
|
|
|
|
function setLoading(progress, text) {
|
|
if (loadingBar) loadingBar.style.width = `${progress}%`
|
|
if (loadingText) loadingText.textContent = text
|
|
}
|
|
|
|
function hideLoading() {
|
|
if (!loadingOverlay) return
|
|
loadingOverlay.style.transition = 'opacity .35s ease'
|
|
loadingOverlay.style.opacity = '0'
|
|
window.setTimeout(() => {
|
|
loadingOverlay.style.display = 'none'
|
|
}, 350)
|
|
}
|
|
|
|
async function fetchJson(path) {
|
|
const response = await fetch(path, { cache: 'no-store' })
|
|
if (!response.ok) {
|
|
throw new Error(`${response.status} ${response.statusText}`)
|
|
}
|
|
return response.json()
|
|
}
|
|
|
|
async function loadMemo() {
|
|
try {
|
|
const payload = await fetchJson(`${API_BASE}/yesterday-memo?t=${Date.now()}`)
|
|
memoDate.textContent = payload.date || 'memo'
|
|
memoContent.textContent = payload.memo || 'No memo available.'
|
|
} catch (error) {
|
|
memoDate.textContent = 'memo unavailable'
|
|
memoContent.textContent = 'Unable to load the office memo.'
|
|
console.error('Failed to load office memo', error)
|
|
}
|
|
}
|
|
|
|
function normalizeState(state) {
|
|
const raw = String(state || '').trim().toLowerCase()
|
|
if (!raw) return 'idle'
|
|
if (raw === 'working' || raw === 'run' || raw === 'running') return 'executing'
|
|
if (raw === 'sync') return 'syncing'
|
|
if (raw === 'research') return 'researching'
|
|
return STATUS_META[raw] ? raw : 'idle'
|
|
}
|
|
|
|
function updateHud() {
|
|
const meta = STATUS_META[mainState] || STATUS_META.idle
|
|
statusText.textContent = `[${meta.label}] ${mainDetail || meta.detail}`
|
|
stateChip.textContent = `STATE / ${meta.label}`
|
|
agentsCount.textContent = String(lastAgentsCount)
|
|
}
|
|
|
|
function clearBubble(target) {
|
|
if (target) {
|
|
target.destroy()
|
|
}
|
|
}
|
|
|
|
function spawnBubble(anchorX, anchorY, text, tint = 0xffffff) {
|
|
const bg = game.add.rectangle(anchorX, anchorY, Math.max(110, text.length * 8 + 24), 28, 0xffffff, 0.96)
|
|
bg.setStrokeStyle(2, tint)
|
|
const label = game.add.text(anchorX, anchorY, text, {
|
|
fontFamily: 'ArkPixel, ArkPixelLatin, monospace',
|
|
fontSize: '11px',
|
|
color: '#031019',
|
|
align: 'center',
|
|
}).setOrigin(0.5)
|
|
const container = game.add.container(0, 0, [bg, label])
|
|
container.setDepth(2400)
|
|
return container
|
|
}
|
|
|
|
function maybeShowMainBubble(time) {
|
|
if (!game || mainState === 'idle') return
|
|
if (time - lastBubbleAt < 7000) return
|
|
lastBubbleAt = time
|
|
|
|
clearBubble(mainBubble)
|
|
const meta = STATUS_META[mainState] || STATUS_META.idle
|
|
const text = meta.bubble[Math.floor(Math.random() * meta.bubble.length)]
|
|
|
|
let anchor = AREA_POSITIONS.breakroom
|
|
if (mainState === 'syncing') anchor = AREA_POSITIONS.syncing
|
|
else if (mainState === 'error') anchor = AREA_POSITIONS.error
|
|
else if (mainState !== 'idle') anchor = AREA_POSITIONS.writing
|
|
|
|
mainBubble = spawnBubble(anchor.x, anchor.y - 88, text, 0x61d3ff)
|
|
window.setTimeout(() => {
|
|
clearBubble(mainBubble)
|
|
mainBubble = null
|
|
}, 3200)
|
|
}
|
|
|
|
function maybeShowCatBubble(time) {
|
|
if (!game || !window.catSprite) return
|
|
if (time - lastCatBubbleAt < 17000) return
|
|
lastCatBubbleAt = time
|
|
clearBubble(catBubble)
|
|
catBubble = spawnBubble(window.catSprite.x, window.catSprite.y - 64, 'mrrp', 0xd4a574)
|
|
window.setTimeout(() => {
|
|
clearBubble(catBubble)
|
|
catBubble = null
|
|
}, 2200)
|
|
}
|
|
|
|
function applyMainState(nextState, detail) {
|
|
mainState = normalizeState(nextState)
|
|
mainDetail = detail || STATUS_META[mainState].detail
|
|
updateHud()
|
|
|
|
sofa.setTexture('sofa_idle')
|
|
|
|
if (mainState === 'idle') {
|
|
starIdle.setVisible(true)
|
|
if (!starIdle.anims.isPlaying) starIdle.anims.play('star_idle', true)
|
|
starWorking.setVisible(false)
|
|
starWorking.anims.stop()
|
|
syncAnim.setVisible(false)
|
|
syncAnim.anims.stop()
|
|
syncAnim.setFrame(0)
|
|
errorBug.setVisible(false)
|
|
errorBug.anims.stop()
|
|
serverRoom.anims.stop()
|
|
serverRoom.setFrame(0)
|
|
return
|
|
}
|
|
|
|
starIdle.setVisible(false)
|
|
starIdle.anims.stop()
|
|
|
|
if (mainState === 'syncing') {
|
|
starWorking.setVisible(false)
|
|
starWorking.anims.stop()
|
|
errorBug.setVisible(false)
|
|
errorBug.anims.stop()
|
|
syncAnim.setVisible(true)
|
|
syncAnim.anims.play('sync_anim', true)
|
|
serverRoom.anims.play('serverroom_on', true)
|
|
return
|
|
}
|
|
|
|
syncAnim.setVisible(false)
|
|
syncAnim.anims.stop()
|
|
syncAnim.setFrame(0)
|
|
|
|
if (mainState === 'error') {
|
|
starWorking.setVisible(false)
|
|
starWorking.anims.stop()
|
|
errorBug.setVisible(true)
|
|
errorBug.anims.play('error_bug', true)
|
|
serverRoom.anims.play('serverroom_on', true)
|
|
return
|
|
}
|
|
|
|
errorBug.setVisible(false)
|
|
errorBug.anims.stop()
|
|
starWorking.setVisible(true)
|
|
starWorking.anims.play('star_working', true)
|
|
serverRoom.anims.play('serverroom_on', true)
|
|
}
|
|
|
|
function randomizeDemoVisitors() {
|
|
const states = Object.keys(STATUS_META)
|
|
DEMO_VISITORS.forEach((agent, index) => {
|
|
agent.state = states[(index + Math.floor(Math.random() * states.length)) % states.length]
|
|
})
|
|
}
|
|
|
|
function destroyGuestSprites() {
|
|
Object.values(guestSprites).forEach((entry) => entry.destroy())
|
|
guestSprites = {}
|
|
}
|
|
|
|
function renderVisitors(apiAgents = []) {
|
|
const visitors = apiAgents.filter((agent) => !agent.isMain)
|
|
const effectiveVisitors = visitors.length > 0 ? visitors : DEMO_VISITORS
|
|
destroyGuestSprites()
|
|
lastAgentsCount = Math.max(1, apiAgents.length)
|
|
updateHud()
|
|
|
|
effectiveVisitors.forEach((agent, index) => {
|
|
const state = normalizeState(agent.state)
|
|
const area = STATUS_META[state]?.area || 'breakroom'
|
|
const slot = VISITOR_SLOTS[area][index % VISITOR_SLOTS[area].length]
|
|
const sheetKey = `guest_anim_${(index % 6) + 1}`
|
|
const sprite = game.add.sprite(slot.x, slot.y, sheetKey, 0).setOrigin(0.5)
|
|
const animKey = `${sheetKey}_idle`
|
|
if (game.anims.exists(animKey)) {
|
|
sprite.anims.play(animKey, true)
|
|
}
|
|
sprite.setDepth(1200)
|
|
const label = game.add.text(slot.x, slot.y - 34, agent.name, {
|
|
fontFamily: 'ArkPixel, ArkPixelLatin, monospace',
|
|
fontSize: '11px',
|
|
color: '#ecfdf5',
|
|
stroke: '#0f172a',
|
|
strokeThickness: 4,
|
|
}).setOrigin(0.5)
|
|
label.setDepth(1201)
|
|
guestSprites[agent.agentId || `demo-${index}`] = game.add.container(0, 0, [sprite, label])
|
|
})
|
|
}
|
|
|
|
async function pollOfficeState(force = false) {
|
|
const now = Date.now()
|
|
if (!force && now - lastPollAt < POLL_MS) return
|
|
lastPollAt = now
|
|
|
|
try {
|
|
const [statusPayload, agentsPayload] = await Promise.all([
|
|
fetchJson(`${API_BASE}/status?t=${now}`),
|
|
fetchJson(`${API_BASE}/agents?t=${now}`),
|
|
])
|
|
applyMainState(statusPayload.state, statusPayload.detail)
|
|
lastRemoteAgents = Array.isArray(agentsPayload) ? agentsPayload : []
|
|
renderVisitors(lastRemoteAgents)
|
|
} catch (error) {
|
|
applyMainState('error', 'Office API unavailable')
|
|
console.error('Failed to poll office state', error)
|
|
}
|
|
}
|
|
|
|
function preload() {
|
|
setLoading(2, 'Loading Star Office room...')
|
|
|
|
const fileCount = 17
|
|
let loadedCount = 0
|
|
|
|
this.load.on('filecomplete', () => {
|
|
loadedCount += 1
|
|
setLoading(Math.min(95, Math.round((loadedCount / fileCount) * 100)), 'Streaming pixel office assets...')
|
|
})
|
|
|
|
this.load.on('complete', () => {
|
|
setLoading(100, 'Star Office ready.')
|
|
hideLoading()
|
|
})
|
|
|
|
this.load.image('office_bg', `${ASSET_BASE}/office_bg_small.webp`)
|
|
this.load.spritesheet('star_idle', `${ASSET_BASE}/star-idle-v5.png`, { frameWidth: 256, frameHeight: 256 })
|
|
this.load.image('sofa_idle', `${ASSET_BASE}/sofa-idle-v3.png`)
|
|
this.load.image('sofa_shadow', `${ASSET_BASE}/sofa-shadow-v1.png`)
|
|
this.load.spritesheet('plants', `${ASSET_BASE}/plants-spritesheet.webp`, { frameWidth: 160, frameHeight: 160 })
|
|
this.load.spritesheet('posters', `${ASSET_BASE}/posters-spritesheet.webp`, { frameWidth: 160, frameHeight: 160 })
|
|
this.load.spritesheet('coffee_machine', `${ASSET_BASE}/coffee-machine-v3-grid.webp`, { frameWidth: 230, frameHeight: 230 })
|
|
this.load.image('coffee_machine_shadow', `${ASSET_BASE}/coffee-machine-shadow-v1.png`)
|
|
this.load.spritesheet('serverroom', `${ASSET_BASE}/serverroom-spritesheet.webp`, { frameWidth: 180, frameHeight: 251 })
|
|
this.load.spritesheet('error_bug', `${ASSET_BASE}/error-bug-spritesheet-grid.webp`, { frameWidth: 220, frameHeight: 220 })
|
|
this.load.spritesheet('cats', `${ASSET_BASE}/cats-spritesheet.webp`, { frameWidth: 160, frameHeight: 160 })
|
|
this.load.spritesheet('star_working', `${ASSET_BASE}/star-working-spritesheet-grid.webp`, { frameWidth: 300, frameHeight: 300 })
|
|
this.load.spritesheet('sync_anim', `${ASSET_BASE}/sync-animation-v3-grid.webp`, { frameWidth: 256, frameHeight: 256 })
|
|
this.load.image('desk_v2', `${ASSET_BASE}/desk-v3.webp`)
|
|
this.load.spritesheet('flowers', `${ASSET_BASE}/flowers-bloom-v2.webp`, { frameWidth: 128, frameHeight: 128 })
|
|
|
|
for (let index = 1; index <= 6; index += 1) {
|
|
this.load.spritesheet(`guest_anim_${index}`, `${ASSET_BASE}/guest_anim_${index}.webp`, { frameWidth: 32, frameHeight: 32 })
|
|
}
|
|
}
|
|
|
|
function create() {
|
|
game = this
|
|
officeBgSprite = this.add.image(640, 360, 'office_bg')
|
|
officeBgSprite.setDisplaySize(GAME_WIDTH, GAME_HEIGHT)
|
|
|
|
sofaShadow = this.add.image(798, 272, 'sofa_shadow').setOrigin(0.5)
|
|
sofaShadow.setDepth(9)
|
|
sofa = this.add.sprite(798, 272, 'sofa_idle').setOrigin(0.5)
|
|
sofa.setDepth(10)
|
|
|
|
this.anims.create({
|
|
key: 'star_idle',
|
|
frames: this.anims.generateFrameNumbers('star_idle', { start: 0, end: Math.max(0, this.textures.get('star_idle').frameTotal - 1) }),
|
|
frameRate: 12,
|
|
repeat: -1,
|
|
})
|
|
|
|
this.anims.create({
|
|
key: 'coffee_machine',
|
|
frames: this.anims.generateFrameNumbers('coffee_machine', { start: 0, end: Math.max(0, this.textures.get('coffee_machine').frameTotal - 2) }),
|
|
frameRate: 12,
|
|
repeat: -1,
|
|
})
|
|
|
|
this.anims.create({
|
|
key: 'serverroom_on',
|
|
frames: this.anims.generateFrameNumbers('serverroom', { start: 0, end: Math.max(0, this.textures.get('serverroom').frameTotal - 2) }),
|
|
frameRate: 6,
|
|
repeat: -1,
|
|
})
|
|
|
|
this.anims.create({
|
|
key: 'star_working',
|
|
frames: this.anims.generateFrameNumbers('star_working', { start: 0, end: 37 }),
|
|
frameRate: 12,
|
|
repeat: -1,
|
|
})
|
|
|
|
this.anims.create({
|
|
key: 'error_bug',
|
|
frames: this.anims.generateFrameNumbers('error_bug', { start: 0, end: 71 }),
|
|
frameRate: 12,
|
|
repeat: -1,
|
|
})
|
|
|
|
this.anims.create({
|
|
key: 'sync_anim',
|
|
frames: this.anims.generateFrameNumbers('sync_anim', { start: 1, end: 52 }),
|
|
frameRate: 12,
|
|
repeat: -1,
|
|
})
|
|
|
|
for (let index = 1; index <= 6; index += 1) {
|
|
this.anims.create({
|
|
key: `guest_anim_${index}_idle`,
|
|
frames: this.anims.generateFrameNumbers(`guest_anim_${index}`, { start: 0, end: 7 }),
|
|
frameRate: 8,
|
|
repeat: -1,
|
|
})
|
|
}
|
|
|
|
starIdle = game.physics.add.sprite(798, 272, 'star_idle')
|
|
starIdle.setOrigin(0.5)
|
|
starIdle.setScale(0.58)
|
|
starIdle.setDepth(20)
|
|
starIdle.anims.play('star_idle', true)
|
|
|
|
const plantFrames = 16
|
|
;[
|
|
[565, 178],
|
|
[230, 185],
|
|
[977, 496],
|
|
].forEach(([x, y]) => {
|
|
const plant = game.add.sprite(x, y, 'plants', Math.floor(Math.random() * plantFrames)).setOrigin(0.5)
|
|
plant.setDepth(5)
|
|
})
|
|
|
|
const poster = game.add.sprite(252, 66, 'posters', Math.floor(Math.random() * Math.max(1, this.textures.get('posters').frameTotal - 1))).setOrigin(0.5)
|
|
poster.setDepth(4)
|
|
|
|
const cat = game.add.sprite(94, 557, 'cats', Math.floor(Math.random() * Math.max(1, this.textures.get('cats').frameTotal - 1))).setOrigin(0.5)
|
|
cat.setDepth(2000)
|
|
window.catSprite = cat
|
|
|
|
const coffeeShadow = this.add.image(659, 397, 'coffee_machine_shadow').setOrigin(0.5)
|
|
coffeeShadow.setDepth(98)
|
|
const coffeeMachine = this.add.sprite(659, 397, 'coffee_machine').setOrigin(0.5)
|
|
coffeeMachine.setDepth(99)
|
|
coffeeMachine.anims.play('coffee_machine', true)
|
|
|
|
serverRoom = this.add.sprite(1021, 142, 'serverroom', 0).setOrigin(0.5)
|
|
serverRoom.setDepth(2)
|
|
serverRoom.setFrame(0)
|
|
|
|
const desk = this.add.image(218, 417, 'desk_v2').setOrigin(0.5)
|
|
desk.setDepth(1001)
|
|
|
|
const flower = this.add.sprite(310, 390, 'flowers', Math.floor(Math.random() * 16)).setOrigin(0.5)
|
|
flower.setScale(0.8)
|
|
flower.setDepth(1100)
|
|
|
|
errorBug = this.add.sprite(1007, 221, 'error_bug', 0).setOrigin(0.5)
|
|
errorBug.setDepth(50)
|
|
errorBug.setScale(0.9)
|
|
errorBug.setVisible(false)
|
|
|
|
starWorking = this.add.sprite(217, 343, 'star_working', 0).setOrigin(0.5)
|
|
starWorking.setVisible(false)
|
|
starWorking.setScale(0.9)
|
|
starWorking.setDepth(900)
|
|
|
|
syncAnim = this.add.sprite(1157, 592, 'sync_anim', 0).setOrigin(0.5)
|
|
syncAnim.setDepth(40)
|
|
syncAnim.setVisible(false)
|
|
|
|
applyMainState('idle', STATUS_META.idle.detail)
|
|
randomizeDemoVisitors()
|
|
renderVisitors([])
|
|
pollOfficeState(true)
|
|
loadMemo()
|
|
}
|
|
|
|
function update(time) {
|
|
if (time - lastPollAt > POLL_MS) {
|
|
pollOfficeState()
|
|
}
|
|
|
|
if (time - lastVisitorShuffleAt > 8000) {
|
|
lastVisitorShuffleAt = time
|
|
randomizeDemoVisitors()
|
|
if (lastRemoteAgents.filter((agent) => !agent.isMain).length === 0) {
|
|
renderVisitors(lastRemoteAgents)
|
|
}
|
|
}
|
|
|
|
maybeShowMainBubble(time)
|
|
maybeShowCatBubble(time)
|
|
}
|
|
|
|
const config = {
|
|
type: Phaser.AUTO,
|
|
width: GAME_WIDTH,
|
|
height: GAME_HEIGHT,
|
|
parent: 'game-container',
|
|
pixelArt: true,
|
|
physics: {
|
|
default: 'arcade',
|
|
arcade: { gravity: { y: 0 }, debug: false },
|
|
},
|
|
scene: { preload, create, update },
|
|
}
|
|
|
|
setLoading(0, 'Booting Star Office runtime...')
|
|
new Phaser.Game(config)
|
|
})()
|