331 lines
15 KiB
JavaScript
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');
|