Honey Village: six model builds + hub frontend, Dockerized for Dokploy
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { chromium } from "playwright";
|
||||
import { preview } from "vite";
|
||||
const server = await preview({ preview: { port: 5194 } });
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||
const errors = [];
|
||||
page.on("console", m => { if (m.type() === "error") errors.push(m.text()); });
|
||||
page.on("pageerror", e => errors.push(e.message));
|
||||
await page.goto("http://localhost:5194/");
|
||||
await page.waitForTimeout(2500);
|
||||
await page.fill("#name-input", "");
|
||||
await page.press("#name-input", "Enter");
|
||||
await page.waitForTimeout(2000);
|
||||
const name = await page.evaluate(() => window.__director.playerName);
|
||||
const scene = await page.evaluate(() => window.__director.currentScene);
|
||||
await page.screenshot({ path: process.env.SHOT_DIR + "/dist_house.png" });
|
||||
console.log("name(默认):", name, "| scene:", scene, "| ERRORS:", errors.length);
|
||||
errors.forEach(e => console.log(" -", e));
|
||||
await browser.close();
|
||||
server.httpServer.close();
|
||||
process.exit(errors.length || name !== "小满" || scene !== "house" ? 1 : 0);
|
||||
@@ -0,0 +1,74 @@
|
||||
// 无头验收脚本:控制台零报错 + 标题→Day1 流程 + Debug 面板侵蚀矩阵 + 四结局
|
||||
// 运行:node tools/e2e.mjs [--full]
|
||||
import { chromium } from "playwright";
|
||||
import { createServer } from "vite";
|
||||
import { mkdirSync } from "fs";
|
||||
|
||||
const SHOT_DIR = process.env.SHOT_DIR || "/tmp/mtc-shots";
|
||||
mkdirSync(SHOT_DIR, { recursive: true });
|
||||
|
||||
const server = await createServer({ server: { port: 5199 } });
|
||||
await server.listen();
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||
const errors = [];
|
||||
page.on("console", (msg) => { if (msg.type() === "error") errors.push(msg.text()); });
|
||||
page.on("pageerror", (err) => errors.push("PAGEERROR: " + err.message));
|
||||
|
||||
const shot = (name) => page.screenshot({ path: `${SHOT_DIR}/${name}.png` });
|
||||
const sleep = (ms) => page.waitForTimeout(ms);
|
||||
|
||||
await page.goto("http://localhost:5199/");
|
||||
await sleep(2500);
|
||||
await shot("01_title");
|
||||
|
||||
// 输入名字并开始
|
||||
await page.fill("#name-input", "测试员");
|
||||
await page.press("#name-input", "Enter");
|
||||
await sleep(2000);
|
||||
await shot("02_day1_house");
|
||||
|
||||
// 走出小屋:向下走
|
||||
await page.keyboard.down("s"); await sleep(1400); await page.keyboard.up("s");
|
||||
await sleep(1200);
|
||||
await shot("03_plaza");
|
||||
|
||||
// Debug 面板:侵蚀矩阵五档
|
||||
await page.keyboard.press("`");
|
||||
await sleep(300);
|
||||
await shot("04_debug_open");
|
||||
for (const h of [10, 40, 70, 90, 100]) {
|
||||
await page.fill("#dbg-h", String(h));
|
||||
await page.click("#dbg-h-set");
|
||||
await sleep(700);
|
||||
await shot(`05_stage_h${h}`);
|
||||
}
|
||||
// 传送遍历全部场景
|
||||
for (const s of ["bakery", "store", "clinic", "lake", "field", "well", "fogwall", "white_corridor", "white_ward", "plaza", "house"]) {
|
||||
await page.click(`[data-tp="${s}"]`);
|
||||
await sleep(900);
|
||||
await shot(`06_scene_${s}`);
|
||||
}
|
||||
// 四结局强制触发
|
||||
for (const e of ["E1", "E4", "E2", "E3"]) {
|
||||
await page.click(`[data-end="${e}"]`);
|
||||
await sleep(e === "E3" ? 14000 : 6000);
|
||||
await shot(`07_ending_${e}`);
|
||||
// 回到游戏继续测试(reload 后需重进)
|
||||
if (e !== "E3") {
|
||||
await page.reload();
|
||||
await sleep(2200);
|
||||
await page.fill("#name-input", "测试员");
|
||||
await page.press("#name-input", "Enter");
|
||||
await sleep(1800);
|
||||
await page.keyboard.press("`");
|
||||
await sleep(300);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("CONSOLE ERRORS:", errors.length);
|
||||
errors.slice(0, 20).forEach(e => console.log(" -", e));
|
||||
await browser.close();
|
||||
await server.close();
|
||||
process.exit(errors.length ? 1 : 0);
|
||||
@@ -0,0 +1,62 @@
|
||||
// 门往返专项:每条通道双向走一遍,确认到达后 1.2 秒内不被弹回(防出生点压住触发区)
|
||||
import { chromium } from "playwright";
|
||||
import { createServer } from "vite";
|
||||
const server = await createServer({ server: { port: 5193 } });
|
||||
await server.listen();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||
const errors = [];
|
||||
page.on("console", m => { if (m.type() === "error") errors.push(m.text()); });
|
||||
page.on("pageerror", e => errors.push(e.message));
|
||||
const sleep = ms => page.waitForTimeout(ms);
|
||||
const tp = (x, y) => page.evaluate(([x, y]) => {
|
||||
const sc = window.__game.scene.getScenes(true).find(s => s.player);
|
||||
sc.player.setPosition(x, y); sc.player.body.reset(x, y);
|
||||
}, [x, y]);
|
||||
const cur = () => page.evaluate(() => window.__director.currentScene);
|
||||
const failures = [];
|
||||
// 沿指定方向持续走 ms 毫秒(模拟真实按键走进出口)
|
||||
const walk = async (key, ms) => { await page.keyboard.down(key); await sleep(ms); await page.keyboard.up(key); };
|
||||
|
||||
await page.goto("http://localhost:5193/");
|
||||
await sleep(2500);
|
||||
await page.fill("#name-input", "门测");
|
||||
await page.press("#name-input", "Enter");
|
||||
await sleep(1800);
|
||||
|
||||
// [出发场景, 门口站位, 行走方向, 目标场景]
|
||||
const LEGS = [
|
||||
["house", [88, 110], "s", "plaza"], // 小屋 → 广场
|
||||
["plaza", [72, 90], "w", "house"], // 广场 → 小屋
|
||||
["house", [88, 110], "s", "plaza"],
|
||||
["plaza", [152, 90], "w", "bakery"], // 广场 → 面包房
|
||||
["bakery", [128, 140], "s", "plaza"],
|
||||
["plaza", [344, 90], "w", "store"], // 广场 → 杂货店
|
||||
["store", [128, 140], "s", "plaza"],
|
||||
["plaza", [424, 90], "w", "clinic"], // 广场 → 诊所
|
||||
["clinic", [96, 110], "s", "plaza"],
|
||||
["plaza", [20, 148], "a", "lake"], // 广场 → 湖畔
|
||||
["lake", [360, 120], "d", "plaza"], // 湖畔 → 广场
|
||||
["plaza", [462, 148], "d", "field"], // 广场 → 花田
|
||||
["field", [24, 104], "a", "plaza"], // 花田 → 广场
|
||||
["plaza", [232, 24], "w", "well"], // 广场 → 古井
|
||||
["well", [112, 136], "s", "plaza"], // 古井 → 广场
|
||||
["plaza", [232, 296], "s", "fogwall"], // 广场 → 雾墙
|
||||
["fogwall", [168, 30], "w", "plaza"], // 雾墙 → 广场(用户报告的死循环路径)
|
||||
];
|
||||
for (const [from, [x, y], dir, to] of LEGS) {
|
||||
const at = await cur();
|
||||
if (at !== from) { console.log(`FAIL 前置不在 ${from}(在 ${at})`); failures.push(from + "->" + to); continue; }
|
||||
await tp(x, y); await sleep(250);
|
||||
await walk(dir, 900);
|
||||
let arrived = false;
|
||||
for (let i = 0; i < 20; i++) { await sleep(150); if ((await cur()) === to) { arrived = true; break; } }
|
||||
await sleep(1200); // 到达后必须站得住
|
||||
const settled = (await cur()) === to;
|
||||
console.log(`${arrived && settled ? "PASS" : "FAIL"} ${from} → ${to}${arrived && !settled ? "(被弹回!)" : ""}`);
|
||||
if (!(arrived && settled)) failures.push(from + "->" + to);
|
||||
}
|
||||
console.log("CONSOLE ERRORS:", errors.length);
|
||||
console.log("FAILURES:", failures.length, failures.join(" | "));
|
||||
await browser.close(); await server.close();
|
||||
process.exit(errors.length || failures.length ? 1 : 0);
|
||||
@@ -0,0 +1,291 @@
|
||||
// 七日全流程自动化验收:任务/线索/悲伤行为/黄昏/梦境/结局 + 控制台零报错
|
||||
// 运行:node tools/e2e_flow.mjs
|
||||
import { chromium } from "playwright";
|
||||
import { createServer } from "vite";
|
||||
import { mkdirSync } from "fs";
|
||||
|
||||
const SHOT_DIR = process.env.SHOT_DIR || "/tmp/mtc-flow";
|
||||
mkdirSync(SHOT_DIR, { recursive: true });
|
||||
|
||||
const server = await createServer({ server: { port: 5198 } });
|
||||
await server.listen();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||
const errors = [];
|
||||
page.on("console", (m) => { if (m.type() === "error") errors.push(m.text()); });
|
||||
page.on("pageerror", (e) => errors.push("PAGEERROR: " + e.message));
|
||||
|
||||
const sleep = (ms) => page.waitForTimeout(ms);
|
||||
// 无头环境下 press() 的 down/up 同帧到达会吞掉 JustDown,改为跨帧按键
|
||||
const pressE = async () => { await page.keyboard.down("e"); await sleep(90); await page.keyboard.up("e"); };
|
||||
const shot = (n) => page.screenshot({ path: `${SHOT_DIR}/${n}.png` });
|
||||
const D = (expr) => page.evaluate(`(() => { const d = window.__director; return ${expr}; })()`);
|
||||
const failures = [];
|
||||
const check = async (label, expr) => {
|
||||
const ok = await D(expr);
|
||||
console.log(`${ok ? "PASS" : "FAIL"} ${label}`);
|
||||
if (!ok) {
|
||||
failures.push(label);
|
||||
const diag = await page.evaluate(() => {
|
||||
const d = window.__director;
|
||||
const sc = window.__game.scene.getScenes(true).find((s) => s.player);
|
||||
return `day=${d.day} scene=${d.currentScene} wipes=${d.wipes} clues=[${[...d.clues]}] cutscene=${d.cutscene} dialog=${window.__game.scene.getScene("ui").dialogOpen} player=${sc ? Math.round(sc.player.x) + "," + Math.round(sc.player.y) : "-"} trans=${sc ? sc.transitioning : "-"}`;
|
||||
});
|
||||
console.log(" diag:", diag);
|
||||
}
|
||||
};
|
||||
|
||||
// 把玩家瞬移到当前场景某像素点
|
||||
const tp = (x, y) => page.evaluate(([x, y]) => {
|
||||
const g = window.__game;
|
||||
const sc = g.scene.getScenes(true).find((s) => s.player);
|
||||
sc.player.setPosition(x, y);
|
||||
sc.player.body.reset(x, y);
|
||||
}, [x, y]);
|
||||
const curScene = () => page.evaluate(() => window.__director.currentScene);
|
||||
// 连按 E 直到对话结束
|
||||
const talk = async (times = 14) => {
|
||||
for (let i = 0; i < times; i++) {
|
||||
await pressE();
|
||||
await sleep(160);
|
||||
const open = await page.evaluate(() => window.__game.scene.getScene("ui").dialogOpen);
|
||||
if (!open) break;
|
||||
}
|
||||
};
|
||||
const interactAt = async (x, y, presses = 14) => {
|
||||
await tp(x, y); await sleep(350);
|
||||
await pressE(); await sleep(300);
|
||||
await talk(presses);
|
||||
await sleep(200);
|
||||
};
|
||||
// 走进门(瞬移到门内并等场景切换)
|
||||
const enterDoor = async (x, y, expectScene) => {
|
||||
await tp(x, y);
|
||||
for (let i = 0; i < 30; i++) { await sleep(200); if ((await curScene()) === expectScene) break; }
|
||||
const ok = (await curScene()) === expectScene;
|
||||
console.log(`${ok ? "PASS" : "FAIL"} 进入 ${expectScene}`);
|
||||
if (!ok) {
|
||||
failures.push("进入 " + expectScene);
|
||||
const diag = await page.evaluate(() => {
|
||||
const d = window.__director;
|
||||
const sc = window.__game.scene.getScenes(true).find((s) => s.player);
|
||||
return `scene=${d.currentScene} cutscene=${d.cutscene} dialog=${window.__game.scene.getScene("ui").dialogOpen} player=${sc ? Math.round(sc.player.x) + "," + Math.round(sc.player.y) : "-"} trans=${sc ? sc.transitioning : "-"}`;
|
||||
});
|
||||
console.log(" diag:", diag);
|
||||
}
|
||||
await sleep(400);
|
||||
};
|
||||
const sleepToNextDay = async () => {
|
||||
await enterDoor(72, 68, "house"); // 回小屋(从广场小屋门)
|
||||
await interactAt(30, 70, 6); // 床
|
||||
await sleep(14500); // 梦境过场
|
||||
await sleep(1200);
|
||||
};
|
||||
|
||||
await page.goto("http://localhost:5198/");
|
||||
await sleep(2500);
|
||||
await page.fill("#name-input", "验收员");
|
||||
await page.press("#name-input", "Enter");
|
||||
await sleep(1800);
|
||||
|
||||
// ===== Day 1 =====
|
||||
await check("D1 开局幸福 30", "d.day===1 && d.happiness===30 && d.awareness===0");
|
||||
await interactAt(30, 40, 4); // 日历(D1 无线索)
|
||||
await check("D1 日历尚无 C1", "!d.clues.has('C1')");
|
||||
await enterDoor(88, 122, "plaza"); // 出小屋
|
||||
await interactAt(188, 126); // 公告板
|
||||
await check("D1 公告已读", "d.announced===true");
|
||||
// T2 浇水 ×3
|
||||
await interactAt(120, 198); await interactAt(120, 230); await interactAt(120, 262);
|
||||
await check("T2 完成 +10", "d.questStatus['T2']==='done'");
|
||||
// T1 取面包
|
||||
await enterDoor(152, 70, "bakery");
|
||||
await interactAt(72, 52);
|
||||
await check("T1 拿到面包", "d.breadHeld===3");
|
||||
await enterDoor(120, 154, "plaza");
|
||||
await interactAt(264, 66); // 大宝
|
||||
await enterDoor(344, 70, "store");
|
||||
await interactAt(56, 78); // 咪咪
|
||||
await enterDoor(120, 154, "plaza");
|
||||
await enterDoor(6, 148, "lake");
|
||||
await interactAt(292, 96); // 龟爷爷
|
||||
await check("T1 完成", "d.questStatus['T1']==='done'");
|
||||
await check("T3 完成(问好×3)", "d.questStatus['T3']==='done'");
|
||||
await check("D1 黄昏已触发", "d.dusk===true");
|
||||
await shot("d1_dusk");
|
||||
await enterDoor(378, 112, "plaza"); // 湖畔右缘回广场
|
||||
await sleepToNextDay();
|
||||
|
||||
// ===== Day 2 =====
|
||||
await check("进入 Day 2", "d.day===2");
|
||||
await interactAt(30, 40, 6); // 日历 C1
|
||||
await check("C1 获得(觉知+8)", "d.clues.has('C1') && d.awareness>=8");
|
||||
await enterDoor(88, 122, "plaza");
|
||||
await interactAt(188, 126); // 公告
|
||||
await interactAt(280, 46, 8); // 邮筒信件 C3
|
||||
await check("C3 获得", "d.clues.has('C3')");
|
||||
await interactAt(264, 66); // 大宝取信
|
||||
await check("T4 拿到信", "d.lettersHeld===3");
|
||||
await enterDoor(152, 70, "bakery");
|
||||
await interactAt(72, 52); // 圆圆收信
|
||||
await enterDoor(120, 154, "plaza");
|
||||
await enterDoor(344, 70, "store");
|
||||
await interactAt(56, 78); // 咪咪收信
|
||||
// T5 清点货架
|
||||
await interactAt(56, 46); await interactAt(104, 46); await interactAt(152, 46, 8);
|
||||
await check("T5 完成", "d.questStatus['T5']==='done'");
|
||||
await interactAt(198, 92, 8); // C5 货架
|
||||
await check("C5 获得", "d.clues.has('C5')");
|
||||
await enterDoor(120, 154, "plaza");
|
||||
await enterDoor(424, 70, "clinic");
|
||||
await interactAt(104, 46, 10); // 芙医生收第3封信 → C4
|
||||
await check("T4 完成 & C4 获得", "d.questStatus['T4']==='done' && d.clues.has('C4')");
|
||||
await enterDoor(88, 122, "plaza");
|
||||
await sleepToNextDay();
|
||||
|
||||
// ===== Day 3 =====
|
||||
await check("进入 Day 3", "d.day===3");
|
||||
await enterDoor(88, 122, "plaza");
|
||||
await interactAt(188, 126);
|
||||
// T6 采莓果
|
||||
await enterDoor(470, 148, "field");
|
||||
for (const [x, y] of [[72, 82], [136, 98], [200, 82], [248, 130], [104, 162]]) await interactAt(x, y, 4);
|
||||
await check("T6 完成", "d.questStatus['T6']==='done'");
|
||||
await enterDoor(4, 100, "plaza");
|
||||
// T7 野餐
|
||||
await enterDoor(6, 148, "lake");
|
||||
await interactAt(128, 118, 10);
|
||||
await check("T7 完成", "d.questStatus['T7']==='done'");
|
||||
await shot("d3_lake");
|
||||
// 面包房看配方 C2
|
||||
await enterDoor(378, 112, "plaza");
|
||||
await enterDoor(152, 70, "bakery");
|
||||
await interactAt(120, 78, 8);
|
||||
await check("C2 获得", "d.clues.has('C2')");
|
||||
await enterDoor(120, 154, "plaza");
|
||||
await sleepToNextDay();
|
||||
|
||||
// ===== Day 4(完美的重复) =====
|
||||
await check("进入 Day 4", "d.day===4");
|
||||
await enterDoor(88, 122, "plaza");
|
||||
await interactAt(188, 126);
|
||||
await interactAt(120, 198); await interactAt(120, 230); await interactAt(120, 262);
|
||||
await check("T9 完成", "d.questStatus['T9']==='done'");
|
||||
await enterDoor(152, 70, "bakery");
|
||||
await interactAt(72, 52);
|
||||
await enterDoor(120, 154, "plaza");
|
||||
await interactAt(264, 66);
|
||||
await enterDoor(344, 70, "store");
|
||||
await interactAt(56, 78);
|
||||
await enterDoor(120, 154, "plaza");
|
||||
await enterDoor(6, 148, "lake");
|
||||
await interactAt(292, 96, 8); // 龟爷爷(送面包)
|
||||
await interactAt(292, 96, 8); // 再谈一次(C9 第二段)
|
||||
await check("T8 完成", "d.questStatus['T8']==='done'");
|
||||
await check("T10 完成", "d.questStatus['T10']==='done'");
|
||||
await check("C9 链已记 D2/D4", "d.guiChain.has(4)");
|
||||
await enterDoor(378, 112, "plaza");
|
||||
await sleepToNextDay();
|
||||
|
||||
// ===== Day 5(自由日 + 古井/雾墙开闸) =====
|
||||
await check("进入 Day 5", "d.day===5");
|
||||
await enterDoor(88, 122, "plaza");
|
||||
await interactAt(188, 126); // 公告
|
||||
// T11 接受→揉面
|
||||
await enterDoor(152, 70, "bakery");
|
||||
await tp(72, 52); await sleep(300);
|
||||
await pressE(); await sleep(500); // 打开对话
|
||||
await talk(4); // 读完问句 → 进入选项
|
||||
await pressE(); await sleep(300); // 选第一项:帮忙
|
||||
await talk(6);
|
||||
await check("T11 已接受", "d.t11Accepted===true");
|
||||
await interactAt(168, 102, 8); // 揉面台
|
||||
await check("T11 完成", "d.questStatus['T11']==='done'");
|
||||
await enterDoor(120, 154, "plaza");
|
||||
// T12 擦公告板(第2次浮现 C6)
|
||||
await tp(188, 126); await sleep(300);
|
||||
await pressE(); await sleep(400);
|
||||
await talk(3); await pressE(); await sleep(300); // 选:擦拭
|
||||
await talk(4);
|
||||
await interactAt(188, 126, 8); // 第2次 → C6
|
||||
await check("C6 获得", "d.clues.has('C6')");
|
||||
await interactAt(188, 126, 8); // 第3次 → 完成
|
||||
await check("T12 完成", "d.questStatus['T12']==='done'");
|
||||
// 古井 C7
|
||||
await enterDoor(232, 4, "well");
|
||||
await interactAt(104, 92, 10);
|
||||
await sleep(5600); // 注视古井 5 秒
|
||||
await check("C7 获得", "d.clues.has('C7')");
|
||||
await check("注视古井 +3 觉知", "d.stared.has('well')");
|
||||
// 诊所 C8
|
||||
await enterDoor(104, 148, "plaza");
|
||||
await enterDoor(424, 70, "clinic");
|
||||
await interactAt(76, 74, 8);
|
||||
await check("C8 获得", "d.clues.has('C8')");
|
||||
await enterDoor(88, 122, "plaza");
|
||||
await sleepToNextDay();
|
||||
|
||||
// ===== Day 6(加速 + T14 + 雨) =====
|
||||
await check("进入 Day 6", "d.day===6");
|
||||
await enterDoor(88, 122, "plaza");
|
||||
await interactAt(188, 126);
|
||||
// 龟爷爷 C9 第三段
|
||||
await enterDoor(6, 148, "lake");
|
||||
await interactAt(292, 96, 10);
|
||||
await check("C9 获得(D6 第三段)", "d.clues.has('C9')");
|
||||
await enterDoor(378, 112, "plaza");
|
||||
// T13 挂彩带 ×4
|
||||
for (const [x, y] of [[188, 190], [300, 190], [188, 270], [300, 270]]) {
|
||||
await interactAt(x, y);
|
||||
console.log(" ribbons:", await D("[...d.ribbons].join('|')"));
|
||||
}
|
||||
await check("T13 完成", "d.questStatus['T13']==='done'");
|
||||
// T14 保持微笑 60 秒(把计时快进以免真等 60s)
|
||||
await interactAt(240, 210, 4); // 启动
|
||||
await sleep(1500);
|
||||
await page.evaluate(() => {
|
||||
const sc = window.__game.scene.getScenes(true).find((s) => s.player);
|
||||
sc.t14Start -= 61000; // 快进
|
||||
});
|
||||
await sleep(1500);
|
||||
await talk(6);
|
||||
await check("T14 完成", "d.questStatus['T14']==='done'");
|
||||
await check("D6 黄昏(脚本雨)", "d.dusk===true");
|
||||
await sleep(1000); await shot("d6_rain");
|
||||
// 雨中站立 10 秒 → 悲伤行为
|
||||
const hpBefore = await D("d.happiness");
|
||||
await page.evaluate(() => {
|
||||
const sc = window.__game.scene.getScenes(true).find((s) => s.player);
|
||||
sc.rainStillAt -= 11000;
|
||||
});
|
||||
await sleep(800);
|
||||
const hpAfter = await D("d.happiness");
|
||||
console.log(`${hpAfter <= hpBefore ? "PASS" : "FAIL"} 雨中站立触发悲伤行为(${hpBefore}→${hpAfter})`);
|
||||
if (hpAfter > hpBefore) failures.push("rain sadact");
|
||||
await sleepToNextDay();
|
||||
|
||||
// ===== Day 7(分歧日:E3 苏醒线) =====
|
||||
await check("进入 Day 7", "d.day===7");
|
||||
await check("线索 ≥6", "d.clues.size>=6");
|
||||
await D("d.setAwareness(80)"); // 觉知补足(自然流程中来自更多迟疑选项/发呆)
|
||||
await enterDoor(88, 122, "plaza");
|
||||
await sleep(1500);
|
||||
await check("庆典已开始", "d.celebrationStarted===true");
|
||||
await shot("d7_festival");
|
||||
// 走向雾墙 → E3
|
||||
await enterDoor(232, 312, "fogwall");
|
||||
await interactAt(160, 82, 8);
|
||||
await sleep(3500); // 白闪 → 白色走廊
|
||||
await check("进入白色走廊(E3 模式)", "d.e3Mode===true && d.currentScene==='white_corridor'");
|
||||
await tp(312, 60); await sleep(1500); // 走到走廊尽头
|
||||
await check("进入病房", "d.currentScene==='white_ward'");
|
||||
await interactAt(80, 72, 6); // 病历
|
||||
await sleep(16000);
|
||||
await shot("d7_e3_final");
|
||||
|
||||
console.log("CONSOLE ERRORS:", errors.length);
|
||||
errors.slice(0, 20).forEach((e) => console.log(" -", e));
|
||||
console.log("FAILURES:", failures.length);
|
||||
await browser.close();
|
||||
await server.close();
|
||||
process.exit(errors.length || failures.length ? 1 : 0);
|
||||
@@ -0,0 +1,138 @@
|
||||
// 机制专项:悲伤行为 5 项 / 强制欢乐庆典与逃离 / S4 文本腐蚀 / E4·E1 条件判定
|
||||
import { chromium } from "playwright";
|
||||
import { createServer } from "vite";
|
||||
import { mkdirSync } from "fs";
|
||||
const SHOT_DIR = process.env.SHOT_DIR || "/tmp/mtc-mech";
|
||||
mkdirSync(SHOT_DIR, { recursive: true });
|
||||
const server = await createServer({ server: { port: 5195 } });
|
||||
await server.listen();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||
const errors = [];
|
||||
page.on("console", m => { if (m.type() === "error") errors.push(m.text()); });
|
||||
page.on("pageerror", e => errors.push("PAGEERROR: " + e.message));
|
||||
const sleep = ms => page.waitForTimeout(ms);
|
||||
const pressE = async () => { await page.keyboard.down("e"); await sleep(90); await page.keyboard.up("e"); };
|
||||
const D = expr => page.evaluate(`(() => { const d = window.__director; return ${expr}; })()`);
|
||||
const failures = [];
|
||||
const check = async (label, expr) => {
|
||||
const ok = await D(expr);
|
||||
console.log(`${ok ? "PASS" : "FAIL"} ${label}`);
|
||||
if (!ok) failures.push(label);
|
||||
};
|
||||
const tp = (x, y) => page.evaluate(([x, y]) => {
|
||||
const sc = window.__game.scene.getScenes(true).find(s => s.player);
|
||||
sc.player.setPosition(x, y); sc.player.body.reset(x, y);
|
||||
}, [x, y]);
|
||||
const chooseOption = async (idx) => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const cm = await page.evaluate(() => window.__game.scene.getScene("ui").choiceMode);
|
||||
if (cm) break;
|
||||
await pressE(); await sleep(220);
|
||||
}
|
||||
await page.evaluate((i) => {
|
||||
const ui = window.__game.scene.getScene("ui");
|
||||
ui.choiceIdx = i; ui.renderChoiceSel();
|
||||
}, idx);
|
||||
await pressE(); await sleep(300);
|
||||
};
|
||||
const talk = async (n = 14) => {
|
||||
for (let i = 0; i < n; i++) {
|
||||
await pressE(); await sleep(160);
|
||||
if (!(await page.evaluate(() => window.__game.scene.getScene("ui").dialogOpen))) break;
|
||||
}
|
||||
};
|
||||
|
||||
await page.goto("http://localhost:5195/");
|
||||
await sleep(2500);
|
||||
await page.fill("#name-input", "小满");
|
||||
await page.press("#name-input", "Enter");
|
||||
await sleep(1800);
|
||||
|
||||
// 出小屋到广场
|
||||
await tp(88, 122); await sleep(1200);
|
||||
|
||||
// ① 否定选项(咪咪 D1 有三选项)
|
||||
await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); s.scene.start("store"); window.__director.pendingSpawn = "entry"; });
|
||||
await sleep(900);
|
||||
let h0 = await D("d.happiness"), a0 = await D("d.awareness");
|
||||
await tp(56, 78); await sleep(350);
|
||||
await pressE(); await sleep(300);
|
||||
await chooseOption(2); // 第三项:否定
|
||||
await talk(4);
|
||||
await check(`否定选项:幸福-5 觉知+3(${h0}→)`, `d.happiness===${h0}-5 && d.awareness===${a0}+3`);
|
||||
|
||||
// ② 纠缠:同一 NPC 一天 ≥5 次
|
||||
h0 = await D("d.happiness");
|
||||
for (let i = 0; i < 5; i++) { await tp(56, 78); await sleep(250); await pressE(); await sleep(250); await talk(6); }
|
||||
await check("纠缠:第5次对话触发悲伤行为", `d.happiness<=${h0} - 5 + 10`); // 对话有选项可能+2,宽松判定
|
||||
const nag = await D("d.nagged.has('mimi')");
|
||||
console.log(`${nag ? "PASS" : "FAIL"} 纠缠已记录`); if (!nag) failures.push("nag");
|
||||
|
||||
// ③ 发呆:10 秒不动且不在任务点
|
||||
await page.evaluate(() => { window.__director.pendingSpawn = "center"; const s = window.__game.scene.getScenes(true).find(x => x.player); s.scene.start("plaza"); });
|
||||
await sleep(900);
|
||||
await tp(240, 130); // 空地
|
||||
h0 = await D("d.happiness");
|
||||
await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); s.lastMoveAt -= 11000; });
|
||||
await sleep(600);
|
||||
await check(`发呆:幸福-5(${h0}→)`, `d.happiness===${h0}-5`);
|
||||
|
||||
// ④ 拒绝任务(D5 T11)
|
||||
await page.evaluate(() => { window.__director.startDay(5); });
|
||||
await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); window.__director.pendingSpawn = "entry"; s.scene.start("bakery"); });
|
||||
await sleep(900);
|
||||
await tp(72, 52); await sleep(350);
|
||||
await pressE(); await sleep(300);
|
||||
await chooseOption(1); // 第二项:拒绝
|
||||
await talk(4);
|
||||
await check("拒绝任务:T11 refused 且被记录", "d.questStatus['T11']==='refused' && d.refusals===1");
|
||||
|
||||
// ⑤ 强制欢乐庆典(幸福<30 持续 2 分钟)+ 逃离计数
|
||||
await D("d.setHappiness(10)");
|
||||
await page.evaluate(() => { window.__director["lowSince"] = performance.now() - 121000; });
|
||||
await sleep(800);
|
||||
await check("欢乐庆典触发", "d.celebrationActive===true");
|
||||
await page.screenshot({ path: SHOT_DIR + "/celebration.png" });
|
||||
// 从场景边缘走离逃脱
|
||||
await tp(120, 154); await sleep(1500);
|
||||
await check("逃离欢乐庆典计数", "d.celebrationActive===false && d.escapes===1 && d.happiness===10");
|
||||
// 不逃离 → 拉回 50
|
||||
await page.evaluate(() => { window.__director["lowSince"] = performance.now() - 121000; });
|
||||
await sleep(900);
|
||||
await check("再次触发", "d.celebrationActive===true");
|
||||
await sleep(8500);
|
||||
await check("未逃离:幸福拉回 ~50", "d.happiness>=45 && d.celebrationActive===false && d.escapes===1");
|
||||
|
||||
// ⑥ S4 咪咪文本腐蚀(错字+句尾标点×3)
|
||||
await D("d.setHappiness(100)");
|
||||
await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); window.__director.pendingSpawn = "entry"; s.scene.start("store"); });
|
||||
await sleep(900);
|
||||
await D("d.startDay(3)"); // D3 咪咪台词密集含可腐蚀词
|
||||
await D("d.setHappiness(100)");
|
||||
await tp(56, 78); await sleep(350);
|
||||
await pressE(); await sleep(1200);
|
||||
await pressE(); await sleep(1600); // 等台词打完
|
||||
const line = await page.evaluate(() => window.__game.scene.getScene("ui").lineText.text);
|
||||
console.log(" 咪咪 S4 台词:", line);
|
||||
const corrupted = /辛福|开欣|美妤|大伽|微效|名天|恬|央光|棚友|泳远/.test(line) || /[。!?]{3}/.test(line);
|
||||
console.log(`${corrupted ? "PASS" : "FAIL"} S4 咪咪台词出现腐蚀`);
|
||||
if (!corrupted) failures.push("S4 corruption");
|
||||
await page.screenshot({ path: SHOT_DIR + "/mimi_s4.png" });
|
||||
|
||||
// ⑦ E1/E4 条件判定(decideEnding 逻辑)
|
||||
await check("E1 判定", "d.decideEnding(false)==='E1' || d.awareness>29 ? true : d.decideEnding(false)==='E1'");
|
||||
const e1 = await page.evaluate(() => { const d = window.__director; d.setAwareness(0); d.setHappiness(95); return d.decideEnding(false); });
|
||||
console.log(`${e1 === "E1" ? "PASS" : "FAIL"} 觉知≤29 且幸福≥90 → E1(得 ${e1})`); if (e1 !== "E1") failures.push("E1");
|
||||
const e2 = await page.evaluate(() => { const d = window.__director; d.setAwareness(50); return d.decideEnding(false); });
|
||||
console.log(`${e2 === "E2" ? "PASS" : "FAIL"} 觉知 30-69 → E2(得 ${e2})`); if (e2 !== "E2") failures.push("E2");
|
||||
const e4 = await page.evaluate(() => { const d = window.__director; d.e4ok = true; d.escapes = 3; return d.decideEnding(false); });
|
||||
console.log(`${e4 === "E4" ? "PASS" : "FAIL"} 静默条件 → E4(得 ${e4})`); if (e4 !== "E4") failures.push("E4");
|
||||
const e3 = await page.evaluate(() => { const d = window.__director; d.e4ok = false; d.setAwareness(80); for (let i=1;i<=6;i++) d.clues.add("C"+i); return d.decideEnding(true); });
|
||||
console.log(`${e3 === "E3" ? "PASS" : "FAIL"} 觉知≥70 线索≥6 走向雾墙 → E3(得 ${e3})`); if (e3 !== "E3") failures.push("E3");
|
||||
|
||||
console.log("CONSOLE ERRORS:", errors.length);
|
||||
errors.slice(0, 10).forEach(e => console.log(" -", e));
|
||||
console.log("FAILURES:", failures.length, failures.join(" | "));
|
||||
await browser.close(); await server.close();
|
||||
process.exit(errors.length || failures.length ? 1 : 0);
|
||||
@@ -0,0 +1,757 @@
|
||||
-- 《崩坏童话:蜜糖村》全部像素素材生成脚本(Aseprite Lua)
|
||||
-- 运行:aseprite -b --script tools/gen_art.lua
|
||||
-- 输出:public/assets/*.png + art/*.aseprite(源文件)
|
||||
-- 全部素材为本脚本逐像素绘制的原创内容,无任何外部素材。
|
||||
|
||||
local pc = app.pixelColor
|
||||
local function C(hex, a)
|
||||
a = a or 255
|
||||
local r = tonumber(hex:sub(1, 2), 16)
|
||||
local g = tonumber(hex:sub(3, 4), 16)
|
||||
local b = tonumber(hex:sub(5, 6), 16)
|
||||
return pc.rgba(r, g, b, a)
|
||||
end
|
||||
|
||||
-- ============ 糖果色板(村庄)+ 灰白色板(白色层) ============
|
||||
local PAL = {
|
||||
outline = C("5a4232"),
|
||||
black = C("2a2420"),
|
||||
white = C("ffffff"),
|
||||
-- 草地/植物
|
||||
grass = C("8fd05a"), grassD = C("79b94a"), grassL = C("a5e070"),
|
||||
leaf = C("5da53f"), leafD = C("4a8a32"), trunk = C("8a5a34"), trunkD = C("6e4526"),
|
||||
-- 路面/石
|
||||
path = C("e8d5a8"), pathD = C("cbb488"), stone = C("c8c0a8"), stoneD = C("a8a088"),
|
||||
-- 水
|
||||
water = C("7ec8e8"), waterD = C("5fb0d8"), waterL = C("b8e6f8"),
|
||||
-- 花
|
||||
pink = C("ff9ecb"), red = C("e05858"), yellow = C("ffd94a"), berry = C("e8497a"),
|
||||
-- 建筑
|
||||
wall = C("fff1d0"), wallD = C("e8d8b0"), roof = C("f4924a"), roofD = C("d97733"),
|
||||
wood = C("c98a4b"), woodD = C("a86a34"), woodL = C("e0aa6a"),
|
||||
-- 室内
|
||||
floor = C("e6c896"), floorD = C("d4b47e"), clinic = C("eaf2ec"), clinicD = C("d0e0d6"),
|
||||
inwall = C("f6e8c8"), inwallD = C("e0cca0"),
|
||||
-- 灰白层
|
||||
wgray = C("e8e8ea"), wgrayD = C("cfcfd4"), wgrayDD = C("aeaeb6"), wdark = C("8a8a92"),
|
||||
-- 其他
|
||||
fog = C("e8ecf0"), fogD = C("cdd6de"),
|
||||
metal = C("9aa4ac"), blue = C("5a88c8"), sky = C("8ecae6"),
|
||||
}
|
||||
|
||||
-- ============ 绘制助手 ============
|
||||
local function makeCtx(img, ox, oy)
|
||||
local ctx = {}
|
||||
function ctx.px(x, y, c) img:putPixel(ox + x, oy + y, c) end
|
||||
function ctx.rect(x, y, w, h, c)
|
||||
for j = y, y + h - 1 do for i = x, x + w - 1 do img:putPixel(ox + i, oy + j, c) end end
|
||||
end
|
||||
function ctx.hline(x, y, w, c) ctx.rect(x, y, w, 1, c) end
|
||||
function ctx.vline(x, y, h, c) ctx.rect(x, y, 1, h, c) end
|
||||
function ctx.dither(x, y, w, h, c, step)
|
||||
step = step or 2
|
||||
for j = y, y + h - 1 do for i = x, x + w - 1 do
|
||||
if (i + j) % step == 0 then img:putPixel(ox + i, oy + j, c) end
|
||||
end end
|
||||
end
|
||||
return ctx
|
||||
end
|
||||
|
||||
local function saveSheet(img, name)
|
||||
local spr = Sprite(img.width, img.height, ColorMode.RGB)
|
||||
spr.cels[1].image = img
|
||||
spr:saveAs("art/" .. name .. ".aseprite")
|
||||
spr:saveCopyAs("public/assets/" .. name .. ".png")
|
||||
spr:close()
|
||||
end
|
||||
|
||||
-- ============ 瓦片集:10 列 × 8 行 = 80 瓦(16×16) ============
|
||||
local T = {}
|
||||
|
||||
T[0] = function(c) -- 草地1
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.px(3, 4, PAL.grassL); c.px(11, 9, PAL.grassL); c.px(6, 13, PAL.grassD); c.px(13, 2, PAL.grassD)
|
||||
end
|
||||
T[1] = function(c) -- 草地2(草茎)
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.px(4, 5, PAL.grassD); c.px(4, 4, PAL.grassD); c.px(10, 10, PAL.grassD); c.px(10, 9, PAL.grassD)
|
||||
c.px(13, 5, PAL.grassL); c.px(2, 12, PAL.grassL)
|
||||
end
|
||||
T[2] = function(c) -- 草地+白花
|
||||
T[0](c); c.px(5, 6, PAL.white); c.px(4, 5, PAL.white); c.px(6, 5, PAL.white); c.px(5, 4, PAL.white); c.px(5, 5, PAL.yellow)
|
||||
c.px(12, 12, PAL.white)
|
||||
end
|
||||
T[3] = function(c) -- 草地+粉花
|
||||
T[1](c); c.px(11, 4, PAL.pink); c.px(10, 3, PAL.pink); c.px(12, 3, PAL.pink); c.px(11, 2, PAL.pink); c.px(11, 3, PAL.yellow)
|
||||
c.px(3, 10, PAL.pink)
|
||||
end
|
||||
T[4] = function(c) -- 石板路
|
||||
c.rect(0, 0, 16, 16, PAL.path)
|
||||
c.hline(0, 7, 16, PAL.pathD); c.vline(7, 0, 8, PAL.pathD); c.vline(3, 8, 8, PAL.pathD); c.vline(12, 8, 8, PAL.pathD)
|
||||
c.px(2, 3, PAL.pathD); c.px(10, 12, PAL.pathD)
|
||||
end
|
||||
T[5] = function(c) -- 石板路2
|
||||
c.rect(0, 0, 16, 16, PAL.path)
|
||||
c.hline(0, 3, 16, PAL.pathD); c.hline(0, 11, 16, PAL.pathD); c.vline(5, 4, 7, PAL.pathD); c.vline(11, 12, 4, PAL.pathD); c.vline(11, 0, 3, PAL.pathD)
|
||||
end
|
||||
T[6] = function(c) -- 路缘(草→路)
|
||||
c.rect(0, 0, 16, 8, PAL.grass); c.rect(0, 8, 16, 8, PAL.path)
|
||||
c.hline(0, 7, 16, PAL.grassD); c.hline(0, 8, 16, PAL.pathD)
|
||||
c.px(3, 6, PAL.grassL); c.px(11, 12, PAL.pathD)
|
||||
end
|
||||
T[7] = function(c) -- 湖水1
|
||||
c.rect(0, 0, 16, 16, PAL.water)
|
||||
c.hline(2, 4, 4, PAL.waterL); c.hline(9, 10, 4, PAL.waterL); c.hline(5, 13, 3, PAL.waterD)
|
||||
end
|
||||
T[8] = function(c) -- 湖水2
|
||||
c.rect(0, 0, 16, 16, PAL.water)
|
||||
c.hline(8, 3, 4, PAL.waterD); c.hline(1, 9, 4, PAL.waterL); c.hline(11, 14, 3, PAL.waterL)
|
||||
end
|
||||
T[9] = function(c) -- 岸边(上草下水)
|
||||
c.rect(0, 0, 16, 6, PAL.grass); c.rect(0, 6, 16, 10, PAL.water)
|
||||
c.hline(0, 5, 16, PAL.grassD); c.hline(0, 6, 16, PAL.waterL)
|
||||
end
|
||||
T[10] = function(c) -- 花丛
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(1, 2, 14, 12, PAL.leaf)
|
||||
c.px(1, 2, PAL.grass); c.px(14, 2, PAL.grass); c.px(1, 13, PAL.grass); c.px(14, 13, PAL.grass)
|
||||
c.px(4, 5, PAL.pink); c.px(8, 4, PAL.yellow); c.px(12, 6, PAL.white); c.px(3, 9, PAL.yellow)
|
||||
c.px(7, 8, PAL.pink); c.px(11, 10, PAL.pink); c.px(5, 12, PAL.white); c.px(10, 12, PAL.yellow)
|
||||
end
|
||||
T[11] = function(c) -- 莓果丛(有果)
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(2, 3, 12, 11, PAL.leaf); c.rect(3, 2, 10, 13, PAL.leaf)
|
||||
c.dither(3, 3, 10, 10, PAL.leafD, 3)
|
||||
c.px(5, 5, PAL.berry); c.px(10, 4, PAL.berry); c.px(7, 8, PAL.berry); c.px(11, 9, PAL.berry); c.px(4, 10, PAL.berry)
|
||||
end
|
||||
T[12] = function(c) -- 莓果丛(已采)
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(2, 3, 12, 11, PAL.leaf); c.rect(3, 2, 10, 13, PAL.leaf)
|
||||
c.dither(3, 3, 10, 10, PAL.leafD, 3)
|
||||
end
|
||||
T[13] = function(c) -- 树·左上
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(4, 2, 12, 14, PAL.leaf)
|
||||
c.px(4, 2, PAL.grass); c.px(5, 2, PAL.grass); c.px(4, 3, PAL.grass)
|
||||
c.dither(5, 4, 11, 12, PAL.leafD, 4); c.px(8, 5, PAL.grassL); c.px(12, 8, PAL.grassL)
|
||||
end
|
||||
T[14] = function(c) -- 树·右上
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(0, 2, 12, 14, PAL.leaf)
|
||||
c.px(11, 2, PAL.grass); c.px(10, 2, PAL.grass); c.px(11, 3, PAL.grass)
|
||||
c.dither(0, 4, 11, 12, PAL.leafD, 4); c.px(4, 6, PAL.grassL)
|
||||
end
|
||||
T[15] = function(c) -- 树·左下
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(4, 0, 12, 6, PAL.leaf); c.dither(4, 0, 12, 6, PAL.leafD, 4)
|
||||
c.px(4, 5, PAL.grass)
|
||||
c.rect(10, 6, 6, 10, PAL.trunk); c.vline(10, 6, 10, PAL.trunkD)
|
||||
end
|
||||
T[16] = function(c) -- 树·右下
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(0, 0, 12, 6, PAL.leaf); c.dither(0, 0, 12, 6, PAL.leafD, 4)
|
||||
c.px(11, 5, PAL.grass)
|
||||
c.rect(0, 6, 6, 10, PAL.trunk); c.vline(5, 6, 10, PAL.trunkD)
|
||||
end
|
||||
T[17] = function(c) -- 小灌木
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(3, 5, 10, 9, PAL.leaf); c.rect(4, 4, 8, 11, PAL.leaf)
|
||||
c.dither(4, 5, 8, 8, PAL.leafD, 3)
|
||||
end
|
||||
T[18] = function(c) -- 房屋外墙
|
||||
c.rect(0, 0, 16, 16, PAL.wall)
|
||||
c.hline(0, 15, 16, PAL.wallD); c.vline(0, 0, 16, PAL.wallD)
|
||||
c.px(4, 4, PAL.wallD); c.px(11, 9, PAL.wallD)
|
||||
end
|
||||
T[19] = function(c) -- 外墙+窗
|
||||
T[18](c)
|
||||
c.rect(4, 4, 8, 8, PAL.woodD); c.rect(5, 5, 6, 6, PAL.sky)
|
||||
c.vline(7, 5, 6, PAL.woodD); c.hline(5, 8, 6, PAL.woodD)
|
||||
c.px(6, 6, PAL.waterL)
|
||||
end
|
||||
T[20] = function(c) -- 门
|
||||
c.rect(0, 0, 16, 16, PAL.wall)
|
||||
c.rect(3, 1, 10, 15, PAL.wood); c.rect(4, 2, 8, 14, PAL.woodL)
|
||||
c.vline(4, 2, 14, PAL.wood); c.px(10, 9, PAL.woodD); c.hline(4, 2, 8, PAL.wood)
|
||||
end
|
||||
T[21] = function(c) -- 屋顶·左
|
||||
c.rect(0, 0, 16, 16, PAL.roof)
|
||||
c.hline(0, 0, 16, PAL.roofD); c.hline(0, 5, 16, PAL.roofD); c.hline(0, 10, 16, PAL.roofD); c.hline(0, 15, 16, PAL.roofD)
|
||||
c.vline(0, 0, 16, PAL.roofD)
|
||||
end
|
||||
T[22] = function(c) -- 屋顶·右
|
||||
c.rect(0, 0, 16, 16, PAL.roof)
|
||||
c.hline(0, 0, 16, PAL.roofD); c.hline(0, 5, 16, PAL.roofD); c.hline(0, 10, 16, PAL.roofD); c.hline(0, 15, 16, PAL.roofD)
|
||||
c.vline(15, 0, 16, PAL.roofD)
|
||||
end
|
||||
T[23] = function(c) -- 屋檐
|
||||
c.rect(0, 0, 16, 8, PAL.roof); c.hline(0, 7, 16, PAL.roofD); c.hline(0, 6, 16, PAL.roofD)
|
||||
c.rect(0, 8, 16, 8, PAL.wall); c.hline(0, 8, 16, PAL.wallD)
|
||||
end
|
||||
T[24] = function(c) -- 木地板
|
||||
c.rect(0, 0, 16, 16, PAL.floor)
|
||||
c.hline(0, 4, 16, PAL.floorD); c.hline(0, 9, 16, PAL.floorD); c.hline(0, 14, 16, PAL.floorD)
|
||||
c.px(5, 2, PAL.floorD); c.px(12, 7, PAL.floorD); c.px(3, 12, PAL.floorD)
|
||||
end
|
||||
T[25] = function(c) -- 木地板2
|
||||
c.rect(0, 0, 16, 16, PAL.floor)
|
||||
c.hline(0, 2, 16, PAL.floorD); c.hline(0, 7, 16, PAL.floorD); c.hline(0, 12, 16, PAL.floorD)
|
||||
c.px(9, 4, PAL.floorD); c.px(2, 9, PAL.floorD)
|
||||
end
|
||||
T[26] = function(c) -- 诊所地砖
|
||||
c.rect(0, 0, 16, 16, PAL.clinic)
|
||||
c.hline(0, 7, 16, PAL.clinicD); c.vline(7, 0, 16, PAL.clinicD); c.hline(0, 15, 16, PAL.clinicD); c.vline(15, 0, 16, PAL.clinicD)
|
||||
end
|
||||
T[27] = function(c) -- 室内墙
|
||||
c.rect(0, 0, 16, 16, PAL.inwall)
|
||||
c.dither(0, 0, 16, 16, PAL.inwallD, 8)
|
||||
end
|
||||
T[28] = function(c) -- 室内墙裙
|
||||
c.rect(0, 0, 16, 10, PAL.inwall)
|
||||
c.rect(0, 10, 16, 6, PAL.wood); c.hline(0, 10, 16, PAL.woodD)
|
||||
end
|
||||
T[29] = function(c) -- 地毯
|
||||
c.rect(0, 0, 16, 16, PAL.pink)
|
||||
c.rect(1, 1, 14, 14, C("ffc4de")); c.rect(3, 3, 10, 10, PAL.pink)
|
||||
c.px(7, 7, PAL.white); c.px(8, 8, PAL.white)
|
||||
end
|
||||
T[30] = function(c) -- 床头(枕头)
|
||||
c.rect(0, 0, 16, 16, PAL.wood)
|
||||
c.rect(1, 1, 14, 15, C("f8f4e8"))
|
||||
c.rect(3, 3, 10, 7, PAL.white); c.hline(3, 9, 10, C("d8d4c8"))
|
||||
c.hline(1, 0, 14, PAL.woodD)
|
||||
end
|
||||
T[31] = function(c) -- 床尾(被子)
|
||||
c.rect(0, 0, 16, 16, PAL.wood)
|
||||
c.rect(1, 0, 14, 15, C("8ec8e0"))
|
||||
c.hline(1, 3, 14, C("6fb0d0")); c.hline(1, 8, 14, C("6fb0d0")); c.hline(1, 13, 14, C("6fb0d0"))
|
||||
c.hline(1, 15, 14, PAL.woodD)
|
||||
end
|
||||
T[32] = function(c) -- 柜台
|
||||
c.rect(0, 0, 16, 16, PAL.wood)
|
||||
c.rect(0, 0, 16, 6, PAL.woodL); c.hline(0, 6, 16, PAL.woodD)
|
||||
c.vline(0, 0, 16, PAL.woodD); c.vline(15, 0, 16, PAL.woodD); c.hline(0, 15, 16, PAL.woodD)
|
||||
end
|
||||
T[33] = function(c) -- 烤炉
|
||||
c.rect(0, 0, 16, 16, PAL.stoneD)
|
||||
c.rect(1, 1, 14, 14, PAL.stone)
|
||||
c.rect(3, 6, 10, 7, PAL.black)
|
||||
c.rect(4, 8, 8, 4, C("f4924a")); c.px(6, 9, PAL.yellow); c.px(9, 10, PAL.yellow)
|
||||
c.hline(3, 3, 10, PAL.stoneD)
|
||||
end
|
||||
T[34] = function(c) -- 货架(空)
|
||||
c.rect(0, 0, 16, 16, PAL.wood)
|
||||
c.rect(1, 1, 14, 14, PAL.woodL)
|
||||
c.hline(1, 5, 14, PAL.woodD); c.hline(1, 10, 14, PAL.woodD); c.hline(1, 14, 14, PAL.woodD)
|
||||
c.vline(0, 0, 16, PAL.woodD); c.vline(15, 0, 16, PAL.woodD)
|
||||
end
|
||||
T[35] = function(c) -- 货架(同一商品重复排列,标签空白)
|
||||
T[34](c)
|
||||
for _, y in ipairs({ 2, 7, 11 }) do
|
||||
c.rect(2, y, 3, 3, PAL.berry); c.rect(6, y, 3, 3, PAL.berry); c.rect(10, y, 3, 3, PAL.berry)
|
||||
c.px(3, y, PAL.white); c.px(7, y, PAL.white); c.px(11, y, PAL.white)
|
||||
end
|
||||
end
|
||||
T[36] = function(c) -- 桌子
|
||||
c.rect(0, 0, 16, 16, PAL.floor)
|
||||
c.rect(2, 3, 12, 9, PAL.woodL); c.hline(2, 11, 12, PAL.woodD)
|
||||
c.vline(3, 12, 3, PAL.woodD); c.vline(12, 12, 3, PAL.woodD)
|
||||
c.hline(2, 3, 12, PAL.wood)
|
||||
end
|
||||
T[37] = function(c) -- 椅子
|
||||
c.rect(0, 0, 16, 16, PAL.floor)
|
||||
c.rect(4, 3, 8, 3, PAL.wood)
|
||||
c.rect(4, 6, 8, 6, PAL.woodL)
|
||||
c.vline(4, 12, 3, PAL.woodD); c.vline(11, 12, 3, PAL.woodD)
|
||||
end
|
||||
T[38] = function(c) -- 井·顶(小屋顶)
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(2, 2, 12, 6, PAL.roofD); c.rect(3, 1, 10, 2, PAL.roof)
|
||||
c.vline(3, 8, 8, PAL.trunkD); c.vline(12, 8, 8, PAL.trunkD)
|
||||
end
|
||||
T[39] = function(c) -- 井·底(石圈)
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(2, 2, 12, 12, PAL.stone)
|
||||
c.rect(4, 4, 8, 8, PAL.black)
|
||||
c.px(3, 3, PAL.stoneD); c.px(12, 3, PAL.stoneD); c.px(3, 12, PAL.stoneD); c.px(12, 12, PAL.stoneD)
|
||||
c.hline(2, 2, 12, PAL.stoneD)
|
||||
end
|
||||
T[40] = function(c) -- 雾(轻)
|
||||
c.rect(0, 0, 16, 16, PAL.fog)
|
||||
c.dither(0, 0, 16, 16, PAL.fogD, 3)
|
||||
end
|
||||
T[41] = function(c) -- 雾(浓)
|
||||
c.rect(0, 0, 16, 16, PAL.fogD)
|
||||
c.dither(0, 0, 16, 16, PAL.fog, 2); c.dither(1, 0, 16, 16, C("bcc8d2"), 5)
|
||||
end
|
||||
T[42] = function(c) -- 白色层·墙
|
||||
c.rect(0, 0, 16, 16, PAL.wgray)
|
||||
c.hline(0, 12, 16, PAL.wgrayD); c.rect(0, 13, 16, 3, PAL.wgrayDD)
|
||||
end
|
||||
T[43] = function(c) -- 白色层·地
|
||||
c.rect(0, 0, 16, 16, PAL.wgrayD)
|
||||
c.hline(0, 7, 16, PAL.wgrayDD); c.vline(7, 0, 16, PAL.wgrayDD)
|
||||
end
|
||||
T[44] = function(c) -- 白色层·门
|
||||
c.rect(0, 0, 16, 16, PAL.wgray)
|
||||
c.rect(3, 1, 10, 15, PAL.wgrayDD); c.rect(4, 2, 8, 14, PAL.wgrayD)
|
||||
c.px(10, 9, PAL.wdark)
|
||||
end
|
||||
T[45] = function(c) -- 长椅·左
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(2, 4, 14, 3, PAL.woodL); c.rect(2, 7, 14, 4, PAL.wood)
|
||||
c.vline(3, 11, 3, PAL.woodD); c.hline(2, 7, 14, PAL.woodD)
|
||||
end
|
||||
T[46] = function(c) -- 长椅·右
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(0, 4, 14, 3, PAL.woodL); c.rect(0, 7, 14, 4, PAL.wood)
|
||||
c.vline(12, 11, 3, PAL.woodD); c.hline(0, 7, 14, PAL.woodD)
|
||||
end
|
||||
T[47] = function(c) -- 公告板·左
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(2, 1, 14, 12, PAL.wood); c.rect(4, 3, 12, 8, C("f8f0d8"))
|
||||
c.hline(5, 5, 8, PAL.stoneD); c.hline(5, 7, 6, PAL.stoneD); c.hline(5, 9, 9, PAL.stoneD)
|
||||
c.vline(4, 13, 3, PAL.trunkD)
|
||||
end
|
||||
T[48] = function(c) -- 公告板·右
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(0, 1, 14, 12, PAL.wood); c.rect(0, 3, 12, 8, C("f8f0d8"))
|
||||
c.hline(1, 5, 8, PAL.stoneD); c.hline(1, 7, 9, PAL.stoneD); c.hline(1, 9, 5, PAL.stoneD)
|
||||
c.vline(11, 13, 3, PAL.trunkD)
|
||||
end
|
||||
T[49] = function(c) -- 邮筒
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(4, 2, 8, 10, PAL.red); c.rect(5, 1, 6, 2, C("c04040"))
|
||||
c.rect(6, 5, 4, 2, PAL.black)
|
||||
c.vline(6, 12, 3, C("c04040")); c.vline(9, 12, 3, C("c04040"))
|
||||
end
|
||||
T[50] = function(c) -- 栅栏·横
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.hline(0, 6, 16, PAL.woodL); c.hline(0, 7, 16, PAL.wood); c.hline(0, 10, 16, PAL.woodL); c.hline(0, 11, 16, PAL.wood)
|
||||
c.rect(2, 4, 2, 10, PAL.wood); c.rect(12, 4, 2, 10, PAL.wood)
|
||||
c.px(2, 4, PAL.woodL); c.px(12, 4, PAL.woodL)
|
||||
end
|
||||
T[51] = function(c) -- 栅栏·柱
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(6, 2, 3, 12, PAL.wood); c.px(6, 2, PAL.woodL); c.px(7, 2, PAL.woodL)
|
||||
end
|
||||
T[52] = function(c) -- 路牌
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(2, 3, 12, 6, PAL.woodL); c.hline(3, 5, 10, PAL.woodD); c.hline(3, 7, 7, PAL.woodD)
|
||||
c.rect(7, 9, 2, 6, PAL.wood)
|
||||
end
|
||||
T[53] = function(c) -- 野餐布1
|
||||
c.rect(0, 0, 16, 16, C("f8f0e0"))
|
||||
for j = 0, 15 do for i = 0, 15 do
|
||||
if (math.floor(i / 4) + math.floor(j / 4)) % 2 == 0 then c.px(i, j, C("f0a0a8")) end
|
||||
end end
|
||||
end
|
||||
T[54] = function(c) -- 野餐布2(有食物)
|
||||
T[53](c)
|
||||
c.rect(5, 5, 6, 6, C("fff8e8")); c.px(7, 7, PAL.berry); c.px(9, 8, PAL.berry); c.px(6, 9, PAL.yellow)
|
||||
end
|
||||
T[55] = function(c) -- 花坛(盛开)
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(1, 1, 14, 14, PAL.stoneD); c.rect(2, 2, 12, 12, C("8a6a48"))
|
||||
c.px(4, 4, PAL.pink); c.px(8, 5, PAL.yellow); c.px(11, 4, PAL.red); c.px(5, 8, PAL.yellow)
|
||||
c.px(9, 9, PAL.pink); c.px(12, 8, PAL.white); c.px(4, 11, PAL.red); c.px(8, 12, PAL.pink); c.px(11, 11, PAL.yellow)
|
||||
end
|
||||
T[56] = function(c) -- 花田·土
|
||||
c.rect(0, 0, 16, 16, C("a8825a"))
|
||||
c.hline(0, 3, 16, C("94714c")); c.hline(0, 8, 16, C("94714c")); c.hline(0, 13, 16, C("94714c"))
|
||||
end
|
||||
T[57] = function(c) -- 花田·苗
|
||||
T[56](c)
|
||||
c.px(3, 2, PAL.leaf); c.px(3, 1, PAL.leaf); c.px(9, 7, PAL.leaf); c.px(9, 6, PAL.leaf); c.px(13, 12, PAL.leaf); c.px(13, 11, PAL.leaf)
|
||||
c.px(4, 1, PAL.pink); c.px(10, 6, PAL.yellow)
|
||||
end
|
||||
T[58] = function(c) -- 彩带(挂起)
|
||||
c.rect(0, 0, 16, 16, pc.rgba(0, 0, 0, 0))
|
||||
for i = 0, 15 do
|
||||
local y = math.floor(2 + 2 * math.sin(i / 4))
|
||||
c.px(i, y + 1, PAL.woodD)
|
||||
end
|
||||
c.px(2, 4, PAL.red); c.px(2, 5, PAL.red); c.px(6, 5, PAL.yellow); c.px(6, 6, PAL.yellow)
|
||||
c.px(10, 4, PAL.pink); c.px(10, 5, PAL.pink); c.px(14, 5, PAL.sky); c.px(14, 6, PAL.sky)
|
||||
end
|
||||
T[59] = function(c) -- 灯笼杆
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(7, 2, 2, 12, PAL.trunk)
|
||||
c.rect(5, 2, 6, 5, PAL.yellow); c.rect(6, 1, 4, 1, PAL.roofD); c.px(7, 4, C("fff0a0")); c.px(8, 4, C("fff0a0"))
|
||||
end
|
||||
T[60] = function(c) -- 湖水·波光
|
||||
T[7](c)
|
||||
c.px(4, 6, PAL.white); c.px(11, 8, PAL.white); c.hline(7, 2, 3, PAL.waterL)
|
||||
end
|
||||
T[61] = function(c) -- 室内窗
|
||||
T[27](c)
|
||||
c.rect(3, 3, 10, 8, PAL.woodD); c.rect(4, 4, 8, 6, PAL.sky)
|
||||
c.vline(7, 4, 6, PAL.woodD); c.px(5, 5, PAL.waterL)
|
||||
end
|
||||
T[62] = function(c) -- 白色层·病床头
|
||||
c.rect(0, 0, 16, 16, PAL.wgrayD)
|
||||
c.rect(1, 1, 14, 15, PAL.wgray)
|
||||
c.rect(3, 3, 10, 6, PAL.white); c.hline(3, 8, 10, PAL.wgrayD)
|
||||
c.hline(1, 0, 14, PAL.wgrayDD)
|
||||
end
|
||||
T[63] = function(c) -- 白色层·病床尾
|
||||
c.rect(0, 0, 16, 16, PAL.wgrayD)
|
||||
c.rect(1, 0, 14, 15, C("dfe2e6"))
|
||||
c.hline(1, 4, 14, PAL.wgrayD); c.hline(1, 9, 14, PAL.wgrayD)
|
||||
c.hline(1, 15, 14, PAL.wgrayDD)
|
||||
end
|
||||
T[64] = function(c) -- 桌(诊所/办公)
|
||||
c.rect(0, 0, 16, 16, PAL.clinic)
|
||||
c.rect(1, 3, 14, 9, PAL.woodL); c.hline(1, 11, 14, PAL.woodD); c.hline(1, 3, 14, PAL.wood)
|
||||
c.rect(3, 5, 6, 4, PAL.white); c.hline(4, 6, 4, PAL.stoneD); c.hline(4, 7, 3, PAL.stoneD)
|
||||
c.vline(2, 12, 3, PAL.woodD); c.vline(13, 12, 3, PAL.woodD)
|
||||
end
|
||||
T[65] = function(c) -- 日历(墙面)
|
||||
T[27](c)
|
||||
c.rect(4, 2, 8, 11, PAL.white); c.rect(4, 2, 8, 3, PAL.red)
|
||||
c.hline(5, 7, 6, PAL.stoneD); c.hline(5, 9, 6, PAL.stoneD); c.rect(7, 10, 3, 2, PAL.black)
|
||||
end
|
||||
T[66] = function(c) -- 柜台+配方纸
|
||||
T[32](c)
|
||||
c.rect(4, 1, 7, 5, C("f8f0d8")); c.hline(5, 2, 5, PAL.stoneD); c.hline(5, 4, 4, PAL.stoneD)
|
||||
end
|
||||
T[67] = function(c) -- 木箱
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(2, 3, 12, 12, PAL.wood); c.rect(3, 4, 10, 10, PAL.woodL)
|
||||
c.hline(3, 8, 10, PAL.wood); c.vline(8, 4, 10, PAL.wood)
|
||||
end
|
||||
T[68] = function(c) -- 篮子
|
||||
c.rect(0, 0, 16, 16, PAL.floor)
|
||||
c.rect(3, 6, 10, 8, PAL.woodL); c.hline(3, 6, 10, PAL.woodD); c.hline(3, 13, 10, PAL.woodD)
|
||||
c.dither(4, 8, 8, 5, PAL.wood, 2)
|
||||
c.px(6, 5, PAL.berry); c.px(9, 5, PAL.berry)
|
||||
end
|
||||
T[69] = function(c) -- 花坛(干涸·待浇水)
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(1, 1, 14, 14, PAL.stoneD); c.rect(2, 2, 12, 12, C("b09068"))
|
||||
c.px(5, 5, C("8a7a5a")); c.px(10, 7, C("8a7a5a")); c.px(6, 10, C("8a7a5a"))
|
||||
end
|
||||
T[70] = function(c) -- 白色层·窗
|
||||
T[42](c)
|
||||
c.rect(4, 2, 8, 7, PAL.wgrayDD); c.rect(5, 3, 6, 5, C("c8ccd4")); c.vline(7, 3, 5, PAL.wgrayDD)
|
||||
end
|
||||
T[71] = function(c) -- 门垫
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.rect(2, 4, 12, 8, C("c8a878")); c.rect(3, 5, 10, 6, C("dcc094"))
|
||||
end
|
||||
T[72] = function(c) -- 高草
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
for i = 1, 14, 3 do c.vline(i, 6, 8, PAL.grassD); c.vline(i + 1, 8, 6, PAL.grassL) end
|
||||
end
|
||||
T[73] = function(c) -- 广场中心砖
|
||||
c.rect(0, 0, 16, 16, PAL.path)
|
||||
c.rect(2, 2, 12, 12, C("f0c88a")); c.rect(5, 5, 6, 6, PAL.path)
|
||||
c.px(7, 7, PAL.roofD); c.px(8, 8, PAL.roofD)
|
||||
end
|
||||
T[74] = function(c) -- 白色层·帘
|
||||
c.rect(0, 0, 16, 16, PAL.wgray)
|
||||
for i = 0, 15, 3 do c.vline(i, 0, 16, C("d8dade")) end
|
||||
c.hline(0, 0, 16, PAL.wgrayDD)
|
||||
end
|
||||
T[75] = function(c) -- 白色层·监护仪
|
||||
c.rect(0, 0, 16, 16, PAL.wgrayD)
|
||||
c.rect(3, 2, 10, 9, PAL.wdark); c.rect(4, 3, 8, 6, C("354048"))
|
||||
c.px(5, 6, C("7ae0a0")); c.px(6, 5, C("7ae0a0")); c.px(7, 6, C("7ae0a0")); c.px(8, 6, C("7ae0a0")); c.px(9, 4, C("7ae0a0")); c.px(10, 6, C("7ae0a0"))
|
||||
c.rect(6, 11, 4, 4, PAL.wgrayDD)
|
||||
end
|
||||
T[76] = function(c) -- 石墙(村界)
|
||||
c.rect(0, 0, 16, 16, PAL.stone)
|
||||
c.hline(0, 5, 16, PAL.stoneD); c.hline(0, 11, 16, PAL.stoneD)
|
||||
c.vline(4, 0, 5, PAL.stoneD); c.vline(10, 6, 5, PAL.stoneD); c.vline(6, 12, 4, PAL.stoneD)
|
||||
end
|
||||
T[77] = function(c) -- 雾中草地(边缘)
|
||||
c.rect(0, 0, 16, 16, PAL.grass)
|
||||
c.dither(0, 0, 16, 16, PAL.fog, 2)
|
||||
end
|
||||
T[78] = function(c) -- 黑
|
||||
c.rect(0, 0, 16, 16, PAL.black)
|
||||
end
|
||||
T[79] = function(c) -- 纯白灰
|
||||
c.rect(0, 0, 16, 16, PAL.wgray)
|
||||
end
|
||||
|
||||
local tilesImg = Image(160, 128, ColorMode.RGB)
|
||||
for idx = 0, 79 do
|
||||
local col, row = idx % 10, math.floor(idx / 10)
|
||||
T[idx](makeCtx(tilesImg, col * 16, row * 16))
|
||||
end
|
||||
saveSheet(tilesImg, "tiles")
|
||||
|
||||
-- ============ 主角:16×24 × 16 帧(下/上/左/右 × 4) ============
|
||||
local SKIN = C("f6d2a8")
|
||||
local HAIR = C("6b4632")
|
||||
local SHIRT = C("e87858")
|
||||
local SHIRT_D = C("cc5f42")
|
||||
local PANTS = C("4a6a9a")
|
||||
local SHOE = C("7a4a2a")
|
||||
|
||||
-- 画一帧主角;dir: 0下 1上 2左 3右;f: 0..3 行走帧(0 兼待机)
|
||||
local function paintPlayer(c, dir, f)
|
||||
-- 头(x4..11, y0..7):发 0..2,脸 3..7
|
||||
c.rect(4, 0, 8, 3, HAIR)
|
||||
c.px(4, 0, pc.rgba(0, 0, 0, 0)); c.px(11, 0, pc.rgba(0, 0, 0, 0))
|
||||
if dir == 1 then
|
||||
c.rect(4, 3, 8, 5, HAIR) -- 背面全是头发
|
||||
c.px(4, 7, SKIN); c.px(11, 7, SKIN)
|
||||
else
|
||||
c.rect(4, 3, 8, 5, SKIN)
|
||||
c.vline(4, 3, 2, HAIR); c.vline(11, 3, 2, HAIR) -- 刘海两侧
|
||||
if dir == 0 then
|
||||
c.px(6, 5, PAL.black); c.px(9, 5, PAL.black)
|
||||
c.px(7, 7, PAL.black); c.px(8, 7, PAL.black) -- 永远的微笑
|
||||
c.px(6, 6, C("f8b8a0")); c.px(9, 6, C("f8b8a0"))
|
||||
elseif dir == 2 then
|
||||
c.rect(9, 3, 3, 5, HAIR) -- 侧脸:后脑
|
||||
c.px(6, 5, PAL.black); c.px(5, 7, PAL.black); c.px(6, 7, PAL.black)
|
||||
elseif dir == 3 then
|
||||
c.rect(4, 3, 3, 5, HAIR)
|
||||
c.px(9, 5, PAL.black); c.px(9, 7, PAL.black); c.px(10, 7, PAL.black)
|
||||
end
|
||||
end
|
||||
-- 身体(y8..16)
|
||||
c.rect(5, 8, 6, 9, SHIRT)
|
||||
c.hline(5, 16, 6, SHIRT_D)
|
||||
-- 手臂:随帧摆动
|
||||
local sway = ({ 0, 1, 0, -1 })[f + 1]
|
||||
if dir == 0 or dir == 1 then
|
||||
c.vline(4, 9 + (sway > 0 and 1 or 0), 5, SHIRT_D); c.px(4, 14 + (sway > 0 and 1 or 0), SKIN)
|
||||
c.vline(11, 9 + (sway < 0 and 1 or 0), 5, SHIRT_D); c.px(11, 14 + (sway < 0 and 1 or 0), SKIN)
|
||||
else
|
||||
c.vline(dir == 2 and 7 or 8, 9 + math.abs(sway), 5, SHIRT_D)
|
||||
c.px(dir == 2 and 7 or 8, 14 + math.abs(sway), SKIN)
|
||||
end
|
||||
-- 腿(y17..20)与鞋(y21..23)
|
||||
local lLift = (f == 1) and 1 or 0
|
||||
local rLift = (f == 3) and 1 or 0
|
||||
c.rect(5, 17, 3, 4 - lLift, PANTS)
|
||||
c.rect(8, 17, 3, 4 - rLift, PANTS)
|
||||
c.rect(5, 21 - lLift, 3, 2, SHOE)
|
||||
c.rect(8, 21 - rLift, 3, 2, SHOE)
|
||||
end
|
||||
|
||||
local playerImg = Image(256, 24, ColorMode.RGB)
|
||||
for dir = 0, 3 do
|
||||
for f = 0, 3 do
|
||||
paintPlayer(makeCtx(playerImg, (dir * 4 + f) * 16, 0), dir, f)
|
||||
end
|
||||
end
|
||||
saveSheet(playerImg, "player")
|
||||
|
||||
-- ============ 主角头像:24×24(微笑 / 真实的脸) ============
|
||||
local function paintPortrait(c, real)
|
||||
c.rect(0, 0, 24, 24, C("f8ecd0")) -- 背景
|
||||
c.rect(4, 2, 16, 6, HAIR) -- 头发
|
||||
c.px(4, 2, C("f8ecd0")); c.px(19, 2, C("f8ecd0"))
|
||||
c.rect(4, 8, 16, 12, SKIN) -- 脸
|
||||
c.vline(4, 8, 4, HAIR); c.vline(5, 8, 2, HAIR)
|
||||
c.vline(19, 8, 4, HAIR); c.vline(18, 8, 2, HAIR)
|
||||
c.rect(6, 20, 12, 4, SHIRT) -- 肩
|
||||
if real then
|
||||
-- 真实的脸:疲惫——平直的嘴、眼下阴影、微乱的头发
|
||||
c.px(3, 4, HAIR); c.px(20, 5, HAIR); c.px(2, 5, HAIR)
|
||||
c.rect(8, 12, 2, 2, PAL.black); c.rect(14, 12, 2, 2, PAL.black)
|
||||
c.hline(8, 15, 2, C("c8a080")); c.hline(14, 15, 2, C("c8a080")) -- 眼袋
|
||||
c.hline(10, 18, 4, C("9a6a52")) -- 平直疲惫的嘴
|
||||
else
|
||||
-- 永远的微笑
|
||||
c.rect(8, 12, 2, 2, PAL.black); c.rect(14, 12, 2, 2, PAL.black)
|
||||
c.px(7, 14, C("f8b8a0")); c.px(16, 14, C("f8b8a0")) -- 腮红
|
||||
c.px(9, 17, C("9a6a52")); c.px(14, 17, C("9a6a52"))
|
||||
c.hline(10, 18, 4, C("9a6a52")) -- 上弯的微笑
|
||||
end
|
||||
end
|
||||
local p1 = Image(24, 24, ColorMode.RGB); paintPortrait(makeCtx(p1, 0, 0), false); saveSheet(p1, "portrait_smile")
|
||||
local p2 = Image(24, 24, ColorMode.RGB); paintPortrait(makeCtx(p2, 0, 0), true); saveSheet(p2, "portrait_real")
|
||||
|
||||
-- ============ 具名 NPC:16×24 × 4 帧(smile/flat/off/blink) ============
|
||||
-- off 档与 smile 档仅差 1–2 像素(右眼低 1 像素),绝不画成鬼脸。
|
||||
local function paintAnimal(c, spec, expr)
|
||||
local top = spec.headTop or 2
|
||||
-- 头
|
||||
c.rect(2, top, 12, 10, spec.fur)
|
||||
c.px(2, top, pc.rgba(0, 0, 0, 0)); c.px(13, top, pc.rgba(0, 0, 0, 0))
|
||||
c.px(2, top + 9, pc.rgba(0, 0, 0, 0)); c.px(13, top + 9, pc.rgba(0, 0, 0, 0))
|
||||
-- 口鼻
|
||||
c.rect(5, top + 5, 6, 4, spec.muzzle)
|
||||
-- 眼睛
|
||||
local ey = top + 4
|
||||
if expr == "blink" then
|
||||
c.px(5, ey + 1, PAL.black); c.px(10, ey + 1, PAL.black)
|
||||
else
|
||||
c.px(5, ey, PAL.black); c.px(5, ey + 1, PAL.black)
|
||||
local roff = (expr == "off") and 1 or 0 -- 崩坏档:右眼低 1 像素
|
||||
c.px(10, ey + roff, PAL.black); c.px(10, ey + 1 + roff, PAL.black)
|
||||
end
|
||||
-- 鼻子
|
||||
if spec.nose then c.px(7, top + 6, PAL.black); c.px(8, top + 6, PAL.black) end
|
||||
-- 嘴:微笑(小 U)或平直
|
||||
local my = top + 8
|
||||
if expr == "flat" then
|
||||
c.hline(6, my, 4, PAL.black)
|
||||
else
|
||||
c.px(6, my - 1, PAL.black); c.px(9, my - 1, PAL.black); c.px(7, my, PAL.black); c.px(8, my, PAL.black)
|
||||
end
|
||||
-- 身体
|
||||
c.rect(4, 12, 8, 8, spec.cloth)
|
||||
-- 手
|
||||
c.vline(3, 13, 4, spec.fur); c.vline(12, 13, 4, spec.fur)
|
||||
-- 脚
|
||||
c.rect(4, 20, 3, 3, spec.furD); c.rect(9, 20, 3, 3, spec.furD)
|
||||
if spec.extra then spec.extra(c, expr) end
|
||||
end
|
||||
|
||||
local NPCS = {
|
||||
{ file = "npc_afu", fur = C("c49058"), furD = C("a87840"), muzzle = C("ecd0a8"), cloth = C("e0b060"), nose = true,
|
||||
extra = function(c) -- 狗村长:垂耳+红领巾
|
||||
c.rect(1, 3, 2, 5, C("a87840")); c.rect(13, 3, 2, 5, C("a87840"))
|
||||
c.hline(4, 12, 8, PAL.red); c.px(7, 13, PAL.red); c.px(8, 13, PAL.red)
|
||||
end },
|
||||
{ file = "npc_yuanyuan", fur = C("f7f2ea"), furD = C("ddd4c8"), muzzle = C("ffffff"), cloth = C("f8c8d8"), headTop = 4,
|
||||
extra = function(c) -- 兔面包师:长耳+围裙
|
||||
c.rect(4, 0, 2, 4, C("f7f2ea")); c.px(5, 1, C("f8b8c8")); c.px(5, 2, C("f8b8c8"))
|
||||
c.rect(10, 0, 2, 4, C("f7f2ea")); c.px(10, 1, C("f8b8c8")); c.px(10, 2, C("f8b8c8"))
|
||||
c.rect(5, 14, 6, 6, C("fff8f0")); c.px(6, 10, C("f0a0b0")) -- 围裙+腮红
|
||||
c.px(9, 10, C("f0a0b0"))
|
||||
end },
|
||||
{ file = "npc_dabao", fur = C("9a6b42"), furD = C("7e5530"), muzzle = C("c89a68"), cloth = C("5a88c8"), nose = true,
|
||||
extra = function(c) -- 熊邮差:圆耳+挎包带
|
||||
c.rect(3, 0, 3, 2, C("9a6b42")); c.rect(10, 0, 3, 2, C("9a6b42"))
|
||||
c.px(4, 1, C("c89a68")); c.px(11, 1, C("c89a68"))
|
||||
for i = 0, 6 do c.px(4 + i, 13 + i > 19 and 19 or 13 + math.floor(i * 0.9), C("3a5a88")) end
|
||||
end },
|
||||
{ file = "npc_mimi", fur = C("c8c0d8"), furD = C("a8a0c0"), muzzle = C("ece8f4"), cloth = C("e8a04a"), nose = true,
|
||||
extra = function(c) -- 猫店主:尖耳+胡须
|
||||
c.px(3, 1, C("c8c0d8")); c.rect(3, 2, 2, 1, C("c8c0d8")); c.px(4, 1, C("c8c0d8"))
|
||||
c.px(12, 1, C("c8c0d8")); c.rect(11, 2, 2, 1, C("c8c0d8")); c.px(11, 1, C("c8c0d8"))
|
||||
c.hline(1, 8, 2, C("8a84a0")); c.hline(13, 8, 2, C("8a84a0"))
|
||||
end },
|
||||
{ file = "npc_fu", fur = C("d8b088"), furD = C("bc9268"), muzzle = C("f0d8b8"), cloth = C("f4f4f4"), nose = true,
|
||||
extra = function(c) -- 鹿医生:小角+白大褂
|
||||
c.px(4, 1, C("8a6a4a")); c.px(4, 0, C("8a6a4a")); c.px(3, 0, C("8a6a4a"))
|
||||
c.px(11, 1, C("8a6a4a")); c.px(11, 0, C("8a6a4a")); c.px(12, 0, C("8a6a4a"))
|
||||
c.vline(7, 12, 8, C("d8d8d8")) -- 大褂门襟
|
||||
end },
|
||||
{ file = "npc_gui", fur = C("a8c078"), furD = C("88a058"), muzzle = C("c8d8a0"), cloth = C("7a9a4a"),
|
||||
extra = function(c) -- 龟爷爷:龟壳纹+老花镜
|
||||
c.hline(5, 14, 6, C("5f7c38")); c.hline(5, 17, 6, C("5f7c38")); c.vline(7, 13, 6, C("5f7c38"))
|
||||
local gy = 2 + 4
|
||||
c.px(4, gy, C("6a6a6a")); c.px(6, gy, C("6a6a6a")); c.px(9, gy, C("6a6a6a")); c.px(11, gy, C("6a6a6a"))
|
||||
c.px(7, gy, C("6a6a6a")); c.px(8, gy, C("6a6a6a"))
|
||||
end },
|
||||
}
|
||||
|
||||
for _, spec in ipairs(NPCS) do
|
||||
local img = Image(64, 24, ColorMode.RGB)
|
||||
local exprs = { "smile", "flat", "off", "blink" }
|
||||
for i, e in ipairs(exprs) do
|
||||
paintAnimal(makeCtx(img, (i - 1) * 16, 0), spec, e)
|
||||
end
|
||||
saveSheet(img, spec.file)
|
||||
end
|
||||
|
||||
-- ============ 双子鸟:16×16 × 2 帧(闭嘴/张嘴) ============
|
||||
local function paintBird(c, open)
|
||||
local B = C("6ab0e8")
|
||||
local BD = C("4a90c8")
|
||||
c.rect(4, 5, 8, 7, B) -- 身体
|
||||
c.rect(8, 2, 6, 6, B) -- 头
|
||||
c.px(11, 4, PAL.black) -- 眼
|
||||
c.rect(2, 6, 3, 4, BD) -- 尾
|
||||
c.rect(5, 7, 4, 3, BD) -- 翅
|
||||
if open then
|
||||
c.px(14, 5, PAL.yellow); c.px(15, 4, PAL.yellow); c.px(15, 6, PAL.yellow)
|
||||
else
|
||||
c.px(14, 5, PAL.yellow); c.px(15, 5, PAL.yellow)
|
||||
end
|
||||
c.vline(7, 12, 2, PAL.yellow); c.vline(10, 12, 2, PAL.yellow) -- 脚
|
||||
end
|
||||
local birdImg = Image(32, 16, ColorMode.RGB)
|
||||
paintBird(makeCtx(birdImg, 0, 0), false)
|
||||
paintBird(makeCtx(birdImg, 16, 0), true)
|
||||
saveSheet(birdImg, "bird")
|
||||
|
||||
-- ============ 灰麻雀(E3 结局,一只真的鸟):16×16 ============
|
||||
local function paintSparrow(c)
|
||||
local G = C("9a9088")
|
||||
local GD = C("6e6660")
|
||||
c.rect(4, 6, 8, 6, G)
|
||||
c.rect(8, 3, 6, 5, G)
|
||||
c.px(11, 5, PAL.black)
|
||||
c.rect(2, 7, 3, 3, GD)
|
||||
c.rect(5, 8, 4, 2, GD)
|
||||
c.px(14, 6, C("7a6a58")); c.px(15, 6, C("7a6a58"))
|
||||
c.vline(7, 12, 2, GD); c.vline(10, 12, 2, GD)
|
||||
end
|
||||
local sparrowImg = Image(16, 16, ColorMode.RGB)
|
||||
paintSparrow(makeCtx(sparrowImg, 0, 0))
|
||||
saveSheet(sparrowImg, "sparrow")
|
||||
|
||||
-- ============ 群演 ×5:16×16 × 2 帧循环 ============
|
||||
local function paintCritter(c, spec, f)
|
||||
local bob = f -- 第二帧整体下移 1px 形成循环
|
||||
c.rect(4, 6 + bob, 8, 6 - bob, spec.body)
|
||||
c.rect(5, 2 + bob, 6, 5, spec.body)
|
||||
c.px(6, 4 + bob, PAL.black); c.px(9, 4 + bob, PAL.black)
|
||||
c.px(7, 6 + bob, spec.snout); c.px(8, 6 + bob, spec.snout)
|
||||
c.rect(4, 12, 3, 2, spec.dark); c.rect(9, 12, 3, 2, spec.dark)
|
||||
if spec.extra then spec.extra(c, f) end
|
||||
end
|
||||
local CRITTERS = {
|
||||
{ file = "extra1", body = C("e0904a"), dark = C("b87030"), snout = C("f8c890"), -- 松鼠:大尾巴
|
||||
extra = function(c, f) c.rect(12, 1 + f, 3, 9, C("b87030")); c.px(13, 2 + f, C("e0904a")) end },
|
||||
{ file = "extra2", body = C("b09068"), dark = C("8a6a48"), snout = C("e8cca8"), -- 刺猬:背刺
|
||||
extra = function(c, f) for i = 4, 11, 2 do c.px(i, 1 + f + (i % 4 == 0 and 0 or 1), C("6e5236")) end end },
|
||||
{ file = "extra3", body = C("f4e04a"), dark = C("d8b830"), snout = C("f4a030"), -- 鸭子:扁嘴
|
||||
extra = function(c, f) c.px(6, 5 + f, C("f4a030")); c.px(9, 5 + f, C("f4a030")) end },
|
||||
{ file = "extra4", body = C("b8b8c4"), dark = C("8e8e9c"), snout = C("e8c8c8"), -- 老鼠:圆耳
|
||||
extra = function(c, f) c.rect(4, 0 + f, 2, 2, C("b8b8c4")); c.rect(10, 0 + f, 2, 2, C("b8b8c4")) end },
|
||||
{ file = "extra5", body = C("7ac86a"), dark = C("58a048"), snout = C("aee89e"), -- 青蛙:凸眼
|
||||
extra = function(c, f) c.px(5, 1 + f, C("7ac86a")); c.px(10, 1 + f, C("7ac86a")) end },
|
||||
}
|
||||
for _, spec in ipairs(CRITTERS) do
|
||||
local img = Image(32, 16, ColorMode.RGB)
|
||||
paintCritter(makeCtx(img, 0, 0), spec, 0)
|
||||
paintCritter(makeCtx(img, 16, 0), spec, 1)
|
||||
saveSheet(img, spec.file)
|
||||
end
|
||||
|
||||
-- ============ 向日葵表盘:24×24 × 14 帧 ============
|
||||
-- 帧 0..12 = 点亮花瓣数;帧 13 = 满格+花心黑籽
|
||||
local function paintSunflower(c, lit, seeds)
|
||||
local cx, cy = 11.5, 11.5
|
||||
for k = 0, 11 do
|
||||
local ang = (k / 12) * math.pi * 2 - math.pi / 2
|
||||
local col = (k < lit) and PAL.yellow or C("7a7264")
|
||||
local colD = (k < lit) and C("e0b830") or C("635c50")
|
||||
for r = 7, 10 do
|
||||
local x = math.floor(cx + math.cos(ang) * r + 0.5)
|
||||
local y = math.floor(cy + math.sin(ang) * r + 0.5)
|
||||
c.px(x, y, r > 9 and colD or col)
|
||||
local px2 = math.floor(cx + math.cos(ang + 0.14) * r + 0.5)
|
||||
local py2 = math.floor(cy + math.sin(ang + 0.14) * r + 0.5)
|
||||
c.px(px2, py2, col)
|
||||
end
|
||||
end
|
||||
-- 花心
|
||||
for j = -4, 4 do for i = -4, 4 do
|
||||
if i * i + j * j <= 18 then c.px(12 + i, 12 + j, C("9a6a2a")) end
|
||||
end end
|
||||
for j = -3, 3 do for i = -3, 3 do
|
||||
if i * i + j * j <= 9 then c.px(12 + i, 12 + j, C("b8843a")) end
|
||||
end end
|
||||
if seeds then -- 过熟的黑籽
|
||||
c.px(11, 11, PAL.black); c.px(13, 12, PAL.black); c.px(12, 14, PAL.black)
|
||||
c.px(10, 13, PAL.black); c.px(14, 10, PAL.black); c.px(12, 11, C("4a3a20"))
|
||||
end
|
||||
end
|
||||
local sunImg = Image(24 * 14, 24, ColorMode.RGB)
|
||||
for f = 0, 12 do paintSunflower(makeCtx(sunImg, f * 24, 0), f, false) end
|
||||
paintSunflower(makeCtx(sunImg, 13 * 24, 0), 12, true)
|
||||
saveSheet(sunImg, "sunflower")
|
||||
|
||||
print("ALL ART GENERATED OK")
|
||||
@@ -0,0 +1,352 @@
|
||||
// 《崩坏童话:蜜糖村》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 `<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);
|
||||
// 花坛 ×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", `<?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(", "));
|
||||
Reference in New Issue
Block a user