Honey Village: six model builds + hub frontend, Dockerized for Dokploy
@@ -0,0 +1,767 @@
|
||||
// 验收测试:PRD §14 共 20 条。用法:先 npm run build && npm run preview,再 node tools/acceptance.mjs
|
||||
// 需要本机 Chrome(puppeteer-core 驱动,无额外下载)。
|
||||
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import fs from 'node:fs';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
|
||||
const BASE = 'http://127.0.0.1:4815/';
|
||||
const CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
const SHOTS = 'tools/shots';
|
||||
fs.mkdirSync(SHOTS, { recursive: true });
|
||||
|
||||
// 自起 preview 服务器(退出时关闭)
|
||||
const preview = spawn('npx', ['vite', 'preview', '--host', '127.0.0.1', '--port', '4815', '--strictPort'], { stdio: 'ignore' });
|
||||
process.on('exit', () => { try { preview.kill(); } catch {} });
|
||||
async function waitPort(port, timeout = 15000) {
|
||||
const t0 = Date.now();
|
||||
for (;;) {
|
||||
const ok = await new Promise((res) => { const s = net.connect(port, '127.0.0.1'); s.once('connect', () => { s.end(); res(true); }); s.once('error', () => res(false)); });
|
||||
if (ok) return;
|
||||
if (Date.now() - t0 > timeout) throw new Error('preview server 未就绪');
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
}
|
||||
}
|
||||
await waitPort(4815);
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
const failures = [];
|
||||
function check(name, cond, detail = '') {
|
||||
if (cond) { pass++; console.log(` PASS ${name}`); }
|
||||
else { fail++; failures.push(name + (detail ? ` — ${detail}` : '')); console.log(` FAIL ${name} ${detail}`); }
|
||||
}
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const read = (p) => fs.readFileSync(p, 'utf8');
|
||||
|
||||
// ================= 静态源码/资源检查 =================
|
||||
console.log('== 静态检查 ==');
|
||||
const ann = read('src/data/announcements.ts');
|
||||
const PRD_ANN = [
|
||||
'早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
|
||||
'早上好呀!新的一天,新的幸福。祝你开心!',
|
||||
'早上好!今天要把幸福装得满满的哦。要开心!',
|
||||
'早上好呀!今天的幸福任务贴在板上啦。祝你开心!',
|
||||
'早上好。今天是自由日,你可以自由地选择幸福。要开心哦。',
|
||||
'明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。',
|
||||
'庆典开始了。来吧。',
|
||||
];
|
||||
for (let i = 0; i < 7; i++) check(`公告 D${i + 1} 逐字`, ann.includes(PRD_ANN[i]), PRD_ANN[i]);
|
||||
check('公告 D4===D1', PRD_ANN[3] === PRD_ANN[0] && (ann.split(PRD_ANN[0]).length - 1) >= 2);
|
||||
|
||||
const tasks = read('src/data/tasks.ts');
|
||||
const REWARDS = { T1: 15, T2: 10, T3: 10, T4: 15, T5: 10, T6: 10, T7: 15, T8: 15, T9: 10, T10: 10, T11: 10, T12: 10, T13: 10, T14: 15 };
|
||||
for (const [id, r] of Object.entries(REWARDS)) check(`任务 ${id} 奖励 +${r}`, new RegExp(`id: '${id}'[\\s\\S]*?reward: ${r}[,}]`).test(tasks));
|
||||
const t1 = /T1:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1];
|
||||
const t8 = /T8:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1];
|
||||
const t2 = /T2:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1];
|
||||
const t9 = /T9:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1];
|
||||
const t3 = /T3:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1];
|
||||
const t10 = /T10:\s*\{[^}]*title: '([^']+)'/.exec(tasks)?.[1];
|
||||
check('T8/T9/T10 与 T1/T2/T3 逐字相同', t1 === t8 && t2 === t9 && t3 === t10 && !!t1);
|
||||
check('T15 标题逐字', tasks.includes('参加第 413 届丰收庆典'));
|
||||
|
||||
const corrupt = read('src/core/TextCorruptor.ts');
|
||||
const PAIRS = [['幸福', '辛福'], ['开心', '开欣'], ['美好', '美妤'], ['大家', '大伽'], ['微笑', '微效'], ['明天', '名天'], ['甜', '恬'], ['阳光', '央光'], ['朋友', '棚友'], ['永远', '泳远']];
|
||||
for (const [a, b] of PAIRS) check(`错字表 ${a}→${b}`, corrupt.includes(`['${a}', '${b}']`));
|
||||
check('腐蚀优先命中咪咪', /mimi/.test(corrupt) && /咪咪/.test(corrupt));
|
||||
|
||||
const dlg = read('src/data/dialogues.ts') + read('src/data/clues.ts');
|
||||
for (const s of ['湖水一直很平静', '面包一直是那个味道', '孩子,今天是第几个今天?', '按疗程配比。甜度:最大。']) check(`线索台词逐字「${s}」`, dlg.includes(s));
|
||||
const endings = read('src/data/endings.ts');
|
||||
for (const s of ['蜜糖谷情绪疗养中心', 'PT-07', '系统检测到疗程无效。', '疗程重新开始。', '从此,每一天都很幸福。', '每一天。']) check(`结局文本逐字「${s}」`, endings.includes(s));
|
||||
|
||||
// 地图 JSON 结构
|
||||
const MAP_KEYS = ['house', 'plaza', 'bakery', 'shop', 'clinic', 'lake', 'field', 'well', 'fog', 'white'];
|
||||
const maps = {};
|
||||
const mapJson = {};
|
||||
for (const key of MAP_KEYS) {
|
||||
const d = JSON.parse(read(`public/assets/maps/${key}.json`));
|
||||
mapJson[key] = d;
|
||||
const names = d.layers.map((l) => l.name);
|
||||
check(`地图 ${key} 三层齐全`, names.includes('ground') && names.includes('obstacles') && names.includes('objects'));
|
||||
const ob = d.layers.find((l) => l.name === 'obstacles');
|
||||
check(`地图 ${key} obstacles collides=true`, (ob.properties || []).some((p) => p.name === 'collides' && p.value === true));
|
||||
check(`地图 ${key} tileset 内嵌`, d.tilesets[0].image === '../tileset.png' && d.tilesets[0].tilewidth === 16);
|
||||
const wpx = d.width * 16, hpx = d.height * 16;
|
||||
const ox = Math.max(0, Math.floor((320 - wpx) / 2));
|
||||
const oy = Math.max(0, Math.floor((180 - hpx) / 2));
|
||||
const og = d.layers.find((l) => l.name === 'objects');
|
||||
maps[key] = og.objects.map((o) => ({
|
||||
name: o.name, x: o.x + ox, y: o.y + oy,
|
||||
props: Object.fromEntries((o.properties || []).map((p) => [p.name, p.value])),
|
||||
}));
|
||||
}
|
||||
// 门双向成对
|
||||
for (const key of MAP_KEYS) {
|
||||
for (const d of maps[key].filter((o) => o.name === 'door')) {
|
||||
const tgt = d.props.target, sp = d.props.spawn;
|
||||
const okSpawn = maps[tgt]?.some((o) => o.name === 'spawn' && o.props.id === sp);
|
||||
const okBack = maps[tgt]?.some((o) => o.name === 'door' && o.props.target === key);
|
||||
check(`门 ${key}→${tgt}(${sp}) 双向`, !!okSpawn && !!okBack);
|
||||
}
|
||||
}
|
||||
// NPC 站位(§4)
|
||||
const npcAt = (m, id) => maps[m].some((o) => o.name === 'npc' && o.props.id === id);
|
||||
for (const [m, id] of [['plaza', 'afu'], ['plaza', 'dabao'], ['plaza', 'bird'], ['bakery', 'yuanyuan'], ['shop', 'mimi'], ['clinic', 'fuyisheng'], ['lake', 'guiyeye']]) {
|
||||
check(`NPC 站位 ${id}@${m}`, npcAt(m, id));
|
||||
}
|
||||
let extras = 0;
|
||||
for (const k of MAP_KEYS) extras += maps[k].filter((o) => o.name === 'npc' && ['squirrel', 'hedgehog', 'fox', 'pig', 'chick'].includes(o.props.id)).length;
|
||||
check('群演 ×5 分布各场景', extras === 5, `实际 ${extras}`);
|
||||
|
||||
// ================= 浏览器部分 =================
|
||||
const SCENE = { house: 'HouseScene', plaza: 'PlazaScene', bakery: 'BakeryScene', shop: 'ShopScene', clinic: 'ClinicScene', lake: 'LakeScene', field: 'FieldScene', well: 'WellScene', fog: 'FogScene', white: 'WhiteScene' };
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: CHROME, headless: true,
|
||||
args: ['--autoplay-policy=no-user-gesture-required', '--mute-audio', '--disable-dev-shm-usage'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
const consoleErrors = [];
|
||||
page.on('console', (m) => { if (m.type() === 'error') consoleErrors.push(m.text()); });
|
||||
page.on('pageerror', (e) => consoleErrors.push('PAGEERROR: ' + e.message));
|
||||
const extReqs = [];
|
||||
page.on('request', (r) => { const u = r.url(); if (!u.startsWith(BASE) && !u.startsWith('data:') && !u.startsWith('blob:') && !u.startsWith('devtools:')) extReqs.push(u); });
|
||||
|
||||
const ev = (fn, ...a) => page.evaluate(fn, ...a);
|
||||
const isActive = (k) => ev((k) => window.__game.scene.isActive(k), k);
|
||||
async function waitActive(k, timeout = 9000) { await page.waitForFunction((kk) => window.__game.scene.isActive(kk), { timeout }, k); }
|
||||
async function startScene(k, spawn = 'default') {
|
||||
await ev((k, s) => {
|
||||
const sm = window.__game.scene;
|
||||
// game.scene.start 不会停掉其它场景:先停掉所有活动地图场景(保留常驻 UIScene)
|
||||
for (const sc of sm.getScenes(true)) {
|
||||
const key = sc.scene.key;
|
||||
if (key !== 'UIScene' && key !== k) sm.stop(key);
|
||||
}
|
||||
sm.start(k, { spawn: s });
|
||||
}, k, spawn);
|
||||
await waitActive(k); await sleep(500);
|
||||
}
|
||||
async function dget(expr) { return ev(new Function(`return window.__director.${expr}`)); }
|
||||
async function dset(expr) { return ev(new Function(`window.__director.${expr}`)); }
|
||||
const darr = (expr) => ev(new Function(`return [...window.__director.${expr}]`));
|
||||
async function setPlayer(sceneKey, x, y) {
|
||||
await ev((k, x, y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0, 0); }, sceneKey, x, y);
|
||||
await sleep(120);
|
||||
}
|
||||
const playerPos = (sceneKey) => ev((k) => { const s = window.__game.scene.getScene(k); return { x: s.player.x, y: s.player.y }; }, sceneKey);
|
||||
const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
const dlgText = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return u && u.dlgBody ? u.dlgBody.text : ''; });
|
||||
// Phaser JustDown 需要按键“按住”跨过至少一个游戏帧;瞬发 press 的 down+up 落在同一帧内会被吞掉
|
||||
async function tap(code, holdMs = 80) {
|
||||
await page.keyboard.down(code);
|
||||
await sleep(holdMs);
|
||||
await page.keyboard.up(code);
|
||||
await sleep(30);
|
||||
}
|
||||
async function drain(choiceKey = 'Digit1', maxIter = 60) {
|
||||
const texts = [];
|
||||
for (let i = 0; i < maxIter; i++) {
|
||||
if (!(await dlgOpen())) break;
|
||||
texts.push(await dlgText());
|
||||
const choosing = await ev(() => { const u = window.__game.scene.getScene('UIScene'); return u.showingChoices; });
|
||||
await tap(choosing ? choiceKey : 'KeyE');
|
||||
await sleep(160);
|
||||
}
|
||||
for (let i = 0; i < 20 && (await dlgOpen()); i++) { await tap('KeyE'); await sleep(140); }
|
||||
return texts;
|
||||
}
|
||||
async function interactAt(mapKey, kind, id, choiceKey = 'Digit1') {
|
||||
const sceneKey = SCENE[mapKey];
|
||||
if (!(await isActive(sceneKey))) await startScene(sceneKey);
|
||||
await drain(); // 清掉残留对话(黄昏/满格等)
|
||||
const obj = maps[mapKey].find((o) => o.name === kind && o.props.id === id);
|
||||
if (!obj) throw new Error(`no ${kind}:${id} in ${mapKey}`);
|
||||
await setPlayer(sceneKey, obj.x + 40, obj.y + 40); // 先离开感应圈(触发目标重武装)
|
||||
await setPlayer(sceneKey, obj.x, obj.y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(400);
|
||||
return drain(choiceKey);
|
||||
}
|
||||
// plaza 代码补区(相对公告板)
|
||||
function plazaZone(id) {
|
||||
const nb = maps.plaza.find((o) => o.name === 'interact' && o.props.id === 'noticeboard');
|
||||
if (id === 'noticeboard') return { x: nb.x, y: nb.y };
|
||||
const rel = {
|
||||
mailbox_c3: [-48, 24], ribbon_1: [-48, -32], ribbon_2: [64, -32], ribbon_3: [-48, 64], ribbon_4: [64, 64],
|
||||
smile_spot: [0, 32], celebration_site: [0, -72],
|
||||
}[id];
|
||||
return { x: nb.x + rel[0], y: nb.y + rel[1] };
|
||||
}
|
||||
async function interactPlazaZone(id, choiceKey = 'Digit1') {
|
||||
if (!(await isActive('PlazaScene'))) await startScene('PlazaScene');
|
||||
await drain();
|
||||
const z = plazaZone(id);
|
||||
await setPlayer('PlazaScene', z.x + 40, z.y + 40); // 先离开感应圈(触发目标重武装)
|
||||
await setPlayer('PlazaScene', z.x, z.y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(400);
|
||||
return drain(choiceKey);
|
||||
}
|
||||
async function shot(name, clip = null) {
|
||||
const box = await (await page.$('canvas')).boundingBox();
|
||||
const c = clip ? { x: box.x + clip.x, y: box.y + clip.y, width: clip.w, height: clip.h } : { x: box.x, y: box.y, width: box.width, height: box.height };
|
||||
await page.screenshot({ path: `${SHOTS}/${name}.png`, clip: c });
|
||||
}
|
||||
async function sleepNight() {
|
||||
await interactAt('house', 'interact', 'bed');
|
||||
await waitActive('DreamScene', 12000).catch(() => {});
|
||||
for (let i = 0; i < 30 && (await isActive('DreamScene')); i++) { await drain(); await sleep(500); }
|
||||
await waitActive('HouseScene', 12000);
|
||||
await sleep(500);
|
||||
}
|
||||
async function boardAnnounce(day) {
|
||||
const texts = await interactAt('plaza', 'interact', 'noticeboard');
|
||||
const found = texts.some((t) => t.includes(PRD_ANN[day - 1].slice(0, 8)));
|
||||
return { texts, found };
|
||||
}
|
||||
|
||||
// ---------- #1 标题 → 名字 → Day1 小屋 ----------
|
||||
console.log('== #1 标题与开局 ==');
|
||||
try {
|
||||
await page.goto(BASE, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 15000 });
|
||||
await waitActive('TitleScene', 20000);
|
||||
await sleep(400);
|
||||
await shot('01_title');
|
||||
check('标题画面进入', true);
|
||||
const hasInput = await ev(() => !!document.querySelector('#game input[type="text"]'));
|
||||
check('名字输入框存在', hasInput);
|
||||
await ev(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await waitActive('HouseScene', 15000); await sleep(600);
|
||||
check('留空默认「小满」', (await dget('playerName')) === '小满');
|
||||
check('Day 1 小屋开始', (await dget('day')) === 1 && (await isActive('HouseScene')));
|
||||
await shot('02_house_d1');
|
||||
|
||||
// ---------- #2 移动/动画/碰撞/相机 ----------
|
||||
console.log('== #2 移动与碰撞 ==');
|
||||
{
|
||||
const p0 = await playerPos('HouseScene');
|
||||
await page.keyboard.down('KeyD'); await sleep(400); await page.keyboard.up('KeyD');
|
||||
const p1 = await playerPos('HouseScene');
|
||||
check('D 键右移', p1.x > p0.x + 10, `${p0.x}→${p1.x}`);
|
||||
await page.keyboard.down('KeyD'); await sleep(120);
|
||||
const anim = await ev(() => window.__game.scene.getScene('HouseScene').player.anims.currentAnim?.key);
|
||||
await page.keyboard.up('KeyD');
|
||||
check('四向动画 walk-right', anim === 'walk-right', String(anim));
|
||||
await sleep(200);
|
||||
const idleFrame = await ev(() => window.__game.scene.getScene('HouseScene').player.frame.name);
|
||||
check('待机帧(右=12)', Number(idleFrame) === 12, String(idleFrame));
|
||||
// 碰撞:向西推墙
|
||||
await setPlayer('HouseScene', 80, 90);
|
||||
await page.keyboard.down('KeyA'); await sleep(700); await page.keyboard.up('KeyA');
|
||||
const px = (await playerPos('HouseScene')).x;
|
||||
check('西墙碰撞拦截', px > 55, `x=${px}`);
|
||||
// 相机钳制(plaza 480×320 > 320×180)
|
||||
await startScene('PlazaScene');
|
||||
await setPlayer('PlazaScene', 8, 16);
|
||||
await sleep(400);
|
||||
const c1 = await ev(() => { const c = window.__game.scene.getScene('PlazaScene').cameras.main; return { x: c.scrollX, y: c.scrollY }; });
|
||||
await setPlayer('PlazaScene', 472, 312);
|
||||
await sleep(500);
|
||||
const c2 = await ev(() => { const c = window.__game.scene.getScene('PlazaScene').cameras.main; return { x: c.scrollX, y: c.scrollY }; });
|
||||
check('相机左上钳制', c1.x <= 0.5 && c1.y <= 0.5, JSON.stringify(c1));
|
||||
check('相机右下钳制', c2.x + 320 <= 480.5 && c2.y + 180 <= 320.5, JSON.stringify(c2));
|
||||
}
|
||||
|
||||
// ---------- #3 十场景可达 ----------
|
||||
console.log('== #3 十场景 ==');
|
||||
for (const [m, s] of Object.entries(SCENE)) {
|
||||
await startScene(s);
|
||||
check(`场景可达 ${s}`, await isActive(s));
|
||||
}
|
||||
await shot('03_plaza');
|
||||
|
||||
// ---------- 七日完整流程(覆盖 #5/#6/#11/#12/#13/#14/#16) ----------
|
||||
console.log('== 七日流程 ==');
|
||||
await startScene('HouseScene');
|
||||
|
||||
// --- Day 1 ---
|
||||
console.log('-- Day 1 --');
|
||||
{
|
||||
const b = await boardAnnounce(1);
|
||||
check('D1 公告逐字显示', b.found, b.texts[0]);
|
||||
// #12 前半:D5 前古井/雾墙劝离
|
||||
const w = await interactAt('well', 'interact', 'ancient_well');
|
||||
check('#12 古井 D5 前劝离', !(await darr('clues')).includes('C7') && w.length > 0);
|
||||
await startScene('PlazaScene');
|
||||
const fogDoor = maps.plaza.find((o) => o.name === 'door' && o.props.target === 'fog');
|
||||
await setPlayer('PlazaScene', fogDoor.x, fogDoor.y + 20);
|
||||
await drain();
|
||||
await page.keyboard.down('KeyW'); await sleep(700); await page.keyboard.up('KeyW');
|
||||
await sleep(400); await drain();
|
||||
check('#12 雾墙门 D5 前劝离(未进 FogScene)', !(await isActive('FogScene')));
|
||||
// T1 取面包 + 送三人(同时完成 T3)
|
||||
await interactAt('bakery', 'interact', 'counter_bread');
|
||||
check('T1 已取面包', await dget("taskProgress['breadTaken']"));
|
||||
await interactAt('plaza', 'npc', 'dabao');
|
||||
await interactAt('shop', 'npc', 'mimi');
|
||||
await interactAt('lake', 'npc', 'guiyeye');
|
||||
check('T1 完成 +15', (await darr('tasksDone')).includes('T1'));
|
||||
check('T3 问好完成 +10', (await darr('tasksDone')).includes('T3'));
|
||||
// T2 浇花 ×3(第三次浇花的对话收尾后同一批 drain 内就会弹出黄昏钟声)
|
||||
await interactAt('plaza', 'interact', 'flowerbed_1');
|
||||
await interactAt('plaza', 'interact', 'flowerbed_2');
|
||||
const fb3 = await interactAt('plaza', 'interact', 'flowerbed_3');
|
||||
check('T2 完成 +10', (await darr('tasksDone')).includes('T2'));
|
||||
// 黄昏钟声(任务完成触发,可能已并入上一步 drain 的文本里)
|
||||
await sleep(1200);
|
||||
const duskTexts = [...fb3, ...(await drain())];
|
||||
check('#5 黄昏钟声出现', duskTexts.some((t) => t.includes('钟声')), duskTexts.join('/').slice(0, 60));
|
||||
// 对话选项:温柔 +2
|
||||
const h0 = await dget('happiness');
|
||||
await interactAt('plaza', 'npc', 'afu', 'Digit1');
|
||||
check('温柔选项 +2', (await dget('happiness')) === h0 + 2, `${h0}→${await dget('happiness')}`);
|
||||
await drain();
|
||||
await sleepNight();
|
||||
check('D1→D2 睡眠过日', (await dget('day')) === 2);
|
||||
}
|
||||
|
||||
// --- Day 2 ---
|
||||
console.log('-- Day 2 --');
|
||||
{
|
||||
check('晨起于小屋', await isActive('HouseScene'));
|
||||
const b = await boardAnnounce(2);
|
||||
check('D2 公告逐字显示', b.found);
|
||||
// C1 日历(D2 起)
|
||||
await interactAt('house', 'interact', 'calendar');
|
||||
check('C1 日历 D2 可发现 +8', (await darr('clues')).includes('C1'));
|
||||
// T4:取信→送 3 人(第 3 封 C4)
|
||||
await interactAt('plaza', 'npc', 'dabao');
|
||||
check('T4 已取信 ×3', (await dget("taskProgress['letters']")) === 3);
|
||||
const dY = await interactAt('bakery', 'npc', 'yuanyuan');
|
||||
const dM = await interactAt('shop', 'npc', 'mimi');
|
||||
const dF = await interactAt('clinic', 'npc', 'fuyisheng');
|
||||
await sleep(700); // 第三封后 400ms 才弹 C4 对话,等它出现后关掉
|
||||
const c4t = await drain();
|
||||
const t4diag = `day=${await dget('day')} active=${await dget("isTaskActive('T4')")} letters=${await dget("taskProgress['letters']")} Y=${dY.join('|').slice(0, 40)} M=${dM.join('|').slice(0, 40)} F=${[...dF, ...c4t].join('|').slice(0, 60)}`;
|
||||
check('T4 完成 +15', (await darr('tasksDone')).includes('T4'), t4diag);
|
||||
check('C4 乱码署名 D2 当日发现', (await darr('clues')).includes('C4'));
|
||||
// T5 + C5
|
||||
await interactAt('shop', 'interact', 'shelf_count');
|
||||
check('T5 完成 +10', (await darr('tasksDone')).includes('T5'));
|
||||
check('C5 货架发现', (await darr('clues')).includes('C5'));
|
||||
// C3 邮筒(D2 起)
|
||||
await interactPlazaZone('mailbox_c3');
|
||||
check('C3 邮筒信件发现', (await darr('clues')).includes('C3'));
|
||||
// C9 第一段(D2)
|
||||
const c9d2 = await interactAt('lake', 'npc', 'guiyeye');
|
||||
check('C9 第一段 D2', (await darr('clues')).includes('C9'));
|
||||
check('C9 D2 台词「湖水一直很平静」', c9d2.some((t) => t.includes('湖水一直很平静')), c9d2.join('/').slice(0, 60));
|
||||
await sleepNight();
|
||||
check('D2→D3', (await dget('day')) === 3);
|
||||
}
|
||||
|
||||
// --- Day 3 ---
|
||||
console.log('-- Day 3 --');
|
||||
{
|
||||
const b = await boardAnnounce(3);
|
||||
check('D3 公告逐字显示', b.found);
|
||||
await interactAt('field', 'interact', 'berry_1');
|
||||
await interactAt('field', 'interact', 'berry_2');
|
||||
await interactAt('field', 'interact', 'berry_3');
|
||||
await interactAt('field', 'interact', 'berry_4');
|
||||
await interactAt('field', 'interact', 'berry_5');
|
||||
check('T6 完成 +10', (await darr('tasksDone')).includes('T6'));
|
||||
await interactAt('lake', 'interact', 'picnic_mat');
|
||||
check('T7 完成 +15', (await darr('tasksDone')).includes('T7'));
|
||||
await interactAt('bakery', 'interact', 'recipe_paper');
|
||||
check('C2 配方纸 D3 起', (await darr('clues')).includes('C2'));
|
||||
await sleepNight();
|
||||
check('D3→D4(梦境闪切已过)', (await dget('day')) === 4);
|
||||
}
|
||||
|
||||
// --- Day 4 ---
|
||||
console.log('-- Day 4 --');
|
||||
{
|
||||
const b = await boardAnnounce(4);
|
||||
check('D4 公告与 D1 逐字相同', b.found);
|
||||
await interactAt('bakery', 'interact', 'counter_bread');
|
||||
await interactAt('plaza', 'npc', 'dabao');
|
||||
await interactAt('shop', 'npc', 'mimi');
|
||||
await interactAt('lake', 'npc', 'guiyeye'); // C9 第二段(D4)+ T10 计数
|
||||
check('T8 完成', (await darr('tasksDone')).includes('T8'));
|
||||
check('T10 完成', (await darr('tasksDone')).includes('T10'));
|
||||
await interactAt('plaza', 'interact', 'flowerbed_1');
|
||||
await interactAt('plaza', 'interact', 'flowerbed_2');
|
||||
await interactAt('plaza', 'interact', 'flowerbed_3');
|
||||
check('T9 完成', (await darr('tasksDone')).includes('T9'));
|
||||
// C9 第二段(D4):T8 完成后再与龟爷爷对话
|
||||
const c9d4 = await interactAt('lake', 'npc', 'guiyeye');
|
||||
check('C9 D4 台词「面包一直是那个味道」', c9d4.some((t) => t.includes('面包一直是那个味道')), c9d4.join('/').slice(0, 60));
|
||||
await sleepNight();
|
||||
check('D4→D5', (await dget('day')) === 5);
|
||||
}
|
||||
|
||||
// --- Day 5 ---
|
||||
console.log('-- Day 5 --');
|
||||
{
|
||||
const b = await boardAnnounce(5);
|
||||
check('D5 公告逐字显示', b.found);
|
||||
// T11 拒绝(悲伤行为:拒绝任务)
|
||||
const aw0 = await dget('awareness');
|
||||
await interactAt('bakery', 'npc', 'yuanyuan', 'Digit2');
|
||||
check('T11 可拒绝且被记录', (await darr('refusedTasks')).includes('T11'));
|
||||
check('#7 拒绝任务计悲伤(觉知+3)', (await dget('awareness')) >= aw0 + 3, `${aw0}→${await dget('awareness')}`);
|
||||
// T12 接受(C6)
|
||||
await interactPlazaZone('noticeboard', 'Digit1');
|
||||
check('T12 完成 + C6 旧字迹', (await darr('tasksDone')).includes('T12') && (await darr('clues')).includes('C6'));
|
||||
// #12 后半:D5 起古井可交互(C7,聆听 5 秒)
|
||||
{
|
||||
const sceneKey = 'WellScene';
|
||||
if (!(await isActive(sceneKey))) await startScene(sceneKey);
|
||||
await drain();
|
||||
const obj = maps.well.find((o) => o.name === 'interact' && o.props.id === 'ancient_well');
|
||||
await setPlayer(sceneKey, obj.x, obj.y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(600); await drain();
|
||||
await page.waitForFunction(() => window.__director.clues.has('C7'), { timeout: 15000 });
|
||||
await drain();
|
||||
check('#12 古井 D5 起可交互', true);
|
||||
check('C7 井底回声 +8', (await darr('clues')).includes('C7'));
|
||||
}
|
||||
// C8 诊所
|
||||
await interactAt('clinic', 'interact', 'doctor_desk');
|
||||
check('C8 诊所表格 D5 起', (await darr('clues')).includes('C8'));
|
||||
await sleepNight();
|
||||
check('D5→D6', (await dget('day')) === 6);
|
||||
}
|
||||
|
||||
// --- Day 6(#13 T14、#14 雨)---
|
||||
console.log('-- Day 6 --');
|
||||
{
|
||||
const b = await boardAnnounce(6);
|
||||
check('D6 公告逐字显示', b.found);
|
||||
await interactPlazaZone('ribbon_1');
|
||||
await interactPlazaZone('ribbon_2');
|
||||
await interactPlazaZone('ribbon_3');
|
||||
await interactPlazaZone('ribbon_4');
|
||||
check('T13 完成 +10', (await darr('tasksDone')).includes('T13'));
|
||||
// C9 第三段(D6)
|
||||
const c9d6 = await interactAt('lake', 'npc', 'guiyeye');
|
||||
check('C9 三段递进完成', (await darr('clues')).includes('C9'));
|
||||
check('C9 D6 台词「孩子,今天是第几个今天?」', c9d6.some((t) => t.includes('孩子,今天是第几个今天?')), c9d6.join('/').slice(0, 60));
|
||||
// T14:保持微笑 60 秒
|
||||
{
|
||||
await startScene('PlazaScene');
|
||||
await drain();
|
||||
const z = plazaZone('smile_spot');
|
||||
await setPlayer('PlazaScene', z.x, z.y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(600); await drain();
|
||||
check('T14 开始(t14Active)', await dget('t14Active'));
|
||||
await sleep(10000);
|
||||
const zoom1 = await ev(() => window.__game.scene.getScene('PlazaScene').cameras.main.zoom);
|
||||
check('#13 镜头缓慢拉近', zoom1 > 1.02, `zoom=${zoom1}`);
|
||||
const staring = await ev(() => window.__game.scene.getScene('PlazaScene').actors.some((a) => a.stareAtPlayer));
|
||||
check('#13 全村注视', staring);
|
||||
await sleep(35000); // t≈45s:渐隐已开始
|
||||
const g1 = await ev(() => window.__audio.master.gain.value);
|
||||
await sleep(6000); // t≈51s
|
||||
const g2 = await ev(() => window.__audio.master.gain.value);
|
||||
check('#13 第 40–60 秒声音渐隐至零', g2 < g1 && g2 < 0.3, `gain ${g1}→${g2}`);
|
||||
await shot('13_t14_smile');
|
||||
await page.waitForFunction(() => window.__director.tasksDone.has('T14'), { timeout: 40000 });
|
||||
await drain();
|
||||
check('T14 完成 +15', true);
|
||||
check('#13 T14 后脚本雨开始', await dget('rainActive'));
|
||||
}
|
||||
// #14 站雨中 ≥10 秒计悲伤(先把幸福调到 50,避开满格溢出池机制)
|
||||
{
|
||||
await dset('setHappiness(50)');
|
||||
const h0 = await dget('happiness'), aw0 = await dget('awareness');
|
||||
await drain();
|
||||
await setPlayer('PlazaScene', 240, 260);
|
||||
await sleep(11000);
|
||||
check('#14 雨中站 10 秒计悲伤', (await dget('happiness')) <= h0 - 5 && (await dget('awareness')) >= aw0 + 3, `h ${h0}→${await dget('happiness')} aw ${aw0}→${await dget('awareness')}`);
|
||||
// NPC 台词对雨零反应:与阿福对话正常弹出
|
||||
const t = await interactAt('plaza', 'npc', 'afu');
|
||||
check('#14 雨中 NPC 台词零反应(正常对话)', t.length > 0);
|
||||
await shot('14_rain');
|
||||
}
|
||||
await sleepNight();
|
||||
check('D6→D7', (await dget('day')) === 7);
|
||||
}
|
||||
|
||||
// --- Day 7(E3 真结局路线)---
|
||||
console.log('-- Day 7 --');
|
||||
{
|
||||
const b = await boardAnnounce(7);
|
||||
check('D7 公告逐字显示', b.found);
|
||||
const aw = await dget('awareness'), nc = (await darr('clues')).length;
|
||||
check('E3 条件就绪(觉知≥70 线索≥6)', aw >= 70 && nc >= 6, `aw=${aw} clues=${nc}`);
|
||||
// 走向雾墙
|
||||
await startScene('FogScene');
|
||||
await drain();
|
||||
const fw = maps.fog.find((o) => o.name === 'interact' && o.props.id === 'fog_wall');
|
||||
await setPlayer('FogScene', fw.x, fw.y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(600); await drain();
|
||||
await waitActive('WhiteScene', 15000);
|
||||
check('E3:雾墙裂开成白色走廊', true);
|
||||
await shot('15_white_corridor');
|
||||
// 走进病房
|
||||
await setPlayer('WhiteScene', 72, 144);
|
||||
await waitActive('EndingScene', 15000);
|
||||
await sleep(2500);
|
||||
await shot('15_e3_record');
|
||||
const e3texts = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n'));
|
||||
check('#15 E3 病历:蜜糖谷情绪疗养中心', e3texts.includes('蜜糖谷情绪疗养中心'));
|
||||
check('#15 E3 病历:姓名=小满', e3texts.includes('小满'));
|
||||
check('#15 E3 病历:PT-07', e3texts.includes('PT-07'));
|
||||
check('#15 E3 病历:真实游玩时长', /\d{1,2}:\d{2}/.test(e3texts), e3texts.slice(0, 120));
|
||||
await sleep(5500); // 等到最后一帧
|
||||
const hasReal = await ev(() => window.__game.scene.getScene('EndingScene').children.list.some((o) => o.texture && o.texture.key === 'portrait_player_real'));
|
||||
check('#15 E3 最后一帧:真实的脸', hasReal);
|
||||
await shot('15_e3_realface');
|
||||
}
|
||||
console.log('== 阶段 A(七日流程)完成 ==');
|
||||
|
||||
// ================= 阶段 B:重开页面做 Debug/矩阵/悲伤/庆典/结局 =================
|
||||
console.log('== 阶段 B ==');
|
||||
await page.goto(BASE, { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 15000 });
|
||||
await waitActive('TitleScene', 20000);
|
||||
await ev(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await waitActive('HouseScene', 15000); await sleep(400);
|
||||
|
||||
// ---------- #13(Debug 面板,PRD §13) ----------
|
||||
console.log('== Debug 面板 ==');
|
||||
{
|
||||
const hidden = await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); return p ? getComputedStyle(p).display === 'none' : null; });
|
||||
check('Debug 默认隐藏无痕迹', hidden === true);
|
||||
await tap('Backquote'); await sleep(200);
|
||||
const shown = await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); return p ? getComputedStyle(p).display !== 'none' : false; });
|
||||
check('反引号打开 Debug 面板', shown);
|
||||
// 设幸福 66(数值行靠「设」按钮生效)
|
||||
await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); const input = p.querySelector('input'); input.value = '66'; input.closest('div').querySelector('button').click(); });
|
||||
await sleep(200);
|
||||
check('Debug 设幸福 66', (await dget('happiness')) === 66);
|
||||
// 传送 bakery
|
||||
await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); const sel = p.querySelector('select'); sel.value = 'BakeryScene'; [...p.querySelectorAll('button')].find((b) => b.textContent === '传送').click(); });
|
||||
await waitActive('BakeryScene', 8000);
|
||||
check('Debug 传送到面包房', true);
|
||||
// 置齐线索
|
||||
await ev(() => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((e) => e.textContent.includes('DEBUG')); [...p.querySelectorAll('button')].find((b) => b.textContent.includes('线索')).click(); });
|
||||
check('Debug 置齐全部线索', ((await darr('clues')).length) === 9);
|
||||
await tap('Backquote'); await sleep(150);
|
||||
}
|
||||
|
||||
// ---------- #9 侵蚀矩阵 ----------
|
||||
console.log('== #9 侵蚀矩阵 ==');
|
||||
{
|
||||
await startScene('PlazaScene');
|
||||
const cases = [
|
||||
[10, 0, 0.9, 1.0], [40, 1, 1.0, 1.0], [70, 2, 1.15, 0.97], [90, 3, 1.3, 0.92], [100, 4, 1.4, 0.84],
|
||||
];
|
||||
for (const [h, st, sat, rate] of cases) {
|
||||
await dset(`setHappiness(${h})`); await sleep(400);
|
||||
const s = await dget('stage');
|
||||
const p = await dget('params');
|
||||
check(`幸福${h}→S${st}`, s === st, `stage=${s}`);
|
||||
check(`S${st} 饱和度${sat}/速率${rate}`, Math.abs(p.saturation - sat) < 1e-6 && Math.abs(p.playbackRate - rate) < 1e-6, JSON.stringify(p));
|
||||
await shot(`09_stage_S${st}_h${h}`);
|
||||
}
|
||||
// 低通与鸟鸣
|
||||
const p3 = await dget('params'); // S4
|
||||
check('S4 低通 4kHz + 鸟鸣静音', p3.lowpass === 4000 && p3.birdMuted === true);
|
||||
await dset('setHappiness(90)'); await sleep(300);
|
||||
const p2 = await dget('params');
|
||||
check('S3 低通 8kHz', p2.lowpass === 8000);
|
||||
// NPC 表情档:h=80 时 咪咪(-10→S3 flat) 与 阿福(0→S2 smile) 不同;圆圆(+15→S2) 与阈值一致
|
||||
await dset('setHappiness(80)'); await sleep(500);
|
||||
await startScene('ShopScene');
|
||||
const mimiFrame = await ev(() => { const a = window.__game.scene.getScene('ShopScene').actors.find((x) => x.def.id === 'mimi'); return a ? a.sprite.frame.name : -1; });
|
||||
await startScene('PlazaScene');
|
||||
const afuFrame = await ev(() => { const a = window.__game.scene.getScene('PlazaScene').actors.find((x) => x.def.id === 'afu'); return a ? a.sprite.frame.name : -1; });
|
||||
check('h=80 咪咪比阿福先崩(flat≠smile)', String(mimiFrame) !== String(afuFrame), `mimi=${mimiFrame} afu=${afuFrame}`);
|
||||
// S4 花瓣过亮 + 黑籽(视觉)+ 双子鸟张嘴动画仍在
|
||||
await dset('setHappiness(100)'); await sleep(400);
|
||||
const birdAnim = await ev(() => { const a = window.__game.scene.getScene('PlazaScene').actors.find((x) => x.def.id === 'bird'); return a ? (a.sprite.anims.isPlaying || a.sprite.anims.currentAnim !== null) : false; });
|
||||
check('S4 双子鸟动画仍在(鸟鸣静音)', birdAnim);
|
||||
await shot('09_hud_s4', { x: 0, y: 0, w: 48 * 3, h: 48 * 3 });
|
||||
await dset('setHappiness(10)'); await sleep(300);
|
||||
await shot('09_hud_s0', { x: 0, y: 0, w: 48 * 3, h: 48 * 3 });
|
||||
await dset('setHappiness(70)'); await sleep(300);
|
||||
await shot('09_hud_s2', { x: 0, y: 0, w: 48 * 3, h: 48 * 3 });
|
||||
}
|
||||
|
||||
// ---------- #4 向日葵表盘 ----------
|
||||
console.log('== #4 向日葵表盘 ==');
|
||||
{
|
||||
const pc = (h) => ev((hh) => window.__petalCount(hh), h);
|
||||
check('#4 h=10 → 0–2 瓣', (await pc(10)) >= 0 && (await pc(10)) <= 2, `petals=${await pc(10)}`);
|
||||
check('#4 h=40 → 3–6 瓣', (await pc(40)) >= 3 && (await pc(40)) <= 6, `petals=${await pc(40)}`);
|
||||
check('#4 h=70 → 7–9 瓣', (await pc(70)) >= 7 && (await pc(70)) <= 9, `petals=${await pc(70)}`);
|
||||
check('#4 h=90 → 10–12 瓣', (await pc(90)) >= 10 && (await pc(90)) <= 12, `petals=${await pc(90)}`);
|
||||
check('#4 h=100 → 12 瓣', (await pc(100)) === 12);
|
||||
await dset('setHappiness(100)'); await sleep(300);
|
||||
check('#4 满格花心黑籽', await ev(() => window.__game.scene.getScene('UIScene').seedsShown));
|
||||
await dset('setHappiness(50)'); await sleep(300);
|
||||
check('#4 未满格无黑籽', !(await ev(() => window.__game.scene.getScene('UIScene').seedsShown)));
|
||||
// 表盘区域(左上 120×120)不得出现任何 Text(无数字)
|
||||
const textsNearDial = await ev(() => window.__game.scene.getScene('UIScene').children.list
|
||||
.filter((o) => o.type === 'Text' && o.visible && o.x < 120 && o.y < 120).length);
|
||||
check('#4 表盘无数字显示', textsNearDial === 0, `文本对象 ${textsNearDial} 个`);
|
||||
}
|
||||
|
||||
// ---------- #16 时长设计(每日 5 分钟兜底,7 天 ≤35 分钟) ----------
|
||||
console.log('== #16 时长 ==');
|
||||
{
|
||||
const srcOk = (() => { const m = /DAY_LENGTH_MS = (\d+) \* 60 \* 1000/.exec(read('src/core/CorruptionDirector.ts')); return m ? Number(m[1]) : 0; })();
|
||||
check('#16 每日兜底 5 分钟(7 天 ≤35 分钟)', srcOk === 5, `DAY_LENGTH_MS=${srcOk}min`);
|
||||
await dset('dayElapsedMs = 5 * 60 * 1000 + 1');
|
||||
check('#16 计时到点可睡觉(黄昏兜底)', await dget('canSleep'));
|
||||
await dset('dayElapsedMs = 0');
|
||||
}
|
||||
|
||||
// ---------- #10 错字替换(运行时) ----------
|
||||
console.log('== #10 运行时文本腐蚀 ==');
|
||||
{
|
||||
await dset('setHappiness(100)'); await sleep(300);
|
||||
await startScene('ShopScene');
|
||||
let found = '';
|
||||
const corruptedChars = PAIRS.map((p) => p[1]);
|
||||
for (let i = 0; i < 8 && !found; i++) {
|
||||
const texts = await interactAt('shop', 'npc', 'mimi');
|
||||
for (const t of texts) {
|
||||
if (corruptedChars.some((c) => t.includes(c)) || /[!?。~]{3}$/.test(t) || /(.)\1{2}$/.test(t)) { found = t; break; }
|
||||
}
|
||||
}
|
||||
check('#10 S4 咪咪台词可见腐蚀/标点×3', found !== '', found || '8 次采样未见');
|
||||
}
|
||||
|
||||
// ---------- #7 悲伤行为:发呆 / 纠缠 / 否定 ----------
|
||||
console.log('== #7 悲伤行为 ==');
|
||||
{
|
||||
await dset('setHappiness(60)'); await dset('setAwareness(0)');
|
||||
await startScene('PlazaScene');
|
||||
await drain();
|
||||
// 发呆 10 秒(远离任务点)
|
||||
await setPlayer('PlazaScene', 60, 280);
|
||||
const glanceP = ev(() => new Promise((r) => window.__director.once('npcGlance', () => r(true))));
|
||||
await sleep(11000);
|
||||
check('#7 发呆:幸福 −5', (await dget('happiness')) === 55, String(await dget('happiness')));
|
||||
check('#7 发呆:觉知 +3', (await dget('awareness')) >= 3, String(await dget('awareness')));
|
||||
check('#7 最近 NPC 转头', await Promise.race([glanceP, sleep(1500).then(() => 'timeout')]) === true);
|
||||
// 否定选项
|
||||
const aw1 = await dget('awareness'), h1 = await dget('happiness');
|
||||
await interactAt('plaza', 'npc', 'afu', 'Digit3');
|
||||
check('#7 否定选项:觉知+2 且计悲伤', (await dget('awareness')) >= aw1 + 5 && (await dget('happiness')) <= h1 - 5, `aw ${aw1}→${await dget('awareness')}`);
|
||||
// 纠缠:同一天对同一 NPC 对话 ≥5 次
|
||||
const aw2 = await dget('awareness');
|
||||
for (let i = 0; i < 5; i++) await interactAt('plaza', 'npc', 'dabao', 'Digit1');
|
||||
check('#7 纠缠 ≥5 次计悲伤(觉知+3)', (await dget('awareness')) >= aw2 + 3, `aw ${aw2}→${await dget('awareness')}`);
|
||||
}
|
||||
|
||||
// ---------- #8 欢乐庆典 ----------
|
||||
console.log('== #8 欢乐庆典 ==');
|
||||
{
|
||||
await dset('setHappiness(20)');
|
||||
await startScene('PlazaScene');
|
||||
await drain();
|
||||
await ev(() => { for (let i = 0; i < 260; i++) window.__director.tick(500); }); // 模拟 130 秒
|
||||
await sleep(400);
|
||||
check('#8 幸福<30 持续 2 分钟触发庆典', await dget('celebrationActive'));
|
||||
await drain();
|
||||
// 逃离:走门离开 → 计数
|
||||
const esc0 = await dget('escapedCelebrations');
|
||||
const door = maps.plaza.find((o) => o.name === 'door' && o.props.target === 'house');
|
||||
await setPlayer('PlazaScene', door.x, door.y + 18);
|
||||
await page.keyboard.down('KeyW'); await sleep(800); await page.keyboard.up('KeyW');
|
||||
await sleep(700); await drain();
|
||||
check('#8 从场景边缘走离逃脱且计数', (await dget('escapedCelebrations')) > esc0, `esc ${esc0}→${await dget('escapedCelebrations')}`);
|
||||
// 再次触发并不逃离 → 拉回 50
|
||||
await dset('setHappiness(20)');
|
||||
await startScene('PlazaScene'); await drain();
|
||||
await ev(() => { for (let i = 0; i < 260; i++) window.__director.tick(500); });
|
||||
await sleep(300); await drain();
|
||||
await ev(() => window.__director.completeCelebration());
|
||||
await drain();
|
||||
check('#8 未逃离则拉回 50', (await dget('happiness')) === 50, String(await dget('happiness')));
|
||||
}
|
||||
|
||||
// ---------- #15 强制结局 E1/E2/E4(Debug 面板按钮) ----------
|
||||
console.log('== #15 强制结局 ==');
|
||||
async function forceEndingViaPanel(e) {
|
||||
await tap('Backquote'); await sleep(150);
|
||||
await ev((e) => { const els = [...document.querySelectorAll('#game div')]; const p = els.find((x) => x.textContent.includes('DEBUG')); [...p.querySelectorAll('button')].find((b) => b.textContent === `强制 ${e}`).click(); }, e);
|
||||
await waitActive('EndingScene', 8000);
|
||||
await tap('Backquote'); await sleep(100);
|
||||
}
|
||||
{
|
||||
await forceEndingViaPanel('E1');
|
||||
await sleep(7200);
|
||||
const t1 = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n'));
|
||||
await shot('15_e1');
|
||||
check('E1 字幕逐字', t1.includes('从此,每一天都很幸福。') && t1.includes('每一天。'), t1.slice(0, 80));
|
||||
const smile = await ev(() => window.__game.scene.getScene('EndingScene').children.list.some((o) => o.texture && o.texture.key === 'portrait_player_smile'));
|
||||
check('E1 主角头像仍是微笑', smile);
|
||||
|
||||
await forceEndingViaPanel('E2');
|
||||
await sleep(5200);
|
||||
const t2 = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n'));
|
||||
await shot('15_e2');
|
||||
check('E2 字幕逐字', t2.includes('疗程重新开始。'), t2.slice(0, 80));
|
||||
|
||||
await forceEndingViaPanel('E4');
|
||||
await sleep(6500);
|
||||
const t4 = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n'));
|
||||
await shot('15_e4');
|
||||
check('E4 字幕逐字', t4.includes('系统检测到疗程无效。'), t4.slice(0, 80));
|
||||
// E3 也可由 Debug 强制触发
|
||||
await forceEndingViaPanel('E3');
|
||||
await sleep(2500);
|
||||
const t3 = await ev(() => window.__game.scene.getScene('EndingScene').children.list.filter((o) => o.type === 'Text').map((o) => o.text).join('\n'));
|
||||
check('Debug 强制 E3 病历含机构/编号', t3.includes('蜜糖谷情绪疗养中心') && t3.includes('PT-07'), t3.slice(0, 80));
|
||||
// 回到标题按钮(等待其出现)
|
||||
await page.waitForFunction(() => window.__game.scene.getScene('EndingScene').children.list.some((o) => o.type === 'Text' && o.text.includes('回到标题')), { timeout: 25000 });
|
||||
await ev(() => { const b = window.__game.scene.getScene('EndingScene').children.list.find((o) => o.type === 'Text' && o.text.includes('回到标题')); b.emit('pointerdown'); });
|
||||
await waitActive('TitleScene', 8000);
|
||||
check('结局后「回到标题」可用', true);
|
||||
}
|
||||
|
||||
// ---------- #17 音频 ----------
|
||||
console.log('== #17 音频 ==');
|
||||
{
|
||||
const a = await ev(() => {
|
||||
const e = window.__audio;
|
||||
return e && e.ctx ? { state: e.ctx.state, rate: e.rate } : null;
|
||||
});
|
||||
check('#17 AudioContext 运行中(程序合成 BGM)', !!a && a.state === 'running', JSON.stringify(a));
|
||||
await dset('setHappiness(100)'); await sleep(1200);
|
||||
const s4 = await ev(() => ({ rate: window.__audio.rate, lp: window.__audio.lp.frequency.value, bird: window.__audio.birdBus.gain.value }));
|
||||
check('#17 S4:速率 0.84 / 低通→4kHz / 鸟鸣静音', Math.abs(s4.rate - 0.84) < 1e-6 && s4.lp < 8000 && s4.bird < 0.1, JSON.stringify(s4));
|
||||
await dset('setHappiness(30)'); await sleep(1200);
|
||||
const s1 = await ev(() => ({ rate: window.__audio.rate, lp: window.__audio.lp.frequency.value, bird: window.__audio.birdBus.gain.value }));
|
||||
check('#17 S1:原速原调、鸟鸣恢复', Math.abs(s1.rate - 1) < 1e-6 && s1.lp > 15000 && s1.bird > 0.3, JSON.stringify(s1));
|
||||
}
|
||||
|
||||
// ---------- #18 缩放与零报错 ----------
|
||||
console.log('== #18 缩放 ==');
|
||||
{
|
||||
await page.setViewport({ width: 1000, height: 700 }); await sleep(300);
|
||||
const s1 = await ev(() => ({ w: document.querySelector('canvas').style.width, h: document.querySelector('canvas').style.height }));
|
||||
check('1000×700 → 整数倍 3×(960×540)', s1.w === '960px' && s1.h === '540px', JSON.stringify(s1));
|
||||
await page.setViewport({ width: 500, height: 500 }); await sleep(300);
|
||||
const s2 = await ev(() => ({ w: document.querySelector('canvas').style.width, h: document.querySelector('canvas').style.height }));
|
||||
check('500×500 → 整数倍 1×(320×180)', s2.w === '320px' && s2.h === '180px', JSON.stringify(s2));
|
||||
await page.setViewport({ width: 960, height: 560 }); await sleep(300);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log('!! 脚本异常中断:', e.message);
|
||||
console.log('已收集控制台错误:', consoleErrors.slice(0, 10).join(' | ') || '(无)');
|
||||
fail++; failures.push('脚本异常中断: ' + e.message);
|
||||
}
|
||||
|
||||
// ---------- 汇总 ----------
|
||||
console.log('== 汇总 ==');
|
||||
check('无外部网络请求', extReqs.length === 0, extReqs.slice(0, 3).join(','));
|
||||
check('全程控制台零报错', consoleErrors.length === 0, consoleErrors.slice(0, 5).join(' | '));
|
||||
console.log(`\n通过 ${pass} / ${pass + fail}`);
|
||||
if (failures.length) { console.log('失败项:'); failures.forEach((f) => console.log(' - ' + f)); }
|
||||
await browser.close();
|
||||
process.exit(fail ? 1 : 0);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- gen_all.lua — regenerate every original pixel asset for 《崩坏童话:蜜糖村》
|
||||
-- Run: /Applications/Aseprite.app/Contents/MacOS/aseprite -b --script tools/aseprite/gen_all.lua
|
||||
local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
dofile(ROOT .. "tools/aseprite/gen_tileset.lua")
|
||||
dofile(ROOT .. "tools/aseprite/gen_player.lua")
|
||||
dofile(ROOT .. "tools/aseprite/gen_npcs.lua")
|
||||
dofile(ROOT .. "tools/aseprite/gen_extras.lua")
|
||||
dofile(ROOT .. "tools/aseprite/gen_portraits.lua")
|
||||
print("ALL_ASSETS_DONE")
|
||||
@@ -0,0 +1,77 @@
|
||||
-- gen_extras.lua — 5 background critters (each 32x24: 2-frame walk loop)
|
||||
-- squirrel / hedgehog / fox / pig / chick (profile, facing left)
|
||||
local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
local H = dofile(ROOT .. "tools/aseprite/helpers.lua")
|
||||
|
||||
local function squirrel(t, f)
|
||||
t.discn(11, 9, 4, "honey_d"); t.discn(12, 6, 3, "honey"); t.pn(13, 4, "honey_l") -- bushy tail
|
||||
t.ellipsen(6, 15, 5, 4, "honey")
|
||||
t.ellipsen(6, 16, 3, 2, "cream")
|
||||
t.discn(4, 10, 3, "honey")
|
||||
t.pn(2, 7, "honey"); t.pn(3, 7, "honey"); t.pn(2, 8, "honey_d") -- ear
|
||||
t.pn(2, 10, "ink"); t.pn(1, 12, "brown_d") -- eye / nose
|
||||
if f == 0 then t.rectn(4, 19, 2, 3, "brown_d"); t.rectn(8, 19, 2, 3, "brown_d")
|
||||
else t.rectn(3, 19, 2, 3, "brown_d"); t.rectn(9, 19, 2, 2, "brown_d") end
|
||||
end
|
||||
|
||||
local function hedgehog(t, f)
|
||||
t.discn(8, 13, 7, "brown_d") -- spiny body
|
||||
t.discn(8, 13, 6, "brown_l")
|
||||
for _, p in ipairs({ { 4, 8 }, { 7, 7 }, { 10, 8 }, { 12, 10 }, { 5, 11 }, { 9, 11 }, { 12, 13 } }) do
|
||||
t.pn(p[1], p[2], "brown_dd") -- spines
|
||||
end
|
||||
t.discn(3, 14, 3, "cream_d") -- face
|
||||
t.pn(2, 13, "ink"); t.pn(0, 15, "brown_dd") -- eye / nose
|
||||
t.pn(3, 16, "brown_d")
|
||||
if f == 0 then t.rectn(5, 19, 2, 2, "brown_d"); t.rectn(9, 19, 2, 2, "brown_d")
|
||||
else t.rectn(4, 19, 2, 2, "brown_d"); t.rectn(10, 19, 2, 2, "brown_d") end
|
||||
end
|
||||
|
||||
local function fox(t, f)
|
||||
t.discn(12, 13, 4, "honey_d"); t.pn(14, 12, "cream"); t.pn(15, 13, "cream") -- tail, white tip
|
||||
t.ellipsen(7, 15, 5, 4, "honey") -- body
|
||||
t.rectn(3, 6, 2, 3, "honey"); t.rectn(6, 6, 2, 3, "honey") -- ears
|
||||
t.pn(3, 6, "brown_d"); t.pn(6, 6, "brown_d")
|
||||
t.discn(4, 10, 4, "honey") -- head
|
||||
t.rectn(1, 11, 3, 3, "honey") -- snout
|
||||
t.pn(1, 12, "ink"); t.pn(3, 9, "ink") -- nose / eye
|
||||
t.rectn(1, 13, 3, 2, "cream") -- cheek
|
||||
if f == 0 then t.rectn(5, 19, 2, 3, "brown_d"); t.rectn(9, 19, 2, 3, "brown_d")
|
||||
else t.rectn(4, 19, 2, 3, "brown_d"); t.rectn(10, 19, 2, 2, "brown_d") end
|
||||
end
|
||||
|
||||
local function pig(t, f)
|
||||
t.ellipsen(8, 14, 6, 5, "pink") -- round body
|
||||
t.pn(13, 12, "pink_d"); t.pn(14, 11, "pink_d"); t.pn(14, 13, "pink_d") -- curly tail
|
||||
t.discn(4, 10, 4, "pink") -- head
|
||||
t.rectn(2, 6, 2, 3, "pink"); t.rectn(6, 6, 2, 3, "pink"); t.pn(2, 6, "pink_d"); t.pn(6, 6, "pink_d") -- ears
|
||||
t.rectn(1, 11, 3, 3, "pink_d") -- snout
|
||||
t.pn(2, 12, "pink_dd"); t.pn(3, 12, "pink_dd") -- nostrils
|
||||
t.pn(3, 9, "ink") -- eye
|
||||
if f == 0 then t.rectn(5, 19, 2, 3, "pink_d"); t.rectn(9, 19, 2, 3, "pink_d")
|
||||
else t.rectn(4, 19, 2, 3, "pink_d"); t.rectn(10, 19, 2, 2, "pink_d") end
|
||||
end
|
||||
|
||||
local function chick(t, f)
|
||||
t.discn(8, 13, 5, "yellow") -- body
|
||||
t.discn(5, 9, 3, "yellow") -- head
|
||||
t.pn(5, 6, "yellow"); t.pn(6, 7, "yellow") -- tuft
|
||||
t.discn(9, 13, 2, "yellow_d") -- wing
|
||||
t.pn(2, 9, "honey"); t.pn(1, 9, "honey_d") -- beak
|
||||
t.pn(4, 8, "ink") -- eye
|
||||
t.pn(11, 11, "yellow_d"); t.pn(12, 10, "yellow_d") -- tail
|
||||
if f == 0 then t.vln(6, 18, 3, "honey_d"); t.vln(9, 18, 3, "honey_d")
|
||||
else t.vln(5, 18, 3, "honey_d"); t.vln(10, 18, 2, "honey_d") end
|
||||
end
|
||||
|
||||
local extras = {
|
||||
{ "squirrel", squirrel }, { "hedgehog", hedgehog }, { "fox", fox },
|
||||
{ "pig", pig }, { "chick", chick },
|
||||
}
|
||||
for _, n in ipairs(extras) do
|
||||
local spr, img = H.new(32, 24)
|
||||
n[2](H.at(img, 0, 0), 0)
|
||||
n[2](H.at(img, 16, 0), 1)
|
||||
spr:saveAs(ROOT .. "public/assets/sprites/extra_" .. n[1] .. ".png")
|
||||
print("extra_" .. n[1] .. ".png done")
|
||||
end
|
||||
@@ -0,0 +1,141 @@
|
||||
-- gen_npcs.lua — named NPC sprites (each 48x24: 3 frames = smile / flat / off)
|
||||
-- plus npc_bird.png (32x24: 2 frames = beak closed / open)
|
||||
local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
local H = dofile(ROOT .. "tools/aseprite/helpers.lua")
|
||||
|
||||
-- ---- shared expression core ------------------------------------------------
|
||||
-- eyes at (lx,ly)/(rx,ry); mouth baseline my. ex in {"smile","flat","off"}.
|
||||
-- "off" = smile mouth but the right eye sits 1px lower (a 1-2px difference).
|
||||
local function expr(t, ex, lx, ly, rx, ry, my, eyeCol, mouthCol)
|
||||
eyeCol = eyeCol or "ink"; mouthCol = mouthCol or "brown_d"
|
||||
if ex == "off" then ry = ry + 1 end
|
||||
t.pn(lx, ly, eyeCol); t.pn(rx, ry, eyeCol)
|
||||
if ex == "flat" then
|
||||
t.hln(lx + 1, my, rx - lx - 1, mouthCol)
|
||||
else
|
||||
t.pn(lx + 1, my, mouthCol); t.pn(lx + 2, my + 1, mouthCol)
|
||||
t.pn(rx - 2, my + 1, mouthCol); t.pn(rx - 1, my, mouthCol)
|
||||
end
|
||||
end
|
||||
|
||||
local function arms(t, c) t.rectn(2, 12, 2, 5, c); t.rectn(12, 12, 2, 5, c) end
|
||||
local function feet(t, c) t.rectn(5, 20, 2, 3, c); t.rectn(9, 20, 2, 3, c) end
|
||||
|
||||
-- ---- species bodies (face forward, 16x24 local) ---------------------------
|
||||
local function afu(t, ex) -- 狗村长 dog village head
|
||||
t.rectn(1, 3, 3, 6, "brown_d"); t.rectn(12, 3, 3, 6, "brown_d") -- floppy ears
|
||||
t.rectn(1, 3, 3, 2, "brown"); t.rectn(12, 3, 3, 2, "brown")
|
||||
t.rectn(3, 3, 10, 9, "brown"); t.rectn(3, 3, 10, 1, "brown_l")
|
||||
t.rectn(6, 8, 4, 4, "cream"); t.pn(7, 8, "ink") -- muzzle + nose
|
||||
t.rectn(4, 12, 8, 7, "brown"); t.rectn(4, 12, 8, 3, "honey") -- vest
|
||||
t.pn(7, 13, "honey_d"); t.pn(8, 13, "honey_d")
|
||||
arms(t, "brown"); feet(t, "brown_d")
|
||||
expr(t, ex, 5, 6, 10, 6, 10)
|
||||
end
|
||||
|
||||
local function yuanyuan(t, ex) -- 兔面包师 rabbit baker
|
||||
t.rectn(4, 0, 2, 5, "cream"); t.rectn(10, 0, 2, 5, "cream") -- long ears
|
||||
t.rectn(5, 1, 1, 3, "pink"); t.rectn(10, 1, 1, 3, "pink") -- inner ear
|
||||
t.rectn(3, 4, 10, 8, "cream"); t.rectn(3, 4, 10, 1, "white")
|
||||
t.pn(7, 9, "pink_d") -- bunny nose
|
||||
t.rectn(4, 12, 8, 7, "pink") -- dress
|
||||
t.rectn(4, 12, 8, 5, "white") -- apron
|
||||
t.rectn(4, 12, 8, 1, "cream_d")
|
||||
t.pn(6, 14, "cream_d"); t.pn(9, 14, "cream_d")
|
||||
arms(t, "cream"); feet(t, "cream_d")
|
||||
expr(t, ex, 5, 7, 10, 7, 10)
|
||||
end
|
||||
|
||||
local function dabao(t, ex) -- 熊邮差 bear postman
|
||||
t.discn(4, 3, 2, "brown"); t.discn(11, 3, 2, "brown") -- round ears
|
||||
t.discn(4, 3, 1, "brown_d"); t.discn(11, 3, 1, "brown_d")
|
||||
t.rectn(2, 3, 12, 9, "brown"); t.rectn(2, 3, 12, 1, "brown_l") -- big head
|
||||
t.rectn(6, 8, 4, 4, "cream"); t.pn(7, 8, "ink")
|
||||
t.rectn(3, 12, 10, 7, "brown") -- big body
|
||||
t.pn(4, 12, "brown_d"); t.pn(5, 13, "brown_d"); t.pn(6, 14, "brown_d")
|
||||
t.pn(7, 15, "brown_d"); t.pn(8, 16, "brown_d") -- satchel strap
|
||||
t.rectn(9, 15, 4, 4, "yellow_d"); t.framen(9, 15, 4, 4, "brown_d"); t.rectn(9, 15, 4, 1, "yellow")
|
||||
t.rectn(1, 12, 2, 5, "brown"); t.rectn(13, 12, 2, 5, "brown") -- arms (wider)
|
||||
feet(t, "brown_d")
|
||||
expr(t, ex, 5, 6, 10, 6, 10)
|
||||
end
|
||||
|
||||
local function mimi(t, ex) -- 猫店主 cat shopkeeper
|
||||
t.rectn(3, 2, 2, 3, "honey"); t.rectn(11, 2, 2, 3, "honey") -- pointy ears
|
||||
t.pn(3, 2, "honey_d"); t.pn(12, 2, "honey_d")
|
||||
t.pn(3, 3, "pink"); t.pn(12, 3, "pink")
|
||||
t.rectn(3, 4, 10, 8, "honey"); t.rectn(3, 4, 10, 1, "honey_l")
|
||||
t.pn(4, 4, "honey_d"); t.pn(7, 4, "honey_d"); t.pn(10, 4, "honey_d") -- stripes
|
||||
t.pn(7, 9, "pink_d") -- nose
|
||||
t.rectn(4, 12, 8, 7, "cream")
|
||||
t.rectn(4, 12, 8, 4, "sky"); t.rectn(4, 12, 8, 1, "sky_d") -- apron
|
||||
arms(t, "honey"); feet(t, "honey_d")
|
||||
t.pn(1, 8, "honey_d"); t.pn(14, 8, "honey_d") -- whiskers
|
||||
expr(t, ex, 5, 7, 10, 7, 10)
|
||||
end
|
||||
|
||||
local function fuyisheng(t, ex) -- 鹿医生 deer doctor
|
||||
t.vln(4, 0, 3, "brown_d"); t.pn(3, 1, "brown_d"); t.pn(5, 1, "brown_d") -- antlers
|
||||
t.vln(11, 0, 3, "brown_d"); t.pn(10, 1, "brown_d"); t.pn(12, 1, "brown_d")
|
||||
t.rectn(2, 4, 2, 3, "cream_d"); t.rectn(12, 4, 2, 3, "cream_d") -- ears
|
||||
t.rectn(3, 3, 10, 9, "cream_d"); t.rectn(3, 3, 10, 1, "skin")
|
||||
t.rectn(6, 8, 4, 4, "cream"); t.pn(7, 8, "ink") -- muzzle
|
||||
t.rectn(3, 12, 10, 7, "white"); t.framen(3, 12, 10, 7, "w_lo") -- white coat
|
||||
t.vln(8, 12, 7, "w_lo")
|
||||
t.rectn(5, 12, 6, 2, "skin")
|
||||
t.pn(11, 14, "red"); t.pn(11, 15, "red") -- red cross hint
|
||||
arms(t, "white"); feet(t, "brown_d")
|
||||
expr(t, ex, 5, 6, 10, 6, 10)
|
||||
end
|
||||
|
||||
local function guiyeye(t, ex) -- 龟老者 turtle elder
|
||||
t.rectn(6, 4, 6, 5, "leaf_l"); t.rectn(6, 4, 6, 1, "cream") -- head
|
||||
t.rectn(6, 8, 6, 2, "white") -- beard
|
||||
t.discn(8, 14, 6, "leaf_d") -- shell
|
||||
t.discn(8, 13, 5, "leaf")
|
||||
t.discn(6, 12, 2, "leaf_l")
|
||||
t.pn(8, 12, "leaf_dd"); t.pn(6, 14, "leaf_dd"); t.pn(10, 14, "leaf_dd"); t.pn(8, 16, "leaf_dd")
|
||||
t.rectn(4, 20, 3, 3, "leaf_d"); t.rectn(9, 20, 3, 3, "leaf_d") -- feet
|
||||
t.vln(13, 12, 9, "brown_d"); t.pn(13, 11, "brown") -- cane
|
||||
expr(t, ex, 7, 6, 10, 6, 8, "ink", "brown_d")
|
||||
end
|
||||
|
||||
-- ---- build the 6 three-frame NPC sprites ----------------------------------
|
||||
local npcs = {
|
||||
{ "afu", afu }, { "yuanyuan", yuanyuan }, { "dabao", dabao },
|
||||
{ "mimi", mimi }, { "fuyisheng", fuyisheng }, { "guiyeye", guiyeye },
|
||||
}
|
||||
for _, n in ipairs(npcs) do
|
||||
local spr, img = H.new(48, 24)
|
||||
for i, ex in ipairs({ "smile", "flat", "off" }) do
|
||||
n[2](H.at(img, (i - 1) * 16, 0), ex)
|
||||
end
|
||||
spr:saveAs(ROOT .. "public/assets/sprites/npc_" .. n[1] .. ".png")
|
||||
print("npc_" .. n[1] .. ".png done")
|
||||
end
|
||||
|
||||
-- ---- twin birds (32x24, 2 frames = beak closed / open) ---------------------
|
||||
local function bird(t, open)
|
||||
t.rectn(0, 18, 16, 2, "brown_d") -- branch
|
||||
-- bird A (faces right)
|
||||
t.discn(4, 11, 4, "blue"); t.discn(4, 8, 3, "blue")
|
||||
t.discn(3, 10, 2, "blue_l")
|
||||
t.discn(5, 11, 2, "blue_d") -- wing
|
||||
t.pn(1, 12, "blue_d"); t.pn(0, 13, "blue_d") -- tail
|
||||
t.pn(3, 7, "ink") -- eye
|
||||
if open then t.pn(6, 8, "yellow"); t.pn(6, 9, "yellow_d") else t.pn(6, 8, "yellow") end
|
||||
t.vln(3, 15, 3, "honey_d"); t.vln(5, 15, 3, "honey_d")
|
||||
-- bird B (faces left), slightly lower
|
||||
t.discn(11, 13, 4, "blue"); t.discn(11, 10, 3, "blue")
|
||||
t.discn(12, 12, 2, "blue_l")
|
||||
t.discn(10, 13, 2, "blue_d")
|
||||
t.pn(14, 14, "blue_d"); t.pn(15, 15, "blue_d")
|
||||
t.pn(12, 9, "ink")
|
||||
if open then t.pn(9, 10, "yellow"); t.pn(9, 11, "yellow_d") else t.pn(9, 10, "yellow") end
|
||||
t.vln(10, 16, 2, "honey_d"); t.vln(12, 16, 2, "honey_d")
|
||||
end
|
||||
local bspr, bimg = H.new(32, 24)
|
||||
bird(H.at(bimg, 0, 0), false)
|
||||
bird(H.at(bimg, 16, 0), true)
|
||||
bspr:saveAs(ROOT .. "public/assets/sprites/npc_bird.png")
|
||||
print("npc_bird.png done")
|
||||
@@ -0,0 +1,96 @@
|
||||
-- gen_player.lua — public/assets/sprites/player.png
|
||||
-- 64x96; frame 16x24; cols 0-3 = walk frames (col 0 also idle); rows 0=down 1=up 2=left 3=right
|
||||
-- Gender-neutral chibi, cream / honey-orange palette, gentle "healing" look.
|
||||
local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
local H = dofile(ROOT .. "tools/aseprite/helpers.lua")
|
||||
|
||||
local spr, img = H.new(64, 96)
|
||||
|
||||
-- step: -1 left step, 0 stand, +1 right step
|
||||
local function stepOf(f)
|
||||
if f == 1 then return -1 elseif f == 3 then return 1 else return 0 end
|
||||
end
|
||||
|
||||
-- legs shared by down/up (front/back view alternates a lifted foot)
|
||||
local function legsFB(t, step)
|
||||
if step == 0 then
|
||||
t.rectn(5, 17, 2, 5, "cream_d"); t.rectn(9, 17, 2, 5, "cream_d")
|
||||
t.rectn(4, 22, 3, 2, "brown"); t.rectn(9, 22, 3, 2, "brown")
|
||||
elseif step == -1 then
|
||||
t.rectn(5, 17, 2, 5, "cream_d"); t.rectn(4, 22, 3, 2, "brown") -- left planted
|
||||
t.rectn(9, 17, 2, 4, "cream_d"); t.rectn(9, 21, 3, 2, "brown") -- right lifted
|
||||
else
|
||||
t.rectn(5, 17, 2, 4, "cream_d"); t.rectn(4, 21, 3, 2, "brown") -- left lifted
|
||||
t.rectn(9, 17, 2, 5, "cream_d"); t.rectn(9, 22, 3, 2, "brown") -- right planted
|
||||
end
|
||||
end
|
||||
|
||||
local function drawDown(t, f)
|
||||
local s = stepOf(f)
|
||||
t.rectn(4, 2, 8, 3, "hair")
|
||||
t.rectn(3, 3, 2, 6, "hair"); t.rectn(11, 3, 2, 6, "hair")
|
||||
t.rectn(4, 4, 8, 6, "skin")
|
||||
t.rectn(4, 2, 8, 1, "hair_l")
|
||||
t.pn(5, 7, "ink"); t.pn(10, 7, "ink")
|
||||
t.pn(6, 9, "skin_d"); t.pn(9, 9, "skin_d")
|
||||
t.rectn(4, 11, 8, 6, "honey"); t.rectn(4, 11, 8, 1, "honey_l")
|
||||
-- arms swing opposite to step
|
||||
if s == 1 then t.rectn(3, 11, 2, 5, "honey"); t.rectn(3, 16, 2, 2, "skin")
|
||||
else t.rectn(2, 11, 2, 5, "honey"); t.rectn(2, 16, 2, 2, "skin") end
|
||||
if s == -1 then t.rectn(11, 11, 2, 5, "honey"); t.rectn(11, 16, 2, 2, "skin")
|
||||
else t.rectn(12, 11, 2, 5, "honey"); t.rectn(12, 16, 2, 2, "skin") end
|
||||
legsFB(t, s)
|
||||
end
|
||||
|
||||
local function drawUp(t, f)
|
||||
local s = stepOf(f)
|
||||
t.rectn(3, 2, 10, 7, "hair")
|
||||
t.rectn(4, 2, 8, 2, "hair_l")
|
||||
t.rectn(3, 8, 10, 1, "hair_d")
|
||||
t.rectn(6, 9, 4, 2, "skin_d")
|
||||
t.rectn(4, 11, 8, 6, "honey"); t.rectn(4, 16, 8, 1, "honey_d")
|
||||
t.rectn(2, 11, 2, 5, "honey"); t.rectn(12, 11, 2, 5, "honey")
|
||||
t.rectn(2, 16, 2, 2, "skin"); t.rectn(12, 16, 2, 2, "skin")
|
||||
legsFB(t, s)
|
||||
end
|
||||
|
||||
local function drawLeft(t, f)
|
||||
local s = stepOf(f)
|
||||
-- hair (back of head toward the right / behind)
|
||||
t.rectn(5, 2, 7, 3, "hair"); t.rectn(5, 2, 7, 1, "hair_l")
|
||||
t.rectn(9, 3, 3, 6, "hair")
|
||||
-- face (pointing left)
|
||||
t.rectn(4, 4, 7, 6, "skin")
|
||||
t.pn(5, 7, "ink")
|
||||
t.pn(4, 8, "skin_d")
|
||||
-- torso
|
||||
t.rectn(4, 11, 8, 6, "honey"); t.rectn(4, 11, 8, 1, "honey_l")
|
||||
-- front arm swings with step
|
||||
if s == -1 then t.rectn(2, 11, 2, 5, "honey"); t.rectn(2, 16, 2, 2, "skin")
|
||||
elseif s == 1 then t.rectn(5, 11, 2, 5, "honey"); t.rectn(5, 16, 2, 2, "skin")
|
||||
else t.rectn(3, 11, 2, 5, "honey"); t.rectn(3, 16, 2, 2, "skin") end
|
||||
-- profile legs scissor horizontally
|
||||
if s == 0 then
|
||||
t.rectn(5, 17, 2, 5, "cream_d"); t.rectn(8, 17, 2, 5, "cream_d")
|
||||
t.rectn(4, 22, 3, 2, "brown"); t.rectn(7, 22, 3, 2, "brown")
|
||||
elseif s == -1 then
|
||||
t.rectn(4, 17, 2, 5, "cream_d"); t.rectn(3, 22, 3, 2, "brown") -- front leg forward
|
||||
t.rectn(9, 17, 2, 4, "cream_d"); t.rectn(8, 21, 3, 2, "brown") -- back leg lifted
|
||||
else
|
||||
t.rectn(6, 17, 2, 4, "cream_d"); t.rectn(5, 21, 3, 2, "brown") -- back leg lifted
|
||||
t.rectn(9, 17, 2, 5, "cream_d"); t.rectn(8, 22, 3, 2, "brown") -- front leg forward
|
||||
end
|
||||
end
|
||||
|
||||
for row = 0, 3 do
|
||||
for col = 0, 3 do
|
||||
local t = H.at(img, col * 16, row * 24)
|
||||
if row == 0 then drawDown(t, col)
|
||||
elseif row == 1 then drawUp(t, col)
|
||||
elseif row == 2 then drawLeft(t, col)
|
||||
else drawLeft(H.mirrorX(t, 16), col) end
|
||||
end
|
||||
end
|
||||
|
||||
spr:saveAs(ROOT .. "public/assets/sprites/player.png")
|
||||
print("player.png done")
|
||||
@@ -0,0 +1,130 @@
|
||||
-- gen_portraits.lua — dialogue portraits (each 32x32)
|
||||
-- player_smile / player_real + afu yuanyuan dabao mimi fuyisheng guiyeye bird (all smile)
|
||||
local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
local H = dofile(ROOT .. "tools/aseprite/helpers.lua")
|
||||
|
||||
local function smileArc(t, cx, y, w, c)
|
||||
local half = math.floor(w / 2)
|
||||
t.pn(cx - half, y, c); t.pn(cx + half, y, c)
|
||||
t.pn(cx - half + 1, y + 1, c); t.pn(cx + half - 1, y + 1, c)
|
||||
for i = -half + 2, half - 2 do t.pn(cx + i, y + 2, c) end
|
||||
end
|
||||
local function eyes(t, lx, rx, y, c)
|
||||
t.rectn(lx, y, 3, 4, c); t.rectn(rx, y, 3, 4, c)
|
||||
end
|
||||
|
||||
-- ---- player ---------------------------------------------------------------
|
||||
local function playerSmile(t)
|
||||
t.rectn(6, 2, 20, 6, "hair"); t.rectn(6, 2, 20, 2, "hair_l")
|
||||
t.rectn(4, 4, 4, 14, "hair"); t.rectn(24, 4, 4, 14, "hair")
|
||||
t.rectn(6, 7, 20, 18, "skin")
|
||||
eyes(t, 9, 20, 13, "ink")
|
||||
t.pn(8, 18, "pink"); t.pn(9, 18, "pink"); t.pn(23, 18, "pink"); t.pn(24, 18, "pink")
|
||||
smileArc(t, 16, 20, 8, "brown_d")
|
||||
t.rectn(12, 25, 8, 4, "honey"); t.rectn(12, 25, 8, 1, "honey_l")
|
||||
end
|
||||
|
||||
-- E3 "real face": the smile is gone — a plain, tired, honest face.
|
||||
local function playerReal(t)
|
||||
t.rectn(6, 2, 20, 6, "hair"); t.rectn(6, 2, 20, 2, "hair_l")
|
||||
t.rectn(4, 4, 4, 14, "hair"); t.rectn(24, 4, 4, 14, "hair")
|
||||
t.rectn(6, 7, 20, 18, "skin")
|
||||
t.rectn(9, 15, 3, 2, "ink"); t.rectn(20, 15, 3, 2, "ink") -- half-closed eyes
|
||||
t.rectn(9, 14, 3, 1, "skin_d"); t.rectn(20, 14, 3, 1, "skin_d") -- heavy lids
|
||||
t.pn(9, 18, "skin_d"); t.pn(10, 18, "skin_d") -- bags
|
||||
t.pn(21, 18, "skin_d"); t.pn(22, 18, "skin_d")
|
||||
t.pn(10, 19, "cream_d"); t.pn(21, 19, "cream_d")
|
||||
t.pn(9, 12, "hair_d"); t.pn(10, 12, "hair_d"); t.pn(11, 13, "hair_d") -- weary brows
|
||||
t.pn(22, 12, "hair_d"); t.pn(21, 12, "hair_d"); t.pn(20, 13, "hair_d")
|
||||
t.hln(13, 21, 6, "brown_d") -- flat mouth
|
||||
t.rectn(12, 25, 8, 4, "honey"); t.rectn(12, 25, 8, 1, "honey_d")
|
||||
end
|
||||
|
||||
-- ---- NPC smile portraits ---------------------------------------------------
|
||||
local function afuP(t)
|
||||
t.rectn(2, 6, 6, 15, "brown_d"); t.rectn(24, 6, 6, 15, "brown_d")
|
||||
t.rectn(2, 6, 6, 5, "brown"); t.rectn(24, 6, 6, 5, "brown")
|
||||
t.rectn(6, 4, 20, 23, "brown"); t.rectn(6, 4, 20, 3, "brown_l")
|
||||
eyes(t, 9, 20, 11, "ink")
|
||||
t.rectn(11, 16, 10, 8, "cream"); t.rectn(14, 16, 4, 3, "ink")
|
||||
smileArc(t, 16, 20, 7, "brown_d")
|
||||
t.rectn(12, 27, 8, 3, "honey")
|
||||
end
|
||||
|
||||
local function yuanyuanP(t)
|
||||
t.rectn(8, 0, 4, 9, "cream"); t.rectn(20, 0, 4, 9, "cream")
|
||||
t.rectn(9, 1, 2, 7, "pink"); t.rectn(21, 1, 2, 7, "pink")
|
||||
t.rectn(6, 8, 20, 19, "cream"); t.rectn(6, 8, 20, 2, "white")
|
||||
eyes(t, 10, 20, 14, "ink")
|
||||
t.pn(15, 18, "pink_d"); t.pn(16, 18, "pink_d")
|
||||
smileArc(t, 16, 20, 6, "brown_d")
|
||||
t.rectn(12, 27, 8, 3, "pink")
|
||||
end
|
||||
|
||||
local function dabaoP(t)
|
||||
t.discn(7, 6, 4, "brown"); t.discn(25, 6, 4, "brown")
|
||||
t.discn(7, 6, 2, "brown_d"); t.discn(25, 6, 2, "brown_d")
|
||||
t.rectn(4, 4, 24, 23, "brown"); t.rectn(4, 4, 24, 3, "brown_l")
|
||||
eyes(t, 9, 21, 11, "ink")
|
||||
t.rectn(11, 16, 10, 8, "cream"); t.rectn(14, 16, 4, 3, "ink")
|
||||
smileArc(t, 16, 20, 7, "brown_d")
|
||||
t.rectn(11, 27, 10, 3, "yellow_d")
|
||||
end
|
||||
|
||||
local function mimiP(t)
|
||||
t.pn(7, 2, "honey"); t.rectn(6, 3, 4, 6, "honey") -- left ear
|
||||
t.pn(24, 2, "honey"); t.rectn(22, 3, 4, 6, "honey") -- right ear
|
||||
t.rectn(7, 4, 2, 4, "pink"); t.rectn(23, 4, 2, 4, "pink") -- inner
|
||||
t.rectn(6, 8, 20, 19, "honey"); t.rectn(6, 8, 20, 2, "honey_l")
|
||||
t.pn(9, 8, "honey_d"); t.pn(13, 8, "honey_d"); t.pn(17, 8, "honey_d"); t.pn(21, 8, "honey_d")
|
||||
eyes(t, 10, 20, 14, "ink")
|
||||
t.pn(15, 18, "pink_d"); t.pn(16, 18, "pink_d")
|
||||
smileArc(t, 16, 20, 6, "brown_d")
|
||||
t.hln(3, 17, 3, "honey_d"); t.hln(3, 19, 3, "honey_d")
|
||||
t.hln(26, 17, 3, "honey_d"); t.hln(26, 19, 3, "honey_d")
|
||||
t.rectn(12, 27, 8, 3, "sky")
|
||||
end
|
||||
|
||||
local function fuyishengP(t)
|
||||
t.vln(8, 0, 5, "brown_d"); t.pn(6, 2, "brown_d"); t.pn(7, 3, "brown_d"); t.pn(10, 1, "brown_d")
|
||||
t.vln(23, 0, 5, "brown_d"); t.pn(25, 2, "brown_d"); t.pn(24, 3, "brown_d"); t.pn(21, 1, "brown_d")
|
||||
t.rectn(2, 10, 4, 6, "cream_d"); t.rectn(26, 10, 4, 6, "cream_d")
|
||||
t.rectn(6, 5, 20, 22, "cream_d"); t.rectn(6, 5, 20, 2, "skin")
|
||||
eyes(t, 10, 20, 13, "ink")
|
||||
t.rectn(11, 17, 10, 7, "cream"); t.rectn(14, 17, 4, 3, "ink")
|
||||
smileArc(t, 16, 21, 6, "brown_d")
|
||||
t.rectn(12, 27, 8, 3, "white"); t.pn(12, 28, "red")
|
||||
end
|
||||
|
||||
local function guiyeyeP(t)
|
||||
t.rectn(7, 5, 18, 20, "leaf_l"); t.rectn(7, 5, 18, 2, "cream") -- bald cream top
|
||||
t.rectn(9, 12, 5, 2, "white"); t.rectn(18, 12, 5, 2, "white") -- bushy brows
|
||||
eyes(t, 10, 20, 14, "ink")
|
||||
t.rectn(10, 20, 12, 8, "white"); t.rectn(12, 26, 8, 3, "white") -- beard
|
||||
smileArc(t, 16, 19, 6, "brown_d")
|
||||
t.rectn(4, 28, 24, 3, "leaf_d"); t.rectn(4, 28, 24, 1, "leaf") -- shell hint
|
||||
end
|
||||
|
||||
local function birdP(t)
|
||||
t.discn(10, 15, 8, "blue"); t.discn(9, 12, 4, "blue_l") -- left bird
|
||||
t.discn(11, 16, 3, "blue_d")
|
||||
t.pn(7, 12, "ink")
|
||||
t.pn(4, 15, "yellow"); t.pn(3, 16, "yellow_d")
|
||||
t.discn(22, 17, 7, "blue"); t.discn(23, 14, 3, "blue_l") -- right bird
|
||||
t.discn(21, 18, 3, "blue_d")
|
||||
t.pn(24, 14, "ink")
|
||||
t.pn(28, 16, "yellow"); t.pn(29, 17, "yellow_d")
|
||||
t.rectn(6, 25, 20, 2, "brown_d") -- branch
|
||||
end
|
||||
|
||||
local portraits = {
|
||||
{ "player_smile", playerSmile }, { "player_real", playerReal },
|
||||
{ "afu", afuP }, { "yuanyuan", yuanyuanP }, { "dabao", dabaoP },
|
||||
{ "mimi", mimiP }, { "fuyisheng", fuyishengP }, { "guiyeye", guiyeyeP }, { "bird", birdP },
|
||||
}
|
||||
for _, n in ipairs(portraits) do
|
||||
local spr, img = H.new(32, 32)
|
||||
n[2](H.at(img, 0, 0))
|
||||
spr:saveAs(ROOT .. "public/assets/portraits/" .. n[1] .. ".png")
|
||||
print(n[1] .. ".png done")
|
||||
end
|
||||
@@ -0,0 +1,878 @@
|
||||
-- gen_tileset.lua — public/assets/tileset.png (256x128, 16x16 tiles, id = row*16+col)
|
||||
local ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
local H = dofile(ROOT .. "tools/aseprite/helpers.lua")
|
||||
|
||||
local spr, img = H.new(256, 128)
|
||||
local tiles = {}
|
||||
|
||||
-- ---- shared sub-draws ------------------------------------------------------
|
||||
local function grassBase(t)
|
||||
t.rectn(0, 0, 16, 16, "grass")
|
||||
t.pn(3, 2, "grass_l"); t.pn(11, 5, "grass_l"); t.pn(6, 9, "grass_l")
|
||||
t.pn(13, 12, "grass_l"); t.pn(1, 13, "grass_l"); t.pn(9, 3, "grass_l")
|
||||
t.pn(5, 4, "grass_d"); t.pn(6, 4, "grass_d"); t.pn(6, 5, "grass_d")
|
||||
t.pn(10, 11, "grass_d"); t.pn(11, 11, "grass_d"); t.pn(2, 8, "grass_d")
|
||||
t.pn(14, 3, "grass_d"); t.pn(9, 14, "grass_d"); t.pn(12, 8, "grass_d")
|
||||
end
|
||||
|
||||
local function roof(t, base, d, l, edge)
|
||||
t.rectn(0, 0, 16, 16, base)
|
||||
t.hln(0, 4, 16, d); t.hln(0, 9, 16, d); t.hln(0, 14, 16, d)
|
||||
for x = 2, 15, 4 do t.vln(x, 5, 4, d) end
|
||||
for x = 0, 15, 4 do t.vln(x, 10, 4, d) end
|
||||
t.hln(0, 0, 16, l)
|
||||
t.hln(0, 15, 16, d)
|
||||
if edge == "L" then t.vln(0, 0, 16, d); t.vln(1, 0, 16, base) end
|
||||
if edge == "R" then t.vln(15, 0, 16, d); t.vln(14, 0, 16, base) end
|
||||
end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 0 (ids 0-15) : ground / water / flowerbeds
|
||||
-- ===========================================================================
|
||||
tiles[0] = function(t) grassBase(t) end
|
||||
|
||||
tiles[1] = function(t)
|
||||
grassBase(t)
|
||||
H.flower(t, 8, 7, "pink", "yellow")
|
||||
H.flower(t, 4, 11, "white", "yellow")
|
||||
t.pn(12, 4, "pink"); t.pn(12, 5, "pink"); t.pn(11, 4, "pink")
|
||||
end
|
||||
|
||||
tiles[2] = function(t)
|
||||
grassBase(t)
|
||||
H.flower(t, 6, 6, "yellow", "honey_d")
|
||||
H.flower(t, 11, 10, "red", "yellow")
|
||||
t.pn(3, 5, "white"); t.pn(4, 5, "white")
|
||||
end
|
||||
|
||||
tiles[3] = function(t)
|
||||
t.rectn(0, 0, 16, 16, "grass_d")
|
||||
t.pn(3, 3, "grass"); t.pn(10, 6, "grass"); t.pn(6, 11, "grass"); t.pn(13, 13, "grass")
|
||||
t.pn(5, 5, "grass_dd"); t.pn(6, 5, "grass_dd"); t.pn(11, 11, "grass_dd")
|
||||
t.pn(2, 9, "grass_dd"); t.pn(14, 4, "grass_dd"); t.pn(9, 14, "grass_dd")
|
||||
end
|
||||
|
||||
tiles[4] = function(t) -- dirt road
|
||||
t.rectn(0, 0, 16, 16, "dirt")
|
||||
t.pn(3, 3, "dirt_d"); t.pn(4, 3, "dirt_d"); t.pn(11, 6, "dirt_d")
|
||||
t.pn(7, 10, "dirt_d"); t.pn(13, 12, "dirt_d"); t.pn(2, 13, "dirt_d")
|
||||
t.pn(6, 4, "dirt_l"); t.pn(12, 8, "dirt_l"); t.pn(9, 13, "dirt_l"); t.pn(1, 6, "dirt_l")
|
||||
end
|
||||
|
||||
tiles[5] = function(t) -- stone road
|
||||
t.rectn(0, 0, 16, 16, "stone")
|
||||
t.hln(0, 0, 16, "stone_d"); t.hln(0, 8, 16, "stone_d")
|
||||
t.vln(0, 0, 8, "stone_d"); t.vln(8, 0, 8, "stone_d")
|
||||
t.vln(4, 8, 8, "stone_d"); t.vln(12, 8, 8, "stone_d")
|
||||
t.pn(2, 3, "stone_l"); t.pn(10, 3, "stone_l"); t.pn(6, 11, "stone_l"); t.pn(13, 12, "stone_l")
|
||||
end
|
||||
|
||||
tiles[6] = function(t) -- stone road alt
|
||||
t.rectn(0, 0, 16, 16, "stone")
|
||||
t.hln(0, 0, 16, "stone_dd"); t.hln(0, 8, 16, "stone_dd")
|
||||
t.vln(5, 0, 8, "stone_dd"); t.vln(13, 0, 8, "stone_dd")
|
||||
t.vln(1, 8, 8, "stone_dd"); t.vln(9, 8, 8, "stone_dd")
|
||||
t.pn(3, 3, "stone_l"); t.pn(11, 4, "stone_l"); t.pn(4, 11, "stone_l"); t.pn(12, 12, "stone_l")
|
||||
t.pn(8, 2, "stone_d"); t.pn(7, 12, "stone_d")
|
||||
end
|
||||
|
||||
tiles[7] = function(t) -- plaza brick
|
||||
t.rectn(0, 0, 16, 16, "stone_l")
|
||||
t.hln(0, 0, 16, "stone_d"); t.hln(0, 8, 16, "stone_d")
|
||||
t.vln(0, 0, 16, "stone_d"); t.vln(8, 0, 16, "stone_d")
|
||||
t.pn(1, 1, "stone_dd"); t.pn(9, 1, "stone_dd"); t.pn(1, 9, "stone_dd"); t.pn(9, 9, "stone_dd")
|
||||
end
|
||||
|
||||
tiles[8] = function(t) -- lake water A
|
||||
t.rectn(0, 0, 16, 16, "water")
|
||||
t.hln(2, 4, 3, "water_l"); t.hln(9, 4, 3, "water_l")
|
||||
t.hln(5, 11, 3, "water_l"); t.hln(12, 11, 2, "water_l")
|
||||
t.pn(7, 7, "water_d"); t.pn(13, 2, "water_d"); t.pn(3, 13, "water_d")
|
||||
end
|
||||
|
||||
tiles[9] = function(t) -- lake water B
|
||||
t.rectn(0, 0, 16, 16, "water_d")
|
||||
t.hln(3, 5, 3, "water"); t.hln(10, 5, 3, "water")
|
||||
t.hln(6, 12, 3, "water"); t.hln(12, 12, 2, "water")
|
||||
t.pn(8, 8, "water_dd"); t.pn(2, 3, "water_dd"); t.pn(13, 13, "water_dd")
|
||||
end
|
||||
|
||||
-- water banks (shore toward the named direction)
|
||||
local function bankH(t, shoreTop)
|
||||
t.rectn(0, 0, 16, 16, "water")
|
||||
local by = { 6, 6, 7, 7, 6, 6, 5, 6, 6, 7, 7, 6, 6, 5, 6, 6 }
|
||||
for x = 0, 15 do
|
||||
local b = by[x + 1]
|
||||
if shoreTop then
|
||||
t.rectn(x, 0, 1, b, "stone_l")
|
||||
t.pn(x, b, "cream_d")
|
||||
t.pn(x, b + 1, "water_l")
|
||||
else
|
||||
t.rectn(x, 16 - b, 1, b, "stone_l")
|
||||
t.pn(x, 15 - b, "cream_d")
|
||||
t.pn(x, 14 - b, "water_l")
|
||||
end
|
||||
end
|
||||
end
|
||||
local function bankV(t, shoreLeft)
|
||||
t.rectn(0, 0, 16, 16, "water")
|
||||
local bx = { 6, 6, 7, 7, 6, 6, 5, 6, 6, 7, 7, 6, 6, 5, 6, 6 }
|
||||
for y = 0, 15 do
|
||||
local b = bx[y + 1]
|
||||
if shoreLeft then
|
||||
t.rectn(0, y, b, 1, "stone_l")
|
||||
t.pn(b, y, "cream_d")
|
||||
t.pn(b + 1, y, "water_l")
|
||||
else
|
||||
t.rectn(16 - b, y, b, 1, "stone_l")
|
||||
t.pn(15 - b, y, "cream_d")
|
||||
t.pn(14 - b, y, "water_l")
|
||||
end
|
||||
end
|
||||
end
|
||||
tiles[10] = function(t) bankH(t, true) end -- 水岸北 (shore north/top)
|
||||
tiles[11] = function(t) bankH(t, false) end -- 水岸南
|
||||
tiles[12] = function(t) bankV(t, true) end -- 水岸西
|
||||
tiles[13] = function(t) bankV(t, false) end -- 水岸东
|
||||
|
||||
local function flowerbed(t, c1, c2)
|
||||
t.rectn(0, 0, 16, 16, "dirt_d")
|
||||
t.rectn(0, 0, 16, 16, "dirt")
|
||||
t.framen(0, 0, 16, 16, "dirt_d")
|
||||
t.pn(3, 6, "leaf"); t.pn(10, 7, "leaf"); t.pn(7, 11, "leaf"); t.pn(13, 5, "leaf"); t.pn(5, 12, "leaf")
|
||||
H.flower(t, 4, 5, c1, "yellow")
|
||||
H.flower(t, 10, 4, c1, "yellow")
|
||||
H.flower(t, 7, 10, c2, "yellow")
|
||||
H.flower(t, 13, 9, c1, "yellow")
|
||||
H.flower(t, 3, 12, c2, "yellow")
|
||||
end
|
||||
tiles[14] = function(t) flowerbed(t, "pink", "pink_l") end
|
||||
tiles[15] = function(t) flowerbed(t, "yellow", "yellow_l") end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 1 (ids 16-31) : bushes / tree / fence / lamp / bunting
|
||||
-- ===========================================================================
|
||||
local function bush(t, berries)
|
||||
t.ellipsen(8, 14, 6, 2, "grass_dd")
|
||||
t.discn(8, 8, 6, "leaf")
|
||||
t.discn(6, 6, 3, "leaf_l")
|
||||
t.discn(10, 10, 3, "leaf_d")
|
||||
t.discn(8, 11, 3, "leaf_d")
|
||||
if berries then
|
||||
t.pn(6, 7, "berry"); t.pn(9, 5, "berry"); t.pn(11, 9, "berry")
|
||||
t.pn(7, 10, "berry"); t.pn(10, 11, "berry"); t.pn(5, 9, "berry"); t.pn(9, 8, "berry_d")
|
||||
end
|
||||
end
|
||||
tiles[16] = function(t) bush(t, true) end
|
||||
tiles[17] = function(t) bush(t, false) end
|
||||
|
||||
-- tree (18-21) drawn once across its 2x2 block (origin of tile 18)
|
||||
local function drawTree(img)
|
||||
local tr = H.at(img, (18 % 16) * 16, math.floor(18 / 16) * 16)
|
||||
tr.ellipsen(16, 31, 8, 1, "grass_dd")
|
||||
tr.rectn(14, 21, 4, 11, "brown")
|
||||
tr.vln(14, 21, 11, "brown_l"); tr.vln(17, 21, 11, "brown_d")
|
||||
tr.discn(16, 12, 13, "leaf")
|
||||
tr.discn(16, 16, 6, "leaf_d")
|
||||
tr.discn(21, 15, 5, "leaf_d")
|
||||
tr.discn(12, 8, 6, "leaf_l")
|
||||
tr.discn(9, 13, 3, "leaf")
|
||||
tr.discn(23, 11, 3, "leaf")
|
||||
end
|
||||
|
||||
tiles[22] = function(t) -- round bush
|
||||
t.ellipsen(8, 14, 5, 2, "grass_dd")
|
||||
t.discn(8, 9, 5, "leaf")
|
||||
t.discn(6, 7, 2, "leaf_l")
|
||||
t.discn(10, 11, 2, "leaf_d")
|
||||
end
|
||||
|
||||
tiles[23] = function(t) -- tree stump
|
||||
t.ellipsen(8, 14, 6, 2, "brown_d")
|
||||
t.rectn(4, 7, 8, 7, "brown")
|
||||
t.vln(4, 7, 7, "brown_d"); t.vln(11, 7, 7, "brown_dd")
|
||||
t.ellipsen(8, 7, 4, 2, "wood_l")
|
||||
t.ellipsen(8, 7, 2, 1, "wood_d")
|
||||
t.pn(8, 7, "brown_dd")
|
||||
end
|
||||
|
||||
tiles[24] = function(t) -- rock
|
||||
t.ellipsen(8, 13, 7, 2, "grey_d")
|
||||
t.ellipsen(8, 9, 6, 4, "grey")
|
||||
t.ellipsen(6, 8, 3, 2, "grey_l")
|
||||
t.ellipsen(10, 11, 3, 2, "grey_d")
|
||||
end
|
||||
|
||||
tiles[25] = function(t) -- signpost
|
||||
t.rectn(7, 9, 2, 7, "brown_d")
|
||||
t.rectn(3, 3, 10, 6, "wood")
|
||||
t.framen(3, 3, 10, 6, "brown_d")
|
||||
t.hln(4, 5, 8, "brown_d"); t.hln(4, 7, 6, "brown_d")
|
||||
t.pn(12, 4, "brown_dd")
|
||||
end
|
||||
|
||||
tiles[26] = function(t) -- fence horizontal
|
||||
t.rectn(1, 4, 3, 11, "brown"); t.rectn(12, 4, 3, 11, "brown")
|
||||
t.pn(1, 4, "brown_l"); t.pn(12, 4, "brown_l")
|
||||
t.hln(0, 7, 16, "wood"); t.hln(0, 8, 16, "wood_d")
|
||||
t.hln(0, 11, 16, "wood"); t.hln(0, 12, 16, "wood_d")
|
||||
t.vln(1, 4, 11, "brown_d"); t.vln(12, 4, 11, "brown_d")
|
||||
end
|
||||
|
||||
tiles[27] = function(t) -- fence vertical
|
||||
t.rectn(4, 0, 3, 16, "wood"); t.rectn(9, 0, 3, 16, "wood")
|
||||
t.vln(4, 0, 16, "wood_d"); t.vln(9, 0, 16, "wood_d")
|
||||
t.rectn(2, 6, 12, 3, "brown")
|
||||
t.hln(2, 6, 12, "brown_l"); t.hln(2, 8, 12, "brown_d")
|
||||
end
|
||||
|
||||
tiles[28] = function(t) -- street lamp
|
||||
t.rectn(7, 6, 2, 10, "grey_d")
|
||||
t.rectn(6, 15, 4, 1, "grey")
|
||||
t.rectn(5, 3, 6, 4, "metal")
|
||||
t.framen(5, 3, 6, 4, "grey_d")
|
||||
t.rectn(6, 4, 4, 2, "yellow_l")
|
||||
t.pn(7, 2, "yellow"); t.pn(8, 2, "yellow")
|
||||
t.pn(6, 8, "grey")
|
||||
end
|
||||
|
||||
local function bunting(t, c)
|
||||
t.hln(0, 3, 16, "brown_d")
|
||||
t.hln(4, 4, 8, c)
|
||||
t.hln(5, 5, 6, c)
|
||||
t.hln(6, 6, 4, c)
|
||||
t.hln(7, 7, 2, c)
|
||||
t.pn(8, 8, c)
|
||||
end
|
||||
tiles[29] = function(t) bunting(t, "red") end
|
||||
tiles[30] = function(t) bunting(t, "yellow") end
|
||||
tiles[31] = function(t) bunting(t, "blue") end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 2 (ids 32-47) : buildings / well / mailbox / noticeboard
|
||||
-- ===========================================================================
|
||||
local function creamWall(t)
|
||||
t.rectn(0, 0, 16, 16, "cream")
|
||||
t.hln(0, 0, 16, "cream_d"); t.hln(0, 8, 16, "cream_d")
|
||||
t.vln(0, 0, 8, "cream_d"); t.vln(8, 0, 8, "cream_d")
|
||||
t.vln(4, 8, 8, "cream_d"); t.vln(12, 8, 8, "cream_d")
|
||||
t.pn(2, 4, "cream_dd"); t.pn(10, 12, "cream_dd")
|
||||
end
|
||||
tiles[32] = function(t) creamWall(t) end
|
||||
tiles[33] = function(t)
|
||||
creamWall(t)
|
||||
t.rectn(4, 3, 8, 9, "cream") -- clear brick under window
|
||||
H.window(t, 4, 3, 8, 9, "sky")
|
||||
end
|
||||
|
||||
tiles[34] = function(t) -- wood plank wall
|
||||
t.rectn(0, 0, 16, 16, "wood")
|
||||
for y = 0, 15, 4 do
|
||||
t.hln(0, y, 16, "wood_d"); t.hln(0, y + 1, 16, "wood_l")
|
||||
end
|
||||
t.pn(2, 3, "wood_dd"); t.pn(13, 3, "wood_dd"); t.pn(2, 7, "wood_dd")
|
||||
t.pn(13, 7, "wood_dd"); t.pn(2, 11, "wood_dd"); t.pn(13, 11, "wood_dd"); t.pn(2, 15, "wood_dd")
|
||||
end
|
||||
|
||||
tiles[35] = function(t) roof(t, "honey", "honey_d", "honey_l", "L") end
|
||||
tiles[36] = function(t) roof(t, "honey", "honey_d", "honey_l", nil) end
|
||||
tiles[37] = function(t) roof(t, "honey", "honey_d", "honey_l", "R") end
|
||||
|
||||
tiles[38] = function(t) -- door (lower)(两侧补墙色:室内门放在 ground 层,透明边会露黑底)
|
||||
t.rectn(0, 0, 3, 16, "cream"); t.rectn(13, 0, 3, 16, "cream")
|
||||
t.vln(3, 0, 16, "brown_d"); t.vln(12, 0, 16, "brown_d")
|
||||
t.rectn(4, 0, 8, 16, "wood")
|
||||
t.rectn(5, 2, 6, 5, "wood_d"); t.rectn(5, 9, 6, 5, "wood_d")
|
||||
t.vln(5, 0, 16, "wood_l")
|
||||
t.pn(10, 7, "yellow"); t.pn(10, 8, "yellow_d")
|
||||
t.hln(4, 15, 8, "wood_d")
|
||||
end
|
||||
|
||||
tiles[39] = function(t) -- door frame top (arch), sits above tile 38
|
||||
t.vln(3, 4, 12, "brown_d"); t.vln(12, 4, 12, "brown_d")
|
||||
t.rectn(4, 6, 8, 10, "wood")
|
||||
t.vln(5, 6, 10, "wood_l")
|
||||
-- arch
|
||||
t.discn(8, 6, 5, "brown_d")
|
||||
t.discn(8, 6, 4, "sky")
|
||||
t.rectn(4, 6, 8, 3, "sky")
|
||||
t.discn(8, 6, 3, "sky")
|
||||
t.pn(6, 5, "sky_d"); t.pn(9, 4, "sky_d")
|
||||
t.hln(4, 6, 8, "brown_d")
|
||||
end
|
||||
|
||||
tiles[40] = function(t) -- standalone window (transparent surround)
|
||||
H.window(t, 3, 4, 10, 8, "sky")
|
||||
t.pn(4, 5, "white")
|
||||
end
|
||||
|
||||
tiles[41] = function(t) -- chimney
|
||||
t.rectn(5, 3, 6, 13, "swall_d")
|
||||
t.hln(5, 6, 6, "swall_dd"); t.hln(5, 9, 6, "swall_dd"); t.hln(5, 12, 6, "swall_dd")
|
||||
t.vln(7, 3, 3, "swall_dd"); t.vln(6, 6, 3, "swall_dd"); t.vln(8, 9, 3, "swall_dd")
|
||||
t.rectn(4, 2, 8, 2, "swall")
|
||||
t.hln(4, 2, 8, "swall_l")
|
||||
t.discn(6, 1, 1, "w_mid"); t.discn(9, 0, 1, "w_lo")
|
||||
end
|
||||
|
||||
-- well (42-45) drawn once across its 2x2 block (origin of tile 42)
|
||||
local function drawWell(img)
|
||||
local w = H.at(img, (42 % 16) * 16, math.floor(42 / 16) * 16)
|
||||
w.ellipsen(16, 28, 12, 3, "swall_dd")
|
||||
w.discn(16, 16, 12, "swall")
|
||||
w.discn(16, 14, 11, "swall_l")
|
||||
w.discn(16, 19, 10, "swall_d")
|
||||
w.discn(16, 15, 7, "swall_dd")
|
||||
w.discn(16, 16, 6, "black")
|
||||
w.discn(16, 17, 4, "water_d")
|
||||
w.rectn(3, 9, 26, 2, "wood_d")
|
||||
w.hln(3, 9, 26, "wood")
|
||||
w.vln(16, 11, 7, "brown_dd")
|
||||
w.rectn(14, 17, 5, 4, "wood")
|
||||
w.framen(14, 17, 5, 4, "wood_d")
|
||||
end
|
||||
|
||||
tiles[46] = function(t) -- mailbox
|
||||
t.rectn(6, 9, 2, 7, "brown_d")
|
||||
t.rectn(2, 15, 6, 1, "brown_dd")
|
||||
t.rectn(3, 3, 10, 6, "red")
|
||||
t.framen(3, 3, 10, 6, "red_d")
|
||||
t.rectn(3, 3, 10, 2, "red_l")
|
||||
t.pn(7, 6, "yellow"); t.pn(8, 6, "yellow")
|
||||
t.rectn(5, 8, 6, 1, "red_d")
|
||||
end
|
||||
|
||||
tiles[47] = function(t) -- notice board
|
||||
t.rectn(3, 12, 2, 4, "brown_d"); t.rectn(11, 12, 2, 4, "brown_d")
|
||||
t.rectn(1, 1, 14, 2, "brown_d")
|
||||
t.rectn(2, 2, 12, 10, "wood")
|
||||
t.framen(2, 2, 12, 10, "brown_d")
|
||||
t.rectn(4, 4, 3, 4, "cream"); t.rectn(8, 4, 4, 3, "cream")
|
||||
t.rectn(4, 8, 6, 3, "cream_d")
|
||||
t.pn(5, 5, "ink"); t.pn(6, 5, "ink"); t.pn(9, 5, "ink"); t.pn(10, 5, "ink"); t.pn(5, 9, "ink"); t.pn(7, 9, "ink")
|
||||
end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 3 (ids 48-63) : interior floor / walls / furniture
|
||||
-- ===========================================================================
|
||||
tiles[48] = function(t) -- wood floor A (horizontal planks)
|
||||
t.rectn(0, 0, 16, 16, "wood_l")
|
||||
for y = 0, 15, 4 do t.hln(0, y, 16, "wood") end
|
||||
t.pn(3, 2, "wood"); t.pn(10, 6, "wood"); t.pn(6, 10, "wood"); t.pn(13, 14, "wood")
|
||||
end
|
||||
|
||||
tiles[49] = function(t) -- wood floor B (vertical planks)
|
||||
t.rectn(0, 0, 16, 16, "wood_l")
|
||||
for x = 0, 15, 4 do t.vln(x, 0, 16, "wood") end
|
||||
t.pn(2, 3, "wood"); t.pn(6, 9, "wood"); t.pn(10, 5, "wood"); t.pn(14, 12, "wood")
|
||||
end
|
||||
|
||||
local function indoorWall(t)
|
||||
t.rectn(0, 0, 16, 16, "cream")
|
||||
t.hln(0, 12, 16, "cream_d")
|
||||
t.rectn(0, 13, 16, 3, "wood_d")
|
||||
t.hln(0, 13, 16, "wood_l")
|
||||
end
|
||||
tiles[50] = function(t) indoorWall(t) end
|
||||
tiles[51] = function(t)
|
||||
t.rectn(0, 0, 16, 16, "cream")
|
||||
H.window(t, 4, 2, 8, 8, "sky")
|
||||
t.hln(0, 12, 16, "cream_d")
|
||||
t.rectn(0, 13, 16, 3, "wood_d")
|
||||
t.hln(0, 13, 16, "wood_l")
|
||||
end
|
||||
|
||||
-- bed (52-55) drawn once across 2x2 (origin of tile 52)
|
||||
local function drawBed(img)
|
||||
local b = H.at(img, (52 % 16) * 16, math.floor(52 / 16) * 16)
|
||||
b.rectn(1, 2, 30, 29, "wood_d")
|
||||
b.framen(1, 2, 30, 29, "brown_dd")
|
||||
b.rectn(1, 2, 30, 5, "wood")
|
||||
b.framen(1, 2, 30, 5, "wood_d")
|
||||
b.rectn(3, 7, 26, 22, "cream")
|
||||
b.rectn(4, 8, 24, 6, "white"); b.framen(4, 8, 24, 6, "cream_d")
|
||||
b.rectn(3, 16, 26, 13, "blue")
|
||||
b.framen(3, 16, 26, 13, "blue_d")
|
||||
b.rectn(3, 16, 26, 2, "blue_l")
|
||||
b.rectn(1, 28, 30, 3, "wood")
|
||||
end
|
||||
|
||||
local function counter(t)
|
||||
t.rectn(0, 0, 16, 3, "cream")
|
||||
t.hln(0, 2, 16, "cream_d")
|
||||
t.rectn(0, 3, 16, 13, "wood")
|
||||
t.vln(0, 3, 13, "wood_d"); t.vln(8, 3, 13, "wood_d")
|
||||
t.rectn(2, 8, 12, 5, "wood_d"); t.framen(2, 8, 12, 5, "wood_dd")
|
||||
t.hln(0, 3, 16, "wood_l")
|
||||
end
|
||||
tiles[56] = function(t) counter(t) end
|
||||
tiles[57] = function(t)
|
||||
counter(t)
|
||||
t.rectn(9, 0, 5, 2, "honey") -- a little bread/register on the right half
|
||||
t.pn(10, 0, "honey_d"); t.pn(12, 0, "honey_d")
|
||||
end
|
||||
|
||||
tiles[58] = function(t) -- oven
|
||||
t.rectn(1, 2, 14, 14, "swall_d")
|
||||
t.framen(1, 2, 14, 14, "swall_dd")
|
||||
t.hln(1, 5, 14, "swall_dd"); t.hln(1, 13, 14, "swall_dd")
|
||||
t.discn(8, 9, 5, "swall_dd")
|
||||
t.rectn(3, 9, 10, 6, "black"); t.discn(8, 9, 5, "black")
|
||||
t.discn(8, 11, 3, "flame_d"); t.discn(8, 11, 2, "flame")
|
||||
t.rectn(1, 2, 14, 2, "swall")
|
||||
end
|
||||
|
||||
local function shelf(t, full)
|
||||
t.rectn(1, 2, 14, 14, "wood")
|
||||
t.framen(1, 2, 14, 14, "brown_d")
|
||||
t.rectn(2, 3, 12, 12, "wood_d")
|
||||
t.hln(2, 7, 12, "brown_d"); t.hln(2, 11, 12, "brown_d")
|
||||
if full then
|
||||
t.rectn(3, 4, 2, 3, "honey"); t.rectn(6, 4, 2, 3, "pink"); t.rectn(9, 4, 3, 3, "sky")
|
||||
t.rectn(3, 8, 3, 3, "red"); t.rectn(7, 8, 2, 3, "yellow"); t.rectn(10, 8, 3, 3, "leaf")
|
||||
t.rectn(4, 12, 2, 3, "brown_l"); t.rectn(8, 12, 4, 3, "honey_d")
|
||||
end
|
||||
end
|
||||
tiles[59] = function(t) shelf(t, false) end
|
||||
tiles[60] = function(t) shelf(t, true) end
|
||||
|
||||
tiles[61] = function(t) -- table
|
||||
t.rectn(1, 4, 14, 4, "wood")
|
||||
t.hln(1, 4, 14, "wood_l"); t.hln(1, 7, 14, "wood_d")
|
||||
t.rectn(2, 8, 2, 7, "wood_d"); t.rectn(12, 8, 2, 7, "wood_d")
|
||||
t.hln(2, 12, 12, "wood_d")
|
||||
end
|
||||
|
||||
tiles[62] = function(t) -- chair
|
||||
t.rectn(4, 2, 8, 2, "wood"); t.hln(4, 2, 8, "wood_l")
|
||||
t.rectn(4, 2, 2, 8, "wood_d")
|
||||
t.rectn(4, 7, 8, 3, "wood"); t.hln(4, 9, 8, "wood_d")
|
||||
t.rectn(4, 10, 2, 5, "wood_d"); t.rectn(10, 10, 2, 5, "wood_d")
|
||||
end
|
||||
|
||||
tiles[63] = function(t) -- rug(烘焙木地板底:放在 ground 层,透明边会露黑底)
|
||||
t.rectn(0, 0, 16, 16, "wood_l")
|
||||
for y = 0, 15, 4 do t.hln(0, y, 16, "wood") end
|
||||
t.rectn(1, 3, 14, 10, "pink")
|
||||
t.framen(1, 3, 14, 10, "pink_d")
|
||||
t.framen(3, 5, 10, 6, "cream")
|
||||
t.discn(8, 8, 2, "honey"); t.pn(8, 8, "honey_d")
|
||||
end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 4 (ids 64-79) : papers / picnic / bench / appliances
|
||||
-- ===========================================================================
|
||||
tiles[64] = function(t) -- calendar
|
||||
t.rectn(4, 2, 8, 11, "white"); t.framen(4, 2, 8, 11, "grey_d")
|
||||
t.rectn(4, 2, 8, 2, "red"); t.pn(6, 3, "white"); t.pn(7, 3, "white")
|
||||
t.hln(5, 6, 6, "grey"); t.hln(5, 8, 6, "grey"); t.hln(5, 10, 6, "grey")
|
||||
t.vln(7, 5, 6, "grey"); t.vln(9, 5, 6, "grey")
|
||||
t.pn(6, 9, "red"); t.pn(6, 10, "red")
|
||||
end
|
||||
|
||||
tiles[65] = function(t) -- recipe paper
|
||||
t.rectn(4, 3, 8, 10, "cream"); t.framen(4, 3, 8, 10, "cream_d")
|
||||
t.hln(5, 5, 6, "brown_d"); t.hln(5, 7, 6, "brown_d"); t.hln(5, 9, 4, "brown_d"); t.hln(5, 11, 5, "brown_d")
|
||||
t.pn(10, 11, "honey"); t.pn(11, 10, "honey_d")
|
||||
end
|
||||
|
||||
tiles[66] = function(t) -- document paper
|
||||
t.rectn(4, 2, 8, 12, "white"); t.framen(4, 2, 8, 12, "grey")
|
||||
for y = 4, 9, 2 do t.hln(5, y, 6, "grey_d") end
|
||||
t.discn(9, 11, 2, "red"); t.pn(9, 11, "white")
|
||||
end
|
||||
|
||||
-- picnic mat (67-70) drawn once across 2x2 (origin of tile 67)
|
||||
local function drawPicnic(img)
|
||||
local m = H.at(img, (67 % 16) * 16, math.floor(67 / 16) * 16)
|
||||
for y = 0, 31, 4 do
|
||||
for x = 0, 31, 4 do
|
||||
local c = (((x / 4) + (y / 4)) % 2 == 0) and "red" or "white"
|
||||
m.rectn(x, y, 4, 4, c)
|
||||
end
|
||||
end
|
||||
m.framen(0, 0, 32, 32, "red_d")
|
||||
m.framen(1, 1, 30, 30, "red_l")
|
||||
end
|
||||
|
||||
local function bench(t)
|
||||
t.rectn(0, 2, 16, 2, "wood"); t.hln(0, 2, 16, "wood_l")
|
||||
t.rectn(0, 5, 16, 2, "wood")
|
||||
t.rectn(0, 9, 16, 3, "wood_l"); t.hln(0, 11, 16, "wood_d")
|
||||
t.rectn(1, 12, 2, 4, "wood_d"); t.rectn(13, 12, 2, 4, "wood_d")
|
||||
t.vln(1, 2, 3, "wood_d"); t.vln(14, 2, 3, "wood_d")
|
||||
end
|
||||
tiles[71] = function(t) bench(t) end
|
||||
tiles[72] = function(t) bench(t) end
|
||||
|
||||
tiles[73] = function(t) -- stool
|
||||
t.rectn(4, 6, 8, 3, "wood"); t.hln(4, 6, 8, "wood_l"); t.hln(4, 8, 8, "wood_d")
|
||||
t.rectn(5, 9, 2, 6, "wood_d"); t.rectn(9, 9, 2, 6, "wood_d")
|
||||
t.hln(5, 12, 6, "wood_d")
|
||||
end
|
||||
|
||||
tiles[74] = function(t) -- cabinet
|
||||
t.rectn(2, 2, 12, 14, "wood"); t.framen(2, 2, 12, 14, "brown_d")
|
||||
t.vln(8, 3, 12, "brown_d")
|
||||
t.rectn(3, 4, 4, 5, "wood_d"); t.rectn(9, 4, 4, 5, "wood_d")
|
||||
t.pn(7, 10, "yellow"); t.pn(9, 10, "yellow")
|
||||
t.hln(2, 2, 12, "wood_l")
|
||||
end
|
||||
|
||||
tiles[75] = function(t) -- fridge
|
||||
t.rectn(3, 1, 10, 15, "white"); t.framen(3, 1, 10, 15, "grey")
|
||||
t.hln(3, 6, 10, "grey")
|
||||
t.rectn(11, 2, 1, 4, "grey_d"); t.rectn(11, 7, 1, 8, "grey_d")
|
||||
t.hln(4, 2, 8, "grey_l")
|
||||
end
|
||||
|
||||
tiles[76] = function(t) -- sink
|
||||
t.rectn(1, 6, 14, 10, "metal"); t.framen(1, 6, 14, 10, "metal_d")
|
||||
t.rectn(3, 8, 10, 5, "grey_d")
|
||||
t.rectn(4, 9, 8, 3, "water_l"); t.hln(4, 9, 8, "water")
|
||||
t.rectn(7, 3, 2, 4, "metal_d"); t.pn(7, 3, "metal"); t.pn(6, 3, "metal")
|
||||
t.hln(2, 7, 12, "grey_l")
|
||||
end
|
||||
|
||||
tiles[77] = function(t) -- potted plant
|
||||
t.discn(8, 6, 4, "leaf"); t.discn(7, 5, 2, "leaf_l"); t.discn(9, 7, 2, "leaf_d"); t.pn(8, 3, "leaf")
|
||||
t.rectn(5, 10, 6, 6, "brown"); t.framen(5, 10, 6, 6, "brown_d"); t.hln(5, 10, 6, "brown_l")
|
||||
end
|
||||
|
||||
tiles[78] = function(t) -- barrel
|
||||
t.ellipsen(8, 8, 6, 7, "wood")
|
||||
t.rectn(2, 1, 12, 14, "wood"); t.ellipsen(8, 8, 6, 7, "wood")
|
||||
t.hln(3, 4, 10, "wood_d"); t.hln(3, 11, 10, "wood_d")
|
||||
t.vln(8, 2, 12, "wood_d")
|
||||
t.framen(2, 1, 12, 14, "wood_d")
|
||||
t.hln(4, 2, 8, "wood_l")
|
||||
end
|
||||
|
||||
tiles[79] = function(t) -- crate
|
||||
t.rectn(2, 3, 12, 11, "wood"); t.framen(2, 3, 12, 11, "wood_d")
|
||||
t.vln(8, 3, 11, "wood_d"); t.hln(2, 8, 12, "wood_d")
|
||||
t.pn(3, 4, "wood_dd"); t.pn(12, 4, "wood_dd"); t.pn(3, 12, "wood_dd"); t.pn(12, 12, "wood_dd")
|
||||
t.hln(3, 4, 10, "wood_l")
|
||||
end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 5 (ids 80-95) : fog / white-layer
|
||||
-- ===========================================================================
|
||||
local function fog(t, seed)
|
||||
local a1, a2 = 150, 110
|
||||
if seed == 0 then
|
||||
t.ellipse(8, 8, 7, 3, H.col("w_hi", a1))
|
||||
t.ellipse(5, 6, 4, 2, H.col("w_mid", a2))
|
||||
t.ellipse(11, 10, 4, 2, H.col("w_mid", a2))
|
||||
elseif seed == 1 then
|
||||
t.ellipse(7, 9, 6, 3, H.col("w_hi", a1))
|
||||
t.ellipse(11, 7, 4, 2, H.col("w_mid", a2))
|
||||
t.ellipse(4, 11, 3, 2, H.col("w_mid", a2))
|
||||
else
|
||||
t.ellipse(9, 7, 7, 3, H.col("w_hi", a1))
|
||||
t.ellipse(5, 9, 4, 2, H.col("w_mid", a2))
|
||||
t.ellipse(12, 11, 3, 2, H.col("w_mid", a2))
|
||||
end
|
||||
end
|
||||
tiles[80] = function(t) fog(t, 0) end
|
||||
tiles[81] = function(t) fog(t, 1) end
|
||||
tiles[82] = function(t) fog(t, 2) end
|
||||
|
||||
tiles[83] = function(t) -- white floor
|
||||
t.rectn(0, 0, 16, 16, "w_hi")
|
||||
t.hln(0, 0, 16, "w_mid"); t.hln(0, 8, 16, "w_mid")
|
||||
t.vln(0, 0, 16, "w_mid"); t.vln(8, 0, 16, "w_mid")
|
||||
t.pn(4, 4, "w_lo"); t.pn(12, 12, "w_lo"); t.pn(12, 4, "w_lo"); t.pn(4, 12, "w_lo")
|
||||
end
|
||||
|
||||
tiles[84] = function(t) -- white wall
|
||||
t.rectn(0, 0, 16, 16, "w_hi")
|
||||
t.hln(0, 0, 16, "w_lo"); t.hln(0, 8, 16, "w_lo")
|
||||
t.vln(0, 0, 16, "w_lo"); t.vln(8, 0, 16, "w_lo")
|
||||
t.rectn(0, 14, 16, 2, "w_mid"); t.hln(0, 14, 16, "w_hi")
|
||||
end
|
||||
|
||||
tiles[85] = function(t) -- white wall panel
|
||||
t.rectn(0, 0, 16, 16, "w_hi")
|
||||
t.framen(2, 2, 12, 11, "w_lo")
|
||||
t.vln(8, 2, 11, "w_mid")
|
||||
t.rectn(0, 14, 16, 2, "w_mid")
|
||||
end
|
||||
|
||||
tiles[86] = function(t) -- white door
|
||||
t.rectn(0, 0, 16, 16, "w_hi")
|
||||
t.rectn(3, 1, 10, 15, "w_mid"); t.framen(3, 1, 10, 15, "w_lo")
|
||||
t.rectn(4, 2, 8, 6, "w_hi"); t.framen(4, 2, 8, 6, "w_lo")
|
||||
t.rectn(4, 9, 8, 6, "w_hi"); t.framen(4, 9, 8, 6, "w_lo")
|
||||
t.pn(11, 8, "w_dk"); t.pn(11, 9, "w_dk")
|
||||
end
|
||||
|
||||
-- white hospital bed (87-90) across 2x2 (origin of tile 87)
|
||||
local function drawWhiteBed(img)
|
||||
local b = H.at(img, (87 % 16) * 16, math.floor(87 / 16) * 16)
|
||||
b.rectn(1, 2, 30, 27, "metal_d")
|
||||
b.rectn(3, 4, 26, 23, "white"); b.framen(3, 4, 26, 23, "w_lo")
|
||||
b.rectn(4, 6, 24, 6, "w_hi"); b.framen(4, 6, 24, 6, "w_mid")
|
||||
b.rectn(3, 17, 26, 2, "w_mid")
|
||||
b.rectn(1, 2, 30, 3, "metal"); b.rectn(1, 26, 30, 3, "metal")
|
||||
b.rectn(1, 2, 2, 27, "metal"); b.rectn(29, 2, 2, 27, "metal")
|
||||
b.rectn(2, 29, 3, 2, "w_dk"); b.rectn(27, 29, 3, 2, "w_dk")
|
||||
end
|
||||
|
||||
tiles[91] = function(t) -- white cabinet
|
||||
t.rectn(2, 2, 12, 14, "w_mid"); t.framen(2, 2, 12, 14, "w_lo")
|
||||
t.vln(8, 3, 12, "w_lo")
|
||||
t.rectn(3, 4, 4, 4, "w_hi"); t.rectn(9, 4, 4, 4, "w_hi")
|
||||
t.pn(7, 10, "w_dk"); t.pn(9, 10, "w_dk")
|
||||
t.hln(2, 2, 12, "w_hi")
|
||||
end
|
||||
|
||||
tiles[92] = function(t) -- white monitor (medical)
|
||||
t.rectn(3, 3, 10, 7, "w_lo"); t.framen(3, 3, 10, 7, "w_dk")
|
||||
t.rectn(4, 4, 8, 5, "screen")
|
||||
t.pn(4, 6, "screen_l"); t.pn(5, 6, "screen_l"); t.pn(6, 5, "screen_l")
|
||||
t.pn(7, 7, "screen_l"); t.pn(8, 6, "screen_l"); t.pn(9, 6, "screen_l"); t.pn(10, 6, "screen_l")
|
||||
t.rectn(7, 10, 2, 3, "w_dk"); t.rectn(5, 13, 6, 2, "w_lo"); t.hln(5, 14, 6, "w_dk")
|
||||
end
|
||||
|
||||
tiles[93] = function(t) -- white curtain
|
||||
t.rectn(2, 1, 12, 14, "w_hi")
|
||||
t.vln(4, 1, 14, "w_mid"); t.vln(7, 1, 14, "w_mid"); t.vln(10, 1, 14, "w_mid"); t.vln(12, 1, 14, "w_lo")
|
||||
t.rectn(2, 1, 12, 1, "w_lo")
|
||||
t.hln(2, 14, 12, "w_mid")
|
||||
end
|
||||
|
||||
tiles[94] = function(t) -- white floor alt
|
||||
t.rectn(0, 0, 16, 16, "w_hi")
|
||||
for y = 0, 15, 4 do
|
||||
for x = 0, 15, 4 do
|
||||
t.pn((x + 2) % 16, (y + 2) % 16, "w_lo")
|
||||
end
|
||||
end
|
||||
t.pn(2, 2, "w_mid"); t.pn(10, 10, "w_mid")
|
||||
end
|
||||
|
||||
tiles[95] = function(t) -- white ceiling light
|
||||
t.rectn(2, 6, 12, 4, "white"); t.framen(2, 6, 12, 4, "w_lo")
|
||||
t.rectn(3, 7, 10, 2, "w_hi")
|
||||
t.pn(7, 8, "white")
|
||||
end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 6 (ids 96-111) : ladder / bridge / props
|
||||
-- ===========================================================================
|
||||
tiles[96] = function(t) -- ladder
|
||||
t.vln(4, 0, 16, "wood_d"); t.vln(11, 0, 16, "wood_d")
|
||||
for y = 2, 14, 3 do t.hln(4, y, 8, "wood"); t.hln(4, y, 8, "wood_l") end
|
||||
end
|
||||
|
||||
tiles[97] = function(t) -- bridge horizontal
|
||||
for y = 3, 12, 3 do
|
||||
t.rectn(0, y, 16, 2, "wood"); t.hln(0, y, 16, "wood_l"); t.hln(0, y + 1, 16, "wood_d")
|
||||
end
|
||||
t.rectn(0, 1, 16, 1, "wood_d"); t.rectn(0, 14, 16, 1, "wood_d")
|
||||
t.vln(0, 1, 14, "wood_d"); t.vln(15, 1, 14, "wood_d")
|
||||
end
|
||||
|
||||
tiles[98] = function(t) -- bridge vertical
|
||||
for x = 3, 12, 3 do
|
||||
t.rectn(x, 0, 2, 16, "wood"); t.vln(x, 0, 16, "wood_l"); t.vln(x + 1, 0, 16, "wood_d")
|
||||
end
|
||||
t.rectn(1, 0, 1, 16, "wood_d"); t.rectn(14, 0, 1, 16, "wood_d")
|
||||
t.hln(1, 0, 14, "wood_d"); t.hln(1, 15, 14, "wood_d")
|
||||
end
|
||||
|
||||
tiles[99] = function(t) -- mushroom(烘焙草地底:放在 ground 层,透明区会露黑底)
|
||||
grassBase(t)
|
||||
t.rectn(7, 8, 2, 6, "cream"); t.vln(7, 8, 6, "cream_d")
|
||||
t.discn(8, 6, 5, "red"); t.rectn(3, 6, 10, 2, "red")
|
||||
t.discn(8, 5, 4, "red_l")
|
||||
t.pn(6, 5, "white"); t.pn(9, 4, "white"); t.pn(10, 7, "white"); t.pn(5, 7, "white")
|
||||
end
|
||||
|
||||
local function bigflower(t, c)
|
||||
grassBase(t) -- 烘焙草地底:该瓦片放在 ground 层,透明区会露出黑底
|
||||
t.vln(8, 9, 6, "leaf"); t.pn(6, 12, "leaf"); t.pn(10, 11, "leaf"); t.pn(7, 13, "leaf_d")
|
||||
t.discn(8, 6, 3, c)
|
||||
t.discn(8, 6, 1, "yellow")
|
||||
t.pn(8, 6, "honey_d")
|
||||
end
|
||||
tiles[100] = function(t) bigflower(t, "red") end
|
||||
tiles[101] = function(t) bigflower(t, "yellow") end
|
||||
tiles[102] = function(t) bigflower(t, "blue") end
|
||||
|
||||
tiles[103] = function(t) -- honey jar
|
||||
t.rectn(5, 4, 6, 10, "honey"); t.framen(5, 4, 6, 10, "honey_d")
|
||||
t.rectn(5, 4, 6, 2, "honey_l")
|
||||
t.rectn(4, 2, 8, 2, "brown_d"); t.hln(4, 2, 8, "brown")
|
||||
t.rectn(6, 8, 4, 4, "honey_l"); t.pn(8, 10, "honey_dd")
|
||||
end
|
||||
|
||||
tiles[104] = function(t) -- bread
|
||||
t.ellipsen(8, 10, 7, 4, "honey")
|
||||
t.ellipsen(8, 9, 6, 3, "honey_l")
|
||||
t.pn(5, 9, "honey_d"); t.pn(8, 8, "honey_d"); t.pn(11, 9, "honey_d")
|
||||
t.hln(3, 12, 10, "honey_d")
|
||||
end
|
||||
|
||||
tiles[105] = function(t) -- letter
|
||||
t.rectn(3, 5, 10, 7, "cream"); t.framen(3, 5, 10, 7, "cream_d")
|
||||
for i = 0, 4 do t.pn(4 + i, 5 + i, "cream_d"); t.pn(11 - i, 5 + i, "cream_d") end
|
||||
t.pn(8, 9, "red")
|
||||
end
|
||||
|
||||
tiles[106] = function(t) -- book
|
||||
t.rectn(3, 4, 10, 9, "red"); t.framen(3, 4, 10, 9, "red_d")
|
||||
t.vln(8, 4, 9, "red_d"); t.hln(3, 4, 10, "red_l")
|
||||
t.rectn(4, 6, 3, 5, "red_d"); t.pn(5, 8, "yellow")
|
||||
end
|
||||
|
||||
tiles[107] = function(t) -- candle
|
||||
t.rectn(6, 6, 4, 9, "cream"); t.framen(6, 6, 4, 9, "cream_d")
|
||||
t.rectn(6, 6, 4, 1, "cream_d")
|
||||
t.pn(8, 5, "brown_d")
|
||||
t.discn(8, 3, 2, "flame"); t.pn(8, 2, "flame_d"); t.pn(8, 4, "flame_d")
|
||||
end
|
||||
|
||||
tiles[108] = function(t) -- basket
|
||||
t.rectn(3, 7, 10, 7, "wood"); t.framen(3, 7, 10, 7, "wood_d")
|
||||
t.hln(3, 9, 10, "wood_d"); t.hln(3, 11, 10, "wood_d")
|
||||
t.vln(6, 7, 7, "wood_d"); t.vln(9, 7, 7, "wood_d")
|
||||
t.vln(3, 4, 4, "wood_d"); t.vln(12, 4, 4, "wood_d"); t.hln(3, 4, 10, "wood_d")
|
||||
t.rectn(4, 5, 8, 2, "wood")
|
||||
end
|
||||
|
||||
tiles[109] = function(t) -- bucket
|
||||
t.rectn(4, 6, 8, 9, "metal"); t.framen(4, 6, 8, 9, "metal_d")
|
||||
t.rectn(4, 6, 8, 2, "water_l"); t.hln(4, 6, 8, "water")
|
||||
t.vln(4, 4, 3, "metal_d"); t.vln(11, 4, 3, "metal_d"); t.hln(4, 4, 8, "metal_d")
|
||||
t.hln(5, 10, 6, "metal_d")
|
||||
end
|
||||
|
||||
tiles[110] = function(t) -- broom
|
||||
t.vln(10, 1, 8, "wood_d"); t.vln(9, 1, 8, "wood")
|
||||
t.rectn(4, 9, 7, 6, "yellow_d"); t.framen(4, 9, 7, 6, "honey_d")
|
||||
t.hln(4, 9, 7, "brown_d")
|
||||
t.pn(3, 14, "yellow_d"); t.pn(11, 14, "yellow_d"); t.pn(4, 15, "honey_d"); t.pn(10, 15, "honey_d")
|
||||
end
|
||||
|
||||
tiles[111] = function(t) -- cake
|
||||
t.rectn(3, 8, 10, 6, "pink"); t.framen(3, 8, 10, 6, "pink_d")
|
||||
t.rectn(3, 8, 10, 2, "white")
|
||||
t.pn(4, 10, "pink_d"); t.pn(7, 10, "pink_d"); t.pn(10, 10, "pink_d")
|
||||
t.rectn(3, 14, 10, 1, "cream_d")
|
||||
t.vln(6, 5, 3, "red"); t.vln(9, 5, 3, "blue")
|
||||
t.pn(6, 4, "flame"); t.pn(9, 4, "flame")
|
||||
end
|
||||
|
||||
-- ===========================================================================
|
||||
-- ROW 7 (ids 112-127) : colored roofs / walls / misc
|
||||
-- ===========================================================================
|
||||
tiles[112] = function(t) roof(t, "red", "red_d", "red_l", "L") end
|
||||
tiles[113] = function(t) roof(t, "red", "red_d", "red_l", nil) end
|
||||
tiles[114] = function(t) roof(t, "red", "red_d", "red_l", "R") end
|
||||
tiles[115] = function(t) roof(t, "blue", "blue_d", "blue_l", "L") end
|
||||
tiles[116] = function(t) roof(t, "blue", "blue_d", "blue_l", nil) end
|
||||
tiles[117] = function(t) roof(t, "blue", "blue_d", "blue_l", "R") end
|
||||
|
||||
local function pinkWall(t)
|
||||
t.rectn(0, 0, 16, 16, "pink")
|
||||
t.hln(0, 0, 16, "pink_d"); t.hln(0, 8, 16, "pink_d")
|
||||
t.vln(0, 0, 8, "pink_d"); t.vln(8, 0, 8, "pink_d")
|
||||
t.vln(4, 8, 8, "pink_d"); t.vln(12, 8, 8, "pink_d")
|
||||
t.pn(2, 4, "pink_l"); t.pn(10, 12, "pink_l")
|
||||
end
|
||||
tiles[118] = function(t) pinkWall(t) end
|
||||
tiles[119] = function(t)
|
||||
pinkWall(t)
|
||||
t.rectn(4, 3, 8, 9, "pink")
|
||||
H.window(t, 4, 3, 8, 9, "sky")
|
||||
end
|
||||
|
||||
local function stoneWall(t)
|
||||
t.rectn(0, 0, 16, 16, "swall")
|
||||
t.hln(0, 0, 16, "swall_d"); t.hln(0, 5, 16, "swall_d"); t.hln(0, 10, 16, "swall_d")
|
||||
t.vln(0, 0, 5, "swall_d"); t.vln(8, 0, 5, "swall_d")
|
||||
t.vln(4, 5, 5, "swall_d"); t.vln(12, 5, 5, "swall_d")
|
||||
t.vln(0, 10, 6, "swall_d"); t.vln(8, 10, 6, "swall_d")
|
||||
t.pn(3, 2, "swall_l"); t.pn(10, 7, "swall_l"); t.pn(6, 12, "swall_l")
|
||||
end
|
||||
tiles[120] = function(t) stoneWall(t) end
|
||||
tiles[121] = function(t)
|
||||
stoneWall(t)
|
||||
t.rectn(4, 3, 8, 9, "swall")
|
||||
H.window(t, 4, 3, 8, 9, "sky")
|
||||
end
|
||||
|
||||
tiles[122] = function(t) -- awning
|
||||
for x = 0, 15 do
|
||||
local c = (math.floor(x / 2) % 2 == 0) and "red" or "white"
|
||||
t.vln(x, 0, 6, c)
|
||||
end
|
||||
t.hln(0, 6, 16, "red_d")
|
||||
for x = 0, 15 do if x % 4 < 2 then t.pn(x, 7, "red") else t.pn(x, 7, "red_d") end end
|
||||
t.hln(0, 0, 16, "red_l")
|
||||
end
|
||||
|
||||
tiles[123] = function(t) -- clock
|
||||
t.discn(8, 8, 6, "brown_d"); t.discn(8, 8, 5, "cream")
|
||||
t.pn(8, 3, "brown_d"); t.pn(8, 13, "brown_d"); t.pn(3, 8, "brown_d"); t.pn(13, 8, "brown_d")
|
||||
t.vln(8, 4, 4, "ink"); t.hln(8, 8, 3, "ink"); t.pn(8, 8, "ink")
|
||||
end
|
||||
|
||||
tiles[124] = function(t) -- flag
|
||||
t.vln(3, 1, 15, "grey_d")
|
||||
t.rectn(4, 2, 9, 6, "red"); t.rectn(4, 2, 9, 2, "red_l")
|
||||
t.pn(13, 3, "red"); t.pn(13, 4, "red")
|
||||
t.rectn(2, 15, 3, 1, "grey")
|
||||
t.pn(4, 4, "yellow")
|
||||
end
|
||||
|
||||
tiles[125] = function(t) -- garland corner
|
||||
for x = 1, 12 do
|
||||
t.pn(x, 2, "leaf"); t.pn(x, 3, "leaf_d")
|
||||
if x % 3 == 0 then H.flower(t, x, 2, "pink", "yellow") end
|
||||
end
|
||||
for y = 1, 12 do
|
||||
t.pn(2, y, "leaf"); t.pn(3, y, "leaf_d")
|
||||
if y % 3 == 1 then H.flower(t, 2, y, "yellow", "honey") end
|
||||
end
|
||||
end
|
||||
|
||||
tiles[126] = function(t) -- garland mid (horizontal, tileable)
|
||||
for x = 0, 15 do t.pn(x, 4, "leaf"); t.pn(x, 5, "leaf_d") end
|
||||
H.flower(t, 3, 3, "pink", "yellow")
|
||||
H.flower(t, 8, 4, "red", "yellow")
|
||||
H.flower(t, 13, 3, "yellow", "honey")
|
||||
end
|
||||
|
||||
-- tile 127 intentionally left transparent (empty)
|
||||
|
||||
-- ===========================================================================
|
||||
-- render
|
||||
-- ===========================================================================
|
||||
-- 2x2 拼块的自然落点是 id、id+1、id+16、id+17,但 G 表/地图引用连续 4 个 id
|
||||
-- (id..id+3)。故每画完一块,立即把下半两个象限复制到 id+2、id+3;
|
||||
-- 自然落点 id+16/id+17 的残留随后被 tiles 循环里已定义的瓦片重绘覆盖。
|
||||
local function copyBlock(img, tlId)
|
||||
local ox = (tlId % 16) * 16
|
||||
local oy = math.floor(tlId / 16) * 16
|
||||
for dy = 0, 15 do
|
||||
for dx = 0, 15 do
|
||||
img:putPixel(ox + 32 + dx, oy + dy, img:getPixel(ox + dx, oy + 16 + dy)) -- 自然 BL → 槽位 id+2
|
||||
img:putPixel(ox + 48 + dx, oy + dy, img:getPixel(ox + 16 + dx, oy + 16 + dy)) -- 自然 BR → 槽位 id+3
|
||||
end
|
||||
end
|
||||
end
|
||||
drawTree(img); copyBlock(img, 18)
|
||||
drawWell(img); copyBlock(img, 42)
|
||||
drawBed(img); copyBlock(img, 52) -- 须在 drawPicnic 之前复制:二者自然落点都覆盖 68/69
|
||||
drawPicnic(img); copyBlock(img, 67)
|
||||
drawWhiteBed(img); copyBlock(img, 87)
|
||||
|
||||
for id = 0, 127 do
|
||||
if tiles[id] then tiles[id](H.tile(img, id)) end
|
||||
end
|
||||
|
||||
spr:saveAs(ROOT .. "public/assets/tileset.png")
|
||||
print("tileset.png done")
|
||||
@@ -0,0 +1,229 @@
|
||||
-- helpers.lua — shared palette + pixel primitives for asset generation
|
||||
-- Usage: local H = dofile(ROOT .. "tools/aseprite/helpers.lua")
|
||||
-- All draw helpers operate on an Aseprite Image via a "pen" table bound to an origin.
|
||||
|
||||
local H = {}
|
||||
|
||||
H.ROOT = "/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/"
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Candy palette (CONTRACT §1) + white-layer grays + a few flat utility shades.
|
||||
-- Stored as {r,g,b}. No gradients, no AA — every pixel is one of these.
|
||||
-- ----------------------------------------------------------------------------
|
||||
H.P = {
|
||||
-- grass greens
|
||||
grass = {125, 201, 94}, -- #7DC95E
|
||||
grass_d = { 95, 179, 74}, -- #5FB34A
|
||||
grass_dd = { 74, 148, 58},
|
||||
grass_l = {158, 216, 128},
|
||||
-- cream
|
||||
cream = {255, 243, 214}, -- #FFF3D6
|
||||
cream_d = {238, 220, 178},
|
||||
cream_dd = {216, 196, 150},
|
||||
-- honey orange
|
||||
honey = {245, 166, 35}, -- #F5A623
|
||||
honey_d = {224, 143, 18},
|
||||
honey_dd = {196, 120, 10},
|
||||
honey_l = {250, 193, 96},
|
||||
-- sky blue
|
||||
sky = {143, 211, 255}, -- #8FD3FF
|
||||
sky_d = {110, 186, 240},
|
||||
-- stone road
|
||||
stone = {232, 220, 200}, -- #E8DCC8
|
||||
stone_d = {206, 190, 162},
|
||||
stone_dd = {178, 160, 130},
|
||||
stone_l = {245, 237, 224},
|
||||
-- lake water
|
||||
water = {110, 193, 228}, -- #6EC1E4
|
||||
water_d = { 79, 168, 216}, -- #4FA8D8
|
||||
water_dd = { 56, 140, 190},
|
||||
water_l = {158, 216, 242},
|
||||
-- pink
|
||||
pink = {247, 183, 203}, -- #F7B7CB
|
||||
pink_d = {232, 150, 176},
|
||||
pink_dd = {208, 120, 150},
|
||||
pink_l = {252, 208, 222},
|
||||
-- brown (trunk / wood dark)
|
||||
brown = {139, 90, 43}, -- #8B5A2B
|
||||
brown_d = {108, 68, 30},
|
||||
brown_dd = { 84, 52, 22},
|
||||
brown_l = {172, 122, 72},
|
||||
-- dirt road
|
||||
dirt = {205, 172, 122},
|
||||
dirt_d = {182, 148, 98},
|
||||
dirt_l = {220, 190, 146},
|
||||
-- white-layer grays (CONTRACT §1)
|
||||
w_hi = {245, 245, 245}, -- #F5F5F5
|
||||
w_mid = {218, 218, 218}, -- #DADADA
|
||||
w_lo = {189, 189, 189}, -- #BDBDBD
|
||||
w_dk = {150, 150, 150},
|
||||
w_dd = {110, 110, 110},
|
||||
-- reds
|
||||
red = {226, 87, 77},
|
||||
red_d = {198, 66, 56},
|
||||
red_l = {240, 130, 120},
|
||||
-- blues
|
||||
blue = { 91, 155, 213},
|
||||
blue_d = { 62, 125, 190},
|
||||
blue_l = {140, 190, 232},
|
||||
-- yellows
|
||||
yellow = {247, 214, 92},
|
||||
yellow_d = {224, 186, 55},
|
||||
yellow_l = {252, 228, 140},
|
||||
-- foliage
|
||||
leaf = { 86, 168, 78},
|
||||
leaf_d = { 64, 138, 60},
|
||||
leaf_dd = { 46, 108, 44},
|
||||
leaf_l = {120, 192, 110},
|
||||
-- wood
|
||||
wood = {201, 152, 96},
|
||||
wood_d = {176, 126, 74},
|
||||
wood_dd = {150, 104, 58},
|
||||
wood_l = {220, 178, 124},
|
||||
-- stone wall (cool gray)
|
||||
swall = {196, 190, 178},
|
||||
swall_d = {164, 158, 146},
|
||||
swall_dd = {134, 128, 116},
|
||||
swall_l = {214, 209, 200},
|
||||
-- berry
|
||||
berry = {206, 62, 96},
|
||||
berry_d = {172, 44, 76},
|
||||
-- misc
|
||||
skin = {255, 224, 190},
|
||||
skin_d = {238, 198, 156},
|
||||
hair = {120, 84, 52},
|
||||
hair_d = { 96, 66, 40},
|
||||
hair_l = {152, 112, 74},
|
||||
black = { 54, 44, 40},
|
||||
ink = { 40, 34, 30},
|
||||
white = {255, 255, 255},
|
||||
grey = {150, 150, 150},
|
||||
grey_d = {108, 108, 108},
|
||||
grey_l = {196, 196, 196},
|
||||
screen = { 44, 58, 54},
|
||||
screen_l = {120, 220, 160},
|
||||
metal = {178, 184, 190},
|
||||
metal_d = {140, 146, 152},
|
||||
flame = {255, 214, 92},
|
||||
flame_d = {245, 140, 40},
|
||||
leaf2 = { 60, 150, 70},
|
||||
}
|
||||
|
||||
-- make an Aseprite color from a palette name (or {r,g,b} table) + optional alpha
|
||||
function H.col(name, a)
|
||||
local t = name
|
||||
if type(name) == "string" then t = H.P[name] end
|
||||
return app.pixelColor.rgba(t[1], t[2], t[3], a or 255)
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Pen: a table of draw methods bound to an image + origin (ox, oy).
|
||||
-- All coordinates passed to pen methods are LOCAL to the origin.
|
||||
-- ----------------------------------------------------------------------------
|
||||
function H.at(img, ox, oy)
|
||||
local t = { img = img, ox = ox, oy = oy }
|
||||
function t.p(x, y, c) img:drawPixel(ox + x, oy + y, c) end
|
||||
function t.pn(x, y, name, a) img:drawPixel(ox + x, oy + y, H.col(name, a)) end
|
||||
function t.rect(x, y, w, h, c)
|
||||
for j = 0, h - 1 do for i = 0, w - 1 do img:drawPixel(ox + x + i, oy + y + j, c) end end
|
||||
end
|
||||
function t.rectn(x, y, w, h, name, a)
|
||||
local c = H.col(name, a)
|
||||
for j = 0, h - 1 do for i = 0, w - 1 do img:drawPixel(ox + x + i, oy + y + j, c) end end
|
||||
end
|
||||
function t.hl(x, y, len, c) for i = 0, len - 1 do img:drawPixel(ox + x + i, oy + y, c) end end
|
||||
function t.hln(x, y, len, name, a)
|
||||
local c = H.col(name, a); for i = 0, len - 1 do img:drawPixel(ox + x + i, oy + y, c) end
|
||||
end
|
||||
function t.vl(x, y, len, c) for j = 0, len - 1 do img:drawPixel(ox + x, oy + y + j, c) end end
|
||||
function t.vln(x, y, len, name, a)
|
||||
local c = H.col(name, a); for j = 0, len - 1 do img:drawPixel(ox + x, oy + y + j, c) end
|
||||
end
|
||||
-- outline rectangle
|
||||
function t.frame(x, y, w, h, c)
|
||||
t.hl(x, y, w, c); t.hl(x, y + h - 1, w, c)
|
||||
t.vl(x, y, h, c); t.vl(x + w - 1, y, h, c)
|
||||
end
|
||||
function t.framen(x, y, w, h, name, a)
|
||||
t.frame(x, y, w, h, H.col(name, a))
|
||||
end
|
||||
-- filled disc (filled circle), integer radius
|
||||
function t.disc(cx, cy, r, c)
|
||||
for yy = -r, r do for xx = -r, r do
|
||||
if xx * xx + yy * yy <= r * r then t.p(cx + xx, cy + yy, c) end
|
||||
end end
|
||||
end
|
||||
function t.discn(cx, cy, r, name, a) t.disc(cx, cy, r, H.col(name, a)) end
|
||||
-- filled ellipse
|
||||
function t.ellipse(cx, cy, rx, ry, c)
|
||||
for yy = -ry, ry do for xx = -rx, rx do
|
||||
if (xx * xx) * (ry * ry) + (yy * yy) * (rx * rx) <= (rx * rx) * (ry * ry) then
|
||||
t.p(cx + xx, cy + yy, c)
|
||||
end
|
||||
end end
|
||||
end
|
||||
function t.ellipsen(cx, cy, rx, ry, name, a) t.ellipse(cx, cy, rx, ry, H.col(name, a)) end
|
||||
return t
|
||||
end
|
||||
|
||||
-- pen for a 16x16 tile by id (tileset is 16 columns)
|
||||
function H.tile(img, id)
|
||||
local ox = (id % 16) * 16
|
||||
local oy = math.floor(id / 16) * 16
|
||||
return H.at(img, ox, oy)
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Motif helpers (drawn through a pen `t`, local coords)
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
-- a small 5-petal flower centered at (cx,cy)
|
||||
function H.flower(t, cx, cy, petal, center)
|
||||
local pc = H.col(petal); local cc = H.col(center or "yellow")
|
||||
t.p(cx, cy - 1, pc); t.p(cx, cy + 1, pc)
|
||||
t.p(cx - 1, cy, pc); t.p(cx + 1, cy, pc)
|
||||
t.p(cx, cy, cc)
|
||||
end
|
||||
|
||||
-- a window with brown frame + sky panes, top-left (x,y), size w x h
|
||||
function H.window(t, x, y, w, h, glass)
|
||||
glass = glass or "sky"
|
||||
t.rectn(x, y, w, h, "brown_d")
|
||||
t.rectn(x + 1, y + 1, w - 2, h - 2, glass)
|
||||
-- mullions
|
||||
local mx = x + math.floor(w / 2)
|
||||
local my = y + math.floor(h / 2)
|
||||
t.vln(mx, y + 1, h - 2, "brown_d")
|
||||
t.hln(x + 1, my, w - 2, "brown_d")
|
||||
-- glass highlight
|
||||
t.pn(x + 2, y + 2, "sky_l" == glass and "white" or "water_l")
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Mirror pen: wraps a pen `t`, flipping x within a `w`-wide frame.
|
||||
-- Lets one directional drawing serve both left and right facings.
|
||||
-- ----------------------------------------------------------------------------
|
||||
function H.mirrorX(t, w)
|
||||
local m = {}
|
||||
function m.p(x, y, c) t.p(w - 1 - x, y, c) end
|
||||
function m.pn(x, y, name, a) t.pn(w - 1 - x, y, name, a) end
|
||||
function m.rect(x, y, ww, h, c) t.rect(w - x - ww, y, ww, h, c) end
|
||||
function m.rectn(x, y, ww, h, name, a) t.rectn(w - x - ww, y, ww, h, name, a) end
|
||||
function m.hl(x, y, len, c) t.hl(w - x - len, y, len, c) end
|
||||
function m.hln(x, y, len, name, a) t.hln(w - x - len, y, len, name, a) end
|
||||
function m.vl(x, y, len, c) t.vl(w - 1 - x, y, len, c) end
|
||||
function m.vln(x, y, len, name, a) t.vln(w - 1 - x, y, len, name, a) end
|
||||
function m.framen(x, y, ww, h, name, a) t.framen(w - x - ww, y, ww, h, name, a) end
|
||||
function m.discn(cx, cy, r, name, a) t.discn(w - 1 - cx, cy, r, name, a) end
|
||||
function m.ellipsen(cx, cy, rx, ry, name, a) t.ellipsen(w - 1 - cx, cy, rx, ry, name, a) end
|
||||
return m
|
||||
end
|
||||
|
||||
-- create a fresh RGB sprite + return (sprite, image)
|
||||
function H.new(w, h)
|
||||
local spr = Sprite(w, h, ColorMode.RGB)
|
||||
local img = app.activeImage
|
||||
return spr, img
|
||||
end
|
||||
|
||||
return H
|
||||
@@ -0,0 +1,42 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
await sleep(400);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const open = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
if (!open) break;
|
||||
await tap('KeyE'); await sleep(200);
|
||||
}
|
||||
const dump = () => page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('HouseScene');
|
||||
return {
|
||||
player: { x: Math.round(s.player.x), y: Math.round(s.player.y) },
|
||||
vel: { x: Math.round(s.player.body.velocity.x), y: Math.round(s.player.body.velocity.y) },
|
||||
doors: s.doors,
|
||||
dlg: window.__director.dialogueActive,
|
||||
transitioning: s.transitioning,
|
||||
armed: s.armedDoors ? s.armedDoors.size : -1,
|
||||
};
|
||||
});
|
||||
console.log('before:', JSON.stringify(await dump()));
|
||||
await page.keyboard.down('KeyS'); await sleep(1200); await page.keyboard.up('KeyS');
|
||||
console.log('after KeyS:', JSON.stringify(await dump()));
|
||||
await page.keyboard.down('KeyS'); await sleep(1500); await page.keyboard.up('KeyS');
|
||||
console.log('after more KeyS:', JSON.stringify(await dump()));
|
||||
console.log('plaza active:', await page.evaluate(() => window.__game.scene.isActive('PlazaScene')));
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,50 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
await sleep(400);
|
||||
// 记录对话开关
|
||||
await page.evaluate(() => {
|
||||
window.__dlgLog = [];
|
||||
const d = window.__director;
|
||||
const origShow = d.showDialogue.bind(d);
|
||||
d.showDialogue = (p) => { window.__dlgLog.push(`${Date.now()%100000} OPEN 「${(p.lines&&p.lines[0]||'').slice(0,18)}」`); return origShow(p); };
|
||||
const origRes = d.resolveDialogue.bind(d);
|
||||
d.resolveDialogue = (c) => { window.__dlgLog.push(`${Date.now()%100000} CLOSE`); return origRes(c); };
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); }
|
||||
sm.start('PlazaScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 });
|
||||
await sleep(500);
|
||||
// 清残留
|
||||
for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); }
|
||||
await page.evaluate(() => { window.__dlgLog.length = 0; });
|
||||
// 站到阿福旁,以真人节奏 ~500ms 按 E
|
||||
await page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const a = s.actors.find(a => a.def.id === 'afu');
|
||||
s.player.setPosition(a.sprite.x, a.sprite.y + 8); s.player.setVelocity(0, 0);
|
||||
});
|
||||
await sleep(150);
|
||||
console.log('--- 开始:第 1 次按 E(开对话)');
|
||||
await tap('KeyE');
|
||||
for (let i = 0; i < 8; i++) { await sleep(500); console.log(` t+${(i+1)*0.5}s 按 E`); await tap('KeyE'); }
|
||||
console.log('--- 停止按键,观察 4 秒是否自动重开 ---');
|
||||
await sleep(4000);
|
||||
for (const l of await page.evaluate(() => window.__dlgLog)) console.log(l);
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,70 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
await sleep(400);
|
||||
// 记录对话开关
|
||||
await page.evaluate(() => {
|
||||
window.__dlgLog = [];
|
||||
const d = window.__director;
|
||||
const origShow = d.showDialogue.bind(d);
|
||||
d.showDialogue = (p) => { window.__dlgLog.push(`OPEN 「${(p.lines&&p.lines[0]||'').slice(0,18)}」`); return origShow(p); };
|
||||
const origRes = d.resolveDialogue.bind(d);
|
||||
d.resolveDialogue = (c) => { window.__dlgLog.push('CLOSE'); return origRes(c); };
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); }
|
||||
sm.start('PlazaScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 });
|
||||
await sleep(500);
|
||||
// 清残留对话
|
||||
for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); }
|
||||
await page.evaluate(() => { window.__dlgLog.length = 0; });
|
||||
// 传送到雾门正上方(触发圈内 dist≈10),模拟玩家从上方走近雾门后松开按键
|
||||
const info = await page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const d = s.doors.find(d => d.target === 'fog');
|
||||
s.player.setPosition(d.x, d.y - 10 + 8); // 测量点 player.y-8 → py = d.y-10
|
||||
s.player.setVelocity(0, 0);
|
||||
return { door: { x: d.x, y: d.y }, day: window.__director.day };
|
||||
});
|
||||
console.log('雾门位置', info.door, 'day', info.day);
|
||||
// 等待劝离对话自动弹出(靠近即触发,无需按键)
|
||||
await sleep(1500);
|
||||
const afterTrigger = await page.evaluate(() => window.__dlgLog.filter(l => l.startsWith('OPEN')).length);
|
||||
console.log('触发后 OPEN 次数:', afterTrigger);
|
||||
if (afterTrigger !== 1) { console.log('FAIL: 劝离对话应恰好弹出 1 次'); process.exit(1); }
|
||||
// 按 E 关闭对话(打字机:第一下补全文字,第二下才关闭;canUseDoor 在对话关闭后才返回并执行推离)
|
||||
for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); }
|
||||
await sleep(400);
|
||||
const afterClose = await page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const d = s.doors.find(d => d.target === 'fog');
|
||||
const py = s.player.y - 8;
|
||||
return { dist: Math.hypot(s.player.x - d.x, py - d.y).toFixed(1) };
|
||||
});
|
||||
console.log('关闭并推离后玩家距门:', afterClose.dist);
|
||||
if (parseFloat(afterClose.dist) <= 14) { console.log('FAIL: 劝离推离后玩家仍在触发圈内 dist=' + afterClose.dist); process.exit(1); }
|
||||
// 静止观察 5 秒,不得重开
|
||||
await sleep(5000);
|
||||
const final = await page.evaluate(() => window.__dlgLog);
|
||||
console.log('最终日志:', final);
|
||||
const opens = final.filter(l => l.startsWith('OPEN')).length;
|
||||
const closes = final.filter(l => l === 'CLOSE').length;
|
||||
if (opens === 1 && closes >= 1) { console.log('PASS: 劝离对话只弹一次,关闭后 5 秒未重开'); }
|
||||
else { console.log(`FAIL: OPEN=${opens} CLOSE=${closes}`); process.exit(1); }
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,69 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
await sleep(400);
|
||||
await page.evaluate(() => {
|
||||
window.__dlgLog = [];
|
||||
const d = window.__director;
|
||||
const origShow = d.showDialogue.bind(d);
|
||||
d.showDialogue = (p) => { window.__dlgLog.push(`OPEN 「${(p.lines&&p.lines[0]||'').slice(0,14)}」`); return origShow(p); };
|
||||
const origRes = d.resolveDialogue.bind(d);
|
||||
d.resolveDialogue = (c) => { window.__dlgLog.push('CLOSE'); return origRes(c); };
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); }
|
||||
sm.start('PlazaScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 });
|
||||
await sleep(500);
|
||||
for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); }
|
||||
await page.evaluate(() => {
|
||||
window.__dlgLog.length = 0;
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const d = s.doors.find(d => d.target === 'fog');
|
||||
window.__door = d;
|
||||
// 记录 canUseDoor 调用现场
|
||||
const origCUD = s.canUseDoor.bind(s);
|
||||
s.canUseDoor = (t) => {
|
||||
const dd = s.doors.find(x => x.target === t);
|
||||
const dist = dd ? Math.hypot(s.player.x - dd.x, (s.player.y - 8) - dd.y) : -1;
|
||||
window.__dlgLog.push(`canUseDoor(${t}) dist=${dist.toFixed(1)} armed=${dd ? s.armedDoors.has(dd) : '?'}`);
|
||||
return origCUD(t);
|
||||
};
|
||||
s.player.setPosition(d.x, d.y - 10 + 8);
|
||||
s.player.setVelocity(0, 0);
|
||||
});
|
||||
await sleep(1200); // 等劝离对话弹出
|
||||
// 关闭对话
|
||||
for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); }
|
||||
// 高频采样 3 秒
|
||||
const samples = await page.evaluate(async () => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const d = window.__door;
|
||||
const out = [];
|
||||
const t0 = Date.now();
|
||||
return await new Promise((resolve) => {
|
||||
const iv = setInterval(() => {
|
||||
const dist = Math.hypot(s.player.x - d.x, (s.player.y - 8) - d.y);
|
||||
out.push(`${Date.now() - t0}ms pos=(${s.player.x.toFixed(1)},${s.player.y.toFixed(1)}) body=(${s.player.body.x.toFixed(1)},${s.player.body.y.toFixed(1)}) dist=${dist.toFixed(1)} dlg=${window.__director.dialogueActive} log=${window.__dlgLog.length}`);
|
||||
if (Date.now() - t0 > 3000) { clearInterval(iv); resolve(out); }
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
for (const l of samples) console.log(l);
|
||||
console.log('LOG:', await page.evaluate(() => window.__dlgLog));
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,31 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
await sleep(600);
|
||||
// 依次跳转地图并截图
|
||||
for (const [sceneKey, spawn] of [['HouseScene','default'],['PlazaScene','default'],['FieldScene','default'],['WellScene','default'],['LakeScene','default']]) {
|
||||
await page.evaluate(([k, sp]) => {
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const kk = sc.scene.key; if (kk !== 'UIScene' && kk !== k) sm.stop(kk); }
|
||||
sm.start(k, { spawn: sp });
|
||||
}, [sceneKey, spawn]);
|
||||
await page.waitForFunction((k) => window.__game.scene.isActive(k), { timeout: 9000 }, sceneKey);
|
||||
await sleep(900);
|
||||
await page.screenshot({ path: `tools/shots/fix_${sceneKey}.png` });
|
||||
console.log('shot', sceneKey);
|
||||
}
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,43 @@
|
||||
// 最终清扫:任何「含透明像素的瓦片」都不得出现在任何地图的 ground 层(否则露黑底)
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import fs from 'node:fs';
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true });
|
||||
const page = await browser.newPage();
|
||||
// 把 tileset.png 读成 dataURL 注入页面分析 alpha
|
||||
const b64 = fs.readFileSync('public/assets/tileset.png').toString('base64');
|
||||
const transparentIds = await page.evaluate(async (data) => {
|
||||
const img = new Image();
|
||||
await new Promise((res) => { img.onload = res; img.src = data; });
|
||||
const c = document.createElement('canvas');
|
||||
c.width = img.width; c.height = img.height;
|
||||
const ctx = c.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const d = ctx.getImageData(0, 0, img.width, img.height).data;
|
||||
const out = [];
|
||||
for (let id = 0; id < 128; id++) {
|
||||
const ox = (id % 16) * 16, oy = Math.floor(id / 16) * 16;
|
||||
let trans = false;
|
||||
for (let y = 0; y < 16 && !trans; y++)
|
||||
for (let x = 0; x < 16; x++)
|
||||
if (d[((oy + y) * img.width + ox + x) * 4 + 3] < 255) { trans = true; break; }
|
||||
if (trans) out.push(id);
|
||||
}
|
||||
return out;
|
||||
}, 'data:image/png;base64,' + b64);
|
||||
await browser.close();
|
||||
console.log('含透明像素的瓦片 id:', transparentIds.join(','));
|
||||
const maps = ['house','plaza','bakery','shop','clinic','lake','field','well','fog','white'];
|
||||
let bad = 0;
|
||||
for (const key of maps) {
|
||||
const m = JSON.parse(fs.readFileSync(`public/assets/maps/${key}.json`, 'utf8'));
|
||||
const g = m.layers.find((l) => l.name === 'ground');
|
||||
const hits = new Set();
|
||||
for (const gid of g.data) {
|
||||
if (gid === 0) { hits.add('EMPTY'); continue; }
|
||||
const id = gid - 1;
|
||||
if (transparentIds.includes(id)) hits.add(id);
|
||||
}
|
||||
if (hits.size) { bad++; console.log(`FAIL ${key}: ground 层含透明瓦片/空格 ->`, [...hits].join(',')); }
|
||||
else console.log(`OK ${key}`);
|
||||
}
|
||||
process.exit(bad ? 1 : 0);
|
||||
@@ -0,0 +1,131 @@
|
||||
// dbg16:复现「面包无法提交」与「第一天睡觉卡死」
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,600)));
|
||||
page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,300)); });
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
const ev = (fn, ...a) => page.evaluate(fn, ...a);
|
||||
const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
const dlgText = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return u && u.dlgBody ? u.dlgBody.text : ''; });
|
||||
async function drain(maxIter = 40) {
|
||||
const texts = [];
|
||||
for (let i = 0; i < maxIter; i++) {
|
||||
if (!(await dlgOpen())) break;
|
||||
texts.push(await dlgText());
|
||||
const choosing = await ev(() => { const u = window.__game.scene.getScene('UIScene'); return u.showingChoices; });
|
||||
await tap(choosing ? 'Digit1' : 'KeyE');
|
||||
await sleep(160);
|
||||
}
|
||||
return texts;
|
||||
}
|
||||
async function startScene(k, spawn='default') {
|
||||
await ev((k,s) => {
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const kk = sc.scene.key; if (kk !== 'UIScene' && kk !== k) sm.stop(kk); }
|
||||
sm.start(k, { spawn: s });
|
||||
}, k, spawn);
|
||||
await page.waitForFunction((kk) => window.__game.scene.isActive(kk), { timeout: 9000 }, k);
|
||||
await sleep(500);
|
||||
}
|
||||
async function setPlayer(k, x, y) {
|
||||
await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y);
|
||||
await sleep(120);
|
||||
}
|
||||
const dstate = () => ev(() => ({
|
||||
day: window.__director.day,
|
||||
breadTaken: window.__director.taskProgress['breadTaken'] ?? 0,
|
||||
breadTo: [...(window.__director.taskSetProgress['breadTo'] ?? [])],
|
||||
done: [...window.__director.tasksDone],
|
||||
canSleep: window.__director.canSleep,
|
||||
dlg: window.__director.dialogueActive,
|
||||
}));
|
||||
await sleep(600);
|
||||
|
||||
console.log('\n=== A. 面包任务提交 ===');
|
||||
// 1. 面包房取面包
|
||||
await startScene('BakeryScene');
|
||||
await drain();
|
||||
// counter_bread 交互区在地图对象里:从 JSON 读坐标
|
||||
const cbObj = await ev(() => window.__game.scene.getScene('BakeryScene').interactZones.find(z => z.id === 'counter_bread'));
|
||||
console.log('counter_bread zone:', cbObj);
|
||||
await setPlayer('BakeryScene', cbObj.x + 40, cbObj.y + 40);
|
||||
await setPlayer('BakeryScene', cbObj.x, cbObj.y + 8);
|
||||
await tap('KeyE'); await sleep(400);
|
||||
const t1 = await drain();
|
||||
console.log('取面包对话:', t1);
|
||||
console.log('状态:', await dstate());
|
||||
|
||||
// 2. 送 dabao(plaza)
|
||||
await startScene('PlazaScene');
|
||||
await drain();
|
||||
const dabao = await ev(() => { const s = window.__game.scene.getScene('PlazaScene'); const a = s.actors.find(a => a.def.id === 'dabao'); return { x: a.sprite.x, y: a.sprite.y }; });
|
||||
await setPlayer('PlazaScene', dabao.x + 40, dabao.y + 40);
|
||||
await setPlayer('PlazaScene', dabao.x, dabao.y + 8);
|
||||
await tap('KeyE'); await sleep(400);
|
||||
const t2 = await drain();
|
||||
console.log('dabao 对话:', t2);
|
||||
console.log('状态:', await dstate());
|
||||
|
||||
// 3. 送 mimi(shop)与 guiyeye(lake)
|
||||
await startScene('ShopScene');
|
||||
await drain();
|
||||
const mimi = await ev(() => { const s = window.__game.scene.getScene('ShopScene'); const a = s.actors.find(a => a.def.id === 'mimi'); return { x: a.sprite.x, y: a.sprite.y }; });
|
||||
await setPlayer('ShopScene', mimi.x + 40, mimi.y + 40);
|
||||
await setPlayer('ShopScene', mimi.x, mimi.y + 8);
|
||||
await tap('KeyE'); await sleep(400);
|
||||
console.log('mimi 对话:', await drain());
|
||||
console.log('状态:', await dstate());
|
||||
|
||||
await startScene('LakeScene');
|
||||
await drain();
|
||||
const gui = await ev(() => { const s = window.__game.scene.getScene('LakeScene'); const a = s.actors.find(a => a.def.id === 'guiyeye'); return { x: a.sprite.x, y: a.sprite.y }; });
|
||||
await setPlayer('LakeScene', gui.x + 40, gui.y + 40);
|
||||
await setPlayer('LakeScene', gui.x, gui.y + 8);
|
||||
await tap('KeyE'); await sleep(400);
|
||||
console.log('guiyeye 对话:', await drain());
|
||||
console.log('状态:', await dstate());
|
||||
|
||||
console.log('\n=== B. 第一天睡觉(用户场景:任务未全了结,靠黄昏计时) ===');
|
||||
await startScene('HouseScene');
|
||||
await drain();
|
||||
// 强制黄昏计时,使 canSleep=true(模拟用户等满 5 分钟)
|
||||
await ev(() => { window.__director.dayElapsedMs = 300000; });
|
||||
console.log('睡前状态:', await dstate());
|
||||
// 走到床边按 E
|
||||
const bed = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed'));
|
||||
await setPlayer('HouseScene', bed.x + 40, bed.y + 40);
|
||||
await setPlayer('HouseScene', bed.x, bed.y + 8);
|
||||
await tap('KeyE'); await sleep(400);
|
||||
console.log('床交互后对话:', await drain());
|
||||
// 采样 20 秒:场景序列与卡死点
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const s = await ev(() => {
|
||||
const sm = window.__game.scene;
|
||||
return {
|
||||
active: sm.getScenes(true).map(sc => sc.scene.key),
|
||||
day: window.__director.day,
|
||||
dlg: window.__director.dialogueActive,
|
||||
uiPayload: (() => { const u = sm.getScene('UIScene'); return u && u.payload ? (u.payload.lines?.[0] ?? '').slice(0, 20) : null; })(),
|
||||
};
|
||||
});
|
||||
console.log(`t+${(i+1)}s`, JSON.stringify(s));
|
||||
// 模拟玩家按 E 推进梦境对话
|
||||
if (s.uiPayload) { await tap('KeyE'); }
|
||||
await sleep(1000);
|
||||
}
|
||||
console.log('最终状态:', await dstate());
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,84 @@
|
||||
// dbg17:睡觉卡死候选根因验证
|
||||
// (a) 床淡出期间庆典触发 → celebrationActive 卡死 / 对话 resolver 被覆盖
|
||||
// (b) 床淡出期间玩家移动 30px 再按 E → 二次触发床 → 双 scene.start
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,500)));
|
||||
page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,300)); });
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
const ev = (fn, ...a) => page.evaluate(fn, ...a);
|
||||
const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
const state = (tag) => ev((tag) => {
|
||||
const sm = window.__game.scene;
|
||||
const d = window.__director;
|
||||
const s = sm.getScene('HouseScene');
|
||||
const door = s && s.doors ? s.doors[0] : null;
|
||||
const pd = door && s.player ? { px: +s.player.x.toFixed(0), py: +(s.player.y - 8).toFixed(0),
|
||||
dist: +Math.hypot(s.player.x - door.x, (s.player.y - 8) - door.y).toFixed(1),
|
||||
armed: s.armedDoors.has(door), trans: s.transitioning } : null;
|
||||
return { tag, active: sm.getScenes(true).map(sc => sc.scene.key), day: d.day,
|
||||
dlg: d.dialogueActive, celeb: d.celebrationActive, happy: d.happiness,
|
||||
ui: (() => { const u = sm.getScene('UIScene'); return u && u.payload ? (u.payload.lines?.[0] ?? '').slice(0, 18) : null; })(),
|
||||
pd };
|
||||
}, tag).then(s => console.log(JSON.stringify(s)));
|
||||
async function drain(maxIter = 20) {
|
||||
for (let i = 0; i < maxIter; i++) { if (!(await dlgOpen())) break; await tap('KeyE'); await sleep(160); }
|
||||
}
|
||||
async function setPlayer(k, x, y) {
|
||||
await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y);
|
||||
await sleep(120);
|
||||
}
|
||||
await sleep(600);
|
||||
const bed = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed'));
|
||||
|
||||
console.log('\n=== (a) 床淡出期间庆典触发 ===');
|
||||
// 低幸福 + 低幸福计时逼近阈值;强制可睡觉
|
||||
await ev(() => {
|
||||
const d = window.__director;
|
||||
d.setHappiness(20);
|
||||
d.lowHappyMs = 119900;
|
||||
d.dayElapsedMs = 300000;
|
||||
});
|
||||
// 按 E 睡觉 → 淡出 400ms 内 tick 必触发庆典
|
||||
await setPlayer('HouseScene', bed.x, bed.y + 8);
|
||||
await tap('KeyE');
|
||||
for (let i = 0; i < 12; i++) { await sleep(500); await state(`a t+${(i+1)*0.5}s`); if (await dlgOpen()) await tap('KeyE'); }
|
||||
// 尝试走到门边逃脱(门在 (88,104))
|
||||
await state('a 结束,尝试过门');
|
||||
await setPlayer('HouseScene', 88 + 30, 104);
|
||||
await setPlayer('HouseScene', 88, 104);
|
||||
await sleep(1500);
|
||||
await state('a 过门后');
|
||||
|
||||
console.log('\n=== (b) 床淡出期间二次触发 ===');
|
||||
await ev(() => {
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'HouseScene') sm.stop(k); }
|
||||
sm.start('HouseScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 9000 });
|
||||
await sleep(500);
|
||||
await drain();
|
||||
await ev(() => { window.__director.dayElapsedMs = 300000; });
|
||||
const bed2 = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed'));
|
||||
await setPlayer('HouseScene', bed2.x, bed2.y + 8);
|
||||
await tap('KeyE'); // 第一次:触发床,淡出开始
|
||||
await sleep(150);
|
||||
await setPlayer('HouseScene', bed2.x + 40, bed2.y + 8); // 移动 >30px 使 spent 重武装
|
||||
await tap('KeyE'); // 第二次:可能二次触发床
|
||||
for (let i = 0; i < 10; i++) { await sleep(500); await state(`b t+${(i+1)*0.5}s`); if (await dlgOpen()) await tap('KeyE'); }
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,116 @@
|
||||
// dbg18:睡觉卡死根因验证(干净页面)
|
||||
// A 基线:真实流程睡觉+连按 E,应正常过日到 D2
|
||||
// B 嫌疑2:床淡出 400ms 内移动 30px 二次按 E → 是否双触发/卡死
|
||||
// C 嫌疑1:梦境对话期间注入第二个 showDialogue → resolver 覆盖是否导致 run() 永久悬挂
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,500)));
|
||||
page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,300)); });
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
const ev = (fn, ...a) => page.evaluate(fn, ...a);
|
||||
const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
const state = (tag) => ev((tag) => {
|
||||
const sm = window.__game.scene;
|
||||
const d = window.__director;
|
||||
return { tag, active: sm.getScenes(true).map(sc => sc.scene.key), day: d.day,
|
||||
dlg: d.dialogueActive, celeb: d.celebrationActive, happy: d.happiness,
|
||||
ui: (() => { const u = sm.getScene('UIScene'); return u && u.payload ? (u.payload.lines?.[0] ?? '').slice(0, 22) : null; })() };
|
||||
}, tag).then(s => console.log(JSON.stringify(s)));
|
||||
async function drain(maxIter = 30) {
|
||||
for (let i = 0; i < maxIter; i++) { if (!(await dlgOpen())) break; await tap('KeyE'); await sleep(180); }
|
||||
}
|
||||
async function setPlayer(k, x, y) {
|
||||
await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y);
|
||||
await sleep(120);
|
||||
}
|
||||
async function restartHouse() {
|
||||
await ev(() => {
|
||||
window.__director.forceIdle();
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'HouseScene') sm.stop(k); }
|
||||
if (!sm.isActive('HouseScene')) sm.start('HouseScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 9000 });
|
||||
await sleep(400);
|
||||
await drain();
|
||||
}
|
||||
await sleep(600);
|
||||
|
||||
console.log('\n=== A 基线:真实睡觉流程 + 连按 E ===');
|
||||
await ev(() => { window.__director.dayElapsedMs = 300000; });
|
||||
const bed = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed'));
|
||||
await setPlayer('HouseScene', bed.x, bed.y + 8);
|
||||
await tap('KeyE');
|
||||
let okA = false;
|
||||
for (let i = 0; i < 24; i++) {
|
||||
await sleep(500);
|
||||
await state(`A t+${(i+1)*0.5}s`);
|
||||
if (await dlgOpen()) await tap('KeyE');
|
||||
const d = await ev(() => window.__director.day);
|
||||
if (d === 2) { okA = true; console.log('A 结果: 正常过日到 D2 ✓'); break; }
|
||||
}
|
||||
if (!okA) console.log('A 结果: 卡死 ✗');
|
||||
|
||||
console.log('\n=== B 床淡出期间二次触发 ===');
|
||||
await restartHouse();
|
||||
await ev(() => { const d = window.__director; d.setDay(1); d.dayElapsedMs = 300000; });
|
||||
await ev(() => { window.__dreamStarts = 0; const sm = window.__game.scene;
|
||||
if (!sm.__patched) { const orig = sm.start.bind(sm); sm.start = (k, ...a) => { if (k === 'DreamScene') window.__dreamStarts++; return orig(k, ...a); }; sm.__patched = true; } });
|
||||
const bedB = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed'));
|
||||
await setPlayer('HouseScene', bedB.x, bedB.y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(150);
|
||||
await setPlayer('HouseScene', bedB.x + 40, bedB.y + 8); // 移开 >30px 重武装
|
||||
await tap('KeyE'); // 二次按 E
|
||||
let okB = false;
|
||||
for (let i = 0; i < 24; i++) {
|
||||
await sleep(500);
|
||||
const extra = await ev(() => window.__dreamStarts);
|
||||
await state(`B t+${(i+1)*0.5}s dreamStarts=${extra}`);
|
||||
if (await dlgOpen()) await tap('KeyE');
|
||||
const d = await ev(() => window.__director.day);
|
||||
if (d === 2) { okB = true; console.log(`B 结果: 过日成功, DreamScene 启动次数=${extra}`); break; }
|
||||
}
|
||||
if (!okB) console.log('B 结果: 卡死 ✗');
|
||||
|
||||
console.log('\n=== C 梦境对话期间注入第二个 showDialogue ===');
|
||||
await restartHouse();
|
||||
await ev(() => { const d = window.__director; d.setDay(1); d.dayElapsedMs = 300000; });
|
||||
const bedC = await ev(() => window.__game.scene.getScene('HouseScene').interactZones.find(z => z.id === 'bed'));
|
||||
await setPlayer('HouseScene', bedC.x, bedC.y + 8);
|
||||
await tap('KeyE');
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('DreamScene'), { timeout: 9000 });
|
||||
await page.waitForFunction(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }, { timeout: 9000 });
|
||||
await state('C 梦境第一行已开');
|
||||
// 注入第二个 showDialogue(模拟resolver被覆盖)
|
||||
await ev(() => { window.__injectedP = window.__director.showDialogue({ lines: ['【注入】测试对话'] }); window.__injectedP.then(r => { window.__injectedResolved = r; }); });
|
||||
await sleep(300);
|
||||
await state('C 注入后');
|
||||
await drain(); // 按 E 关掉注入对话
|
||||
await sleep(300);
|
||||
await state('C 注入对话关闭后');
|
||||
// 继续按 E 试图推进梦境
|
||||
let okC = false;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await sleep(500);
|
||||
await state(`C t+${(i+1)*0.5}s`);
|
||||
if (await dlgOpen()) await tap('KeyE');
|
||||
const d = await ev(() => window.__director.day);
|
||||
if (d === 2) { okC = true; console.log('C 结果: 梦境继续并过日 ✓'); break; }
|
||||
}
|
||||
if (!okC) console.log('C 结果: 梦境 run() 永久悬挂 = 卡死根因证实 ✗');
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,123 @@
|
||||
// dbg19:修复后全流程真人验证
|
||||
// 1. D1 与圆圆对话取面包 → 送大宝/咪咪/龟爷爷 → T1 完成(不经隐形柜台)
|
||||
// 2. D2 与咪咪对话清点货架 → T5 完成(不经隐形货架区)
|
||||
// 3. 睡觉竞态:低幸福+计时逼近阈值时睡觉 → 不卡死,D2 可正常交互
|
||||
// 4. 庆典未被误抑制:低幸福计时到点不睡觉 → 庆典正常触发
|
||||
// 5. T13 彩带 ×4 新坐标可触发
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,200)); });
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
const ev = (fn, ...a) => page.evaluate(fn, ...a);
|
||||
const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
const dlgText = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return u && u.dlgBody ? u.dlgBody.text : ''; });
|
||||
async function drain(maxIter = 40) {
|
||||
const texts = [];
|
||||
for (let i = 0; i < maxIter; i++) { if (!(await dlgOpen())) break; texts.push(await dlgText()); await tap('KeyE'); await sleep(160); }
|
||||
return texts;
|
||||
}
|
||||
async function startScene(k) {
|
||||
await ev((k) => { const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const key = sc.scene.key; if (key !== 'UIScene' && key !== k) sm.stop(key); }
|
||||
sm.start(k, { spawn: 'default' }); }, k);
|
||||
await page.waitForFunction((kk) => window.__game.scene.isActive(kk), { timeout: 9000 }, k);
|
||||
await sleep(400); await drain();
|
||||
}
|
||||
async function setPlayer(k, x, y) {
|
||||
await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y);
|
||||
await sleep(120);
|
||||
}
|
||||
async function talkNpc(sceneKey, x, y) {
|
||||
await setPlayer(sceneKey, x + 40, y + 40);
|
||||
await setPlayer(sceneKey, x, y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(300);
|
||||
return drain();
|
||||
}
|
||||
const dget = (e) => ev(new Function(`return window.__director.${e}`));
|
||||
const darr = (e) => ev(new Function(`return [...window.__director.${e}]`));
|
||||
let fails = 0;
|
||||
const check = (name, ok, extra = '') => { console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${extra ? ' ' + extra : ''}`); if (!ok) fails++; };
|
||||
await sleep(500);
|
||||
|
||||
console.log('\n=== 1. D1 与圆圆对话取面包 → 送三人 ===');
|
||||
// 小地图居中偏移:bakery/shop ox=40,oy=10;house ox=64,oy=26;plaza/lake 无偏移
|
||||
await startScene('BakeryScene');
|
||||
const t1 = await talkNpc('BakeryScene', 128, 50); // yuanyuan (88,40)+(40,10)
|
||||
check('对话圆圆拿到面包', await dget("taskProgress['breadTaken']") === 1, JSON.stringify(t1).slice(0, 80));
|
||||
await startScene('PlazaScene');
|
||||
await talkNpc('PlazaScene', 56, 136); // dabao
|
||||
await startScene('ShopScene');
|
||||
await talkNpc('ShopScene', 80, 98); // mimi(D1:只是送面包对象,非 T5)
|
||||
await startScene('LakeScene');
|
||||
await talkNpc('LakeScene', 200, 136); // guiyeye
|
||||
const done1 = await darr('tasksDone');
|
||||
check('T1 送面包完成', done1.includes('T1'), done1.join(','));
|
||||
check('T3 问好完成', done1.includes('T3'), done1.join(','));
|
||||
|
||||
console.log('\n=== 2. D2 与咪咪对话清点货架 ===');
|
||||
await ev(() => window.__director.setDay(2));
|
||||
await startScene('ShopScene');
|
||||
await talkNpc('ShopScene', 80, 98); // mimi
|
||||
const done2 = await darr('tasksDone');
|
||||
check('T5 清点完成', done2.includes('T5'), done2.join(','));
|
||||
check('C5 线索获得', (await darr('clues')).includes('C5'));
|
||||
|
||||
console.log('\n=== 3. 睡觉竞态:低幸福计时逼近阈值时睡觉 ===');
|
||||
await ev(() => { const d = window.__director; d.forceIdle(); d.setDay(1); d.setHappiness(20); });
|
||||
await startScene('HouseScene');
|
||||
// lowHappyMs=119700:阈值将在 ~300ms 后(即床淡出 400ms 中途)到达,正中竞态窗口
|
||||
await ev(() => { const d = window.__director; d.setHappiness(20); d.lowHappyMs = 119700; d.dayElapsedMs = 300000; });
|
||||
await setPlayer('HouseScene', 88, 90); // 床 (24,56)+(64,26)=(88,82),站 y+8
|
||||
await tap('KeyE');
|
||||
let reachedD2 = false, stuckCeleb = false;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await sleep(500);
|
||||
if (await dlgOpen()) await tap('KeyE');
|
||||
const day = await dget('day');
|
||||
if (day === 2) { reachedD2 = true; break; }
|
||||
}
|
||||
check('睡觉正常过日到 D2(无庆典抢占)', reachedD2);
|
||||
stuckCeleb = await dget('celebrationActive');
|
||||
check('D2 celebrationActive=false(E 可用)', !stuckCeleb);
|
||||
// D2 交互验证:与日历对话
|
||||
await startScene('HouseScene');
|
||||
const cal = await talkNpc('HouseScene', 120, 66); // calendar (56,40)+(64,26)
|
||||
check('D2 交互正常(日历对话)', cal.length > 0, JSON.stringify(cal).slice(0, 60));
|
||||
|
||||
console.log('\n=== 4. 庆典未被误抑制 ===');
|
||||
await ev(() => { const d = window.__director; d.forceIdle(); d.setHappiness(20); d.lowHappyMs = 119900; });
|
||||
await sleep(800);
|
||||
check('低幸福计时到点→庆典正常触发', await dget('celebrationActive'));
|
||||
await ev(() => window.__director.forceIdle());
|
||||
|
||||
console.log('\n=== 5. T13 彩带 ×4 新坐标 ===');
|
||||
await ev(() => window.__director.setDay(6));
|
||||
await startScene('PlazaScene');
|
||||
const ribbons = [[184, 120], [296, 120], [184, 216], [296, 216]];
|
||||
for (const [x, y] of ribbons) {
|
||||
await setPlayer('PlazaScene', x + 40, y + 40);
|
||||
await setPlayer('PlazaScene', x, y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(300);
|
||||
await drain();
|
||||
}
|
||||
check('T13 彩带完成', (await darr('tasksDone')).includes('T13'), (await darr('tasksDone')).join(','));
|
||||
|
||||
console.log(`\n${fails === 0 ? '全部通过 ✓' : `失败 ${fails} 项 ✗`}`);
|
||||
await browser.close();
|
||||
process.exit(fails === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,120 @@
|
||||
// dbg20:审计修复验证
|
||||
// 1. 广场地图:邮筒瓦片(gid47)在 (11,10);4 个挂彩带点改为路灯(gid 29),无预置彩带
|
||||
// 2. C3 邮筒交互可达(D2)
|
||||
// 3. T13 挂彩带前无彩带图像,挂上后出现(帧 29/30/31)
|
||||
// 4. 阿福指引:D6→T14_HINT,D7→T15_HINT
|
||||
// 5. 门淡出庆典竞态:低幸福计时逼近阈值时过门 → celebrationActive 不卡死,新场景可交互
|
||||
import fs from 'node:fs';
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
let fails = 0;
|
||||
const check = (name, ok, extra = '') => { console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${extra ? ' ' + extra : ''}`); if (!ok) fails++; };
|
||||
|
||||
// ---- 静态地图检查 ----
|
||||
const plaza = JSON.parse(fs.readFileSync('public/assets/maps/plaza.json', 'utf8'));
|
||||
const ob = plaza.layers.find(l => l.name === 'obstacles').data;
|
||||
const W = plaza.width;
|
||||
const g = (x, y) => ob[y * W + x];
|
||||
check('邮筒瓦片 gid47 @ (11,10)', g(11, 10) === 47);
|
||||
check('挂彩带点全部改为路灯 gid29', g(11,7)===29 && g(18,7)===29 && g(11,13)===29 && g(18,13)===29);
|
||||
check('无预置彩带 gid30/31/32', ![30,31,32].includes(g(11,7)) && ![30,31,32].includes(g(18,7)) && ![30,31,32].includes(g(11,13)) && ![30,31,32].includes(g(18,13)));
|
||||
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
page.on('console', m => { if (m.type()==='error') console.log('[console.error]', m.text().slice(0,200)); });
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
const ev = (fn, ...a) => page.evaluate(fn, ...a);
|
||||
const dlgOpen = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
const dlgText = () => ev(() => { const u = window.__game.scene.getScene('UIScene'); return u && u.dlgBody ? u.dlgBody.text : ''; });
|
||||
async function drain(maxIter = 40) {
|
||||
const texts = [];
|
||||
for (let i = 0; i < maxIter; i++) { if (!(await dlgOpen())) break; texts.push(await dlgText()); await tap('KeyE'); await sleep(160); }
|
||||
return texts;
|
||||
}
|
||||
async function startScene(k) {
|
||||
await ev((k) => { const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const key = sc.scene.key; if (key !== 'UIScene' && key !== k) sm.stop(key); }
|
||||
sm.start(k, { spawn: 'default' }); }, k);
|
||||
await page.waitForFunction((kk) => window.__game.scene.isActive(kk), { timeout: 9000 }, k);
|
||||
await sleep(400); await drain();
|
||||
}
|
||||
async function setPlayer(k, x, y) {
|
||||
await ev((k,x,y) => { const s = window.__game.scene.getScene(k); s.player.setPosition(x, y); s.player.setVelocity(0,0); }, k, x, y);
|
||||
await sleep(120);
|
||||
}
|
||||
async function interactAt(sceneKey, x, y) {
|
||||
await setPlayer(sceneKey, x + 40, y + 40);
|
||||
await setPlayer(sceneKey, x, y + 8);
|
||||
await tap('KeyE');
|
||||
await sleep(300);
|
||||
return drain();
|
||||
}
|
||||
const dget = (e) => ev(new Function(`return window.__director.${e}`));
|
||||
const darr = (e) => ev(new Function(`return [...window.__director.${e}]`));
|
||||
await sleep(500);
|
||||
|
||||
console.log('\n=== 2. C3 邮筒交互(D2) ===');
|
||||
await ev(() => window.__director.setDay(2));
|
||||
await startScene('PlazaScene');
|
||||
const texts2 = await interactAt('PlazaScene', 184, 176); // mailbox_c3 区
|
||||
check('C3 线索获得', (await darr('clues')).includes('C3'), JSON.stringify(texts2).slice(0, 60));
|
||||
|
||||
console.log('\n=== 3. T13 挂彩带视觉反馈 ===');
|
||||
await ev(() => window.__director.setDay(6));
|
||||
await startScene('PlazaScene');
|
||||
const imgBefore = await ev(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
return s.children.list.filter(o => o.type === 'Image' && o.texture.key === 'tiles16' && [29,30,31].includes(o.frame.name)).length;
|
||||
});
|
||||
check('挂彩带前场景无彩带图像', imgBefore === 0, `实际 ${imgBefore}`);
|
||||
await interactAt('PlazaScene', 184, 120); // ribbon_1
|
||||
const imgAfter = await ev(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
return s.children.list.filter(o => o.type === 'Image' && o.texture.key === 'tiles16' && [29,30,31].includes(o.frame.name)).map(o => ({ x: o.x, y: o.y, f: o.frame.name }));
|
||||
});
|
||||
check('挂上后出现彩带图像(帧29红@184,120)', imgAfter.some(o => o.x === 184 && o.y === 120 && o.f === 29), JSON.stringify(imgAfter));
|
||||
|
||||
console.log('\n=== 4. 阿福地点指引 ===');
|
||||
const t14 = await interactAt('PlazaScene', 248, 136); // afu @D6
|
||||
check('D6 阿福 T14 指引', t14.some(t => t.includes('广场中央站好')), JSON.stringify(t14).slice(0, 70));
|
||||
await ev(() => window.__director.setDay(7));
|
||||
await startScene('PlazaScene');
|
||||
const t15 = await interactAt('PlazaScene', 248, 136); // afu @D7
|
||||
check('D7 阿福 T15 指引', t15.some(t => t.includes('广场北边')), JSON.stringify(t15).slice(0, 70));
|
||||
|
||||
console.log('\n=== 5. 门淡出庆典竞态 ===');
|
||||
await ev(() => { const d = window.__director; d.forceIdle(); d.setDay(3); d.setHappiness(20); });
|
||||
await startScene('PlazaScene');
|
||||
// 阈值 ~300ms 后到达:落在门淡出 250ms 窗口内
|
||||
await ev(() => { const d = window.__director; d.setHappiness(20); d.lowHappyMs = 119700; });
|
||||
// 广场→小屋门 (72,88):先到 30px 外武装,再踏上门
|
||||
await setPlayer('PlazaScene', 72 + 30, 88);
|
||||
await setPlayer('PlazaScene', 72, 88);
|
||||
let arrived = false;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await sleep(300);
|
||||
if (await dlgOpen()) await tap('KeyE');
|
||||
if (await ev(() => window.__game.scene.isActive('HouseScene'))) { arrived = true; break; }
|
||||
}
|
||||
check('过门到达小屋', arrived);
|
||||
check('celebrationActive 未卡死', !(await dget('celebrationActive')));
|
||||
await drain();
|
||||
// 新场景可交互:日历 (56,40)+(64,26)=(120,66)
|
||||
const cal = await interactAt('HouseScene', 120, 66);
|
||||
check('新场景交互正常(日历)', cal.length > 0, JSON.stringify(cal).slice(0, 50));
|
||||
|
||||
console.log(`\n${fails === 0 ? '全部通过 ✓' : `失败 ${fails} 项 ✗`}`);
|
||||
await browser.close();
|
||||
process.exit(fails === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,22 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('console', m => { if (['error','warning'].includes(m.type())) console.log('[console]', m.type(), m.text().slice(0,400)); });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,800)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 15000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
const r = await page.evaluate(() => {
|
||||
try { window.__game.scene.start('HouseScene', { spawn: 'default' }); return 'started'; }
|
||||
catch (e) { return 'THROW: ' + (e.stack||e.message); }
|
||||
});
|
||||
console.log('manual start:', r);
|
||||
await new Promise(r2=>setTimeout(r2,3000));
|
||||
console.log('active:', await page.evaluate(() => window.__game.scene.getScenes(true).map(s=>s.scene.key)));
|
||||
await browser.close();
|
||||
@@ -0,0 +1,58 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,500)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 15000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
await new Promise(r=>setTimeout(r,600));
|
||||
// 切到 PlazaScene(停其它地图场景)
|
||||
await page.evaluate(() => {
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); }
|
||||
sm.start('PlazaScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 });
|
||||
await new Promise(r=>setTimeout(r,600));
|
||||
const dump1 = await page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const u = window.__game.scene.getScene('UIScene');
|
||||
return {
|
||||
day: window.__director.day,
|
||||
dialogueActive: window.__director.dialogueActive,
|
||||
uiActive: window.__game.scene.isActive('UIScene'),
|
||||
player: { x: s.player.x, y: s.player.y },
|
||||
zones: s.interactZones.map(z => ({ id: z.id, x: z.x, y: z.y })).filter(z => z.id === 'noticeboard' || z.id.startsWith('flowerbed')),
|
||||
payload: u.payload ? u.payload.lines : null,
|
||||
};
|
||||
});
|
||||
console.log('dump1', JSON.stringify(dump1, null, 1));
|
||||
// 传送到公告板旁并按 E
|
||||
await page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const z = s.interactZones.find(z => z.id === 'noticeboard');
|
||||
s.player.setPosition(z.x, z.y + 8); s.player.setVelocity(0, 0);
|
||||
});
|
||||
await new Promise(r=>setTimeout(r,200));
|
||||
await page.keyboard.press('KeyE');
|
||||
await new Promise(r=>setTimeout(r,600));
|
||||
const dump2 = await page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const u = window.__game.scene.getScene('UIScene');
|
||||
return {
|
||||
dialogueActive: window.__director.dialogueActive,
|
||||
payload: u.payload ? u.payload.lines : null,
|
||||
body: u.dlgBody ? u.dlgBody.text : null,
|
||||
prompt: u.promptText ? u.promptText.text : null,
|
||||
};
|
||||
});
|
||||
console.log('dump2', JSON.stringify(dump2, null, 1));
|
||||
await browser.close();
|
||||
@@ -0,0 +1,84 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,600)));
|
||||
page.on('console', m => { if (m.type() === 'error') console.log('[console.error]', m.text().slice(0,300)); });
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
await new Promise(r=>setTimeout(r,600));
|
||||
await page.evaluate(() => {
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); }
|
||||
sm.start('PlazaScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 });
|
||||
await new Promise(r=>setTimeout(r,600));
|
||||
|
||||
// 路径 A:直接调用 activateInteract('noticeboard'),绕过键盘
|
||||
const pathA = await page.evaluate(async () => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const z = s.interactZones.find(z => z.id === 'noticeboard');
|
||||
s.player.setPosition(z.x, z.y + 8); s.player.setVelocity(0, 0);
|
||||
let err = null;
|
||||
try { s.activateInteract('noticeboard'); } catch (e) { err = String(e && e.stack || e); }
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
const u = window.__game.scene.getScene('UIScene');
|
||||
return { err, dialogueActive: window.__director.dialogueActive, payload: u.payload ? u.payload.lines : null, body: u.dlgBody ? u.dlgBody.text : null };
|
||||
});
|
||||
console.log('pathA(direct call):', JSON.stringify(pathA, null, 1));
|
||||
|
||||
// 清掉对话
|
||||
await page.evaluate(() => window.__director.forceIdle());
|
||||
await new Promise(r=>setTimeout(r,300));
|
||||
|
||||
// 路径 B:合成 DOM 键盘事件
|
||||
const pathB = await page.evaluate(async () => {
|
||||
const fire = (type) => window.dispatchEvent(new KeyboardEvent(type, { code: 'KeyE', key: 'e', keyCode: 69, which: 69, bubbles: true, cancelable: true }));
|
||||
fire('keydown');
|
||||
await new Promise(r => setTimeout(r, 120));
|
||||
fire('keyup');
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
const u = window.__game.scene.getScene('UIScene');
|
||||
return { dialogueActive: window.__director.dialogueActive, payload: u.payload ? u.payload.lines : null };
|
||||
});
|
||||
console.log('pathB(synthetic DOM key):', JSON.stringify(pathB, null, 1));
|
||||
|
||||
await page.evaluate(() => window.__director.forceIdle());
|
||||
await new Promise(r=>setTimeout(r,300));
|
||||
|
||||
// 路径 C:puppeteer 键盘(与验收脚本相同),先 dump JustDown 相关状态
|
||||
await page.keyboard.press('KeyE');
|
||||
await new Promise(r=>setTimeout(r,500));
|
||||
const pathC = await page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
const u = window.__game.scene.getScene('UIScene');
|
||||
return {
|
||||
dialogueActive: window.__director.dialogueActive,
|
||||
payload: u.payload ? u.payload.lines : null,
|
||||
keyE: s.keyE ? { isDown: s.keyE.isDown, enabled: s.keyE.enabled } : null,
|
||||
kbEnabled: s.input.keyboard ? s.input.keyboard.enabled : null,
|
||||
};
|
||||
});
|
||||
console.log('pathC(puppeteer key):', JSON.stringify(pathC, null, 1));
|
||||
|
||||
// 路径 D:talkTo 直调
|
||||
const pathD = await page.evaluate(async () => {
|
||||
const s = window.__game.scene.getScene('PlazaScene');
|
||||
let err = null;
|
||||
try { s.talkTo('afu'); } catch (e) { err = String(e && e.stack || e); }
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
const u = window.__game.scene.getScene('UIScene');
|
||||
return { err, dialogueActive: window.__director.dialogueActive, payload: u.payload ? u.payload.lines : null, body: u.dlgBody ? u.dlgBody.text : null };
|
||||
});
|
||||
console.log('pathD(talkTo afu):', JSON.stringify(pathD, null, 1));
|
||||
|
||||
await browser.close();
|
||||
@@ -0,0 +1,60 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,600)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
await sleep(500);
|
||||
// 设定 D2 + 已取信
|
||||
await page.evaluate(() => {
|
||||
const d = window.__director;
|
||||
d.setDay(2);
|
||||
d.taskProgress['letters'] = 3;
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'BakeryScene') sm.stop(k); }
|
||||
sm.start('BakeryScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('BakeryScene'), { timeout: 9000 });
|
||||
await sleep(600);
|
||||
const dump1 = await page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('BakeryScene');
|
||||
return {
|
||||
day: window.__director.day,
|
||||
letters: window.__director.taskProgress['letters'],
|
||||
t4active: window.__director.isTaskActive('T4'),
|
||||
actors: s.actors.map(a => ({ id: a.def.id, x: Math.round(a.sprite.x), y: Math.round(a.sprite.y) })),
|
||||
player: { x: s.player.x, y: s.player.y },
|
||||
};
|
||||
});
|
||||
console.log('dump1', JSON.stringify(dump1));
|
||||
// 传送到圆圆旁(actor 实际坐标)
|
||||
await page.evaluate(() => {
|
||||
const s = window.__game.scene.getScene('BakeryScene');
|
||||
const a = s.actors.find(a => a.def.id === 'yuanyuan');
|
||||
s.player.setPosition(a.sprite.x, a.sprite.y); s.player.setVelocity(0,0);
|
||||
});
|
||||
await sleep(200);
|
||||
await tap('KeyE');
|
||||
await sleep(500);
|
||||
const dump2 = await page.evaluate(() => {
|
||||
const u = window.__game.scene.getScene('UIScene');
|
||||
return {
|
||||
dialogueActive: window.__director.dialogueActive,
|
||||
payload: u.payload ? u.payload.lines : null,
|
||||
lettersTo: window.__director.taskSetProgress['lettersTo'] ? [...window.__director.taskSetProgress['lettersTo']] : null,
|
||||
tasksDone: [...window.__director.tasksDone],
|
||||
};
|
||||
});
|
||||
console.log('dump2', JSON.stringify(dump2, null, 1));
|
||||
await browser.close();
|
||||
@@ -0,0 +1,69 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,600)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
await sleep(400);
|
||||
// 事件日志钩子
|
||||
await page.evaluate(() => {
|
||||
const d = window.__director;
|
||||
window.__log = [];
|
||||
const orig = d.emit.bind(d);
|
||||
d.emit = (ev, ...a) => { window.__log.push(`${Date.now()%100000} ${ev} ${JSON.stringify(a).slice(0,80)}`); return orig(ev, ...a); };
|
||||
d.setDay(2);
|
||||
d.taskProgress['letters'] = 3;
|
||||
});
|
||||
const SCENE = { bakery: 'BakeryScene', shop: 'ShopScene', clinic: 'ClinicScene' };
|
||||
async function startScene(k) {
|
||||
await page.evaluate((k) => {
|
||||
const sm = window.__game.scene;
|
||||
for (const sc of sm.getScenes(true)) { const key = sc.scene.key; if (key !== 'UIScene' && key !== k) sm.stop(key); }
|
||||
sm.start(k, { spawn: 'default' });
|
||||
}, k);
|
||||
await page.waitForFunction((k) => window.__game.scene.isActive(k), { timeout: 9000 }, k);
|
||||
await sleep(400);
|
||||
}
|
||||
async function deliver(mapKey, npcId) {
|
||||
await startScene(SCENE[mapKey]);
|
||||
await page.evaluate((npcId) => {
|
||||
const k = Object.keys(window.__game.scene.scenes.map ? {} : {});
|
||||
const s = window.__game.scene.getScenes(true).find(sc => sc.actors && sc.actors.some(a => a.def.id === npcId));
|
||||
const a = s.actors.find(a => a.def.id === npcId);
|
||||
s.player.setPosition(a.sprite.x, a.sprite.y + 8); s.player.setVelocity(0,0);
|
||||
}, npcId);
|
||||
await sleep(150);
|
||||
await tap('KeyE');
|
||||
await sleep(400);
|
||||
// drain 最多 8 次
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const open = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
if (!open) break;
|
||||
await tap('KeyE');
|
||||
await sleep(200);
|
||||
}
|
||||
const st = await page.evaluate(() => ({
|
||||
lettersTo: window.__director.taskSetProgress['lettersTo'] ? [...window.__director.taskSetProgress['lettersTo']] : null,
|
||||
clues: [...window.__director.clues], tasks: [...window.__director.tasksDone],
|
||||
dlg: window.__director.dialogueActive,
|
||||
}));
|
||||
console.log(`after ${npcId}:`, JSON.stringify(st));
|
||||
}
|
||||
await deliver('bakery', 'yuanyuan');
|
||||
await deliver('shop', 'mimi');
|
||||
await deliver('clinic', 'fuyisheng');
|
||||
await sleep(800);
|
||||
console.log('--- event log ---');
|
||||
for (const l of await page.evaluate(() => window.__log)) console.log(l);
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,43 @@
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' });
|
||||
process.on('exit', () => { try{preview.kill()}catch{} });
|
||||
for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); }
|
||||
const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] });
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 960, height: 560 });
|
||||
page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400)));
|
||||
await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' });
|
||||
await page.waitForFunction('window.__game && window.__director', { timeout: 20000 });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 });
|
||||
await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); });
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 });
|
||||
const sleep = (ms) => new Promise(r=>setTimeout(r,ms));
|
||||
const active = (k) => page.evaluate((k) => window.__game.scene.isActive(k), k);
|
||||
const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); };
|
||||
// 清掉开场对话
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const open = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); });
|
||||
if (!open) break;
|
||||
await tap('KeyE'); await sleep(200);
|
||||
}
|
||||
await sleep(300);
|
||||
|
||||
// 1) 从小屋自然走出去:先右移对齐门(x=152),再向下穿出
|
||||
await page.keyboard.down('KeyD'); await sleep(420); await page.keyboard.up('KeyD');
|
||||
await page.keyboard.down('KeyS'); await sleep(800); await page.keyboard.up('KeyS');
|
||||
await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 });
|
||||
console.log('出门 → PlazaScene OK');
|
||||
// 2) 等待 2 秒:不得回弹
|
||||
await sleep(2000);
|
||||
const still = await active('PlazaScene');
|
||||
const houseBack = await active('HouseScene');
|
||||
console.log('2 秒后仍在广场:', still, '| 弹回小屋:', houseBack, still && !houseBack ? 'PASS' : 'FAIL');
|
||||
// 3) 立即折返:向上走回门里(落点 24px 已武装,应可正常回屋)
|
||||
await page.keyboard.down('KeyW'); await sleep(900); await page.keyboard.up('KeyW');
|
||||
await sleep(800);
|
||||
const backHome = await active('HouseScene');
|
||||
console.log('折返回家:', backHome ? 'PASS' : 'FAIL');
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,474 @@
|
||||
// genmaps.js — 生成《崩坏童话:蜜糖村》全部 10 张 Tiled 地图 (.tmx)
|
||||
// 用法: /Applications/Tiled.app/Contents/MacOS/Tiled -e tools/tiled/genmaps.js
|
||||
// 说明: -e 模式无编辑器,tiled.open/TileMap 绘制 API 不可用,故用 TextFile 直接写 .tmx XML。
|
||||
// 导出: Tiled --export-map --embed-tilesets in.tmx out.json (必须 --embed-tilesets,JSON 才有 image 字段)
|
||||
//
|
||||
// 约定(与 CONTRACT.md §3 一致):
|
||||
// - 固定三层:ground(瓦片层)/ obstacles(瓦片层,图层属性 collides=true)/ objects(点对象层)
|
||||
// - 瓦片 gid = CONTRACT §2 瓦片 id + 1;obstacles 中空格 = gid 0
|
||||
// - 点对象坐标 = 所在瓦片中心像素 (tx*16+8, ty*16+8)
|
||||
// - door 属性 target=目标地图 key, spawn=目标地图中的 spawn id(= "from_"+本图 key),双向成对
|
||||
|
||||
var OUT = '/Users/leidianyayi/Documents/GitHub/ModelTest20260714/Kimi-K3/public/assets/maps/';
|
||||
|
||||
// ---- 瓦片 gid 表(id+1)----
|
||||
var G = {
|
||||
grass:1, flowerA:2, flowerB:3, deepGrass:4, dirt:5, stone:6, stoneAlt:7, brick:8,
|
||||
waterA:9, waterB:10, shoreN:11, shoreS:12, shoreW:13, shoreE:14,
|
||||
bedPink:15, bedYellow:16, berry:17, berryEmpty:18,
|
||||
treeTL:19, treeTR:20, treeBL:21, treeBR:22, bush:23, stump:24, rock:25, sign:26,
|
||||
fenceH:27, fenceV:28, lamp:29, ribbonR:30, ribbonY:31, ribbonB:32,
|
||||
creamWall:33, creamWallWin:34, woodWall:35, roofOL:36, roofOM:37, roofOR:38,
|
||||
doorT:39, doorTop:40, window:41, chimney:42, wellTL:43, wellTR:44, wellBL:45, wellBR:46,
|
||||
mailbox:47, board:48, floorA:49, floorB:50, wallIn:51, wallInWin:52,
|
||||
bedTL:53, bedTR:54, bedBL:55, bedBR:56, counterL:57, counterR:58, oven:59,
|
||||
shelfE:60, shelfF:61, table:62, chair:63, rug:64, calendar:65, recipe:66, fileP:67,
|
||||
picTL:68, picTR:69, picBL:70, picBR:71, benchL:72, benchR:73, stool:74, cabinet:75,
|
||||
fridge:76, sink:77, plant:78, barrel:79, crate:80, fogA:81, fogB:82, fogC:83,
|
||||
wFloor:84, wWall:85, wPanel:86, wDoor:87, wBedTL:88, wBedTR:89, wBedBL:90, wBedBR:91,
|
||||
wCab:92, wMon:93, wCur:94, wFloor2:95, wLamp:96, ladder:97, bridgeH:98, bridgeV:99,
|
||||
mushroom:100, flR:101, flY:102, flB:103, honey:104, bread:105, letter:106, book:107,
|
||||
candle:108, basket:109, bucket:110, broom:111, cake:112, roofRL:113, roofRM:114, roofRR:115,
|
||||
roofBL:116, roofBM:117, roofBR:118, pinkWall:119, pinkWallWin:120, stoneWall:121,
|
||||
stoneWallWin:122, awning:123, clock:124, flag:125, garC:126, garM:127, empty:128
|
||||
};
|
||||
|
||||
// ---- 基础工具(ES5)----
|
||||
function grid(w, h, v) {
|
||||
var a = [];
|
||||
for (var y = 0; y < h; y++) {
|
||||
var r = [];
|
||||
for (var x = 0; x < w; x++) r.push(v);
|
||||
a.push(r);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
function newMap(w, h) { return { w: w, h: h, ground: grid(w, h, 0), obs: grid(w, h, 0), objs: [] }; }
|
||||
function fill(l, x, y, w, h, g) {
|
||||
for (var j = y; j < y + h; j++)
|
||||
for (var i = x; i < x + w; i++) l[j][i] = g;
|
||||
}
|
||||
function put(l, x, y, g) { l[y][x] = g; }
|
||||
// 确定性散点随机数(固定种子,结果可复现)
|
||||
function rnd(x, y, s) {
|
||||
var n = Math.sin(x * 127.1 + y * 311.7 + s * 74.7) * 43758.5453;
|
||||
return n - Math.floor(n);
|
||||
}
|
||||
function obj(m, name, tx, ty, props) {
|
||||
m.objs.push({ name: name, x: tx * 16 + 8, y: ty * 16 + 8, props: props });
|
||||
}
|
||||
function spawn(m, tx, ty, id) { obj(m, 'spawn', tx, ty, [['id', id]]); }
|
||||
function door(m, tx, ty, target, sp) { obj(m, 'door', tx, ty, [['target', target], ['spawn', sp]]); }
|
||||
function npc(m, tx, ty, id) { obj(m, 'npc', tx, ty, [['id', id]]); }
|
||||
function inter(m, tx, ty, id) { obj(m, 'interact', tx, ty, [['id', id]]); }
|
||||
function tree2x2(m, x, y) {
|
||||
put(m.obs, x, y, G.treeTL); put(m.obs, x + 1, y, G.treeTR);
|
||||
put(m.obs, x, y + 1, G.treeBL); put(m.obs, x + 1, y + 1, G.treeBR);
|
||||
}
|
||||
// 草地散点装饰(ground 层,无碰撞)
|
||||
function scatter(m, seed, d) {
|
||||
for (var y = 0; y < m.h; y++) {
|
||||
for (var x = 0; x < m.w; x++) {
|
||||
var r = rnd(x, y, seed);
|
||||
var g = 0;
|
||||
if (r < d[0]) g = G.flowerA;
|
||||
else if (r < d[1]) g = G.flowerB;
|
||||
else if (r < d[2]) g = G.deepGrass;
|
||||
else if (r < d[3]) g = [G.flR, G.flY, G.flB][(x + y) % 3];
|
||||
if (g) m.ground[y][x] = g;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 室外房屋:5 宽 × 4 高,朝南单门,整体进 obstacles
|
||||
function houseS(m, x, y, roofL, roofM, roofR, chimney) {
|
||||
for (var i = 0; i < 5; i++) {
|
||||
var t = (i === 0) ? roofL : (i === 4 ? roofR : roofM);
|
||||
put(m.obs, x + i, y, t);
|
||||
put(m.obs, x + i, y + 1, t);
|
||||
}
|
||||
if (chimney) put(m.obs, x + 1, y, G.chimney);
|
||||
put(m.obs, x, y + 2, G.creamWall);
|
||||
put(m.obs, x + 1, y + 2, G.creamWallWin);
|
||||
put(m.obs, x + 2, y + 2, G.doorTop);
|
||||
put(m.obs, x + 3, y + 2, G.creamWallWin);
|
||||
put(m.obs, x + 4, y + 2, G.creamWall);
|
||||
put(m.obs, x, y + 3, G.creamWall);
|
||||
put(m.obs, x + 1, y + 3, G.creamWall);
|
||||
put(m.obs, x + 2, y + 3, G.doorT);
|
||||
put(m.obs, x + 3, y + 3, G.creamWall);
|
||||
put(m.obs, x + 4, y + 3, G.creamWall);
|
||||
}
|
||||
// 室内外壳:木地板满铺(ground),墙围边(obstacles),底边留门(门瓦片在 ground)
|
||||
function indoorShell(m, wins, doorX) {
|
||||
for (var y = 0; y < m.h; y++)
|
||||
for (var x = 0; x < m.w; x++)
|
||||
m.ground[y][x] = ((x + y) % 2 === 0) ? G.floorA : G.floorB;
|
||||
m.ground[m.h - 1][doorX] = G.doorT;
|
||||
for (var x2 = 0; x2 < m.w; x2++) {
|
||||
m.obs[0][x2] = G.wallIn;
|
||||
if (x2 !== doorX) m.obs[m.h - 1][x2] = G.wallIn;
|
||||
}
|
||||
for (var i = 0; i < wins.length; i++) m.obs[0][wins[i]] = G.wallInWin;
|
||||
for (var y2 = 1; y2 < m.h - 1; y2++) {
|
||||
m.obs[y2][0] = G.wallIn;
|
||||
m.obs[y2][m.w - 1] = G.wallIn;
|
||||
}
|
||||
}
|
||||
|
||||
// ================= 1. house 12×8 小满小屋 =================
|
||||
function buildHouse() {
|
||||
var m = newMap(12, 8);
|
||||
indoorShell(m, [3, 8], 5);
|
||||
// 床(睡觉过日)+ 床头日历(C1)
|
||||
put(m.obs, 1, 1, G.bedTL); put(m.obs, 2, 1, G.bedTR);
|
||||
put(m.obs, 1, 2, G.bedBL); put(m.obs, 2, 2, G.bedBR);
|
||||
put(m.obs, 3, 1, G.calendar);
|
||||
// 桌椅 + 盆栽 + 地毯
|
||||
put(m.obs, 8, 4, G.table); put(m.obs, 7, 4, G.chair); put(m.obs, 9, 4, G.chair);
|
||||
put(m.obs, 10, 1, G.plant);
|
||||
put(m.ground, 5, 3, G.rug); put(m.ground, 6, 3, G.rug);
|
||||
put(m.ground, 5, 4, G.rug); put(m.ground, 6, 4, G.rug);
|
||||
spawn(m, 3, 3, 'default');
|
||||
spawn(m, 5, 5, 'from_plaza');
|
||||
door(m, 5, 6, 'plaza', 'from_house');
|
||||
inter(m, 1, 3, 'bed');
|
||||
inter(m, 3, 2, 'calendar');
|
||||
return m;
|
||||
}
|
||||
|
||||
// ================= 2. plaza 30×20 村广场(枢纽) =================
|
||||
function buildPlaza() {
|
||||
var m = newMap(30, 20);
|
||||
fill(m.ground, 0, 0, 30, 20, G.grass);
|
||||
scatter(m, 7, [0.05, 0.10, 0.15, 0.18]);
|
||||
// 石路:南北 x=14/15,东西 y=9/10,通往四座房屋的小路
|
||||
var y, x;
|
||||
for (y = 0; y < 20; y++) {
|
||||
put(m.ground, 14, y, (y % 3 === 0) ? G.stoneAlt : G.stone);
|
||||
put(m.ground, 15, y, (y % 3 === 0) ? G.stoneAlt : G.stone);
|
||||
}
|
||||
for (x = 0; x < 30; x++) {
|
||||
put(m.ground, x, 9, (x % 3 === 0) ? G.stoneAlt : G.stone);
|
||||
put(m.ground, x, 10, (x % 3 === 0) ? G.stoneAlt : G.stone);
|
||||
}
|
||||
var stubs = [4, 11, 18, 25];
|
||||
for (var s = 0; s < stubs.length; s++)
|
||||
for (y = 5; y <= 9; y++) put(m.ground, stubs[s], y, G.stone);
|
||||
// 中央广场砖(庆典场地核心)
|
||||
fill(m.ground, 11, 8, 8, 5, G.brick);
|
||||
// 庆典彩带(广场四角,装饰无碰撞;透明瓦片须放 obstacles 层,ground 层会露黑底)
|
||||
put(m.obs, 11, 7, G.ribbonR); put(m.obs, 18, 7, G.ribbonY);
|
||||
put(m.obs, 11, 13, G.ribbonB); put(m.obs, 18, 13, G.ribbonR);
|
||||
// 四周房屋:小屋/杂货店=橙屋顶,面包房/诊所=红屋顶
|
||||
houseS(m, 2, 1, G.roofOL, G.roofOM, G.roofOR, true); // house 门 x=4
|
||||
houseS(m, 9, 1, G.roofRL, G.roofRM, G.roofRR, true); // bakery 门 x=11
|
||||
houseS(m, 16, 1, G.roofOL, G.roofOM, G.roofOR, true); // shop 门 x=18
|
||||
houseS(m, 23, 1, G.roofRL, G.roofRM, G.roofRR, false); // clinic 门 x=25
|
||||
// 公告板 + 三个花坛 + 双子鸟大树
|
||||
put(m.obs, 14, 8, G.board);
|
||||
put(m.obs, 10, 8, G.bedPink); put(m.obs, 19, 8, G.bedYellow); put(m.obs, 14, 13, G.bedPink);
|
||||
tree2x2(m, 12, 14);
|
||||
// 村口邮筒(大宝)+ 路灯 + 装饰树/灌木
|
||||
put(m.obs, 2, 8, G.mailbox);
|
||||
put(m.obs, 10, 6, G.lamp); put(m.obs, 19, 6, G.lamp);
|
||||
tree2x2(m, 1, 6); tree2x2(m, 27, 14); tree2x2(m, 1, 16);
|
||||
put(m.obs, 8, 16, G.bush); put(m.obs, 21, 17, G.bush); put(m.obs, 16, 15, G.bush);
|
||||
// 地图边缘装饰(留出 4 个路口)
|
||||
function skip(x2, y2) {
|
||||
return (y2 === 0 && (x2 === 14 || x2 === 15)) || (y2 === 19 && (x2 === 14 || x2 === 15)) ||
|
||||
(x2 === 0 && (y2 === 9 || y2 === 10)) || (x2 === 29 && (y2 === 9 || y2 === 10));
|
||||
}
|
||||
function edge(x2, y2) {
|
||||
var r = rnd(x2, y2, 9);
|
||||
put(m.obs, x2, y2, r < 0.5 ? G.bush : (r < 0.75 ? G.rock : G.stump));
|
||||
}
|
||||
for (x = 0; x < 30; x++) { if (!skip(x, 0)) edge(x, 0); if (!skip(x, 19)) edge(x, 19); }
|
||||
for (y = 1; y < 19; y++) { if (!skip(0, y)) edge(0, y); if (!skip(29, y)) edge(29, y); }
|
||||
// 对象
|
||||
spawn(m, 14, 11, 'default');
|
||||
spawn(m, 4, 7, 'from_house'); // 落点距门 24px,防回弹
|
||||
spawn(m, 11, 7, 'from_bakery');
|
||||
spawn(m, 18, 7, 'from_shop');
|
||||
spawn(m, 25, 7, 'from_clinic');
|
||||
spawn(m, 1, 10, 'from_lake');
|
||||
spawn(m, 28, 10, 'from_field');
|
||||
spawn(m, 14, 2, 'from_well'); // 落点距门 24px,防回弹
|
||||
spawn(m, 15, 18, 'from_fog');
|
||||
door(m, 4, 5, 'house', 'from_plaza');
|
||||
door(m, 11, 5, 'bakery', 'from_plaza');
|
||||
door(m, 18, 5, 'shop', 'from_plaza');
|
||||
door(m, 25, 5, 'clinic', 'from_plaza');
|
||||
door(m, 0, 9, 'lake', 'from_plaza');
|
||||
door(m, 29, 10, 'field', 'from_plaza');
|
||||
door(m, 14, 0, 'well', 'from_plaza');
|
||||
door(m, 15, 19, 'fog', 'from_plaza');
|
||||
npc(m, 15, 8, 'afu'); // 公告板旁
|
||||
npc(m, 12, 14, 'bird'); // 双子鸟,树上
|
||||
npc(m, 7, 12, 'squirrel');
|
||||
npc(m, 22, 14, 'fox');
|
||||
npc(m, 3, 8, 'dabao'); // 村口邮筒旁(PRD §4)
|
||||
inter(m, 14, 9, 'noticeboard');
|
||||
inter(m, 10, 9, 'flowerbed_1');
|
||||
inter(m, 19, 9, 'flowerbed_2');
|
||||
inter(m, 14, 14, 'flowerbed_3');
|
||||
return m;
|
||||
}
|
||||
|
||||
// ================= 3. bakery 15×10 面包房 =================
|
||||
function buildBakery() {
|
||||
var m = newMap(15, 10);
|
||||
indoorShell(m, [3, 7, 11], 7);
|
||||
fill(m.ground, 5, 5, 3, 2, G.rug);
|
||||
// 两段柜台 + 烤炉(C2 配方纸在桌上)
|
||||
put(m.obs, 4, 3, G.counterL); put(m.obs, 5, 3, G.counterR);
|
||||
put(m.obs, 8, 3, G.counterL); put(m.obs, 9, 3, G.counterR);
|
||||
put(m.obs, 12, 1, G.oven);
|
||||
put(m.obs, 10, 6, G.table); put(m.obs, 11, 6, G.recipe);
|
||||
put(m.obs, 13, 8, G.barrel); put(m.obs, 1, 8, G.plant);
|
||||
spawn(m, 7, 6, 'default');
|
||||
spawn(m, 7, 7, 'from_plaza');
|
||||
door(m, 7, 8, 'plaza', 'from_bakery');
|
||||
npc(m, 5, 2, 'yuanyuan'); // 柜台后
|
||||
inter(m, 4, 4, 'counter_bread');
|
||||
inter(m, 11, 7, 'recipe_paper');
|
||||
return m;
|
||||
}
|
||||
|
||||
// ================= 4. shop 15×10 杂货店 =================
|
||||
function buildShop() {
|
||||
var m = newMap(15, 10);
|
||||
indoorShell(m, [3, 7, 11], 7);
|
||||
fill(m.ground, 6, 6, 3, 2, G.rug);
|
||||
// 上排 9 格满货架(C5:同一商品重复 9 格)+ 下排空满混排
|
||||
var x;
|
||||
for (x = 3; x <= 11; x++) put(m.obs, x, 1, G.shelfF);
|
||||
for (x = 3; x <= 11; x++) put(m.obs, x, 4, (x % 3 === 0) ? G.shelfE : G.shelfF);
|
||||
put(m.obs, 1, 7, G.crate); put(m.obs, 1, 8, G.barrel); put(m.obs, 13, 1, G.plant);
|
||||
spawn(m, 7, 6, 'default');
|
||||
spawn(m, 7, 7, 'from_plaza');
|
||||
door(m, 7, 8, 'plaza', 'from_shop');
|
||||
npc(m, 2, 5, 'mimi');
|
||||
npc(m, 11, 7, 'hedgehog');
|
||||
inter(m, 7, 2, 'shelf_count');
|
||||
return m;
|
||||
}
|
||||
|
||||
// ================= 5. clinic 12×8 诊所 =================
|
||||
function buildClinic() {
|
||||
var m = newMap(12, 8);
|
||||
indoorShell(m, [3, 8], 5);
|
||||
// 芙医生的桌 + 桌上文件(C8)
|
||||
put(m.obs, 8, 2, G.table); put(m.obs, 9, 2, G.fileP);
|
||||
put(m.obs, 9, 3, G.chair);
|
||||
put(m.obs, 1, 1, G.cabinet); put(m.obs, 10, 6, G.plant);
|
||||
spawn(m, 6, 5, 'default');
|
||||
spawn(m, 5, 5, 'from_plaza');
|
||||
door(m, 5, 6, 'plaza', 'from_clinic');
|
||||
npc(m, 8, 1, 'fuyisheng'); // 桌后
|
||||
inter(m, 8, 3, 'doctor_desk');
|
||||
return m;
|
||||
}
|
||||
|
||||
// ================= 6. lake 24×16 湖畔 =================
|
||||
function buildLake() {
|
||||
var m = newMap(24, 16);
|
||||
fill(m.ground, 0, 0, 24, 16, G.grass);
|
||||
scatter(m, 21, [0.06, 0.11, 0.15, 0.18]);
|
||||
var x, y;
|
||||
// 石路:东门(23,10)通向长椅与野餐区
|
||||
for (x = 3; x <= 23; x++) put(m.ground, x, 10, (x % 3 === 0) ? G.stoneAlt : G.stone);
|
||||
// 野餐布(T7,走上去触发,放 ground 可行走)
|
||||
put(m.ground, 15, 10, G.picTL); put(m.ground, 16, 10, G.picTR);
|
||||
put(m.ground, 15, 11, G.picBL); put(m.ground, 16, 11, G.picBR);
|
||||
put(m.ground, 3, 12, G.mushroom);
|
||||
// 湖水:北半边大片湖,水岸瓦片围边(全部进 obstacles)
|
||||
for (y = 0; y <= 5; y++)
|
||||
for (x = 2; x <= 21; x++)
|
||||
put(m.obs, x, y, ((x + y) % 2 === 0) ? G.waterA : G.waterB);
|
||||
for (x = 1; x <= 22; x++) put(m.obs, x, 6, G.shoreS);
|
||||
for (y = 0; y <= 5; y++) { put(m.obs, 1, y, G.shoreW); put(m.obs, 22, y, G.shoreE); }
|
||||
// 长椅(龟爷爷)+ 树木花草装饰
|
||||
put(m.obs, 10, 8, G.benchL); put(m.obs, 11, 8, G.benchR);
|
||||
tree2x2(m, 1, 13); tree2x2(m, 20, 12);
|
||||
put(m.obs, 8, 14, G.bush); put(m.obs, 12, 13, G.bush);
|
||||
put(m.obs, 5, 9, G.rock); put(m.obs, 14, 14, G.stump);
|
||||
spawn(m, 21, 11, 'default');
|
||||
spawn(m, 22, 10, 'from_plaza');
|
||||
door(m, 23, 10, 'plaza', 'from_lake');
|
||||
npc(m, 12, 8, 'guiyeye'); // 长椅旁
|
||||
npc(m, 6, 12, 'pig');
|
||||
inter(m, 16, 11, 'picnic_mat');
|
||||
return m;
|
||||
}
|
||||
|
||||
// ================= 7. field 20×14 花田 =================
|
||||
function buildField() {
|
||||
var m = newMap(20, 14);
|
||||
fill(m.ground, 0, 0, 20, 14, G.grass);
|
||||
scatter(m, 33, [0.10, 0.18, 0.24, 0.32]);
|
||||
var x, y;
|
||||
// 土路横穿(西门进出)
|
||||
for (x = 0; x <= 19; x++) put(m.ground, x, 7, G.dirt);
|
||||
// 浇花点花丛(T2/T12 用,ground 装饰)
|
||||
put(m.ground, 15, 9, G.flR); put(m.ground, 16, 9, G.flY); put(m.ground, 17, 9, G.flB);
|
||||
put(m.ground, 15, 10, G.flY); put(m.ground, 16, 10, G.flR); put(m.ground, 17, 10, G.flB);
|
||||
// 栅栏围边,西门留口
|
||||
for (x = 0; x <= 19; x++) { put(m.obs, x, 0, G.fenceH); put(m.obs, x, 13, G.fenceH); }
|
||||
for (y = 1; y <= 12; y++) { if (y !== 7) put(m.obs, 0, y, G.fenceV); put(m.obs, 19, y, G.fenceV); }
|
||||
// 5 丛莓果(T6)
|
||||
var berries = [[4, 4], [9, 4], [14, 4], [6, 9], [12, 9]];
|
||||
for (var b = 0; b < berries.length; b++) put(m.obs, berries[b][0], berries[b][1], G.berry);
|
||||
// 树木装饰
|
||||
tree2x2(m, 16, 1);
|
||||
put(m.obs, 2, 11, G.bush); put(m.obs, 10, 11, G.bush); put(m.obs, 2, 2, G.rock);
|
||||
spawn(m, 2, 7, 'default');
|
||||
spawn(m, 1, 7, 'from_plaza');
|
||||
door(m, 0, 7, 'plaza', 'from_field');
|
||||
npc(m, 10, 7, 'chick');
|
||||
inter(m, 4, 5, 'berry_1');
|
||||
inter(m, 9, 5, 'berry_2');
|
||||
inter(m, 14, 5, 'berry_3');
|
||||
inter(m, 6, 10, 'berry_4');
|
||||
inter(m, 12, 10, 'berry_5');
|
||||
inter(m, 16, 10, 'water_point');
|
||||
return m;
|
||||
}
|
||||
|
||||
// ================= 8. well 14×10 古井空地 =================
|
||||
function buildWell() {
|
||||
var m = newMap(14, 10);
|
||||
fill(m.ground, 0, 0, 14, 10, G.grass);
|
||||
scatter(m, 45, [0.07, 0.13, 0.17, 0.19]);
|
||||
var y;
|
||||
// 土场 + 南门土路
|
||||
fill(m.ground, 5, 3, 4, 4, G.dirt);
|
||||
for (y = 7; y <= 9; y++) { put(m.ground, 6, y, G.dirt); put(m.ground, 7, y, G.dirt); }
|
||||
put(m.ground, 3, 4, G.mushroom); put(m.ground, 10, 6, G.mushroom);
|
||||
// 中央古井(C7)
|
||||
put(m.obs, 6, 4, G.wellTL); put(m.obs, 7, 4, G.wellTR);
|
||||
put(m.obs, 6, 5, G.wellBL); put(m.obs, 7, 5, G.wellBR);
|
||||
// 四周树木
|
||||
tree2x2(m, 1, 1); tree2x2(m, 11, 1); tree2x2(m, 1, 7); tree2x2(m, 11, 7);
|
||||
put(m.obs, 4, 2, G.bush); put(m.obs, 9, 2, G.bush);
|
||||
put(m.obs, 4, 8, G.bush); put(m.obs, 9, 8, G.bush);
|
||||
put(m.obs, 4, 5, G.rock); put(m.obs, 9, 5, G.rock);
|
||||
spawn(m, 6, 8, 'default');
|
||||
spawn(m, 7, 8, 'from_plaza');
|
||||
door(m, 7, 9, 'plaza', 'from_well');
|
||||
inter(m, 6, 6, 'ancient_well');
|
||||
return m;
|
||||
}
|
||||
|
||||
// ================= 9. fog 20×8 雾墙边界 =================
|
||||
function buildFog() {
|
||||
var m = newMap(20, 8);
|
||||
fill(m.ground, 0, 0, 20, 8, G.grass);
|
||||
scatter(m, 57, [0.05, 0.09, 0.20, 0.20]);
|
||||
var x, y;
|
||||
// 雾前深草带 + 北门土路
|
||||
for (x = 0; x <= 19; x++) put(m.ground, x, 4, G.deepGrass);
|
||||
for (y = 0; y <= 4; y++) { put(m.ground, 9, y, G.dirt); put(m.ground, 10, y, G.dirt); }
|
||||
// 远端(南侧)雾墙铺满,进 obstacles(E3 入口)
|
||||
for (y = 5; y <= 7; y++)
|
||||
for (x = 0; x <= 19; x++)
|
||||
put(m.obs, x, y, [G.fogA, G.fogB, G.fogC][(x + y) % 3]);
|
||||
// 两侧树木
|
||||
tree2x2(m, 1, 1); tree2x2(m, 17, 1);
|
||||
put(m.obs, 4, 3, G.bush); put(m.obs, 15, 3, G.bush);
|
||||
put(m.obs, 0, 3, G.bush); put(m.obs, 19, 3, G.bush);
|
||||
put(m.obs, 6, 2, G.rock);
|
||||
spawn(m, 10, 2, 'default');
|
||||
spawn(m, 9, 2, 'from_plaza'); // 落点距门 >24px,防回弹
|
||||
door(m, 10, 0, 'plaza', 'from_fog');
|
||||
inter(m, 10, 4, 'fog_wall'); // 雾前可达格
|
||||
return m;
|
||||
}
|
||||
|
||||
// ================= 10. white 20×14 白色层(走廊 + 病房) =================
|
||||
function buildWhite() {
|
||||
var m = newMap(20, 14);
|
||||
var x, y;
|
||||
// 默认白墙铺底;上半 20×6 走廊 + 左下 10×8 病房刷白地板
|
||||
fill(m.ground, 0, 0, 20, 14, G.wWall);
|
||||
for (y = 1; y <= 5; y++)
|
||||
for (x = 1; x <= 18; x++)
|
||||
m.ground[y][x] = (x % 4 === 0 && y % 2 === 0) ? G.wFloor2 : G.wFloor;
|
||||
for (y = 7; y <= 12; y++)
|
||||
for (x = 1; x <= 8; x++)
|
||||
m.ground[y][x] = ((x + y) % 3 === 0) ? G.wFloor2 : G.wFloor;
|
||||
put(m.ground, 4, 6, G.wFloor); put(m.ground, 5, 6, G.wFloor); // 走廊↔病房通道格
|
||||
// 墙体(obstacles):北墙(含墙板/天花板灯)、东西墙、南墙、右下实心墙块、病房北/东墙
|
||||
for (x = 0; x <= 19; x++) put(m.obs, x, 0, G.wWall);
|
||||
put(m.obs, 5, 0, G.wPanel); put(m.obs, 10, 0, G.wPanel); put(m.obs, 15, 0, G.wPanel);
|
||||
put(m.obs, 3, 0, G.wLamp); put(m.obs, 8, 0, G.wLamp);
|
||||
put(m.obs, 13, 0, G.wLamp); put(m.obs, 18, 0, G.wLamp);
|
||||
for (y = 1; y <= 13; y++) { put(m.obs, 0, y, G.wWall); put(m.obs, 19, y, G.wWall); }
|
||||
for (x = 1; x <= 9; x++) put(m.obs, x, 13, G.wWall);
|
||||
fill(m.obs, 10, 6, 10, 8, G.wWall); // 右下 10×8 实心(房间之外)
|
||||
for (x = 1; x <= 9; x++) if (x !== 4 && x !== 5) put(m.obs, x, 6, G.wWall);
|
||||
for (y = 6; y <= 13; y++) put(m.obs, 9, y, G.wWall);
|
||||
put(m.obs, 9, 8, G.wCur); put(m.obs, 9, 9, G.wCur); // 白窗帘挂在东墙上
|
||||
// 病房家具:白床 + 白监护仪 + 白柜
|
||||
put(m.obs, 2, 8, G.wBedTL); put(m.obs, 3, 8, G.wBedTR);
|
||||
put(m.obs, 2, 9, G.wBedBL); put(m.obs, 3, 9, G.wBedBR);
|
||||
put(m.obs, 4, 8, G.wMon);
|
||||
put(m.obs, 7, 12, G.wCab);
|
||||
spawn(m, 17, 3, 'default'); // 走廊一端
|
||||
return m;
|
||||
}
|
||||
|
||||
// ================= 序列化 =================
|
||||
function csv(l, w, h) {
|
||||
var s = '';
|
||||
for (var y = 0; y < h; y++) {
|
||||
var row = '';
|
||||
for (var x = 0; x < w; x++) {
|
||||
row += l[y][x];
|
||||
if (x < w - 1) row += ',';
|
||||
}
|
||||
s += row;
|
||||
if (y < h - 1) s += ',\n';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
function tmx(m) {
|
||||
var s = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
s += '<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down"' +
|
||||
' width="' + m.w + '" height="' + m.h + '" tilewidth="16" tileheight="16" infinite="0"' +
|
||||
' nextlayerid="4" nextobjectid="' + (m.objs.length + 1) + '">\n';
|
||||
s += ' <tileset firstgid="1" source="tileset.tsx"/>\n';
|
||||
s += ' <layer id="1" name="ground" width="' + m.w + '" height="' + m.h + '">\n';
|
||||
s += ' <data encoding="csv">\n' + csv(m.ground, m.w, m.h) + '\n </data>\n </layer>\n';
|
||||
s += ' <layer id="2" name="obstacles" width="' + m.w + '" height="' + m.h + '">\n';
|
||||
s += ' <properties>\n <property name="collides" type="bool" value="true"/>\n </properties>\n';
|
||||
s += ' <data encoding="csv">\n' + csv(m.obs, m.w, m.h) + '\n </data>\n </layer>\n';
|
||||
s += ' <objectgroup id="3" name="objects">\n';
|
||||
for (var i = 0; i < m.objs.length; i++) {
|
||||
var o = m.objs[i];
|
||||
s += ' <object id="' + (i + 1) + '" name="' + o.name + '" x="' + o.x + '" y="' + o.y + '">\n';
|
||||
s += ' <properties>\n';
|
||||
for (var p = 0; p < o.props.length; p++)
|
||||
s += ' <property name="' + o.props[p][0] + '" value="' + o.props[p][1] + '"/>\n';
|
||||
s += ' </properties>\n <point/>\n </object>\n';
|
||||
}
|
||||
s += ' </objectgroup>\n</map>\n';
|
||||
return s;
|
||||
}
|
||||
function writeMap(name, m) {
|
||||
var f = new TextFile(OUT + name + '.tmx', TextFile.WriteOnly);
|
||||
f.write(tmx(m));
|
||||
f.commit();
|
||||
tiled.log('wrote ' + name + '.tmx (' + m.w + 'x' + m.h + ', ' + m.objs.length + ' objects)');
|
||||
}
|
||||
|
||||
writeMap('house', buildHouse());
|
||||
writeMap('plaza', buildPlaza());
|
||||
writeMap('bakery', buildBakery());
|
||||
writeMap('shop', buildShop());
|
||||
writeMap('clinic', buildClinic());
|
||||
writeMap('lake', buildLake());
|
||||
writeMap('field', buildField());
|
||||
writeMap('well', buildWell());
|
||||
writeMap('fog', buildFog());
|
||||
writeMap('white', buildWhite());
|
||||
tiled.log('all 10 maps done');
|
||||