// 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 \n${props.map(([k, v]) => ` `).join('\n')}\n \n ` : ''; return ` ${p}`; }).join('\n'); const nextId = Math.max(1, ...m.objects.map((o) => o.id)) + 1; return ` ${csv(m.ground)} ${csv(m.obs)} ${objXml} `; } 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');