Files
ModelTest20260714/Fable-5-High/tools/gen_maps.mjs
T

353 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 《崩坏童话:蜜糖村》10 张地图生成脚本
// 生成 maps/*.tmxTiled 源文件),随后由 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 `<properties>${e.map(([k, v]) => `<property name="${k}" value="${v}"/>`).join("")}</properties>`;
};
const objs = this.objects.map(o => {
const size = o.w || o.h ? ` width="${o.w}" height="${o.h}"` : "";
const body = props(o.props);
return ` <object id="${o.id}" name="${o.name}" type="${o.type}" x="${o.x}" y="${o.y}"${size}>${body}${o.w || o.h ? "" : "<point/>"}</object>`;
}).join("\n");
return `<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="${this.w}" height="${this.h}" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="${this.oid}">
<tileset firstgid="1" source="tiles.tsx"/>
<layer id="1" name="ground" width="${this.w}" height="${this.h}">
<data encoding="csv">
${csv(this.ground)}
</data>
</layer>
<layer id="2" name="obstacles" width="${this.w}" height="${this.h}">
<properties><property name="collides" type="bool" value="true"/></properties>
<data encoding="csv">
${csv(this.obst)}
</data>
</layer>
<objectgroup id="3" name="objects">
${objs}
</objectgroup>
</map>
`;
}
}
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);
// 花坛 ×3T2 浇水)
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); }
// 莓果丛 ×5T6
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", `<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.12.2" name="tiles" tilewidth="16" tileheight="16" tilecount="80" columns="10">
<image source="../public/assets/tiles.png" width="160" height="128"/>
</tileset>
`);
for (const m of maps) writeFileSync(`maps/${m.name}.tmx`, m.tmx());
console.log("TMX written:", maps.map(m => m.name).join(", "));