Initial TTRPG character meta-builder with visual sheet editor.
Offline Electron app seeds 10 system presets, supports create-from-scratch designer with palette/drag/resize/bricks, and exports .ttrpg-system.json for the player. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { build } from 'esbuild';
|
||||
import { spawn } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, '..');
|
||||
|
||||
await build({
|
||||
entryPoints: [path.join(root, 'app/main/index.ts')],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile: path.join(root, 'dist/main/index.cjs'),
|
||||
external: ['electron', 'better-sqlite3'],
|
||||
sourcemap: true,
|
||||
});
|
||||
|
||||
await build({
|
||||
entryPoints: [path.join(root, 'app/preload/index.ts')],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile: path.join(root, 'dist/preload/index.cjs'),
|
||||
external: ['electron'],
|
||||
sourcemap: true,
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const p = spawn(
|
||||
process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
||||
['vite', 'build', '--config', 'vite.config.ts'],
|
||||
{ cwd: root, stdio: 'inherit', shell: true },
|
||||
);
|
||||
p.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`vite exit ${code}`))));
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { build } from 'esbuild';
|
||||
import { spawn } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, '..');
|
||||
|
||||
async function buildMain() {
|
||||
await build({
|
||||
entryPoints: [path.join(root, 'app/main/index.ts')],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile: path.join(root, 'dist/main/index.cjs'),
|
||||
external: ['electron', 'better-sqlite3'],
|
||||
sourcemap: true,
|
||||
});
|
||||
await build({
|
||||
entryPoints: [path.join(root, 'app/preload/index.ts')],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile: path.join(root, 'dist/preload/index.cjs'),
|
||||
external: ['electron'],
|
||||
sourcemap: true,
|
||||
});
|
||||
}
|
||||
|
||||
function electronPath() {
|
||||
const name = process.platform === 'win32' ? 'electron.cmd' : 'electron';
|
||||
const bin = path.join(root, 'node_modules', '.bin', name);
|
||||
if (fs.existsSync(bin)) return bin;
|
||||
return path.join(root, 'node_modules', 'electron', 'cli.js');
|
||||
}
|
||||
|
||||
await buildMain();
|
||||
|
||||
const vite = spawn(
|
||||
process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
||||
['vite', '--config', 'vite.config.ts'],
|
||||
{ cwd: root, stdio: 'inherit', shell: true },
|
||||
);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
const elec = spawn(electronPath(), ['.'], {
|
||||
cwd: root,
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
env: { ...process.env, VITE_DEV_SERVER_URL: 'http://localhost:5174' },
|
||||
});
|
||||
|
||||
elec.on('exit', (code) => {
|
||||
vite.kill();
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, '..');
|
||||
|
||||
// Run via tsx so we can import TS modules
|
||||
const { validateSystemDefinition } = await import('../app/shared/systemValidate.ts');
|
||||
const { computeDerived } = await import('../app/shared/derived.ts');
|
||||
|
||||
const presetsDir = path.join(root, 'presets');
|
||||
const files = fs.readdirSync(presetsDir).filter((f) => f.endsWith('.json') && !f.includes('sample'));
|
||||
|
||||
let failed = 0;
|
||||
for (const file of files) {
|
||||
const system = JSON.parse(fs.readFileSync(path.join(presetsDir, file), 'utf8'));
|
||||
const v = validateSystemDefinition(system);
|
||||
if (!v.ok) {
|
||||
console.error('FAIL validate', file, v.errors);
|
||||
failed += 1;
|
||||
continue;
|
||||
}
|
||||
const attrs = {};
|
||||
for (const a of system.bricks.attributes ?? []) {
|
||||
attrs[a.key] = typeof a.default === 'number' ? a.default : 10;
|
||||
}
|
||||
try {
|
||||
computeDerived(system.bricks.derived ?? [], { attributes: attrs });
|
||||
console.log('OK', file);
|
||||
} catch (e) {
|
||||
console.error('FAIL derived', file, e);
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
console.error(`preset-smoke: ${failed} failed`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`preset-smoke: ${files.length} presets ok`);
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
/** Sync portable characterSheet module into dnd_player (meta_builder). */
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const src = path.resolve(__dirname, '../app/shared');
|
||||
const dest = path.resolve(__dirname, '../../dnd_player/app/shared/characterSheet');
|
||||
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
for (const file of fs.readdirSync(src)) {
|
||||
if (!file.endsWith('.ts')) continue;
|
||||
let text = fs.readFileSync(path.join(src, file), 'utf8');
|
||||
text = text.replace(/from '\.\/([^']+)\.ts'/g, "from './$1'");
|
||||
fs.writeFileSync(path.join(dest, file), text);
|
||||
console.log('synced', file);
|
||||
}
|
||||
Reference in New Issue
Block a user