@neolio42/pixel-office 0.1.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/LICENSE +21 -0
- package/README.md +136 -0
- package/bin.sh +16 -0
- package/bin.ts +162 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +7 -0
- package/package.json +51 -0
- package/postcss.config.mjs +7 -0
- package/public/assets/characters/char_0.png +0 -0
- package/public/assets/characters/char_1.png +0 -0
- package/public/assets/characters/char_2.png +0 -0
- package/public/assets/characters/char_3.png +0 -0
- package/public/assets/characters/char_4.png +0 -0
- package/public/assets/characters/char_5.png +0 -0
- package/public/assets/characters.png +0 -0
- package/public/assets/default-layout-1.json +92 -0
- package/public/assets/floors/floor_0.png +0 -0
- package/public/assets/floors/floor_1.png +0 -0
- package/public/assets/floors/floor_2.png +0 -0
- package/public/assets/floors/floor_3.png +0 -0
- package/public/assets/floors/floor_4.png +0 -0
- package/public/assets/floors/floor_5.png +0 -0
- package/public/assets/floors/floor_6.png +0 -0
- package/public/assets/floors/floor_7.png +0 -0
- package/public/assets/floors/floor_8.png +0 -0
- package/public/assets/furniture/BIN/BIN.png +0 -0
- package/public/assets/furniture/BIN/manifest.json +13 -0
- package/public/assets/furniture/BOOKSHELF/BOOKSHELF.png +0 -0
- package/public/assets/furniture/BOOKSHELF/manifest.json +13 -0
- package/public/assets/furniture/CACTUS/CACTUS.png +0 -0
- package/public/assets/furniture/CACTUS/manifest.json +13 -0
- package/public/assets/furniture/CLOCK/CLOCK.png +0 -0
- package/public/assets/furniture/CLOCK/manifest.json +13 -0
- package/public/assets/furniture/COFFEE/COFFEE.png +0 -0
- package/public/assets/furniture/COFFEE/manifest.json +13 -0
- package/public/assets/furniture/COFFEE_TABLE/COFFEE_TABLE.png +0 -0
- package/public/assets/furniture/COFFEE_TABLE/manifest.json +13 -0
- package/public/assets/furniture/CUSHIONED_BENCH/CUSHIONED_BENCH.png +0 -0
- package/public/assets/furniture/CUSHIONED_BENCH/manifest.json +13 -0
- package/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_BACK.png +0 -0
- package/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_FRONT.png +0 -0
- package/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_SIDE.png +0 -0
- package/public/assets/furniture/CUSHIONED_CHAIR/manifest.json +44 -0
- package/public/assets/furniture/DESK/DESK_FRONT.png +0 -0
- package/public/assets/furniture/DESK/DESK_SIDE.png +0 -0
- package/public/assets/furniture/DESK/manifest.json +33 -0
- package/public/assets/furniture/DOUBLE_BOOKSHELF/DOUBLE_BOOKSHELF.png +0 -0
- package/public/assets/furniture/DOUBLE_BOOKSHELF/manifest.json +13 -0
- package/public/assets/furniture/HANGING_PLANT/HANGING_PLANT.png +0 -0
- package/public/assets/furniture/HANGING_PLANT/manifest.json +13 -0
- package/public/assets/furniture/LARGE_PAINTING/LARGE_PAINTING.png +0 -0
- package/public/assets/furniture/LARGE_PAINTING/manifest.json +13 -0
- package/public/assets/furniture/LARGE_PLANT/LARGE_PLANT.png +0 -0
- package/public/assets/furniture/LARGE_PLANT/manifest.json +13 -0
- package/public/assets/furniture/PC/PC_BACK.png +0 -0
- package/public/assets/furniture/PC/PC_FRONT_OFF.png +0 -0
- package/public/assets/furniture/PC/PC_FRONT_ON_1.png +0 -0
- package/public/assets/furniture/PC/PC_FRONT_ON_2.png +0 -0
- package/public/assets/furniture/PC/PC_FRONT_ON_3.png +0 -0
- package/public/assets/furniture/PC/PC_SIDE.png +0 -0
- package/public/assets/furniture/PC/manifest.json +88 -0
- package/public/assets/furniture/PLANT/PLANT.png +0 -0
- package/public/assets/furniture/PLANT/manifest.json +13 -0
- package/public/assets/furniture/PLANT_2/PLANT_2.png +0 -0
- package/public/assets/furniture/PLANT_2/manifest.json +13 -0
- package/public/assets/furniture/POT/POT.png +0 -0
- package/public/assets/furniture/POT/manifest.json +13 -0
- package/public/assets/furniture/SMALL_PAINTING/SMALL_PAINTING.png +0 -0
- package/public/assets/furniture/SMALL_PAINTING/manifest.json +13 -0
- package/public/assets/furniture/SMALL_PAINTING_2/SMALL_PAINTING_2.png +0 -0
- package/public/assets/furniture/SMALL_PAINTING_2/manifest.json +13 -0
- package/public/assets/furniture/SMALL_TABLE/SMALL_TABLE_FRONT.png +0 -0
- package/public/assets/furniture/SMALL_TABLE/SMALL_TABLE_SIDE.png +0 -0
- package/public/assets/furniture/SMALL_TABLE/manifest.json +33 -0
- package/public/assets/furniture/SOFA/SOFA_BACK.png +0 -0
- package/public/assets/furniture/SOFA/SOFA_FRONT.png +0 -0
- package/public/assets/furniture/SOFA/SOFA_SIDE.png +0 -0
- package/public/assets/furniture/SOFA/manifest.json +44 -0
- package/public/assets/furniture/TABLE_FRONT/TABLE_FRONT.png +0 -0
- package/public/assets/furniture/TABLE_FRONT/manifest.json +13 -0
- package/public/assets/furniture/WHITEBOARD/WHITEBOARD.png +0 -0
- package/public/assets/furniture/WHITEBOARD/manifest.json +13 -0
- package/public/assets/furniture/WOODEN_BENCH/WOODEN_BENCH.png +0 -0
- package/public/assets/furniture/WOODEN_BENCH/manifest.json +13 -0
- package/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_BACK.png +0 -0
- package/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_FRONT.png +0 -0
- package/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_SIDE.png +0 -0
- package/public/assets/furniture/WOODEN_CHAIR/manifest.json +44 -0
- package/public/assets/walls/wall_0.png +0 -0
- package/scripts/setup.ts +158 -0
- package/server.ts +53 -0
- package/src/app/api/focus-terminal/route.ts +65 -0
- package/src/app/api/hooks/notification/route.ts +19 -0
- package/src/app/api/hooks/post-tool-use/route.ts +26 -0
- package/src/app/api/hooks/pre-tool-use/route.ts +189 -0
- package/src/app/api/hooks/session-end/route.ts +31 -0
- package/src/app/api/hooks/session-start/route.ts +47 -0
- package/src/app/api/hooks/stop/route.ts +19 -0
- package/src/app/api/hooks/user-prompt/route.ts +92 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +21 -0
- package/src/app/page.tsx +5 -0
- package/src/components/ApprovalToast.tsx +132 -0
- package/src/components/OfficeCanvas.tsx +311 -0
- package/src/components/Terminal.tsx +177 -0
- package/src/components/TerminalTile.tsx +181 -0
- package/src/components/WorkerPanel.tsx +261 -0
- package/src/components/WorkerPopup.tsx +116 -0
- package/src/game/asset-loader.ts +172 -0
- package/src/game/office-layout.ts +287 -0
- package/src/game/renderer.ts +369 -0
- package/src/game/sprites.ts +133 -0
- package/src/game/worker-entity.ts +219 -0
- package/src/hooks/usePixelOffice.ts +318 -0
- package/src/hooks/useRecentCwds.ts +27 -0
- package/src/lib/approval-queue.ts +67 -0
- package/src/lib/pty-manager.ts +267 -0
- package/src/lib/store.ts +181 -0
- package/src/lib/tool-classifier.ts +224 -0
- package/src/lib/transcript.ts +109 -0
- package/src/lib/types.ts +58 -0
- package/src/lib/ws-server.ts +270 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Session } from '@/lib/types';
|
|
5
|
+
|
|
6
|
+
const STATE_COLORS: Record<string, string> = {
|
|
7
|
+
idle: '#444',
|
|
8
|
+
typing: '#4a7cbf',
|
|
9
|
+
reading: '#4abf5c',
|
|
10
|
+
waiting: '#bf8b4a',
|
|
11
|
+
walking: '#8b4abf',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
session: Session;
|
|
16
|
+
anchorX: number;
|
|
17
|
+
anchorY: number;
|
|
18
|
+
viewportW: number;
|
|
19
|
+
viewportH: number;
|
|
20
|
+
onDismiss: () => void;
|
|
21
|
+
onOpenTerminal?: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function WorkerPopup({ session, anchorX, anchorY, viewportW, viewportH, onDismiss, onOpenTerminal }: Props) {
|
|
25
|
+
const [focusing, setFocusing] = useState(false);
|
|
26
|
+
|
|
27
|
+
async function handleFocusTerminal() {
|
|
28
|
+
setFocusing(true);
|
|
29
|
+
try {
|
|
30
|
+
await fetch('/api/focus-terminal', {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
body: JSON.stringify({ sessionId: session.sessionId }),
|
|
34
|
+
});
|
|
35
|
+
} finally {
|
|
36
|
+
setFocusing(false);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const popupW = 250;
|
|
41
|
+
const popupH = 140;
|
|
42
|
+
|
|
43
|
+
let left = anchorX + 12;
|
|
44
|
+
let top = anchorY - popupH - 12;
|
|
45
|
+
if (left + popupW > viewportW - 8) left = anchorX - popupW - 12;
|
|
46
|
+
if (left < 8) left = 8;
|
|
47
|
+
if (top < 8) top = anchorY + 40;
|
|
48
|
+
if (top + popupH > viewportH - 8) top = viewportH - popupH - 8;
|
|
49
|
+
|
|
50
|
+
const project = session.cwd.split('/').filter(Boolean).pop() || session.cwd;
|
|
51
|
+
const stateColor = STATE_COLORS[session.state] ?? '#444';
|
|
52
|
+
const minutesAgo = Math.floor((Date.now() - session.startedAt) / 60000);
|
|
53
|
+
const duration = minutesAgo < 1 ? 'just now' : `${minutesAgo}m`;
|
|
54
|
+
const focus = session.currentFocus;
|
|
55
|
+
const lastTool = session.recentTools[session.recentTools.length - 1];
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
className="fixed z-20 pointer-events-auto"
|
|
60
|
+
style={{ left, top, width: popupW }}
|
|
61
|
+
>
|
|
62
|
+
<div
|
|
63
|
+
className="bg-[#0e0e1e] border border-[#1e1e3e] rounded-lg p-2.5"
|
|
64
|
+
style={{ boxShadow: '0 4px 20px rgba(0,0,0,0.7)' }}
|
|
65
|
+
>
|
|
66
|
+
<div className="flex items-center gap-1.5 mb-1.5">
|
|
67
|
+
<div
|
|
68
|
+
className="w-1.5 h-1.5 rounded-full flex-shrink-0"
|
|
69
|
+
style={{ backgroundColor: stateColor }}
|
|
70
|
+
/>
|
|
71
|
+
<span className="text-[#99a] text-[11px] font-mono font-bold flex-1">{project}</span>
|
|
72
|
+
<span className="text-[#2a2a3a] text-[10px] font-mono">{duration}</span>
|
|
73
|
+
<button
|
|
74
|
+
onClick={onDismiss}
|
|
75
|
+
className="text-[#333] hover:text-[#666] text-[11px] font-mono leading-none px-0.5 transition-colors cursor-pointer"
|
|
76
|
+
>
|
|
77
|
+
✕
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* Focus — primary */}
|
|
82
|
+
{focus && (
|
|
83
|
+
<div className="text-[#aab0b8] text-[11px] font-mono mb-1 ml-3 leading-relaxed">{focus}</div>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{/* Current tool — secondary */}
|
|
87
|
+
{lastTool && (
|
|
88
|
+
<div className="text-[#3a3a5a] text-[10px] font-mono mb-2 ml-3">{lastTool.summary}</div>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{!focus && !lastTool && (
|
|
92
|
+
<div className="text-[#333] text-[11px] font-mono mb-2 ml-3 italic">
|
|
93
|
+
{session.state === 'idle' ? 'Idle' : 'Working'}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{onOpenTerminal ? (
|
|
98
|
+
<button
|
|
99
|
+
onClick={() => { onOpenTerminal(); onDismiss(); }}
|
|
100
|
+
className="w-full py-1 bg-[#0a0a16] hover:bg-[#141428] border border-[#1a1a30] text-[#445] hover:text-[#778] text-[10px] font-mono rounded transition-colors cursor-pointer"
|
|
101
|
+
>
|
|
102
|
+
Open Terminal
|
|
103
|
+
</button>
|
|
104
|
+
) : (
|
|
105
|
+
<button
|
|
106
|
+
onClick={handleFocusTerminal}
|
|
107
|
+
disabled={focusing}
|
|
108
|
+
className="w-full py-1 bg-[#0a0a16] hover:bg-[#141428] border border-[#1a1a30] text-[#445] hover:text-[#778] text-[10px] font-mono rounded transition-colors cursor-pointer disabled:opacity-50"
|
|
109
|
+
>
|
|
110
|
+
{focusing ? 'Focusing…' : 'Focus Terminal'}
|
|
111
|
+
</button>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Asset loader — loads all PNG sprite sheets and pre-colorizes floor/wall tiles
|
|
2
|
+
|
|
3
|
+
export interface AssetBundle {
|
|
4
|
+
characters: HTMLImageElement[]; // char_0.png … char_5.png
|
|
5
|
+
floors: HTMLImageElement[]; // floor_0.png … floor_8.png
|
|
6
|
+
wallSheet: HTMLImageElement; // wall_0.png
|
|
7
|
+
furniture: Map<string, HTMLImageElement>; // keyed by "FOLDER/FILENAME" e.g. "DESK/DESK_FRONT"
|
|
8
|
+
// Pre-colorized offscreen canvases
|
|
9
|
+
colorizedFloors: Map<string, HTMLCanvasElement>; // key = floorIndex:h:s:b:c
|
|
10
|
+
colorizedWall: HTMLCanvasElement;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// HSBC colorize params (Photoshop-style Colorize)
|
|
14
|
+
export interface HsbcParams {
|
|
15
|
+
h: number; // hue 0-360
|
|
16
|
+
s: number; // saturation 0-100
|
|
17
|
+
b: number; // brightness -100..100
|
|
18
|
+
c: number; // contrast -100..100
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Floor color presets referenced by office-layout
|
|
22
|
+
export const FLOOR_COLORS = {
|
|
23
|
+
wood: { h: 25, s: 48, b: -43, c: -88 } as HsbcParams,
|
|
24
|
+
blueCarpet: { h: 209, s: 39, b: -25, c: -80 } as HsbcParams,
|
|
25
|
+
neutralTile: { h: 209, s: 0, b: -16, c: -8 } as HsbcParams,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const WALL_COLORS: HsbcParams = { h: 214, s: 30, b: -100, c: -55 };
|
|
29
|
+
|
|
30
|
+
// ── HSL helpers ────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
33
|
+
s /= 100; l /= 100;
|
|
34
|
+
const k = (n: number) => (n + h / 30) % 12;
|
|
35
|
+
const a = s * Math.min(l, 1 - l);
|
|
36
|
+
const f = (n: number) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
37
|
+
return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Apply Photoshop-style colorize to a grayscale image and return an offscreen canvas
|
|
41
|
+
export function colorizeImage(img: HTMLImageElement, params: HsbcParams): HTMLCanvasElement {
|
|
42
|
+
const canvas = document.createElement('canvas');
|
|
43
|
+
canvas.width = img.naturalWidth;
|
|
44
|
+
canvas.height = img.naturalHeight;
|
|
45
|
+
const ctx = canvas.getContext('2d')!;
|
|
46
|
+
ctx.drawImage(img, 0, 0);
|
|
47
|
+
|
|
48
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
49
|
+
const data = imageData.data;
|
|
50
|
+
const { h, s, b, c } = params;
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
53
|
+
const a = data[i + 3];
|
|
54
|
+
if (a === 0) continue;
|
|
55
|
+
|
|
56
|
+
// 1. Luminance from grayscale source
|
|
57
|
+
const r = data[i], g = data[i + 1], bl2 = data[i + 2];
|
|
58
|
+
let L = (0.299 * r + 0.587 * g + 0.114 * bl2) / 255;
|
|
59
|
+
|
|
60
|
+
// 2. Apply contrast
|
|
61
|
+
L = 0.5 + (L - 0.5) * (100 + c) / 100;
|
|
62
|
+
|
|
63
|
+
// 3. Apply brightness
|
|
64
|
+
L = L + b / 200;
|
|
65
|
+
|
|
66
|
+
// 4. Clamp
|
|
67
|
+
L = Math.max(0, Math.min(1, L));
|
|
68
|
+
|
|
69
|
+
// 5. HSL → RGB
|
|
70
|
+
const [nr, ng, nb] = hslToRgb(h, s, L * 100);
|
|
71
|
+
data[i] = nr;
|
|
72
|
+
data[i + 1] = ng;
|
|
73
|
+
data[i + 2] = nb;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ctx.putImageData(imageData, 0, 0);
|
|
77
|
+
return canvas;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Image loading ──────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function loadImage(src: string): Promise<HTMLImageElement> {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const img = new Image();
|
|
85
|
+
img.onload = () => resolve(img);
|
|
86
|
+
img.onerror = () => reject(new Error(`Failed to load: ${src}`));
|
|
87
|
+
img.src = src;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Furniture items we actually use in the office layout
|
|
92
|
+
const FURNITURE_ASSETS: Array<{ key: string; path: string }> = [
|
|
93
|
+
// Desks & PCs
|
|
94
|
+
{ key: 'DESK/DESK_FRONT', path: '/assets/furniture/DESK/DESK_FRONT.png' },
|
|
95
|
+
{ key: 'PC/PC_FRONT_ON_1', path: '/assets/furniture/PC/PC_FRONT_ON_1.png' },
|
|
96
|
+
{ key: 'PC/PC_FRONT_ON_2', path: '/assets/furniture/PC/PC_FRONT_ON_2.png' },
|
|
97
|
+
{ key: 'PC/PC_FRONT_ON_3', path: '/assets/furniture/PC/PC_FRONT_ON_3.png' },
|
|
98
|
+
{ key: 'PC/PC_FRONT_OFF', path: '/assets/furniture/PC/PC_FRONT_OFF.png' },
|
|
99
|
+
// Bookshelves
|
|
100
|
+
{ key: 'BOOKSHELF/BOOKSHELF', path: '/assets/furniture/BOOKSHELF/BOOKSHELF.png' },
|
|
101
|
+
{ key: 'DOUBLE_BOOKSHELF/DOUBLE_BOOKSHELF', path: '/assets/furniture/DOUBLE_BOOKSHELF/DOUBLE_BOOKSHELF.png' },
|
|
102
|
+
// Plants & greenery
|
|
103
|
+
{ key: 'PLANT/PLANT', path: '/assets/furniture/PLANT/PLANT.png' },
|
|
104
|
+
{ key: 'PLANT_2/PLANT_2', path: '/assets/furniture/PLANT_2/PLANT_2.png' },
|
|
105
|
+
{ key: 'LARGE_PLANT/LARGE_PLANT', path: '/assets/furniture/LARGE_PLANT/LARGE_PLANT.png' },
|
|
106
|
+
{ key: 'CACTUS/CACTUS', path: '/assets/furniture/CACTUS/CACTUS.png' },
|
|
107
|
+
{ key: 'HANGING_PLANT/HANGING_PLANT', path: '/assets/furniture/HANGING_PLANT/HANGING_PLANT.png' },
|
|
108
|
+
{ key: 'POT/POT', path: '/assets/furniture/POT/POT.png' },
|
|
109
|
+
// Wall art
|
|
110
|
+
{ key: 'SMALL_PAINTING/SMALL_PAINTING', path: '/assets/furniture/SMALL_PAINTING/SMALL_PAINTING.png' },
|
|
111
|
+
{ key: 'SMALL_PAINTING_2/SMALL_PAINTING_2', path: '/assets/furniture/SMALL_PAINTING_2/SMALL_PAINTING_2.png' },
|
|
112
|
+
{ key: 'LARGE_PAINTING/LARGE_PAINTING', path: '/assets/furniture/LARGE_PAINTING/LARGE_PAINTING.png' },
|
|
113
|
+
{ key: 'CLOCK/CLOCK', path: '/assets/furniture/CLOCK/CLOCK.png' },
|
|
114
|
+
// Seating & tables
|
|
115
|
+
{ key: 'WHITEBOARD/WHITEBOARD', path: '/assets/furniture/WHITEBOARD/WHITEBOARD.png' },
|
|
116
|
+
{ key: 'SOFA/SOFA_BACK', path: '/assets/furniture/SOFA/SOFA_BACK.png' },
|
|
117
|
+
{ key: 'COFFEE_TABLE/COFFEE_TABLE', path: '/assets/furniture/COFFEE_TABLE/COFFEE_TABLE.png' },
|
|
118
|
+
{ key: 'COFFEE/COFFEE', path: '/assets/furniture/COFFEE/COFFEE.png' },
|
|
119
|
+
{ key: 'WOODEN_CHAIR/WOODEN_CHAIR_BACK', path: '/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_BACK.png' },
|
|
120
|
+
{ key: 'CUSHIONED_CHAIR/CUSHIONED_CHAIR_BACK', path: '/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_BACK.png' },
|
|
121
|
+
// Misc
|
|
122
|
+
{ key: 'BIN/BIN', path: '/assets/furniture/BIN/BIN.png' },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
export async function loadAssets(): Promise<AssetBundle> {
|
|
126
|
+
// Load character sheets
|
|
127
|
+
const characterPromises = Array.from({ length: 6 }, (_, i) =>
|
|
128
|
+
loadImage(`/assets/characters/char_${i}.png`)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Load floor tiles
|
|
132
|
+
const floorPromises = Array.from({ length: 9 }, (_, i) =>
|
|
133
|
+
loadImage(`/assets/floors/floor_${i}.png`)
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Load wall sheet
|
|
137
|
+
const wallPromise = loadImage('/assets/walls/wall_0.png');
|
|
138
|
+
|
|
139
|
+
// Load furniture
|
|
140
|
+
const furniturePromises = FURNITURE_ASSETS.map(({ key, path }) =>
|
|
141
|
+
loadImage(path).then(img => ({ key, img }))
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const [characters, floors, wallSheet, furnitureResults] = await Promise.all([
|
|
145
|
+
Promise.all(characterPromises),
|
|
146
|
+
Promise.all(floorPromises),
|
|
147
|
+
wallPromise,
|
|
148
|
+
Promise.all(furniturePromises),
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
const furniture = new Map<string, HTMLImageElement>();
|
|
152
|
+
for (const { key, img } of furnitureResults) {
|
|
153
|
+
furniture.set(key, img);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Pre-colorize floor tiles for each variant we use
|
|
157
|
+
const colorizedFloors = new Map<string, HTMLCanvasElement>();
|
|
158
|
+
const floorVariants: Array<{ floorIndex: number; params: HsbcParams }> = [
|
|
159
|
+
{ floorIndex: 7, params: FLOOR_COLORS.wood },
|
|
160
|
+
{ floorIndex: 1, params: FLOOR_COLORS.blueCarpet },
|
|
161
|
+
{ floorIndex: 0, params: FLOOR_COLORS.neutralTile },
|
|
162
|
+
];
|
|
163
|
+
for (const { floorIndex, params } of floorVariants) {
|
|
164
|
+
const key = `${floorIndex}:${params.h}:${params.s}:${params.b}:${params.c}`;
|
|
165
|
+
colorizedFloors.set(key, colorizeImage(floors[floorIndex], params));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Pre-colorize wall sheet
|
|
169
|
+
const colorizedWall = colorizeImage(wallSheet, WALL_COLORS);
|
|
170
|
+
|
|
171
|
+
return { characters, floors, wallSheet, furniture, colorizedFloors, colorizedWall };
|
|
172
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// Multi-room pixel office layout — sprite-based
|
|
2
|
+
// Grid: 20 wide × 15 tall
|
|
3
|
+
//
|
|
4
|
+
// Rooms:
|
|
5
|
+
// Main work area: cols 0-12, rows 0-14 (warm wood floor — floor_7)
|
|
6
|
+
// Break room: cols 13-19, rows 0-6 (neutral tile — floor_0)
|
|
7
|
+
// Meeting room: cols 13-19, rows 7-14 (blue carpet — floor_1)
|
|
8
|
+
|
|
9
|
+
import { HsbcParams, FLOOR_COLORS } from './asset-loader';
|
|
10
|
+
|
|
11
|
+
export const TILE_SIZE = 16;
|
|
12
|
+
export const SCALE = 4; // 4× zoom for crisp pixel art
|
|
13
|
+
export const GRID_W = 20;
|
|
14
|
+
export const GRID_H = 15;
|
|
15
|
+
export const CANVAS_W = GRID_W * TILE_SIZE * SCALE; // 1280
|
|
16
|
+
export const CANVAS_H = GRID_H * TILE_SIZE * SCALE; // 960
|
|
17
|
+
|
|
18
|
+
// ── Tile types ──────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export type FloorVariant = 'wood' | 'tile' | 'carpet';
|
|
21
|
+
|
|
22
|
+
export type FurnitureType =
|
|
23
|
+
| 'desk'
|
|
24
|
+
| 'pc'
|
|
25
|
+
| 'bookshelf'
|
|
26
|
+
| 'double_bookshelf'
|
|
27
|
+
| 'plant'
|
|
28
|
+
| 'plant_2'
|
|
29
|
+
| 'large_plant'
|
|
30
|
+
| 'whiteboard'
|
|
31
|
+
| 'sofa'
|
|
32
|
+
| 'coffee_table'
|
|
33
|
+
| 'coffee'
|
|
34
|
+
| 'wooden_chair'
|
|
35
|
+
| 'cushioned_chair'
|
|
36
|
+
| 'cactus'
|
|
37
|
+
| 'clock'
|
|
38
|
+
| 'hanging_plant'
|
|
39
|
+
| 'painting_small'
|
|
40
|
+
| 'painting_small_2'
|
|
41
|
+
| 'painting_large'
|
|
42
|
+
| 'bin'
|
|
43
|
+
| 'pot';
|
|
44
|
+
|
|
45
|
+
export interface GridCell {
|
|
46
|
+
// Floor info
|
|
47
|
+
floorIndex: number; // which floor_*.png to use
|
|
48
|
+
floorParams: HsbcParams; // colorize params
|
|
49
|
+
// Wall info
|
|
50
|
+
isWall: boolean;
|
|
51
|
+
isDoor: boolean;
|
|
52
|
+
wallMask: number; // 4-bit N=1,E=2,S=4,W=8 for auto-tiling
|
|
53
|
+
// Furniture placed at this cell (drawn separately, not per-cell)
|
|
54
|
+
furniture?: FurnitureType;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DeskPosition {
|
|
58
|
+
deskX: number;
|
|
59
|
+
deskY: number;
|
|
60
|
+
chairX: number;
|
|
61
|
+
chairY: number;
|
|
62
|
+
entryX: number;
|
|
63
|
+
entryY: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 5 desks in the main work area
|
|
67
|
+
export const DESK_POSITIONS: DeskPosition[] = [
|
|
68
|
+
{ deskX: 1, deskY: 3, chairX: 1, chairY: 4, entryX: 6, entryY: 13 },
|
|
69
|
+
{ deskX: 4, deskY: 3, chairX: 4, chairY: 4, entryX: 6, entryY: 13 },
|
|
70
|
+
{ deskX: 7, deskY: 3, chairX: 7, chairY: 4, entryX: 6, entryY: 13 },
|
|
71
|
+
{ deskX: 2, deskY: 8, chairX: 2, chairY: 9, entryX: 6, entryY: 13 },
|
|
72
|
+
{ deskX: 7, deskY: 8, chairX: 7, chairY: 9, entryX: 6, entryY: 13 },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export const DOOR_X = 6;
|
|
76
|
+
export const DOOR_Y = 14;
|
|
77
|
+
|
|
78
|
+
// ── Legacy TileType — kept for renderers that may check it ─────────────────
|
|
79
|
+
export type TileType =
|
|
80
|
+
| 'floor_wood' | 'floor_tile' | 'floor_carpet'
|
|
81
|
+
| 'wall' | 'wall_door'
|
|
82
|
+
| 'desk' | 'desk_right' | 'chair' | 'bookshelf' | 'plant'
|
|
83
|
+
| 'vending_machine' | 'painting' | 'break_table' | 'couch'
|
|
84
|
+
| 'whiteboard' | 'window';
|
|
85
|
+
|
|
86
|
+
// ── Furniture placement list ────────────────────────────────────────────────
|
|
87
|
+
// Each entry has tile coords + which asset to draw + optional pixel offsets
|
|
88
|
+
|
|
89
|
+
export interface FurniturePlacement {
|
|
90
|
+
tx: number; // tile column
|
|
91
|
+
ty: number; // tile row (top-left anchor)
|
|
92
|
+
type: FurnitureType;
|
|
93
|
+
// asset key into AssetBundle.furniture
|
|
94
|
+
assetKey: string;
|
|
95
|
+
// native PNG size
|
|
96
|
+
pngW: number;
|
|
97
|
+
pngH: number;
|
|
98
|
+
// optional horizontal flip
|
|
99
|
+
flipX?: boolean;
|
|
100
|
+
// pixel offset within the tile anchor (in source pixels, will be scaled)
|
|
101
|
+
offsetX?: number;
|
|
102
|
+
offsetY?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Floor variant helper ────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
function floorFor(tx: number, ty: number): { floorIndex: number; floorParams: HsbcParams } {
|
|
108
|
+
// Side rooms (cols 14-18 are the floor area inside walls at 13 and 19)
|
|
109
|
+
if (tx >= 14 && tx <= 18) {
|
|
110
|
+
if (ty >= 1 && ty <= 6) return { floorIndex: 1, floorParams: FLOOR_COLORS.blueCarpet };
|
|
111
|
+
if (ty >= 8 && ty <= 14) return { floorIndex: 0, floorParams: FLOOR_COLORS.neutralTile };
|
|
112
|
+
}
|
|
113
|
+
// Default: wood
|
|
114
|
+
return { floorIndex: 7, floorParams: FLOOR_COLORS.wood };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── buildGrid ───────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export function buildGrid(): GridCell[][] {
|
|
120
|
+
// Initialize all as wood floor
|
|
121
|
+
const grid: GridCell[][] = Array.from({ length: GRID_H }, (_, ty) =>
|
|
122
|
+
Array.from({ length: GRID_W }, (_, tx) => ({
|
|
123
|
+
...floorFor(tx, ty),
|
|
124
|
+
isWall: false,
|
|
125
|
+
isDoor: false,
|
|
126
|
+
wallMask: 0,
|
|
127
|
+
}))
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const markWall = (x: number, y: number, isDoor = false) => {
|
|
131
|
+
if (y < 0 || y >= GRID_H || x < 0 || x >= GRID_W) return;
|
|
132
|
+
grid[y][x].isWall = !isDoor;
|
|
133
|
+
grid[y][x].isDoor = isDoor;
|
|
134
|
+
// Wall cells use wood floor underneath for doors
|
|
135
|
+
const { floorIndex, floorParams } = isDoor ? floorFor(x, y) : { floorIndex: 7, floorParams: FLOOR_COLORS.wood };
|
|
136
|
+
grid[y][x].floorIndex = floorIndex;
|
|
137
|
+
grid[y][x].floorParams = floorParams;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Top wall (row 0)
|
|
141
|
+
for (let x = 0; x < GRID_W; x++) markWall(x, 0);
|
|
142
|
+
|
|
143
|
+
// Left wall (col 0)
|
|
144
|
+
for (let y = 0; y < GRID_H; y++) markWall(0, y);
|
|
145
|
+
|
|
146
|
+
// Right wall (col 19)
|
|
147
|
+
for (let y = 0; y < GRID_H; y++) markWall(19, y);
|
|
148
|
+
|
|
149
|
+
// Bottom wall (row 14) — main room bottom with door gap
|
|
150
|
+
for (let x = 0; x < 13; x++) {
|
|
151
|
+
if (x === DOOR_X || x === DOOR_X + 1) {
|
|
152
|
+
markWall(x, 14, true);
|
|
153
|
+
} else {
|
|
154
|
+
markWall(x, 14);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Internal vertical wall (col 13) with doorways
|
|
159
|
+
for (let y = 0; y < GRID_H; y++) {
|
|
160
|
+
if (y === 5 || y === 10) {
|
|
161
|
+
markWall(13, y, true);
|
|
162
|
+
} else {
|
|
163
|
+
markWall(13, y);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Bottom wall for right-side rooms (row 14, cols 14-18)
|
|
168
|
+
for (let x = 14; x <= 18; x++) markWall(x, 14);
|
|
169
|
+
|
|
170
|
+
// Internal horizontal wall (row 7, cols 13-19)
|
|
171
|
+
for (let x = 13; x < GRID_W; x++) markWall(x, 7);
|
|
172
|
+
|
|
173
|
+
// Compute wall bitmasks for auto-tiling
|
|
174
|
+
const isWallOrOob = (x: number, y: number): boolean => {
|
|
175
|
+
if (x < 0 || x >= GRID_W || y < 0 || y >= GRID_H) return true;
|
|
176
|
+
return grid[y][x].isWall;
|
|
177
|
+
};
|
|
178
|
+
for (let ty = 0; ty < GRID_H; ty++) {
|
|
179
|
+
for (let tx = 0; tx < GRID_W; tx++) {
|
|
180
|
+
if (!grid[ty][tx].isWall) continue;
|
|
181
|
+
let mask = 0;
|
|
182
|
+
if (isWallOrOob(tx, ty - 1)) mask |= 1; // N
|
|
183
|
+
if (isWallOrOob(tx + 1, ty)) mask |= 2; // E
|
|
184
|
+
if (isWallOrOob(tx, ty + 1)) mask |= 4; // S
|
|
185
|
+
if (isWallOrOob(tx - 1, ty)) mask |= 8; // W
|
|
186
|
+
grid[ty][tx].wallMask = mask;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return grid;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Furniture placement list ────────────────────────────────────────────────
|
|
194
|
+
// Built once and used by renderer. Positions mirror old buildGrid() furniture.
|
|
195
|
+
|
|
196
|
+
export function buildFurniturePlacements(): FurniturePlacement[] {
|
|
197
|
+
const items: FurniturePlacement[] = [];
|
|
198
|
+
|
|
199
|
+
// ── Desks (DESK_FRONT is 48×32 = 3 tiles wide × 2 tiles tall) ────────────
|
|
200
|
+
for (const d of DESK_POSITIONS) {
|
|
201
|
+
items.push({
|
|
202
|
+
tx: d.deskX, ty: d.deskY,
|
|
203
|
+
type: 'desk', assetKey: 'DESK/DESK_FRONT',
|
|
204
|
+
pngW: 48, pngH: 32,
|
|
205
|
+
});
|
|
206
|
+
items.push({
|
|
207
|
+
tx: d.deskX + 1, ty: d.deskY - 1,
|
|
208
|
+
type: 'pc', assetKey: 'PC/PC_FRONT_ON_1',
|
|
209
|
+
pngW: 16, pngH: 32,
|
|
210
|
+
offsetY: 12,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
215
|
+
// ── MAIN WORK AREA (cols 1-12, rows 1-13) ──────────────────────────────
|
|
216
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
217
|
+
//
|
|
218
|
+
// Back wall layout (row 1, left to right):
|
|
219
|
+
// [1-2] double bookshelf [4] painting [6] plant [8] painting
|
|
220
|
+
// [9-10] double bookshelf [11-12] large painting
|
|
221
|
+
|
|
222
|
+
// Back wall: bookshelves sit against wall, paintings hang on wall
|
|
223
|
+
items.push({ tx: 1, ty: 1, type: 'double_bookshelf', assetKey: 'DOUBLE_BOOKSHELF/DOUBLE_BOOKSHELF', pngW: 32, pngH: 32, offsetY: -16 });
|
|
224
|
+
items.push({ tx: 9, ty: 1, type: 'double_bookshelf', assetKey: 'DOUBLE_BOOKSHELF/DOUBLE_BOOKSHELF', pngW: 32, pngH: 32, offsetY: -16 });
|
|
225
|
+
// Paintings hung HIGH on wall (offsetY -22 places them on the dark wall face, above bookshelves)
|
|
226
|
+
items.push({ tx: 4, ty: 1, type: 'painting_small', assetKey: 'SMALL_PAINTING/SMALL_PAINTING', pngW: 16, pngH: 32, offsetY: -22 });
|
|
227
|
+
items.push({ tx: 8, ty: 1, type: 'painting_small_2', assetKey: 'SMALL_PAINTING_2/SMALL_PAINTING_2', pngW: 16, pngH: 32, offsetY: -22 });
|
|
228
|
+
// Large painting on right end of back wall
|
|
229
|
+
items.push({ tx: 11, ty: 1, type: 'painting_large', assetKey: 'LARGE_PAINTING/LARGE_PAINTING', pngW: 32, pngH: 32, offsetY: -22 });
|
|
230
|
+
|
|
231
|
+
// Plants on FLOOR (row 2+), not touching walls
|
|
232
|
+
// Plant between paintings on back wall — sits on floor at row 2
|
|
233
|
+
items.push({ tx: 6, ty: 2, type: 'plant', assetKey: 'PLANT/PLANT', pngW: 16, pngH: 32 });
|
|
234
|
+
|
|
235
|
+
// Plants in the gap between desk rows (rows 5-7)
|
|
236
|
+
items.push({ tx: 2, ty: 6, type: 'large_plant', assetKey: 'LARGE_PLANT/LARGE_PLANT', pngW: 32, pngH: 48, offsetY: -16 });
|
|
237
|
+
items.push({ tx: 12, ty: 6, type: 'hanging_plant', assetKey: 'HANGING_PLANT/HANGING_PLANT', pngW: 16, pngH: 32 });
|
|
238
|
+
items.push({ tx: 6, ty: 6, type: 'cactus', assetKey: 'CACTUS/CACTUS', pngW: 16, pngH: 32 });
|
|
239
|
+
|
|
240
|
+
// Bottom area plants — row 11 max (row 12+ overlaps bottom wall sprites)
|
|
241
|
+
items.push({ tx: 2, ty: 11, type: 'plant_2', assetKey: 'PLANT_2/PLANT_2', pngW: 16, pngH: 32 });
|
|
242
|
+
items.push({ tx: 10, ty: 11, type: 'plant', assetKey: 'PLANT/PLANT', pngW: 16, pngH: 32 });
|
|
243
|
+
|
|
244
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
245
|
+
// ── BREAK ROOM (cols 14-18, rows 1-6, blue carpet) ─────────────────────
|
|
246
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
247
|
+
//
|
|
248
|
+
// Layout:
|
|
249
|
+
// Row 1: [painting] [clock] (wall-mounted)
|
|
250
|
+
// Row 2: [sofa] (centered)
|
|
251
|
+
// Row 3-4: [coffee table + mug] (centered)
|
|
252
|
+
// Row 5: [plant] (corner accent)
|
|
253
|
+
|
|
254
|
+
items.push({ tx: 14, ty: 1, type: 'painting_small', assetKey: 'SMALL_PAINTING/SMALL_PAINTING', pngW: 16, pngH: 32, offsetY: -22 });
|
|
255
|
+
items.push({ tx: 16, ty: 1, type: 'clock', assetKey: 'CLOCK/CLOCK', pngW: 16, pngH: 32, offsetY: -22 });
|
|
256
|
+
items.push({ tx: 15, ty: 2, type: 'sofa', assetKey: 'SOFA/SOFA_BACK', pngW: 32, pngH: 16 });
|
|
257
|
+
items.push({ tx: 15, ty: 3, type: 'coffee_table', assetKey: 'COFFEE_TABLE/COFFEE_TABLE', pngW: 32, pngH: 32 });
|
|
258
|
+
items.push({ tx: 16, ty: 3, type: 'coffee', assetKey: 'COFFEE/COFFEE', pngW: 16, pngH: 16, offsetY: 4 });
|
|
259
|
+
// Plants — row 4 max (row 5+ overlaps the row 7 internal wall)
|
|
260
|
+
items.push({ tx: 17, ty: 4, type: 'plant', assetKey: 'PLANT/PLANT', pngW: 16, pngH: 32 });
|
|
261
|
+
items.push({ tx: 14, ty: 4, type: 'cactus', assetKey: 'CACTUS/CACTUS', pngW: 16, pngH: 32 });
|
|
262
|
+
|
|
263
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
264
|
+
// ── MEETING ROOM (cols 14-18, rows 8-13, neutral tile) ─────────────────
|
|
265
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
266
|
+
//
|
|
267
|
+
// Layout:
|
|
268
|
+
// Row 8: [whiteboard] (centered on wall)
|
|
269
|
+
// Row 9: (gap)
|
|
270
|
+
// Row 10: [chairs] [table] [chairs] (meeting setup)
|
|
271
|
+
// Row 11: [chairs below]
|
|
272
|
+
// Row 12-13: [plant corner] [bin]
|
|
273
|
+
|
|
274
|
+
items.push({ tx: 15, ty: 8, type: 'whiteboard', assetKey: 'WHITEBOARD/WHITEBOARD', pngW: 32, pngH: 32 });
|
|
275
|
+
// Chairs above table
|
|
276
|
+
items.push({ tx: 15, ty: 10, type: 'cushioned_chair', assetKey: 'CUSHIONED_CHAIR/CUSHIONED_CHAIR_BACK', pngW: 16, pngH: 16, offsetY: -12 });
|
|
277
|
+
items.push({ tx: 16, ty: 10, type: 'cushioned_chair', assetKey: 'CUSHIONED_CHAIR/CUSHIONED_CHAIR_BACK', pngW: 16, pngH: 16, offsetY: -12 });
|
|
278
|
+
// Table
|
|
279
|
+
items.push({ tx: 15, ty: 10, type: 'coffee_table', assetKey: 'COFFEE_TABLE/COFFEE_TABLE', pngW: 32, pngH: 32 });
|
|
280
|
+
// Chairs below table
|
|
281
|
+
items.push({ tx: 15, ty: 11, type: 'wooden_chair', assetKey: 'WOODEN_CHAIR/WOODEN_CHAIR_BACK', pngW: 16, pngH: 32 });
|
|
282
|
+
items.push({ tx: 16, ty: 11, type: 'wooden_chair', assetKey: 'WOODEN_CHAIR/WOODEN_CHAIR_BACK', pngW: 16, pngH: 32 });
|
|
283
|
+
// Corner plant — row 11 max (row 12+ overlaps bottom wall), col 17 (not touching right wall)
|
|
284
|
+
items.push({ tx: 17, ty: 11, type: 'plant_2', assetKey: 'PLANT_2/PLANT_2', pngW: 16, pngH: 32 });
|
|
285
|
+
|
|
286
|
+
return items;
|
|
287
|
+
}
|