// 《崩坏童话:蜜糖村》10 张地图生成脚本 // 生成 maps/*.tmx(Tiled 源文件),随后由 Tiled CLI 导出 JSON 到 public/assets/maps/ // 运行:node tools/gen_maps.mjs import { writeFileSync, mkdirSync } from "fs"; // 瓦片索引(与 tools/gen_art.lua 的 tileset 一一对应),GID = 索引 + 1 const I = { grass: 0, grass2: 1, grassWF: 2, grassPF: 3, path: 4, path2: 5, pathEdge: 6, water1: 7, water2: 8, shore: 9, flowerBush: 10, berryFull: 11, berryPicked: 12, treeTL: 13, treeTR: 14, treeBL: 15, treeBR: 16, bush: 17, wall: 18, wallWin: 19, door: 20, roofL: 21, roofR: 22, eave: 23, floorW: 24, floorW2: 25, floorC: 26, inWall: 27, inWallLow: 28, rug: 29, bedHead: 30, bedFoot: 31, counter: 32, oven: 33, shelfEmpty: 34, shelfGoods: 35, table: 36, chair: 37, wellRoof: 38, wellRing: 39, fogLight: 40, fogDense: 41, wWall: 42, wFloor: 43, wDoor: 44, benchL: 45, benchR: 46, boardL: 47, boardR: 48, mailbox: 49, fenceH: 50, fencePost: 51, sign: 52, picnic1: 53, picnic2: 54, bedBloom: 55, dirt: 56, sprout: 57, bunting: 58, lantern: 59, sparkle: 60, winIn: 61, wBedHead: 62, wBedFoot: 63, desk: 64, calendar: 65, counterPaper: 66, crate: 67, basket: 68, bedDry: 69, wWin: 70, doormat: 71, tallGrass: 72, plazaBrick: 73, wCurtain: 74, monitor: 75, stoneWall: 76, fogGrass: 77, black: 78, wBlank: 79, }; class M { constructor(name, w, h) { this.name = name; this.w = w; this.h = h; this.ground = new Array(w * h).fill(0); this.obst = new Array(w * h).fill(0); this.objects = []; this.oid = 1; } g(x, y, t) { this.ground[y * this.w + x] = t + 1; } o(x, y, t) { this.obst[y * this.w + x] = t + 1; } gFill(t) { for (let i = 0; i < this.ground.length; i++) this.ground[i] = t + 1; } gRect(x, y, w, h, t) { for (let j = y; j < y + h; j++) for (let i = x; i < x + w; i++) this.g(i, j, t); } oRect(x, y, w, h, t) { for (let j = y; j < y + h; j++) for (let i = x; i < x + w; i++) this.o(i, j, t); } // 装饰草:确定性伪随机 grassDecor() { for (let j = 0; j < this.h; j++) for (let i = 0; i < this.w; i++) { const r = (i * 73 + j * 149 + i * j * 7) % 23; if (this.ground[j * this.w + i] === I.grass + 1) { if (r === 0) this.g(i, j, I.grassWF); else if (r === 1) this.g(i, j, I.grassPF); else if (r < 6) this.g(i, j, I.grass2); } } } tree(x, y) { this.o(x, y, I.treeTL); this.o(x + 1, y, I.treeTR); this.o(x, y + 1, I.treeBL); this.o(x + 1, y + 1, I.treeBR); } // 4 宽 ×3 高的房屋立面,door 在底行 dx 处 facade(x, y, dx) { this.o(x, y, I.roofL); this.o(x + 1, y, I.roofL); this.o(x + 2, y, I.roofR); this.o(x + 3, y, I.roofR); for (let i = 0; i < 4; i++) this.o(x + i, y + 1, I.eave); for (let i = 0; i < 4; i++) this.o(x + i, y + 2, i % 3 === 1 ? I.wallWin : I.wall); this.obst[(y + 2) * this.w + (x + dx)] = 0; // 门位可走 this.g(x + dx, y + 2, I.door); this.g(x + dx, y + 3, I.doormat); } obj(name, type, tx, ty, opts = {}) { const o = { id: this.oid++, name, type, x: tx * 16, y: ty * 16, w: (opts.w ?? 0) * 16, h: (opts.h ?? 0) * 16, props: opts.props ?? {} }; this.objects.push(o); return o; } spawn(name, tx, ty) { this.obj(name, "spawn", tx + 0.5, ty + 0.5); } door(tx, ty, w, h, target, spawn) { this.obj("to_" + target, "door", tx, ty, { w, h, props: { target, spawn } }); } npc(name, tx, ty, props = {}) { this.obj(name, "npc", tx + 0.5, ty + 0.5, { props }); } poi(name, tx, ty, props = {}) { this.obj(name, "poi", tx + 0.5, ty + 0.5, { props }); } tmx() { const csv = a => { let out = []; for (let j = 0; j < this.h; j++) out.push(a.slice(j * this.w, (j + 1) * this.w).join(",")); return out.join(",\n"); }; const props = p => { const e = Object.entries(p); if (!e.length) return ""; return `${e.map(([k, v]) => ``).join("")}`; }; const objs = this.objects.map(o => { const size = o.w || o.h ? ` width="${o.w}" height="${o.h}"` : ""; const body = props(o.props); return ` ${body}${o.w || o.h ? "" : ""}`; }).join("\n"); return ` ${csv(this.ground)} ${csv(this.obst)} ${objs} `; } } mkdirSync("maps", { recursive: true }); mkdirSync("public/assets/maps", { recursive: true }); const maps = []; // ============ 小满小屋 12×8 ============ { const m = new M("house", 12, 8); m.gFill(I.floorW); for (let i = 0; i < 12; i++) { m.o(i, 0, I.inWall); m.o(i, 1, i === 6 ? I.winIn : I.inWallLow); } m.o(2, 1, I.calendar); m.o(1, 2, I.bedHead); m.o(1, 3, I.bedFoot); m.o(7, 4, I.table); m.o(8, 4, I.chair); m.o(10, 2, I.crate); m.g(5, 7, I.rug); m.g(6, 7, I.rug); m.o(0, 2, I.crate); // 角落杂物 m.spawn("bed", 2, 3); m.spawn("entry", 5, 6); m.door(5, 7.5, 2, 0.5, "plaza", "fromHouse"); m.poi("calendar", 2, 2, { clue: "C1" }); m.poi("bed", 1, 4); maps.push(m); } // ============ 村广场 30×20 ============ { const m = new M("plaza", 30, 20); m.gFill(I.grass); // 道路 m.gRect(14, 0, 2, 20, I.path); m.gRect(0, 8, 30, 2, I.path); for (let i = 0; i < 30; i += 2) m.g(i, 8, I.path2); for (let j = 1; j < 20; j += 3) m.g(14, j, I.path2); // 庆典场地 m.gRect(12, 11, 6, 4, I.plazaBrick); m.grassDecor(); // 边界:树与栅栏(留出四个出口) for (let i = 0; i < 30; i += 2) { if (i < 12 || i > 17) m.tree(i, 0); } for (let i = 0; i < 30; i += 2) { if (i < 13 || i > 16) { m.o(i, 19, I.fenceH); m.o(i + 1, 19, I.fenceH); } } for (let j = 2; j < 18; j += 2) { if (j < 7 || j > 10) { m.o(0, j, I.fencePost); m.o(29, j, I.fencePost); } } // 房屋立面:小屋 / 面包房 / 杂货店 / 诊所 m.facade(2, 2, 2); m.facade(7, 2, 2); m.facade(19, 2, 2); m.facade(24, 2, 2); m.poi("sign_house", 1, 5); m.o(1, 5, I.sign); // 公告板 m.o(11, 6, I.boardL); m.o(12, 6, I.boardR); m.poi("board", 11.5, 7.5); // 邮筒与散落信件(C3) m.o(17, 1, I.mailbox); m.poi("letters", 17.5, 2.5, { clue: "C3" }); // 双子鸟树 m.tree(20, 6); m.poi("birdtree", 21, 8.5); // 花坛 ×3(T2 浇水) m.o(7, 11, I.bedDry); m.o(7, 13, I.bedDry); m.o(7, 15, I.bedDry); m.poi("flowerbed1", 7.5, 12); m.poi("flowerbed2", 7.5, 14); m.poi("flowerbed3", 7.5, 16); // 庆典灯笼杆 ×4(T13 挂彩带) m.o(11, 10, I.lantern); m.o(18, 10, I.lantern); m.o(11, 15, I.lantern); m.o(18, 15, I.lantern); m.poi("ribbon1", 11.5, 11.2); m.poi("ribbon2", 18.5, 11.2); m.poi("ribbon3", 11.5, 16.2); m.poi("ribbon4", 18.5, 16.2); m.poi("festival", 15, 13); // 装饰 m.o(4, 12, I.flowerBush); m.o(23, 12, I.flowerBush); m.o(25, 15, I.bush); m.o(3, 16, I.bush); m.tree(26, 16); m.tree(21, 17); // NPC m.npc("afu", 12.5, 8.5); m.npc("dabao", 16.5, 3.5); m.npc("bird1", 20.4, 6.3); m.npc("bird2", 21.6, 6.3); m.npc("extra1", 5, 10, { patrol: 40 }); m.npc("extra2", 22, 9, { patrol: 56 }); m.npc("extra3", 24, 17, { patrol: 40 }); m.npc("extra4", 5, 17, { patrol: 24 }); m.npc("extra5", 22, 17, { patrol: 16 }); // 出生点 m.spawn("fromHouse", 4, 6.2); m.spawn("fromBakery", 9, 6.2); m.spawn("fromStore", 21, 6.2); m.spawn("fromClinic", 26, 6.2); m.spawn("fromLake", 1.6, 9); m.spawn("fromField", 28.4, 9); m.spawn("fromWell", 14.5, 1.6); m.spawn("fromFog", 14.5, 17.2); m.spawn("center", 15, 12); // 门与出口 m.door(4, 4.2, 1, 0.8, "house", "entry"); m.door(9, 4.2, 1, 0.8, "bakery", "entry"); m.door(21, 4.2, 1, 0.8, "store", "entry"); m.door(26, 4.2, 1, 0.8, "clinic", "entry"); m.door(0, 7, 0.6, 4, "lake", "entry"); m.door(29.4, 7, 0.6, 4, "field", "entry"); m.door(12.5, 0, 5, 0.7, "well", "entry"); m.door(12.5, 19.3, 5, 0.7, "fogwall", "entry"); maps.push(m); } // ============ 面包房 15×10 ============ { const m = new M("bakery", 15, 10); m.gFill(I.floorW); for (let i = 0; i < 15; i++) { m.o(i, 0, I.inWall); m.o(i, 1, (i === 4 || i === 10) ? I.winIn : I.inWallLow); } m.o(12, 1, I.oven); for (let i = 2; i < 7; i++) m.o(i, 3, I.counter); m.o(7, 3, I.counterPaper); m.o(1, 5, I.basket); m.o(1, 7, I.crate); m.o(13, 5, I.crate); m.o(10, 5, I.table); m.o(11, 5, I.chair); m.g(7, 9, I.rug); m.g(8, 9, I.rug); m.npc("yuanyuan", 4.5, 2.6); m.poi("recipe", 7.5, 4.2, { clue: "C2" }); m.poi("knead", 10.5, 6.2); m.spawn("entry", 7.5, 8); m.door(7, 9.5, 2, 0.5, "plaza", "fromBakery"); maps.push(m); } // ============ 杂货店 15×10 ============ { const m = new M("store", 15, 10); m.gFill(I.floorW2); for (let i = 0; i < 15; i++) { m.o(i, 0, I.inWall); m.o(i, 1, i === 12 ? I.winIn : I.inWallLow); } for (let i = 2; i < 11; i++) m.o(i, 2, I.shelfGoods); m.o(13, 4, I.shelfGoods); m.o(13, 5, I.shelfGoods); m.o(13, 6, I.shelfGoods); m.o(3, 5, I.counter); m.o(4, 5, I.counter); m.o(1, 7, I.crate); m.o(11, 8, I.basket); m.g(7, 9, I.rug); m.g(8, 9, I.rug); m.npc("mimi", 3.5, 4.4); m.poi("shelfA", 3.5, 3.2); m.poi("shelfB", 6.5, 3.2); m.poi("shelfC", 9.5, 3.2); m.poi("shelf_c5", 12.4, 5.5, { clue: "C5" }); m.spawn("entry", 7.5, 8); m.door(7, 9.5, 2, 0.5, "plaza", "fromStore"); maps.push(m); } // ============ 诊所 12×8 ============ { const m = new M("clinic", 12, 8); m.gFill(I.floorC); for (let i = 0; i < 12; i++) { m.o(i, 0, I.wWall); m.o(i, 1, i === 3 ? I.wWin : I.inWallLow); } m.o(5, 3, I.desk); m.o(9, 2, I.wBedHead); m.o(9, 3, I.wBedFoot); m.o(8, 1, I.wCurtain); m.o(1, 2, I.crate); m.g(5, 7, I.rug); m.g(6, 7, I.rug); m.npc("fu", 6.5, 2.6); m.poi("chart", 4.5, 4.2, { clue: "C8" }); m.spawn("entry", 5.5, 6); m.door(5, 7.5, 2, 0.5, "plaza", "fromClinic"); maps.push(m); } // ============ 湖畔 24×16 ============ { const m = new M("lake", 24, 16); m.gFill(I.grass); for (let i = 0; i < 24; i++) { m.g(i, 9, I.shore); for (let j = 10; j < 16; j++) m.g(i, j, (i * 31 + j * 17) % 7 === 0 ? I.sparkle : ((i + j) % 2 ? I.water1 : I.water2)); } m.oRect(0, 10, 24, 6, I.water1); // 水面不可走(碰撞用,绘制层在 ground) for (let j = 10; j < 16; j++) for (let i = 0; i < 24; i++) m.obst[j * m.w + i] = m.ground[j * m.w + i]; // 与水面同贴图 m.grassDecor(); for (let i = 0; i < 24; i += 2) m.tree(i, 0); for (let j = 2; j < 8; j += 2) { m.o(0, j, I.fencePost); } m.tree(2, 3); m.tree(11, 2); m.o(6, 4, I.flowerBush); // 长椅(龟爷爷) m.o(16, 5, I.benchL); m.o(17, 5, I.benchR); m.npc("gui", 18.9, 5.5); m.poi("bench", 16.5, 6.5); // 野餐布 m.g(7, 6, I.picnic1); m.g(8, 6, I.picnic2); m.g(7, 7, I.picnic2); m.g(8, 7, I.picnic1); m.poi("picnic", 8, 7); m.spawn("entry", 22, 7); m.spawn("picnicSpot", 8, 7.6); m.door(23.4, 5, 0.6, 5, "plaza", "fromLake"); maps.push(m); } // ============ 花田 20×14 ============ { const m = new M("field", 20, 14); m.gFill(I.grass); m.gRect(3, 3, 14, 8, I.dirt); for (let j = 3; j < 11; j++) for (let i = 3; i < 17; i++) if ((i + j) % 3 === 0) m.g(i, j, I.sprout); m.grassDecor(); for (let i = 0; i < 20; i += 2) { m.tree(i, 0); if (i < 4 || i > 8) { m.o(i, 13, I.fenceH); m.o(i + 1, 13, I.fenceH); } } for (let j = 2; j < 12; j += 2) { if (j < 5 || j > 8) m.o(19, j, I.fencePost); } // 莓果丛 ×5(T6) const berries = [[4, 4], [8, 5], [12, 4], [15, 7], [6, 9]]; berries.forEach(([x, y], k) => { m.o(x, y, I.berryFull); m.poi("berry" + (k + 1), x + 0.5, y + 1.5); }); // 浇花点 m.o(3, 11, I.bedDry); m.poi("fieldwater", 3.5, 12.2); m.o(17, 11, I.flowerBush); m.o(2, 2, I.bush); m.spawn("entry", 1.6, 6); m.door(0, 4.5, 0.6, 5, "plaza", "fromField"); maps.push(m); } // ============ 古井空地 14×10 ============ { const m = new M("well", 14, 10); m.gFill(I.grass); m.gRect(6, 6, 2, 4, I.path); m.grassDecor(); for (let i = 0; i < 14; i += 2) m.tree(i, 0); for (let j = 2; j < 9; j += 2) { m.o(0, j, I.fencePost); m.o(13, j, I.fencePost); } for (let i = 0; i < 14; i += 2) { if (i < 5 || i > 8) { m.o(i, 9, I.fenceH); m.o(i + 1, 9, I.fenceH); } } m.o(2, 4, I.tallGrass); m.o(3, 4, I.tallGrass); m.o(11, 5, I.tallGrass); m.o(11, 3, I.bush) m.o(6, 3, I.wellRoof); m.o(6, 4, I.wellRing); m.o(9, 4, I.sign); m.poi("well", 6.5, 5.5, { clue: "C7" }); m.spawn("entry", 6.5, 7.6); m.door(4.5, 9.3, 5, 0.7, "plaza", "fromWell"); maps.push(m); } // ============ 雾墙边界 20×8 ============ { const m = new M("fogwall", 20, 8); m.gFill(I.grass); for (let i = 0; i < 20; i++) { m.g(i, 4, I.fogGrass); m.g(i, 5, I.fogLight); m.g(i, 6, I.fogDense); m.g(i, 7, I.fogDense); m.o(i, 6, I.fogDense); m.o(i, 7, I.fogDense); } m.grassDecor(); for (let j = 0; j < 6; j += 2) { m.o(0, j, I.fencePost); m.o(19, j, I.fencePost); } m.tree(2, 1); m.tree(16, 1); m.o(4, 3, I.sign); m.poi("fog", 10, 5.5); m.spawn("entry", 10, 2.4); m.door(7.5, 0, 5, 0.7, "plaza", "fromFog"); maps.push(m); } // ============ 白色层·走廊 20×6 ============ { const m = new M("white_corridor", 20, 6); m.gFill(I.wFloor); for (let i = 0; i < 20; i++) m.o(i, 0, (i % 5 === 2) ? I.wWin : I.wWall); m.o(6, 0, I.wDoor); m.o(13, 0, I.wDoor); m.spawn("entry", 1, 3); m.door(19.5, 0, 0.5, 6, "white_ward", "entry"); maps.push(m); } // ============ 白色层·病房 10×8 ============ { const m = new M("white_ward", 10, 8); m.gFill(I.wFloor); for (let i = 0; i < 10; i++) m.o(i, 0, i === 6 ? I.wWin : I.wWall); m.o(2, 0, I.wCurtain); m.o(4, 2, I.wBedHead); m.o(4, 3, I.wBedFoot); m.o(6, 2, I.monitor); m.poi("chart_ward", 5, 4.5); m.spawn("entry", 1, 5); maps.push(m); } // tiles.tsx writeFileSync("maps/tiles.tsx", ` `); for (const m of maps) writeFileSync(`maps/${m.name}.tmx`, m.tmx()); console.log("TMX written:", maps.map(m => m.name).join(", "));