@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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/bin.sh +16 -0
  4. package/bin.ts +162 -0
  5. package/next-env.d.ts +6 -0
  6. package/next.config.ts +7 -0
  7. package/package.json +51 -0
  8. package/postcss.config.mjs +7 -0
  9. package/public/assets/characters/char_0.png +0 -0
  10. package/public/assets/characters/char_1.png +0 -0
  11. package/public/assets/characters/char_2.png +0 -0
  12. package/public/assets/characters/char_3.png +0 -0
  13. package/public/assets/characters/char_4.png +0 -0
  14. package/public/assets/characters/char_5.png +0 -0
  15. package/public/assets/characters.png +0 -0
  16. package/public/assets/default-layout-1.json +92 -0
  17. package/public/assets/floors/floor_0.png +0 -0
  18. package/public/assets/floors/floor_1.png +0 -0
  19. package/public/assets/floors/floor_2.png +0 -0
  20. package/public/assets/floors/floor_3.png +0 -0
  21. package/public/assets/floors/floor_4.png +0 -0
  22. package/public/assets/floors/floor_5.png +0 -0
  23. package/public/assets/floors/floor_6.png +0 -0
  24. package/public/assets/floors/floor_7.png +0 -0
  25. package/public/assets/floors/floor_8.png +0 -0
  26. package/public/assets/furniture/BIN/BIN.png +0 -0
  27. package/public/assets/furniture/BIN/manifest.json +13 -0
  28. package/public/assets/furniture/BOOKSHELF/BOOKSHELF.png +0 -0
  29. package/public/assets/furniture/BOOKSHELF/manifest.json +13 -0
  30. package/public/assets/furniture/CACTUS/CACTUS.png +0 -0
  31. package/public/assets/furniture/CACTUS/manifest.json +13 -0
  32. package/public/assets/furniture/CLOCK/CLOCK.png +0 -0
  33. package/public/assets/furniture/CLOCK/manifest.json +13 -0
  34. package/public/assets/furniture/COFFEE/COFFEE.png +0 -0
  35. package/public/assets/furniture/COFFEE/manifest.json +13 -0
  36. package/public/assets/furniture/COFFEE_TABLE/COFFEE_TABLE.png +0 -0
  37. package/public/assets/furniture/COFFEE_TABLE/manifest.json +13 -0
  38. package/public/assets/furniture/CUSHIONED_BENCH/CUSHIONED_BENCH.png +0 -0
  39. package/public/assets/furniture/CUSHIONED_BENCH/manifest.json +13 -0
  40. package/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_BACK.png +0 -0
  41. package/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_FRONT.png +0 -0
  42. package/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_SIDE.png +0 -0
  43. package/public/assets/furniture/CUSHIONED_CHAIR/manifest.json +44 -0
  44. package/public/assets/furniture/DESK/DESK_FRONT.png +0 -0
  45. package/public/assets/furniture/DESK/DESK_SIDE.png +0 -0
  46. package/public/assets/furniture/DESK/manifest.json +33 -0
  47. package/public/assets/furniture/DOUBLE_BOOKSHELF/DOUBLE_BOOKSHELF.png +0 -0
  48. package/public/assets/furniture/DOUBLE_BOOKSHELF/manifest.json +13 -0
  49. package/public/assets/furniture/HANGING_PLANT/HANGING_PLANT.png +0 -0
  50. package/public/assets/furniture/HANGING_PLANT/manifest.json +13 -0
  51. package/public/assets/furniture/LARGE_PAINTING/LARGE_PAINTING.png +0 -0
  52. package/public/assets/furniture/LARGE_PAINTING/manifest.json +13 -0
  53. package/public/assets/furniture/LARGE_PLANT/LARGE_PLANT.png +0 -0
  54. package/public/assets/furniture/LARGE_PLANT/manifest.json +13 -0
  55. package/public/assets/furniture/PC/PC_BACK.png +0 -0
  56. package/public/assets/furniture/PC/PC_FRONT_OFF.png +0 -0
  57. package/public/assets/furniture/PC/PC_FRONT_ON_1.png +0 -0
  58. package/public/assets/furniture/PC/PC_FRONT_ON_2.png +0 -0
  59. package/public/assets/furniture/PC/PC_FRONT_ON_3.png +0 -0
  60. package/public/assets/furniture/PC/PC_SIDE.png +0 -0
  61. package/public/assets/furniture/PC/manifest.json +88 -0
  62. package/public/assets/furniture/PLANT/PLANT.png +0 -0
  63. package/public/assets/furniture/PLANT/manifest.json +13 -0
  64. package/public/assets/furniture/PLANT_2/PLANT_2.png +0 -0
  65. package/public/assets/furniture/PLANT_2/manifest.json +13 -0
  66. package/public/assets/furniture/POT/POT.png +0 -0
  67. package/public/assets/furniture/POT/manifest.json +13 -0
  68. package/public/assets/furniture/SMALL_PAINTING/SMALL_PAINTING.png +0 -0
  69. package/public/assets/furniture/SMALL_PAINTING/manifest.json +13 -0
  70. package/public/assets/furniture/SMALL_PAINTING_2/SMALL_PAINTING_2.png +0 -0
  71. package/public/assets/furniture/SMALL_PAINTING_2/manifest.json +13 -0
  72. package/public/assets/furniture/SMALL_TABLE/SMALL_TABLE_FRONT.png +0 -0
  73. package/public/assets/furniture/SMALL_TABLE/SMALL_TABLE_SIDE.png +0 -0
  74. package/public/assets/furniture/SMALL_TABLE/manifest.json +33 -0
  75. package/public/assets/furniture/SOFA/SOFA_BACK.png +0 -0
  76. package/public/assets/furniture/SOFA/SOFA_FRONT.png +0 -0
  77. package/public/assets/furniture/SOFA/SOFA_SIDE.png +0 -0
  78. package/public/assets/furniture/SOFA/manifest.json +44 -0
  79. package/public/assets/furniture/TABLE_FRONT/TABLE_FRONT.png +0 -0
  80. package/public/assets/furniture/TABLE_FRONT/manifest.json +13 -0
  81. package/public/assets/furniture/WHITEBOARD/WHITEBOARD.png +0 -0
  82. package/public/assets/furniture/WHITEBOARD/manifest.json +13 -0
  83. package/public/assets/furniture/WOODEN_BENCH/WOODEN_BENCH.png +0 -0
  84. package/public/assets/furniture/WOODEN_BENCH/manifest.json +13 -0
  85. package/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_BACK.png +0 -0
  86. package/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_FRONT.png +0 -0
  87. package/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_SIDE.png +0 -0
  88. package/public/assets/furniture/WOODEN_CHAIR/manifest.json +44 -0
  89. package/public/assets/walls/wall_0.png +0 -0
  90. package/scripts/setup.ts +158 -0
  91. package/server.ts +53 -0
  92. package/src/app/api/focus-terminal/route.ts +65 -0
  93. package/src/app/api/hooks/notification/route.ts +19 -0
  94. package/src/app/api/hooks/post-tool-use/route.ts +26 -0
  95. package/src/app/api/hooks/pre-tool-use/route.ts +189 -0
  96. package/src/app/api/hooks/session-end/route.ts +31 -0
  97. package/src/app/api/hooks/session-start/route.ts +47 -0
  98. package/src/app/api/hooks/stop/route.ts +19 -0
  99. package/src/app/api/hooks/user-prompt/route.ts +92 -0
  100. package/src/app/favicon.ico +0 -0
  101. package/src/app/globals.css +14 -0
  102. package/src/app/layout.tsx +21 -0
  103. package/src/app/page.tsx +5 -0
  104. package/src/components/ApprovalToast.tsx +132 -0
  105. package/src/components/OfficeCanvas.tsx +311 -0
  106. package/src/components/Terminal.tsx +177 -0
  107. package/src/components/TerminalTile.tsx +181 -0
  108. package/src/components/WorkerPanel.tsx +261 -0
  109. package/src/components/WorkerPopup.tsx +116 -0
  110. package/src/game/asset-loader.ts +172 -0
  111. package/src/game/office-layout.ts +287 -0
  112. package/src/game/renderer.ts +369 -0
  113. package/src/game/sprites.ts +133 -0
  114. package/src/game/worker-entity.ts +219 -0
  115. package/src/hooks/usePixelOffice.ts +318 -0
  116. package/src/hooks/useRecentCwds.ts +27 -0
  117. package/src/lib/approval-queue.ts +67 -0
  118. package/src/lib/pty-manager.ts +267 -0
  119. package/src/lib/store.ts +181 -0
  120. package/src/lib/tool-classifier.ts +224 -0
  121. package/src/lib/transcript.ts +109 -0
  122. package/src/lib/types.ts +58 -0
  123. package/src/lib/ws-server.ts +270 -0
  124. 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
+ }