Files
ModelTest20260714/Opus4-8-xhigh/tools/gen_maps.mjs
T

331 lines
15 KiB
JavaScript

// gen_maps.mjs : builds all 10 Tiled maps as .tmx (embedded tileset), for export via Tiled CLI.
// Original layouts, no external tilesets. Run with node; then Tiled exports each .tmx -> .json.
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dir = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dir, '..');
const TMX_DIR = resolve(__dir, 'tmx');
const PNG = resolve(ROOT, 'public/assets/sprites/tileset.png');
mkdirSync(TMX_DIR, { recursive: true });
// tile index (0-based); GID = idx+1
const G = (i) => i + 1;
const t = {
grass: G(0), grassF: G(1), path: G(2), pathEdge: G(3), water: G(4), waterL: G(5),
flowerBush: G(6), berry: G(7), treeTop: G(8), treeTrunk: G(9), wall: G(10), roof: G(11),
door: G(12), fence: G(13), flowerbed: G(14), well: G(15), fog: G(16), board: G(17),
mailbox: G(18), picnic: G(19), bench: G(20), lamp: G(21), bushS: G(22), sand: G(23),
wfloor: G(24), wfloorB: G(25), iwall: G(26), iwallTop: G(27), bed: G(28), calendar: G(29),
counter: G(30), oven: G(31), recipe: G(32), shelf: G(33), shelfItem: G(34), table: G(35),
chair: G(36), clinicForm: G(37), windowI: G(38), doorI: G(39), wwall: G(40), wfloorW: G(41),
wfloorWb: G(42), hbed: G(43), wdoor: G(44), wwindow: G(45), ribbon: G(46), stage: G(47),
flowersRed: G(48), flowersBlue: G(49), mushroom: G(50), rock: G(51), stump: G(52),
lilypad: G(53), reed: G(54), honey: G(55), crate: G(56), barrel: G(57), bookshelf: G(58),
clock: G(59), painting: G(60), plantPot: G(61), rug: G(62), darkGrass: G(63), grassF2: G(64),
waterDeep: G(65), pathCorner: G(66), roofEdge: G(67), houseWindow: G(68), star: G(69),
snowdrop: G(70), cobble: G(71),
};
function M(w, h, base) {
return {
w, h,
ground: new Array(w * h).fill(base),
obs: new Array(w * h).fill(0),
objects: [],
_oid: 1,
};
}
const idx = (m, c, r) => r * m.w + c;
function setG(m, c, r, g) { if (c >= 0 && c < m.w && r >= 0 && r < m.h) m.ground[idx(m, c, r)] = g; }
function setO(m, c, r, g) { if (c >= 0 && c < m.w && r >= 0 && r < m.h) m.obs[idx(m, c, r)] = g; }
function rectG(m, c, r, w, h, g) { for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) setG(m, c + i, r + j, g); }
function rectO(m, c, r, w, h, g) { for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) setO(m, c + i, r + j, g); }
function borderO(m, g) {
for (let c = 0; c < m.w; c++) { setO(m, c, 0, g); setO(m, c, m.h - 1, g); }
for (let r = 0; r < m.h; r++) { setO(m, 0, r, g); setO(m, m.w - 1, r, g); }
}
function obj(m, name, type, c, r, props = {}, wTiles = 1, hTiles = 1) {
m.objects.push({ id: m._oid++, name, type, x: c * 16, y: r * 16, w: wTiles * 16, h: hTiles * 16, props });
}
// ---- interior room helper (wood floor + walls) ----
function room(m) {
rectG(m, 0, 0, m.w, m.h, t.wfloor);
for (let c = 0; c < m.w; c++) { setO(m, c, 0, t.iwallTop); setO(m, c, m.h - 1, t.iwall); }
for (let r = 0; r < m.h; r++) { setO(m, 0, r, t.iwall); setO(m, m.w - 1, r, t.iwall); }
}
function exitDoor(m, c, r, target, dest) { setG(m, c, r, t.doorI); setO(m, c, r, 0); obj(m, 'door_' + target, 'door', c, r, { target, dest }); }
const maps = {};
// ============ 1. HOME 12x8 ============
{
const m = M(12, 8, t.wfloor); room(m);
setG(m, 5, 1, t.rug); setG(m, 6, 1, t.rug);
setO(m, 1, 1, t.bed); setO(m, 1, 2, t.bed); // bed (2 tiles)
setG(m, 2, 0, t.calendar); // 床头日历 on top wall
setO(m, 9, 1, t.bookshelf); setO(m, 8, 1, t.plantPot);
setO(m, 8, 5, t.table); setO(m, 9, 5, t.chair);
setG(m, 4, 0, t.windowI); setG(m, 7, 0, t.windowI);
exitDoor(m, 6, 7, 'plaza', 'sp_from_home');
obj(m, 'start', 'spawn', 3, 3);
obj(m, 'sp_from_plaza', 'spawn', 6, 6);
obj(m, 'i_bed', 'interact', 2, 2, { kind: 'bed' });
obj(m, 'i_calendar', 'interact', 2, 1, { kind: 'calendar' });
maps.home = m;
}
// ============ 2. PLAZA 30x20 ============
{
const m = M(30, 20, t.grass);
// scatter grass flowers
for (let r = 0; r < 20; r++) for (let c = 0; c < 30; c++) if (((c * 5 + r * 7) % 11) === 0) setG(m, c, r, t.grassF);
borderO(m, t.fence);
// trees along corners
for (const [c, r] of [[1, 1], [2, 1], [27, 1], [28, 1], [1, 18], [28, 18]]) { setO(m, c, r, t.treeTrunk); setG(m, c, r - 1 < 0 ? 0 : c, r); }
// ---- houses along top (home/bakery/grocery/clinic) ----
function house(c, target, dest) {
rectO(m, c, 1, 3, 2, t.roof);
rectO(m, c, 3, 3, 1, t.wall);
setO(m, c, 2, t.wall); setO(m, c + 2, 2, t.wall);
setG(m, c + 1, 3, t.door); setO(m, c + 1, 3, 0);
obj(m, 'door_' + target, 'door', c + 1, 3, { target, dest });
// path down from door
setG(m, c + 1, 4, t.path); setG(m, c + 1, 5, t.path);
obj(m, 'sp_from_' + target, 'spawn', c + 1, 5);
}
house(2, 'home', 'sp_from_plaza');
house(8, 'bakery', 'sp_from_plaza');
house(15, 'grocery', 'sp_from_plaza');
house(22, 'clinic', 'sp_from_plaza');
// central cobble plaza + main path
rectG(m, 3, 9, 24, 3, t.path);
rectG(m, 11, 13, 8, 4, t.cobble);
// festival stage (center) — walkable cobble around; stage tile as decor obstacle at back
rectO(m, 12, 12, 4, 1, t.stage);
// bulletin board (afu)
setO(m, 14, 8, t.board);
// flowerbed (T2 water) cluster left-center
setO(m, 5, 13, t.flowerbed); setO(m, 6, 13, t.flowerbed); setO(m, 7, 13, t.flowerbed);
// twin-bird tree (right)
setO(m, 24, 5, t.treeTrunk); setG(m, 23, 4, t.treeTop); setG(m, 24, 4, t.treeTop); setG(m, 25, 4, t.treeTop); setG(m, 24, 3, t.treeTop);
// mailbox (dabao) bottom-center
setO(m, 15, 17, t.mailbox);
obj(m, 'i_mailbox', 'interact', 15, 16, { kind: 'mailbox' }); // C3 散落信件
// decorative
setO(m, 3, 16, t.bushS); setO(m, 26, 16, t.bushS); setO(m, 9, 8, t.lamp); setO(m, 20, 8, t.lamp);
// edge exits (leave gaps in fence)
setO(m, 0, 10, 0); exitDoor(m, 0, 10, 'field', 'sp_from_plaza'); setG(m, 1, 10, t.path); obj(m, 'sp_from_field', 'spawn', 2, 10);
setO(m, 29, 10, 0); exitDoor(m, 29, 10, 'lake', 'sp_from_plaza'); setG(m, 28, 10, t.path); obj(m, 'sp_from_lake', 'spawn', 27, 10);
setO(m, 4, 19, 0); exitDoor(m, 4, 19, 'well', 'sp_from_plaza'); setG(m, 4, 18, t.path); obj(m, 'sp_from_well', 'spawn', 4, 17);
setO(m, 25, 19, 0); exitDoor(m, 25, 19, 'fog', 'sp_from_plaza'); setG(m, 25, 18, t.path); obj(m, 'sp_from_fog', 'spawn', 25, 17);
// NPCs
obj(m, 'afu', 'npc', 14, 9, { npc: 'afu' });
obj(m, 'dabao', 'npc', 15, 18, { npc: 'dabao' });
obj(m, 'bird1', 'npc', 23, 5, { npc: 'bird1' });
obj(m, 'bird2', 'npc', 25, 5, { npc: 'bird2' });
obj(m, 'extra1', 'npc', 10, 15, { npc: 'extra1' });
obj(m, 'extra2', 'npc', 19, 16, { npc: 'extra2' });
obj(m, 'extra3', 'npc', 6, 6, { npc: 'extra3' });
// interacts
obj(m, 'i_board', 'interact', 14, 9, { kind: 'board' });
obj(m, 'i_flowerbed', 'interact', 6, 14, { kind: 'flowerbed' });
obj(m, 'i_festival', 'interact', 14, 14, { kind: 'festival' });
obj(m, 'i_ribbon0', 'interact', 12, 13, { kind: 'ribbon', n: 0 });
obj(m, 'i_ribbon1', 'interact', 14, 13, { kind: 'ribbon', n: 1 });
obj(m, 'i_ribbon2', 'interact', 16, 13, { kind: 'ribbon', n: 2 });
obj(m, 'i_ribbon3', 'interact', 18, 13, { kind: 'ribbon', n: 3 });
maps.plaza = m;
}
// ============ 3. BAKERY 15x10 ============
{
const m = M(15, 10, t.wfloor); room(m);
rectG(m, 1, 1, 13, 8, t.wfloorB);
rectO(m, 2, 1, 5, 1, t.oven);
rectO(m, 1, 6, 8, 1, t.counter);
setG(m, 4, 5, t.recipe); // 配方纸 (C2 on back)
setO(m, 11, 1, t.shelf); setO(m, 12, 1, t.shelf);
setO(m, 12, 7, t.barrel); setO(m, 13, 4, t.plantPot);
setG(m, 8, 0, t.windowI);
exitDoor(m, 7, 9, 'plaza', 'sp_from_bakery');
obj(m, 'sp_from_plaza', 'spawn', 7, 8);
obj(m, 'yuan', 'npc', 4, 7, { npc: 'yuan' });
obj(m, 'i_recipe', 'interact', 4, 6, { kind: 'recipe' });
maps.bakery = m;
}
// ============ 4. GROCERY 15x10 ============
{
const m = M(15, 10, t.wfloor); room(m);
rectG(m, 1, 1, 13, 8, t.wfloorB);
// shelves: same item repeated 9 (C5)
for (let c = 2; c <= 10; c++) setO(m, c, 1, t.shelfItem); // 9 identical
rectO(m, 1, 6, 8, 1, t.counter);
setO(m, 12, 3, t.crate); setO(m, 13, 6, t.barrel); setO(m, 12, 7, t.crate);
setG(m, 11, 0, t.windowI);
exitDoor(m, 7, 9, 'plaza', 'sp_from_grocery');
obj(m, 'sp_from_plaza', 'spawn', 7, 8);
obj(m, 'mimi', 'npc', 4, 7, { npc: 'mimi' });
obj(m, 'i_shelf', 'interact', 6, 2, { kind: 'shelf' });
maps.grocery = m;
}
// ============ 5. CLINIC 12x8 ============
{
const m = M(12, 8, t.wfloor); room(m);
rectG(m, 1, 1, 10, 6, t.wfloorB);
setO(m, 2, 2, t.table); setG(m, 2, 1, t.clinicForm); // 表格 (C8)
setO(m, 9, 1, t.bookshelf); setO(m, 9, 5, t.plantPot);
setO(m, 5, 5, t.chair);
setG(m, 6, 0, t.windowI);
exitDoor(m, 6, 7, 'plaza', 'sp_from_clinic');
obj(m, 'sp_from_plaza', 'spawn', 6, 6);
obj(m, 'fu', 'npc', 4, 3, { npc: 'fu' });
obj(m, 'i_clinicform', 'interact', 2, 2, { kind: 'clinicform' });
maps.clinic = m;
}
// ============ 6. LAKE 24x16 ============
{
const m = M(24, 16, t.grass);
for (let r = 0; r < 16; r++) for (let c = 0; c < 24; c++) if (((c * 3 + r * 5) % 13) === 0) setG(m, c, r, t.grassF);
// lake fills bottom rows 9-15
for (let r = 9; r < 16; r++) for (let c = 1; c < 23; c++) {
const deep = r >= 12;
setG(m, c, r, deep ? t.waterDeep : (((c + r) % 2) ? t.water : t.waterL));
setO(m, c, r, deep ? t.waterDeep : t.water);
}
// lilypads / reeds on shore
setO(m, 3, 9, t.reed); setO(m, 20, 9, t.reed); setO(m, 12, 10, t.lilypad);
borderO(m, t.fence);
// top exit to plaza
setO(m, 12, 0, 0); exitDoor(m, 12, 0, 'plaza', 'sp_from_lake'); setG(m, 12, 1, t.path); obj(m, 'sp_from_plaza', 'spawn', 12, 2);
// bench (gui) at shore
setO(m, 5, 8, t.bench);
// picnic cloth (T7)
setG(m, 16, 6, t.picnic);
// trees
setO(m, 2, 2, t.treeTrunk); setG(m, 2, 1, t.treeTop); setO(m, 21, 3, t.treeTrunk); setG(m, 21, 2, t.treeTop);
setO(m, 8, 3, t.rock); setO(m, 18, 2, t.bushS);
obj(m, 'gui', 'npc', 5, 7, { npc: 'gui' });
obj(m, 'extra4', 'npc', 15, 3, { npc: 'extra4' });
obj(m, 'i_picnic', 'interact', 16, 6, { kind: 'picnic' });
maps.lake = m;
}
// ============ 7. FIELD 20x14 ============
{
const m = M(20, 14, t.grass);
for (let r = 0; r < 14; r++) for (let c = 0; c < 20; c++) {
const k = (c * 7 + r * 3) % 9;
if (k === 0) setG(m, c, r, t.grassF); else if (k === 3) setG(m, c, r, t.grassF2);
}
borderO(m, t.fence);
// berry bushes (T6, pick x5) — 5 spread
const berries = [[4, 3], [8, 4], [13, 3], [6, 8], [14, 9]];
berries.forEach(([c, r], i) => { setO(m, c, r, t.berry); obj(m, 'i_berry' + i, 'interact', c, r, { kind: 'berry', n: i }); });
// flowerbed (浇花点)
setO(m, 10, 6, t.flowerbed); setO(m, 11, 6, t.flowerbed);
obj(m, 'i_fieldbed', 'interact', 10, 7, { kind: 'flowerbed' });
setO(m, 2, 2, t.treeTrunk); setG(m, 2, 1, t.treeTop); setO(m, 17, 11, t.stump); setO(m, 16, 4, t.mushroom);
// top exit to plaza
setO(m, 10, 0, 0); exitDoor(m, 10, 0, 'plaza', 'sp_from_field'); setG(m, 10, 1, t.path); obj(m, 'sp_from_plaza', 'spawn', 10, 2);
obj(m, 'extra5', 'npc', 15, 7, { npc: 'extra5' });
maps.field = m;
}
// ============ 8. WELL 14x10 ============
{
const m = M(14, 10, t.grass);
for (let r = 0; r < 10; r++) for (let c = 0; c < 14; c++) if (((c + r) % 5) === 0) setG(m, c, r, t.grassF);
rectG(m, 5, 3, 4, 4, t.sand);
borderO(m, t.fence);
setO(m, 6, 4, t.well); setO(m, 7, 4, t.well);
setO(m, 2, 2, t.rock); setO(m, 11, 7, t.bushS); setO(m, 3, 7, t.stump);
// top exit to plaza
setO(m, 7, 0, 0); exitDoor(m, 7, 0, 'plaza', 'sp_from_well'); setG(m, 7, 1, t.path); obj(m, 'sp_from_plaza', 'spawn', 7, 2);
obj(m, 'i_well', 'interact', 7, 5, { kind: 'well' });
maps.well = m;
}
// ============ 9. FOG 20x8 ============
{
const m = M(20, 8, t.grass);
for (let r = 0; r < 8; r++) for (let c = 0; c < 20; c++) if (((c * 2 + r) % 7) === 0) setG(m, c, r, t.grassF);
// fog wall fills top rows 0-2 (collides)
for (let r = 0; r < 3; r++) for (let c = 0; c < 20; c++) { setG(m, c, r, t.fog); setO(m, c, r, t.fog); }
for (let r = 3; r < 8; r++) { setO(m, 0, r, t.fence); setO(m, 19, r, t.fence); }
for (let c = 0; c < 20; c++) setO(m, c, 7, t.fence);
setO(m, 3, 5, t.bushS); setO(m, 16, 5, t.bushS);
// bottom exit to plaza
setO(m, 10, 7, 0); exitDoor(m, 10, 7, 'plaza', 'sp_from_fog'); setG(m, 10, 6, t.path); obj(m, 'sp_from_plaza', 'spawn', 10, 5);
obj(m, 'i_fog', 'interact', 10, 3, { kind: 'fog' });
maps.fog = m;
}
// ============ 10. WHITE (corridor 20x6 stacked over ward) 20x14 ============
{
const m = M(20, 14, t.wfloorW);
rectG(m, 0, 0, 20, 14, t.wfloorW);
rectG(m, 0, 0, 20, 6, t.wfloorWb); // corridor top band
borderO(m, t.wwall);
// ward divider wall (row 6) with a doorway
for (let c = 0; c < 20; c++) setO(m, c, 6, t.wwall);
setO(m, 10, 6, 0); setG(m, 10, 6, t.wdoor);
// corridor windows
setG(m, 4, 0, t.wwindow); setG(m, 15, 0, t.wwindow);
// ward: hospital beds
setO(m, 3, 9, t.hbed); setO(m, 3, 11, t.hbed); setO(m, 16, 9, t.hbed);
obj(m, 'sp_corridor', 'spawn', 10, 2);
obj(m, 'sp_ward', 'spawn', 10, 11);
maps.white = m;
}
// ================= TMX serialization =================
function tmx(m) {
const csv = (arr) => {
let out = '';
for (let r = 0; r < m.h; r++) {
out += arr.slice(r * m.w, r * m.w + m.w).join(',');
if (r < m.h - 1) out += ',';
out += '\n';
}
return out;
};
const objXml = m.objects.map((o) => {
const props = Object.entries(o.props);
const p = props.length
? `\n <properties>\n${props.map(([k, v]) => ` <property name="${k}" value="${v}"/>`).join('\n')}\n </properties>\n `
: '';
return ` <object id="${o.id}" name="${o.name}" type="${o.type}" x="${o.x}" y="${o.y}" width="${o.w}" height="${o.h}">${p}</object>`;
}).join('\n');
const nextId = Math.max(1, ...m.objects.map((o) => o.id)) + 1;
return `<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="${m.w}" height="${m.h}" tilewidth="16" tileheight="16" infinite="0" nextlayerid="5" nextobjectid="${nextId}">
<tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="72" columns="8">
<image source="${PNG}" width="128" height="144"/>
</tileset>
<layer id="1" name="ground" width="${m.w}" height="${m.h}">
<data encoding="csv">
${csv(m.ground)}</data>
</layer>
<layer id="2" name="obstacles" width="${m.w}" height="${m.h}">
<data encoding="csv">
${csv(m.obs)}</data>
</layer>
<objectgroup id="3" name="objects">
${objXml}
</objectgroup>
</map>
`;
}
for (const [name, m] of Object.entries(maps)) {
writeFileSync(resolve(TMX_DIR, name + '.tmx'), tmx(m));
console.log('wrote', name + '.tmx', m.w + 'x' + m.h, m.objects.length + ' objects');
}
console.log('MAPS TMX DONE');