create-miniverse 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/dist/index.js +508 -0
- package/package.json +31 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
import { mkdirSync, writeFileSync, copyFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
var TEMPLATES = path.resolve(__dirname, "..", "templates");
|
|
11
|
+
var y = pc.yellow;
|
|
12
|
+
var c = pc.cyan;
|
|
13
|
+
var m = pc.magenta;
|
|
14
|
+
var g = pc.green;
|
|
15
|
+
var r = pc.red;
|
|
16
|
+
var b = pc.blue;
|
|
17
|
+
var w = pc.white;
|
|
18
|
+
var d = pc.dim;
|
|
19
|
+
var banner = `
|
|
20
|
+
${r(" \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 ")}${b("\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
21
|
+
${r(" \u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 ")}${b("\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D")}
|
|
22
|
+
${r(" \u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 ")}${b("\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 ")}
|
|
23
|
+
${r(" \u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 ")}${b(" \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D ")}
|
|
24
|
+
${r(" \u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 ")}${b(" \u255A\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
25
|
+
${r(" \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D ")}${b(" \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D")}
|
|
26
|
+
${d(" a tiny pixel world for your AI agents")}
|
|
27
|
+
`;
|
|
28
|
+
var WORLDS = [
|
|
29
|
+
{ value: "cozy-startup", label: "Cozy Startup", hint: "warm office with exposed brick" },
|
|
30
|
+
{ value: "posh-highrise", label: "Posh Highrise", hint: "clean modern office with marble floors" },
|
|
31
|
+
{ value: "jungle-treehouse", label: "Jungle Treehouse", hint: "tropical office in the canopy" },
|
|
32
|
+
{ value: "ocean-lab", label: "Ocean Lab", hint: "underwater research station" },
|
|
33
|
+
{ value: "gear-supply", label: "Gear Supply", hint: "industrial tech workspace" }
|
|
34
|
+
];
|
|
35
|
+
var SPRITES = ["morty", "dexter", "nova", "rio"];
|
|
36
|
+
async function main() {
|
|
37
|
+
console.log(banner);
|
|
38
|
+
p.intro(pc.bgGreen(pc.black(" create-miniverse ")));
|
|
39
|
+
const dirArg = process.argv[2];
|
|
40
|
+
const projectName = dirArg ?? await p.text({
|
|
41
|
+
message: "Project name",
|
|
42
|
+
placeholder: "my-miniverse",
|
|
43
|
+
defaultValue: "my-miniverse",
|
|
44
|
+
validate: (v) => {
|
|
45
|
+
if (!v) return "Project name is required";
|
|
46
|
+
if (existsSync(v)) return `Directory "${v}" already exists`;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
if (p.isCancel(projectName)) {
|
|
50
|
+
p.cancel("Cancelled.");
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
const agentInput = await p.text({
|
|
54
|
+
message: "What are your agents called? (comma-separated)",
|
|
55
|
+
placeholder: "Claude, Sage, Nova, Flux",
|
|
56
|
+
defaultValue: "Claude, Sage, Nova, Flux"
|
|
57
|
+
});
|
|
58
|
+
if (p.isCancel(agentInput)) {
|
|
59
|
+
p.cancel("Cancelled.");
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
const agents = agentInput.split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
63
|
+
const worldId = await p.select({
|
|
64
|
+
message: "Pick a world",
|
|
65
|
+
options: WORLDS.map((w2) => ({
|
|
66
|
+
value: w2.value,
|
|
67
|
+
label: w2.label,
|
|
68
|
+
hint: w2.hint
|
|
69
|
+
}))
|
|
70
|
+
});
|
|
71
|
+
if (p.isCancel(worldId)) {
|
|
72
|
+
p.cancel("Cancelled.");
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
const signalMode = await p.select({
|
|
76
|
+
message: "How will your agents send updates?",
|
|
77
|
+
options: [
|
|
78
|
+
{ value: "server", label: "Heartbeat server", hint: "POST /api/heartbeat \u2014 best for AI agents (recommended)" },
|
|
79
|
+
{ value: "mock", label: "Mock data", hint: "random state changes \u2014 good for testing" }
|
|
80
|
+
]
|
|
81
|
+
});
|
|
82
|
+
if (p.isCancel(signalMode)) {
|
|
83
|
+
p.cancel("Cancelled.");
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
const projectDir = path.resolve(process.cwd(), projectName);
|
|
87
|
+
const s = p.spinner();
|
|
88
|
+
s.start("Creating project...");
|
|
89
|
+
mkdirSync(projectDir, { recursive: true });
|
|
90
|
+
mkdirSync(path.join(projectDir, "src"), { recursive: true });
|
|
91
|
+
mkdirSync(path.join(projectDir, "public", "sprites"), { recursive: true });
|
|
92
|
+
mkdirSync(path.join(projectDir, "public", "worlds"), { recursive: true });
|
|
93
|
+
const monorepoRoot = path.resolve(__dirname, "..", "..", "..");
|
|
94
|
+
const coreDir = path.join(monorepoRoot, "packages", "core");
|
|
95
|
+
const serverDir = path.join(monorepoRoot, "packages", "server");
|
|
96
|
+
const useLocal = existsSync(path.join(coreDir, "package.json"));
|
|
97
|
+
const coreDep = useLocal ? `file:${coreDir}` : "^0.1.0";
|
|
98
|
+
const serverDep = useLocal ? `file:${serverDir}` : "^0.1.0";
|
|
99
|
+
const pkg = {
|
|
100
|
+
name: projectName,
|
|
101
|
+
private: true,
|
|
102
|
+
type: "module",
|
|
103
|
+
scripts: {
|
|
104
|
+
dev: "vite",
|
|
105
|
+
build: "vite build",
|
|
106
|
+
preview: "vite preview",
|
|
107
|
+
...signalMode === "server" ? { server: "miniverse" } : {}
|
|
108
|
+
},
|
|
109
|
+
dependencies: {
|
|
110
|
+
"@miniverse/core": coreDep,
|
|
111
|
+
...signalMode === "server" ? { "@miniverse/server": serverDep } : {}
|
|
112
|
+
},
|
|
113
|
+
devDependencies: {
|
|
114
|
+
typescript: "^5.4.0",
|
|
115
|
+
vite: "^5.4.0"
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
writeFileSync(path.join(projectDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
119
|
+
writeFileSync(path.join(projectDir, "tsconfig.json"), JSON.stringify({
|
|
120
|
+
compilerOptions: {
|
|
121
|
+
target: "ES2022",
|
|
122
|
+
module: "ESNext",
|
|
123
|
+
moduleResolution: "bundler",
|
|
124
|
+
strict: true,
|
|
125
|
+
esModuleInterop: true,
|
|
126
|
+
skipLibCheck: true,
|
|
127
|
+
jsx: "preserve"
|
|
128
|
+
},
|
|
129
|
+
include: ["src"]
|
|
130
|
+
}, null, 2) + "\n");
|
|
131
|
+
writeFileSync(path.join(projectDir, "vite.config.ts"), generateViteConfig(worldId));
|
|
132
|
+
writeFileSync(path.join(projectDir, "index.html"), generateIndexHtml(projectName, agents));
|
|
133
|
+
writeFileSync(path.join(projectDir, "src", "main.ts"), generateMainTs(agents, worldId, signalMode));
|
|
134
|
+
writeFileSync(
|
|
135
|
+
path.join(projectDir, "public", "worlds", "index.json"),
|
|
136
|
+
JSON.stringify([WORLDS.find((w2) => w2.value === worldId)].map((w2) => ({ id: w2.value, name: w2.label })), null, 2) + "\n"
|
|
137
|
+
);
|
|
138
|
+
s.stop("Project created");
|
|
139
|
+
s.start(`Downloading ${WORLDS.find((w2) => w2.value === worldId).label} world...`);
|
|
140
|
+
const demoWorldsDir = path.resolve(__dirname, "..", "..", "..", "demo", "public", "worlds", worldId);
|
|
141
|
+
const targetWorldDir = path.join(projectDir, "public", "worlds", worldId);
|
|
142
|
+
if (existsSync(demoWorldsDir)) {
|
|
143
|
+
copyDirSync(demoWorldsDir, targetWorldDir);
|
|
144
|
+
} else {
|
|
145
|
+
const templateWorldDir = path.join(TEMPLATES, "worlds", worldId);
|
|
146
|
+
if (existsSync(templateWorldDir)) {
|
|
147
|
+
copyDirSync(templateWorldDir, targetWorldDir);
|
|
148
|
+
} else {
|
|
149
|
+
mkdirSync(targetWorldDir, { recursive: true });
|
|
150
|
+
writeFileSync(path.join(targetWorldDir, "scene.json"), "{}");
|
|
151
|
+
p.log.warn("World assets not found \u2014 you may need to copy them manually or generate a new world.");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const demoSpritesDir = path.resolve(__dirname, "..", "..", "..", "demo", "public", "sprites");
|
|
155
|
+
const targetSpritesDir = path.join(projectDir, "public", "sprites");
|
|
156
|
+
for (const spriteName of SPRITES) {
|
|
157
|
+
for (const suffix of ["_walk.png", "_actions.png"]) {
|
|
158
|
+
const src = path.join(demoSpritesDir, spriteName + suffix);
|
|
159
|
+
if (existsSync(src)) {
|
|
160
|
+
copyFileSync(src, path.join(targetSpritesDir, spriteName + suffix));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
s.stop("Assets copied");
|
|
165
|
+
const nextSteps = [
|
|
166
|
+
`cd ${projectName}`,
|
|
167
|
+
"npm install",
|
|
168
|
+
"npm run dev",
|
|
169
|
+
...signalMode === "server" ? ["npm run server # in another terminal"] : []
|
|
170
|
+
];
|
|
171
|
+
p.note(nextSteps.join("\n"), "Next steps");
|
|
172
|
+
p.outro(pc.green("Your miniverse is ready! Press E to open the editor."));
|
|
173
|
+
}
|
|
174
|
+
function generateViteConfig(worldId) {
|
|
175
|
+
return `import { defineConfig } from 'vite';
|
|
176
|
+
import fs from 'fs';
|
|
177
|
+
import path from 'path';
|
|
178
|
+
|
|
179
|
+
function sceneSavePlugin() {
|
|
180
|
+
return {
|
|
181
|
+
name: 'scene-save',
|
|
182
|
+
configureServer(server: any) {
|
|
183
|
+
server.middlewares.use('/api/save-scene', (req: any, res: any) => {
|
|
184
|
+
if (req.method !== 'POST') {
|
|
185
|
+
res.statusCode = 405;
|
|
186
|
+
res.end('Method not allowed');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
let body = '';
|
|
190
|
+
req.on('data', (chunk: string) => { body += chunk; });
|
|
191
|
+
req.on('end', () => {
|
|
192
|
+
try {
|
|
193
|
+
const data = JSON.parse(body);
|
|
194
|
+
const worldId = data.worldId ?? '${worldId}';
|
|
195
|
+
delete data.worldId;
|
|
196
|
+
|
|
197
|
+
const safeId = worldId.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
198
|
+
const worldDir = path.resolve(__dirname, 'public/worlds', safeId);
|
|
199
|
+
if (!fs.existsSync(worldDir)) {
|
|
200
|
+
fs.mkdirSync(worldDir, { recursive: true });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const filePath = path.join(worldDir, 'scene.json');
|
|
204
|
+
let existing: Record<string, unknown> = {};
|
|
205
|
+
if (fs.existsSync(filePath)) {
|
|
206
|
+
try { existing = JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch {}
|
|
207
|
+
}
|
|
208
|
+
const merged = { ...existing, ...data };
|
|
209
|
+
|
|
210
|
+
fs.writeFileSync(filePath, JSON.stringify(merged, null, 2) + '\\n');
|
|
211
|
+
res.setHeader('Content-Type', 'application/json');
|
|
212
|
+
res.end(JSON.stringify({ ok: true }));
|
|
213
|
+
console.log('[scene-save] Written to', filePath);
|
|
214
|
+
} catch (e: any) {
|
|
215
|
+
res.statusCode = 400;
|
|
216
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export default defineConfig({
|
|
225
|
+
plugins: [sceneSavePlugin()],
|
|
226
|
+
});
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
function generateIndexHtml(projectName, agents) {
|
|
230
|
+
return `<!DOCTYPE html>
|
|
231
|
+
<html lang="en">
|
|
232
|
+
<head>
|
|
233
|
+
<meta charset="UTF-8" />
|
|
234
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
235
|
+
<title>${projectName}</title>
|
|
236
|
+
<style>
|
|
237
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
238
|
+
body {
|
|
239
|
+
background: #1a1a2e;
|
|
240
|
+
color: #eee;
|
|
241
|
+
font-family: 'Courier New', monospace;
|
|
242
|
+
display: flex;
|
|
243
|
+
flex-direction: column;
|
|
244
|
+
align-items: center;
|
|
245
|
+
justify-content: center;
|
|
246
|
+
min-height: 100vh;
|
|
247
|
+
gap: 16px;
|
|
248
|
+
}
|
|
249
|
+
h1 { font-size: 20px; color: #e94560; letter-spacing: 4px; text-transform: uppercase; }
|
|
250
|
+
.subtitle { font-size: 12px; color: #666; margin-bottom: 8px; }
|
|
251
|
+
#miniverse-container {
|
|
252
|
+
border: 2px solid #333;
|
|
253
|
+
border-radius: 4px;
|
|
254
|
+
overflow: hidden;
|
|
255
|
+
background: #0f0f23;
|
|
256
|
+
display: inline-block;
|
|
257
|
+
line-height: 0;
|
|
258
|
+
transition: border-color 0.15s, border-radius 0.15s;
|
|
259
|
+
}
|
|
260
|
+
#editor-wrapper #miniverse-container {
|
|
261
|
+
border-color: #00ff88;
|
|
262
|
+
border-radius: 4px 0 0 4px;
|
|
263
|
+
}
|
|
264
|
+
#tooltip {
|
|
265
|
+
position: fixed;
|
|
266
|
+
background: rgba(0,0,0,0.85);
|
|
267
|
+
border: 1px solid #e94560;
|
|
268
|
+
padding: 8px 12px;
|
|
269
|
+
border-radius: 4px;
|
|
270
|
+
font-size: 11px;
|
|
271
|
+
pointer-events: none;
|
|
272
|
+
display: none;
|
|
273
|
+
z-index: 100;
|
|
274
|
+
max-width: 200px;
|
|
275
|
+
}
|
|
276
|
+
#tooltip .name { color: #e94560; font-weight: bold; }
|
|
277
|
+
#tooltip .state { color: #aaa; }
|
|
278
|
+
#tooltip .task { color: #66aaff; }
|
|
279
|
+
.status-bar {
|
|
280
|
+
display: flex; gap: 16px; font-size: 11px; color: #555;
|
|
281
|
+
}
|
|
282
|
+
.status-bar .agent { display: flex; align-items: center; gap: 4px; }
|
|
283
|
+
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: #555; }
|
|
284
|
+
.status-dot.working { background: #4ade80; }
|
|
285
|
+
.status-dot.idle { background: #fbbf24; }
|
|
286
|
+
.status-dot.sleeping { background: #818cf8; }
|
|
287
|
+
.status-dot.thinking { background: #f472b6; }
|
|
288
|
+
.status-dot.error { background: #ef4444; }
|
|
289
|
+
.status-dot.speaking { background: #22d3ee; }
|
|
290
|
+
.hint { font-size: 10px; color: #444; }
|
|
291
|
+
</style>
|
|
292
|
+
</head>
|
|
293
|
+
<body>
|
|
294
|
+
<h1>${projectName}</h1>
|
|
295
|
+
<p class="subtitle">powered by miniverse</p>
|
|
296
|
+
<div id="miniverse-container"></div>
|
|
297
|
+
<div class="status-bar" id="status-bar"></div>
|
|
298
|
+
<p class="hint">Press <b>E</b> to toggle editor</p>
|
|
299
|
+
<div id="tooltip">
|
|
300
|
+
<div class="name"></div>
|
|
301
|
+
<div class="state"></div>
|
|
302
|
+
<div class="task"></div>
|
|
303
|
+
</div>
|
|
304
|
+
<script type="module" src="/src/main.ts"></script>
|
|
305
|
+
</body>
|
|
306
|
+
</html>
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
309
|
+
function generateMainTs(agents, worldId, signalMode) {
|
|
310
|
+
const agentIds = agents.map((a) => a.toLowerCase().replace(/[^a-z0-9]/g, ""));
|
|
311
|
+
const spriteAssignments = agentIds.map((id, i) => {
|
|
312
|
+
const sprite = SPRITES[i % SPRITES.length];
|
|
313
|
+
return ` '${id}': charSprites('${sprite}'),`;
|
|
314
|
+
}).join("\n");
|
|
315
|
+
const residentConfigs = agentIds.map((id, i) => {
|
|
316
|
+
const name = agents[i];
|
|
317
|
+
const sprite = SPRITES[i % SPRITES.length];
|
|
318
|
+
return ` { agentId: '${id}', name: '${name}', sprite: '${id}', position: sceneData?.characters?.${id} ?? 'desk_0_0' },`;
|
|
319
|
+
}).join("\n");
|
|
320
|
+
const signalConfig = signalMode === "server" ? ` signal: {
|
|
321
|
+
type: 'websocket',
|
|
322
|
+
url: 'ws://localhost:4321/ws',
|
|
323
|
+
},` : ` signal: {
|
|
324
|
+
type: 'mock',
|
|
325
|
+
mockData: () => [${agentIds.map((id, i) => `
|
|
326
|
+
{ id: '${id}', name: '${agents[i]}', state: (['working', 'idle', 'thinking'] as const)[Math.floor(Math.random() * 3)], task: null, energy: Math.random() },`).join("")}
|
|
327
|
+
],
|
|
328
|
+
interval: 3000,
|
|
329
|
+
},`;
|
|
330
|
+
return `import { Miniverse, FurnitureSystem, Editor } from '@miniverse/core';
|
|
331
|
+
import type { SceneConfig, SpriteSheetConfig } from '@miniverse/core';
|
|
332
|
+
|
|
333
|
+
const WORLD_ID = '${worldId}';
|
|
334
|
+
const basePath = \`/worlds/\${WORLD_ID}\`;
|
|
335
|
+
|
|
336
|
+
function charSprites(name: string): SpriteSheetConfig {
|
|
337
|
+
return {
|
|
338
|
+
sheets: {
|
|
339
|
+
walk: \`/sprites/\${name}_walk.png\`,
|
|
340
|
+
actions: \`/sprites/\${name}_actions.png\`,
|
|
341
|
+
},
|
|
342
|
+
animations: {
|
|
343
|
+
idle_down: { sheet: 'walk', row: 0, frames: 2, speed: 0.5 },
|
|
344
|
+
idle_up: { sheet: 'walk', row: 1, frames: 2, speed: 0.5 },
|
|
345
|
+
walk_down: { sheet: 'walk', row: 0, frames: 4, speed: 0.15 },
|
|
346
|
+
walk_up: { sheet: 'walk', row: 1, frames: 4, speed: 0.15 },
|
|
347
|
+
walk_left: { sheet: 'walk', row: 2, frames: 4, speed: 0.15 },
|
|
348
|
+
walk_right: { sheet: 'walk', row: 3, frames: 4, speed: 0.15 },
|
|
349
|
+
working: { sheet: 'actions', row: 0, frames: 4, speed: 0.3 },
|
|
350
|
+
sleeping: { sheet: 'actions', row: 1, frames: 2, speed: 0.8 },
|
|
351
|
+
talking: { sheet: 'actions', row: 2, frames: 4, speed: 0.15 },
|
|
352
|
+
},
|
|
353
|
+
frameWidth: 64,
|
|
354
|
+
frameHeight: 64,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function buildSceneConfig(cols = 16, rows = 12, savedFloor?: number[][]): SceneConfig {
|
|
359
|
+
const floor: number[][] = [];
|
|
360
|
+
const walkable: boolean[][] = [];
|
|
361
|
+
|
|
362
|
+
for (let r = 0; r < rows; r++) {
|
|
363
|
+
floor[r] = [];
|
|
364
|
+
walkable[r] = [];
|
|
365
|
+
for (let c = 0; c < cols; c++) {
|
|
366
|
+
if (savedFloor && savedFloor[r] && savedFloor[r][c] !== undefined) {
|
|
367
|
+
floor[r][c] = savedFloor[r][c];
|
|
368
|
+
} else if (r <= 1) {
|
|
369
|
+
floor[r][c] = 1;
|
|
370
|
+
} else {
|
|
371
|
+
floor[r][c] = 0;
|
|
372
|
+
}
|
|
373
|
+
walkable[r][c] = floor[r][c] >= 0 && floor[r][c] !== 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
name: 'main',
|
|
379
|
+
tileWidth: 32,
|
|
380
|
+
tileHeight: 32,
|
|
381
|
+
layers: [floor],
|
|
382
|
+
walkable,
|
|
383
|
+
locations: {},
|
|
384
|
+
tilesets: [{
|
|
385
|
+
image: \`\${basePath}/tilesets/tileset.png\`,
|
|
386
|
+
tileWidth: 32,
|
|
387
|
+
tileHeight: 32,
|
|
388
|
+
columns: 16,
|
|
389
|
+
}],
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function main() {
|
|
394
|
+
const container = document.getElementById('miniverse-container')!;
|
|
395
|
+
const tooltip = document.getElementById('tooltip')!;
|
|
396
|
+
const statusBar = document.getElementById('status-bar')!;
|
|
397
|
+
|
|
398
|
+
const sceneData = await fetch(\`\${basePath}/scene.json\`).then(r => r.json()).catch(() => null);
|
|
399
|
+
|
|
400
|
+
const gridCols = sceneData?.gridCols ?? 16;
|
|
401
|
+
const gridRows = sceneData?.gridRows ?? 12;
|
|
402
|
+
const sceneConfig = buildSceneConfig(gridCols, gridRows, sceneData?.floor);
|
|
403
|
+
const tileSize = 32;
|
|
404
|
+
|
|
405
|
+
const spriteSheets: Record<string, SpriteSheetConfig> = {
|
|
406
|
+
${spriteAssignments}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const mv = new Miniverse({
|
|
410
|
+
container,
|
|
411
|
+
world: WORLD_ID,
|
|
412
|
+
scene: 'main',
|
|
413
|
+
${signalConfig}
|
|
414
|
+
residents: [
|
|
415
|
+
${residentConfigs}
|
|
416
|
+
],
|
|
417
|
+
scale: 2,
|
|
418
|
+
width: gridCols * tileSize,
|
|
419
|
+
height: gridRows * tileSize,
|
|
420
|
+
sceneConfig,
|
|
421
|
+
spriteSheets,
|
|
422
|
+
objects: [],
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// --- Furniture system ---
|
|
426
|
+
const furniture = new FurnitureSystem(tileSize, 2);
|
|
427
|
+
|
|
428
|
+
const rawSpriteMap: Record<string, string> = sceneData?.spriteMap ?? {};
|
|
429
|
+
await Promise.all(
|
|
430
|
+
Object.entries(rawSpriteMap).map(([id, src]) => furniture.loadSprite(id, \`\${basePath}\${src}\`)),
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
furniture.setLayout(sceneData?.furniture ?? []);
|
|
434
|
+
if (sceneData?.wanderPoints) {
|
|
435
|
+
furniture.setWanderPoints(sceneData.wanderPoints);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
furniture.setDeadspaceCheck((col, row) => {
|
|
439
|
+
const floor = mv.getFloorLayer();
|
|
440
|
+
return floor?.[row]?.[col] < 0;
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const syncFurniture = () => {
|
|
444
|
+
mv.setTypedLocations(furniture.getLocations());
|
|
445
|
+
mv.updateWalkability(furniture.getBlockedTiles());
|
|
446
|
+
};
|
|
447
|
+
syncFurniture();
|
|
448
|
+
furniture.onSave(syncFurniture);
|
|
449
|
+
|
|
450
|
+
await mv.start();
|
|
451
|
+
|
|
452
|
+
mv.addLayer({ order: 5, render: (ctx) => furniture.renderBelow(ctx) });
|
|
453
|
+
mv.addLayer({ order: 15, render: (ctx) => furniture.renderAbove(ctx) });
|
|
454
|
+
|
|
455
|
+
// --- Editor ---
|
|
456
|
+
const editor = new Editor({
|
|
457
|
+
canvas: mv.getCanvas(),
|
|
458
|
+
furniture,
|
|
459
|
+
miniverse: mv,
|
|
460
|
+
worldId: WORLD_ID,
|
|
461
|
+
onSave: async (scene) => {
|
|
462
|
+
const res = await fetch('/api/save-scene', {
|
|
463
|
+
method: 'POST',
|
|
464
|
+
headers: { 'Content-Type': 'application/json' },
|
|
465
|
+
body: JSON.stringify({ ...scene, worldId: WORLD_ID }),
|
|
466
|
+
});
|
|
467
|
+
if (!res.ok) throw new Error(await res.text());
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
if (sceneData?.tileNames) editor.setTileNames(sceneData.tileNames);
|
|
471
|
+
editor.loadCharacterAssignments(sceneData?.characters);
|
|
472
|
+
mv.addLayer({ order: 50, render: (ctx) => {
|
|
473
|
+
editor.renderOverlay(ctx);
|
|
474
|
+
if (editor.isActive()) syncFurniture();
|
|
475
|
+
} });
|
|
476
|
+
|
|
477
|
+
// --- Tooltip ---
|
|
478
|
+
mv.on('resident:click', (data: unknown) => {
|
|
479
|
+
const d = data as { name: string; state: string; task: string | null };
|
|
480
|
+
tooltip.style.display = 'block';
|
|
481
|
+
tooltip.querySelector('.name')!.textContent = d.name;
|
|
482
|
+
tooltip.querySelector('.state')!.textContent = \`State: \${d.state}\`;
|
|
483
|
+
tooltip.querySelector('.task')!.textContent = d.task ? \`Task: \${d.task}\` : 'No active task';
|
|
484
|
+
setTimeout(() => { tooltip.style.display = 'none'; }, 3000);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
container.addEventListener('mousemove', (e) => {
|
|
488
|
+
tooltip.style.left = e.clientX + 12 + 'px';
|
|
489
|
+
tooltip.style.top = e.clientY + 12 + 'px';
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
main().catch(console.error);
|
|
494
|
+
`;
|
|
495
|
+
}
|
|
496
|
+
function copyDirSync(src, dest) {
|
|
497
|
+
mkdirSync(dest, { recursive: true });
|
|
498
|
+
for (const entry of readdirSync(src)) {
|
|
499
|
+
const srcPath = path.join(src, entry);
|
|
500
|
+
const destPath = path.join(dest, entry);
|
|
501
|
+
if (statSync(srcPath).isDirectory()) {
|
|
502
|
+
copyDirSync(srcPath, destPath);
|
|
503
|
+
} else {
|
|
504
|
+
copyFileSync(srcPath, destPath);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-miniverse",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create a new Miniverse project — a tiny pixel world for your AI agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-miniverse": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"templates"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --external:@clack/prompts --external:picocolors",
|
|
16
|
+
"dev": "npm run build -- --watch"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@clack/prompts": "^0.8.2",
|
|
20
|
+
"picocolors": "^1.1.0"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"miniverse",
|
|
24
|
+
"ai",
|
|
25
|
+
"agents",
|
|
26
|
+
"pixel-art",
|
|
27
|
+
"create",
|
|
28
|
+
"cli"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT"
|
|
31
|
+
}
|