From d6ae48dcec899909c7855151979ceb8c13439ef9 Mon Sep 17 00:00:00 2001 From: lpf Date: Thu, 5 Mar 2026 15:01:06 +0800 Subject: [PATCH] feat(office): render Star furniture layers with animated decor --- webui/src/components/office/OfficeScene.tsx | 183 ++++++++++++++++++++ webui/src/components/office/officeLayout.ts | 14 +- 2 files changed, 190 insertions(+), 7 deletions(-) diff --git a/webui/src/components/office/OfficeScene.tsx b/webui/src/components/office/OfficeScene.tsx index c86a65b..c053f14 100644 --- a/webui/src/components/office/OfficeScene.tsx +++ b/webui/src/components/office/OfficeScene.tsx @@ -35,6 +35,13 @@ type SpriteSpec = { scale: number; }; +type ImageSpec = { + src: string; + width: number; + height: number; + scale: number; +}; + const TICK_FPS = 12; const MAIN_SPRITES: Record<'idle' | 'working' | 'syncing' | 'error', SpriteSpec> = { @@ -91,6 +98,96 @@ const NODE_SPRITES: SpriteSpec[] = Array.from({ length: 6 }, (_, i) => ({ scale: 1.55, })); +const DECOR_SPRITES = { + plants: { + src: '/webui/office/plants-spritesheet.webp', + frameW: 160, + frameH: 160, + cols: 4, + start: 0, + end: 15, + fps: 0, + scale: 1, + } as SpriteSpec, + posters: { + src: '/webui/office/posters-spritesheet.webp', + frameW: 160, + frameH: 160, + cols: 4, + start: 0, + end: 31, + fps: 0, + scale: 1, + } as SpriteSpec, + flowers: { + src: '/webui/office/flowers-bloom-v2.webp', + frameW: 128, + frameH: 128, + cols: 4, + start: 0, + end: 15, + fps: 0, + scale: 0.8, + } as SpriteSpec, + cats: { + src: '/webui/office/cats-spritesheet.webp', + frameW: 160, + frameH: 160, + cols: 4, + start: 0, + end: 15, + fps: 0, + scale: 1, + } as SpriteSpec, + coffeeMachine: { + src: '/webui/office/coffee-machine-v3-grid.webp', + frameW: 230, + frameH: 230, + cols: 12, + start: 0, + end: 94, + fps: 10, + scale: 1, + } as SpriteSpec, + serverroom: { + src: '/webui/office/serverroom-spritesheet.webp', + frameW: 180, + frameH: 251, + cols: 40, + start: 0, + end: 38, + fps: 6, + scale: 1, + } as SpriteSpec, +}; + +const DECOR_IMAGES = { + sofaIdle: { + src: '/webui/office/sofa-idle-v3.png', + width: 212, + height: 143, + scale: 1, + } as ImageSpec, + sofaShadow: { + src: '/webui/office/sofa-shadow-v1.png', + width: 233, + height: 81, + scale: 1, + } as ImageSpec, + desk: { + src: '/webui/office/desk-v3.webp', + width: 304, + height: 264, + scale: 1, + } as ImageSpec, + coffeeShadow: { + src: '/webui/office/coffee-machine-shadow-v1.png', + width: 245, + height: 111, + scale: 1, + } as ImageSpec, +}; + function normalizeZone(z: string | undefined): OfficeZone { const v = (z || '').trim().toLowerCase(); if (v === 'work' || v === 'server' || v === 'bug' || v === 'breakroom') return v; @@ -120,6 +217,19 @@ function frameAtTick(spec: SpriteSpec, tick: number, seed = 0): number { return spec.start + (frame % frameCount); } +function frameFromSeed(spec: SpriteSpec, seedText: string): number { + const frameCount = Math.max(1, spec.end - spec.start + 1); + return spec.start + (textHash(seedText) % frameCount); +} + +function posStyle(x: number, y: number, zIndex: number): React.CSSProperties { + return { + left: `${(x / OFFICE_CANVAS.width) * 100}%`, + top: `${(y / OFFICE_CANVAS.height) * 100}%`, + zIndex, + }; +} + type SpriteProps = { spec: SpriteSpec; frame: number; @@ -146,6 +256,48 @@ const SpriteSheet: React.FC = ({ spec, frame, className }) => { ); }; +type PlacedSpriteProps = { + spec: SpriteSpec; + frame: number; + x: number; + y: number; + zIndex: number; + title?: string; +}; + +const PlacedSprite: React.FC = ({ spec, frame, x, y, zIndex, title }) => ( +
+
+ +
+
+); + +type PlacedImageProps = { + spec: ImageSpec; + x: number; + y: number; + zIndex: number; + title?: string; +}; + +const PlacedImage: React.FC = ({ spec, x, y, zIndex, title }) => ( +
+ +
+); + const OfficeScene: React.FC = ({ main, nodes }) => { const [tick, setTick] = useState(0); @@ -175,17 +327,45 @@ const OfficeScene: React.FC = ({ main, nodes }) => { const mainPoint = OFFICE_ZONE_POINT[mainZone]; const mainSprite = MAIN_SPRITES[normalizeMainSpriteState(main.state)]; const mainFrame = frameAtTick(mainSprite, tick); + const mainSpriteState = normalizeMainSpriteState(main.state); + + const decorSeedBase = `${main.id || 'main'}|${main.name || ''}`; + const plantFrameA = frameFromSeed(DECOR_SPRITES.plants, `${decorSeedBase}|plantA`); + const plantFrameB = frameFromSeed(DECOR_SPRITES.plants, `${decorSeedBase}|plantB`); + const plantFrameC = frameFromSeed(DECOR_SPRITES.plants, `${decorSeedBase}|plantC`); + const posterFrame = frameFromSeed(DECOR_SPRITES.posters, `${decorSeedBase}|poster`); + const flowerFrame = frameFromSeed(DECOR_SPRITES.flowers, `${decorSeedBase}|flower`); + const catFrame = frameFromSeed(DECOR_SPRITES.cats, `${decorSeedBase}|cat`); + const coffeeFrame = frameAtTick(DECOR_SPRITES.coffeeMachine, tick, 300); + const serverFrame = mainSpriteState === 'idle' ? 0 : frameAtTick(DECOR_SPRITES.serverroom, tick, 700); return (
office
+ + + + + + + + + + + + + + + +
@@ -204,6 +384,7 @@ const OfficeScene: React.FC = ({ main, nodes }) => { style={{ left: `${(n.point.x / OFFICE_CANVAS.width) * 100}%`, top: `${(n.point.y / OFFICE_CANVAS.height) * 100}%`, + zIndex: 51, }} title={`${n.name || n.id || 'node'} · ${n.state || 'idle'}${n.detail ? ` · ${n.detail}` : ''}`} > @@ -216,6 +397,8 @@ const OfficeScene: React.FC = ({ main, nodes }) => {
))} + +
diff --git a/webui/src/components/office/officeLayout.ts b/webui/src/components/office/officeLayout.ts index 717812d..290b0de 100644 --- a/webui/src/components/office/officeLayout.ts +++ b/webui/src/components/office/officeLayout.ts @@ -6,7 +6,7 @@ export const OFFICE_CANVAS = { }; export const OFFICE_ZONE_POINT: Record = { - breakroom: { x: 760, y: 325 }, + breakroom: { x: 1070, y: 610 }, work: { x: 300, y: 365 }, server: { x: 1010, y: 235 }, bug: { x: 1125, y: 245 }, @@ -14,12 +14,12 @@ export const OFFICE_ZONE_POINT: Record = { export const OFFICE_ZONE_SLOTS: Record> = { breakroom: [ - { x: 700, y: 360 }, - { x: 760, y: 355 }, - { x: 820, y: 350 }, - { x: 730, y: 420 }, - { x: 790, y: 420 }, - { x: 850, y: 410 }, + { x: 1020, y: 620 }, + { x: 1090, y: 620 }, + { x: 1150, y: 620 }, + { x: 1040, y: 670 }, + { x: 1110, y: 670 }, + { x: 1180, y: 670 }, ], work: [ { x: 240, y: 350 },