Honey Village: six model builds + hub frontend, Dockerized for Dokploy
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4210;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); }
|
||||
await page.click('canvas'); await page.waitForTimeout(200);
|
||||
|
||||
// Try to walk to door: door at tile (5,7)=(80,112). Player at ~(96,64).
|
||||
// Move left first to align x, then down.
|
||||
await page.keyboard.down('a'); await page.waitForTimeout(500); await page.keyboard.up('a');
|
||||
await page.waitForTimeout(100);
|
||||
const afterLeft = await page.evaluate(() => { const p=window.game.scene.getScene('HouseScene').player; return {x:p.x,y:p.y}; });
|
||||
console.log('after left:', JSON.stringify(afterLeft));
|
||||
|
||||
// Now move down toward door
|
||||
await page.keyboard.down('s');
|
||||
for (let i=0;i<8;i++){ await page.waitForTimeout(300); const p=await page.evaluate(()=>{const pl=window.game.scene.getScene('HouseScene').player;return{x:pl.x,y:pl.y,vx:pl.body.velocity.x,vy:pl.body.velocity.y};}); console.log('down step',i,JSON.stringify(p)); if(p.y>100) break; }
|
||||
await page.keyboard.up('s');
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const nearDoor = await page.evaluate(() => {
|
||||
const h = window.game.scene.getScene('HouseScene');
|
||||
const p = h.player;
|
||||
const door = h.doors[0];
|
||||
const dist = Math.hypot(p.x-door.x, p.y-door.y);
|
||||
return { px:p.x, py:p.y, doorX:door.x, doorY:door.y, dist, worldBounds: {w:h.physics.world.bounds.width, h:h.physics.world.bounds.height} };
|
||||
});
|
||||
console.log('near door:', JSON.stringify(nearDoor));
|
||||
|
||||
// press E to transition
|
||||
await page.keyboard.press('e');
|
||||
await page.waitForTimeout(1500);
|
||||
const activeScenes = await page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key));
|
||||
console.log('active scenes after E:', JSON.stringify(activeScenes));
|
||||
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
@@ -0,0 +1,44 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4211;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
const checks={};
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); }
|
||||
await page.click('canvas'); await page.waitForTimeout(300);
|
||||
|
||||
// Walk down toward the door. Door at (88,112). Player starts ~ (96,64).
|
||||
// Just walk straight down — the door auto-trigger should fire when close.
|
||||
await page.keyboard.down('s');
|
||||
let escaped = false;
|
||||
for (let i=0;i<12;i++){
|
||||
await page.waitForTimeout(300);
|
||||
const st = await page.evaluate(() => {
|
||||
const g = window.game;
|
||||
const active = g.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key);
|
||||
return { active, hasPlaza: active.includes('PlazaScene'), hasHouse: active.includes('HouseScene') };
|
||||
});
|
||||
if (st.hasPlaza && !st.hasHouse) { escaped = true; break; }
|
||||
}
|
||||
await page.keyboard.up('s');
|
||||
checks.escaped_to_plaza = escaped;
|
||||
|
||||
// Now from plaza, walk to each door and verify they auto-transition
|
||||
// plaza doors: door_house(bottom center 14,18), door_bakery(left 5,9), etc.
|
||||
// Wait for plaza to settle
|
||||
await page.waitForTimeout(800);
|
||||
const plazaActive = await page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).some(s=>s.scene.key==='PlazaScene'));
|
||||
checks.in_plaza = plazaActive;
|
||||
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
console.log('CHECKS:', JSON.stringify(checks));
|
||||
@@ -0,0 +1,46 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4214;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter'); await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); }
|
||||
await page.click('canvas'); await page.waitForTimeout(300);
|
||||
// house -> plaza
|
||||
await page.keyboard.down('s'); await page.waitForTimeout(1500);
|
||||
// wait for plaza
|
||||
for (let i=0;i<10;i++){ await page.waitForTimeout(300); const a=await page.evaluate(()=>window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); if(a.includes('PlazaScene')) break; }
|
||||
await page.keyboard.up('s'); await page.waitForTimeout(600);
|
||||
|
||||
// Now in plaza. Check player position and doors
|
||||
const state = await page.evaluate(() => {
|
||||
const p = window.game.scene.getScene('PlazaScene');
|
||||
return {
|
||||
px: p.player.x, py: p.player.y,
|
||||
doors: p.doors.map(d=>({x:d.x,y:d.y,target:d.target})),
|
||||
};
|
||||
});
|
||||
console.log('plaza state:', JSON.stringify(state));
|
||||
|
||||
// Directly teleport player to bakery door and check if auto-transition works
|
||||
await page.evaluate(() => { window.game.scene.getScene('PlazaScene').player.setPosition(80, 144); });
|
||||
await page.waitForTimeout(800);
|
||||
let a = await page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key));
|
||||
console.log('after teleport to bakery door:', JSON.stringify(a));
|
||||
|
||||
// If still in plaza, press E
|
||||
if (a.includes('PlazaScene')) {
|
||||
await page.keyboard.press('e');
|
||||
await page.waitForTimeout(800);
|
||||
a = await page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key));
|
||||
console.log('after E at bakery door:', JSON.stringify(a));
|
||||
}
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
@@ -0,0 +1,31 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4219;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter'); await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); }
|
||||
await page.click('canvas'); await page.waitForTimeout(300);
|
||||
// house -> plaza via walking down
|
||||
await page.keyboard.down('s');
|
||||
for (let i=0;i<15;i++){ await page.waitForTimeout(250); const a=await page.evaluate(()=>window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); if(a.includes('PlazaScene')) break; }
|
||||
await page.keyboard.up('s'); await page.waitForTimeout(900);
|
||||
await page.click('canvas'); await page.waitForTimeout(200);
|
||||
|
||||
const st = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene').player; return {x:p.x,y:p.y,active:window.game.scene.getScene('PlazaScene').scene.isActive()}; });
|
||||
console.log('in plaza:', JSON.stringify(st));
|
||||
|
||||
// hold w, check movement
|
||||
await page.keyboard.down('w'); await page.waitForTimeout(600);
|
||||
const w = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene').player; return {x:p.x,y:p.y,vy:p.body.velocity.y}; });
|
||||
console.log('w held:', JSON.stringify(w));
|
||||
await page.keyboard.up('w');
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
@@ -0,0 +1,25 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4222;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter'); await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(120); }
|
||||
await page.click('canvas'); await page.waitForTimeout(300);
|
||||
await page.keyboard.down('s');
|
||||
for (let i=0;i<20;i++){
|
||||
await page.waitForTimeout(250);
|
||||
const s = await page.evaluate(() => { const g=window.game; const a=g.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key); let p=null; try{p={x:g.scene.getScene('HouseScene').player.x,y:g.scene.getScene('HouseScene').player.y};}catch(e){} return {a,p}; });
|
||||
console.log('s step', i, JSON.stringify(s));
|
||||
if (s.a.includes('PlazaScene')) { console.log('ENTERED PLAZA'); break; }
|
||||
}
|
||||
await page.keyboard.up('s');
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
@@ -0,0 +1,36 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4221;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter'); await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(120); }
|
||||
await page.click('canvas'); await page.waitForTimeout(300);
|
||||
// enter plaza
|
||||
await page.keyboard.down('s');
|
||||
for (let i=0;i<15;i++){ await page.waitForTimeout(220); const a=await page.evaluate(()=>window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); if(a.includes('PlazaScene')) break; }
|
||||
await page.keyboard.up('s'); await page.waitForTimeout(800);
|
||||
// teleport player to center of plaza row 8 (y=136) col 14 (x=232)
|
||||
await page.evaluate(() => window.game.scene.getScene('PlazaScene').player.setPosition(232, 136));
|
||||
await page.waitForTimeout(300);
|
||||
const start = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene').player; return {x:p.x,y:p.y}; });
|
||||
console.log('start at row8 center:', JSON.stringify(start));
|
||||
// walk left, sample position every 300ms
|
||||
await page.keyboard.down('a');
|
||||
for (let i=0;i<20;i++){
|
||||
await page.waitForTimeout(250);
|
||||
const s = await page.evaluate(() => { const g=window.game; const a=g.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key); const p=g.scene.getScene('PlazaScene').player; return {a, x:p?p.x:null, y:p?p.y:null}; });
|
||||
console.log('left step', i, JSON.stringify(s));
|
||||
if (s.a.includes('BakeryScene')) { console.log('ENTERED BAKERY'); break; }
|
||||
if (!s.a.includes('PlazaScene')) break;
|
||||
}
|
||||
await page.keyboard.up('a');
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
@@ -0,0 +1,60 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4201;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('console', (msg) => { if (msg.type()==='error'||msg.type()==='warn') console.log('CONSOLE', msg.type(), msg.text()); });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1200);
|
||||
|
||||
// check keyboard setup
|
||||
const setup = await page.evaluate(() => {
|
||||
const house = window.game.scene.getScene('HouseScene');
|
||||
return {
|
||||
kbExists: !!house.input.keyboard,
|
||||
cursorsExists: !!house.cursors,
|
||||
wasdExists: !!house.wasd,
|
||||
wasdKeys: house.wasd ? Object.keys(house.wasd) : null,
|
||||
wasdA: house.wasd && house.wasd.A ? { isDown: house.wasd.A.isDown, keyCode: house.wasd.A.keyCode } : null,
|
||||
playerExists: !!house.player,
|
||||
playerPos: house.player ? {x:house.player.x, y:house.player.y} : null,
|
||||
dialogueActive: window.game.scene.getScene('UIScene').active,
|
||||
};
|
||||
});
|
||||
console.log('SETUP:', JSON.stringify(setup, null, 2));
|
||||
|
||||
// dismiss dialogue if active
|
||||
if (setup.dialogueActive) {
|
||||
await page.keyboard.press('e'); await page.waitForTimeout(200);
|
||||
await page.keyboard.press('e'); await page.waitForTimeout(200);
|
||||
}
|
||||
|
||||
const before = await page.evaluate(() => ({x: window.game.scene.getScene('HouseScene').player.x, y: window.game.scene.getScene('HouseScene').player.y, v: window.game.scene.getScene('HouseScene').player.body.velocity}));
|
||||
|
||||
// press and hold 'd' for 500ms
|
||||
await page.keyboard.down('d');
|
||||
await page.waitForTimeout(500);
|
||||
const during = await page.evaluate(() => ({
|
||||
x: window.game.scene.getScene('HouseScene').player.x,
|
||||
y: window.game.scene.getScene('HouseScene').player.y,
|
||||
v: {x: window.game.scene.getScene('HouseScene').player.body.velocity.x, y: window.game.scene.getScene('HouseScene').player.body.velocity.y},
|
||||
wasdD_isDown: window.game.scene.getScene('HouseScene').wasd.D.isDown,
|
||||
cursorsRight: window.game.scene.getScene('HouseScene').cursors.right.isDown,
|
||||
}));
|
||||
await page.keyboard.up('d');
|
||||
await page.waitForTimeout(100);
|
||||
const after = await page.evaluate(() => ({x: window.game.scene.getScene('HouseScene').player.x, y: window.game.scene.getScene('HouseScene').player.y}));
|
||||
|
||||
console.log('BEFORE:', JSON.stringify(before));
|
||||
console.log('DURING (d held):', JSON.stringify(during));
|
||||
console.log('AFTER:', JSON.stringify(after));
|
||||
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
@@ -0,0 +1,41 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4202;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1200);
|
||||
// dismiss dialogue
|
||||
await page.keyboard.press('e'); await page.waitForTimeout(200); await page.keyboard.press('e'); await page.waitForTimeout(200);
|
||||
|
||||
// Test: is UIScene dialogue blocking? Check UIScene input handlers consume keydown
|
||||
// Focus the canvas
|
||||
await page.click('canvas');
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Test arrow keys (cursors) and check global keydown listener
|
||||
await page.keyboard.down('ArrowRight');
|
||||
await page.waitForTimeout(200);
|
||||
const arrowState = await page.evaluate(() => {
|
||||
const h = window.game.scene.getScene('HouseScene');
|
||||
return { cursorRight: h.cursors.right.isDown, wasdD: h.wasd.D.isDown, inputEnabled: h.input.enabled };
|
||||
});
|
||||
console.log('ArrowRight held:', JSON.stringify(arrowState));
|
||||
await page.keyboard.up('ArrowRight');
|
||||
|
||||
// Check if maybe the scene's update isn't running (frozen?)
|
||||
const updateCheck = await page.evaluate(() => {
|
||||
const h = window.game.scene.getScene('HouseScene');
|
||||
return { sceneActive: h.scene.isActive(), frozen: h.player.frozen, lastUpdate: h.sys.settings.active };
|
||||
});
|
||||
console.log('scene status:', JSON.stringify(updateCheck));
|
||||
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
@@ -0,0 +1,46 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4203;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1200);
|
||||
await page.keyboard.press('e'); await page.waitForTimeout(200); await page.keyboard.press('e'); await page.waitForTimeout(200);
|
||||
|
||||
// Inspect wasd key objects in detail
|
||||
const wasdDetail = await page.evaluate(() => {
|
||||
const h = window.game.scene.getScene('HouseScene');
|
||||
const d = h.wasd.D;
|
||||
return {
|
||||
type: typeof d,
|
||||
hasKeyCode: 'keyCode' in d,
|
||||
keyCode: d.keyCode,
|
||||
isDown: d.isDown,
|
||||
sceneInputEnabled: d.scene && d.scene.input ? d.scene.input.enabled : 'no scene ref',
|
||||
// check which scene the key belongs to
|
||||
sceneKey: d.scene ? d.scene.sys.config.key : null,
|
||||
};
|
||||
});
|
||||
console.log('wasd.D detail:', JSON.stringify(wasdDetail, null, 2));
|
||||
|
||||
// Also test: create a fresh key and see if it tracks
|
||||
const freshTest = await page.evaluate(() => {
|
||||
const h = window.game.scene.getScene('HouseScene');
|
||||
const k = h.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D);
|
||||
h.__testD = k;
|
||||
return { keyCode: k.keyCode };
|
||||
});
|
||||
await page.keyboard.down('d');
|
||||
await page.waitForTimeout(200);
|
||||
const freshDown = await page.evaluate(() => ({ testD: window.game.scene.getScene('HouseScene').__testD.isDown, wasdD: window.game.scene.getScene('HouseScene').wasd.D.isDown }));
|
||||
console.log('fresh key d held:', JSON.stringify(freshDown));
|
||||
await page.keyboard.up('d');
|
||||
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
@@ -0,0 +1,55 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4204;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1200);
|
||||
// fully dismiss dialogue
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); }
|
||||
await page.click('canvas');
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// check player internal keys
|
||||
const keys = await page.evaluate(() => {
|
||||
const p = window.game.scene.getScene('HouseScene').player;
|
||||
return {
|
||||
hasKLeft: !!p.kLeft,
|
||||
kLeftIsDown: p.kLeft && p.kLeft.isDown,
|
||||
kLeftPluginNull: p.kLeft && p.kLeft.plugin == null,
|
||||
wasdLen: p.wasdKeys ? p.wasdKeys.length : null,
|
||||
wasdDisDown: p.wasdKeys && p.wasdKeys[3] ? p.wasdKeys[3].isDown : null,
|
||||
moving: p.moving,
|
||||
frozen: p.frozen,
|
||||
};
|
||||
});
|
||||
console.log('player keys:', JSON.stringify(keys));
|
||||
|
||||
// hold d, check player's internal D key
|
||||
await page.keyboard.down('d');
|
||||
await page.waitForTimeout(400);
|
||||
const during = await page.evaluate(() => {
|
||||
const p = window.game.scene.getScene('HouseScene').player;
|
||||
return {
|
||||
x: p.x, y: p.y,
|
||||
vx: p.body.velocity.x, vy: p.body.velocity.y,
|
||||
wasdDisDown: p.wasdKeys[3].isDown,
|
||||
kRightDown: p.kRight.isDown,
|
||||
moving: p.moving,
|
||||
};
|
||||
});
|
||||
console.log('DURING d-held:', JSON.stringify(during));
|
||||
await page.keyboard.up('d');
|
||||
await page.waitForTimeout(200);
|
||||
const after = await page.evaluate(() => ({x: window.game.scene.getScene('HouseScene').player.x, y: window.game.scene.getScene('HouseScene').player.y}));
|
||||
console.log('AFTER:', JSON.stringify(after));
|
||||
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
@@ -0,0 +1,31 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4205;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
const checks={};
|
||||
const pos = () => page.evaluate(() => { const p=window.game.scene.getScene('HouseScene').player; return {x:p.x,y:p.y}; });
|
||||
let page;
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); }
|
||||
await page.click('canvas'); await page.waitForTimeout(200);
|
||||
|
||||
for (const [key, label] of [['d','right'],['a','left'],['s','down'],['w','up'],['ArrowRight','arrowR'],['ArrowLeft','arrowL'],['ArrowUp','arrowU'],['ArrowDown','arrowD']]) {
|
||||
const b = await pos();
|
||||
await page.keyboard.down(key); await page.waitForTimeout(350); await page.keyboard.up(key); await page.waitForTimeout(80);
|
||||
const a = await pos();
|
||||
const moved = Math.abs(a.x-b.x) > 3 || Math.abs(a.y-b.y) > 3;
|
||||
checks[label] = moved;
|
||||
console.log(`${label}: (${b.x.toFixed(0)},${b.y.toFixed(0)}) -> (${a.x.toFixed(0)},${a.y.toFixed(0)}) moved=${moved}`);
|
||||
}
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
console.log('CHECKS:', JSON.stringify(checks));
|
||||
@@ -0,0 +1,59 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4212;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
const checks={};
|
||||
const getActive = (page) => page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key));
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); }
|
||||
await page.click('canvas'); await page.waitForTimeout(300);
|
||||
|
||||
// helper: walk in a direction until scene changes or timeout
|
||||
async function walkToScene(dir, wantScene, maxSteps=12) {
|
||||
await page.keyboard.down(dir);
|
||||
for (let i=0;i<maxSteps;i++){
|
||||
await page.waitForTimeout(300);
|
||||
const a = await getActive(page);
|
||||
if (a.includes(wantScene)) { await page.keyboard.up(dir); return true; }
|
||||
}
|
||||
await page.keyboard.up(dir);
|
||||
return false;
|
||||
}
|
||||
|
||||
// house -> plaza (walk down)
|
||||
checks.house_to_plaza = await walkToScene('s', 'PlazaScene');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// plaza -> bakery (door at left tile 5,9 = walk left). Player spawns near door_house (bottom center).
|
||||
// First walk up, then left to bakery door
|
||||
checks.plaza_to_bakery = await walkToScene('a', 'BakeryScene');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// bakery -> plaza (door at bottom center 7,9, walk down)
|
||||
checks.bakery_to_plaza = await walkToScene('s', 'PlazaScene');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// plaza -> shop (door at right tile 24,9, walk right)
|
||||
checks.plaza_to_shop = await walkToScene('d', 'ShopScene');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// shop -> plaza
|
||||
checks.shop_to_plaza = await walkToScene('s', 'PlazaScene');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// plaza -> house (door_house bottom center 14,18, walk down)
|
||||
checks.plaza_to_house = await walkToScene('s', 'HouseScene');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
console.log('CHECKS:', JSON.stringify(checks, null, 2));
|
||||
@@ -0,0 +1,50 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4213;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
const checks={};
|
||||
const getActive = (page) => page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key));
|
||||
async function walk(page, dir, ms){ await page.keyboard.down(dir); await page.waitForTimeout(ms); await page.keyboard.up(dir); await page.waitForTimeout(150); }
|
||||
async function walkToScene(page, dir, wantScene, maxSteps=15){
|
||||
await page.keyboard.down(dir);
|
||||
for (let i=0;i<maxSteps;i++){ await page.waitForTimeout(300); const a=await getActive(page); if(a.includes(wantScene)){ await page.keyboard.up(dir); return true; } }
|
||||
await page.keyboard.up(dir); return false;
|
||||
}
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter'); await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); }
|
||||
await page.click('canvas'); await page.waitForTimeout(300);
|
||||
|
||||
// house -> plaza
|
||||
checks.house_to_plaza = await walkToScene(page, 's', 'PlazaScene');
|
||||
await page.waitForTimeout(600);
|
||||
// From plaza door_house (bottom center), walk UP to row 9, then LEFT to bakery door (5,9)
|
||||
await walk(page, 'w', 1800); // walk up
|
||||
checks.plaza_to_bakery = await walkToScene(page, 'a', 'BakeryScene');
|
||||
await page.waitForTimeout(500);
|
||||
// bakery -> plaza
|
||||
checks.bakery_to_plaza = await walkToScene(page, 's', 'PlazaScene');
|
||||
await page.waitForTimeout(500);
|
||||
// plaza: walk up then right to shop door (24,9)
|
||||
await walk(page, 'w', 1800);
|
||||
checks.plaza_to_shop = await walkToScene(page, 'd', 'ShopScene');
|
||||
await page.waitForTimeout(500);
|
||||
checks.shop_to_plaza = await walkToScene(page, 's', 'PlazaScene');
|
||||
await page.waitForTimeout(500);
|
||||
// plaza -> clinic (top center, door 14,1): walk up
|
||||
await walk(page, 'a', 800); // recenter
|
||||
checks.plaza_to_clinic = await walkToScene(page, 'w', 'ClinicScene');
|
||||
await page.waitForTimeout(500);
|
||||
checks.clinic_to_plaza = await walkToScene(page, 's', 'PlazaScene');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
console.log('CHECKS:', JSON.stringify(checks, null, 2));
|
||||
@@ -0,0 +1,64 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4215;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
const checks={};
|
||||
const getActive = (page) => page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key));
|
||||
async function walkToScene(page, dir, wantScene, maxSteps=20){
|
||||
await page.keyboard.down(dir);
|
||||
for (let i=0;i<maxSteps;i++){ await page.waitForTimeout(250); const a=await getActive(page); if(a.includes(wantScene)){ await page.keyboard.up(dir); return true; } }
|
||||
await page.keyboard.up(dir); return false;
|
||||
}
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter'); await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); }
|
||||
await page.click('canvas'); await page.waitForTimeout(300);
|
||||
|
||||
// house -> plaza (walk down to bottom edge door)
|
||||
checks.house_to_plaza = await walkToScene(page, 's', 'PlazaScene');
|
||||
await page.waitForTimeout(600);
|
||||
// plaza -> bakery (walk left to left edge). Player spawns at door_house bottom, offset upward.
|
||||
checks.plaza_to_bakery = await walkToScene(page, 'a', 'BakeryScene');
|
||||
await page.waitForTimeout(600);
|
||||
// bakery -> plaza (walk down)
|
||||
checks.bakery_to_plaza = await walkToScene(page, 's', 'PlazaScene');
|
||||
await page.waitForTimeout(600);
|
||||
// plaza -> shop (walk right)
|
||||
checks.plaza_to_shop = await walkToScene(page, 'd', 'ShopScene');
|
||||
await page.waitForTimeout(600);
|
||||
// shop -> plaza
|
||||
checks.shop_to_plaza = await walkToScene(page, 's', 'PlazaScene');
|
||||
await page.waitForTimeout(600);
|
||||
// plaza -> clinic (walk up to top edge)
|
||||
checks.plaza_to_clinic = await walkToScene(page, 'w', 'ClinicScene');
|
||||
await page.waitForTimeout(600);
|
||||
// clinic -> plaza
|
||||
checks.clinic_to_plaza = await walkToScene(page, 's', 'PlazaScene');
|
||||
await page.waitForTimeout(600);
|
||||
// plaza -> lake (walk left - lake at col0,row10, bakery at col0,row9; both left edge)
|
||||
// player may hit bakery first; let's walk up a bit then left
|
||||
await page.keyboard.down('w'); await page.waitForTimeout(800); await page.keyboard.up('w');
|
||||
// now walk down to row 10 area then left
|
||||
await page.keyboard.down('s'); await page.waitForTimeout(400); await page.keyboard.up('s');
|
||||
checks.plaza_to_lake = await walkToScene(page, 'a', 'LakeScene');
|
||||
await page.waitForTimeout(600);
|
||||
// lake -> plaza (door_plaza at right edge col 22,8 — walk right)
|
||||
checks.lake_to_plaza = await walkToScene(page, 'd', 'PlazaScene');
|
||||
await page.waitForTimeout(600);
|
||||
// plaza -> field (walk right, field at col29 row10)
|
||||
checks.plaza_to_field = await walkToScene(page, 'd', 'FieldScene');
|
||||
await page.waitForTimeout(600);
|
||||
// field -> plaza
|
||||
checks.field_to_plaza = await walkToScene(page, 'a', 'PlazaScene');
|
||||
await page.waitForTimeout(600);
|
||||
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
console.log('CHECKS:', JSON.stringify(checks, null, 2));
|
||||
@@ -0,0 +1,61 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4220;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
const checks={};
|
||||
const getActive = (page) => page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key));
|
||||
async function walk(page, dir, ms){ await page.keyboard.down(dir); await page.waitForTimeout(ms); await page.keyboard.up(dir); await page.waitForTimeout(120); }
|
||||
async function walkToScene(page, dir, wantScene, maxSteps=25){
|
||||
await page.keyboard.down(dir);
|
||||
for (let i=0;i<maxSteps;i++){ await page.waitForTimeout(220); const a=await getActive(page); if(a.includes(wantScene)){ await page.keyboard.up(dir); return true; } }
|
||||
await page.keyboard.up(dir); return false;
|
||||
}
|
||||
async function enterPlaza(page){
|
||||
await page.keyboard.down('s');
|
||||
for (let i=0;i<15;i++){ await page.waitForTimeout(220); const a=await getActive(page); if(a.includes('PlazaScene')) break; }
|
||||
await page.keyboard.up('s'); await page.waitForTimeout(700);
|
||||
}
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter'); await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(120); }
|
||||
await page.click('canvas'); await page.waitForTimeout(300);
|
||||
|
||||
// Test each plaza exit independently by teleporting to plaza center first
|
||||
await enterPlaza(page);
|
||||
// bakery: walk up to row ~8 then left
|
||||
await walk(page, 'w', 2500);
|
||||
checks.bakery = await walkToScene(page, 'a', 'BakeryScene');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// reset: go back to plaza
|
||||
if (!checks.bakery) await page.evaluate(() => { const g=window.game; g.scene.stop('HouseScene'); g.scene.start('PlazaScene',{spawn:'default'}); });
|
||||
else await walkToScene(page, 's', 'PlazaScene');
|
||||
await page.waitForTimeout(700); await page.click('canvas');
|
||||
|
||||
// shop: walk up then right
|
||||
await walk(page, 'w', 2500);
|
||||
checks.shop = await walkToScene(page, 'd', 'ShopScene');
|
||||
await page.waitForTimeout(500);
|
||||
await walkToScene(page, 's', 'PlazaScene'); await page.waitForTimeout(700); await page.click('canvas');
|
||||
|
||||
// clinic: walk up
|
||||
await walk(page, 'a', 600);
|
||||
checks.clinic = await walkToScene(page, 'w', 'ClinicScene');
|
||||
await page.waitForTimeout(500);
|
||||
await walkToScene(page, 's', 'PlazaScene'); await page.waitForTimeout(700); await page.click('canvas');
|
||||
|
||||
// lake: walk down then left (lake at col0 row12 = lower left)
|
||||
await walk(page, 's', 1500);
|
||||
checks.lake = await walkToScene(page, 'a', 'LakeScene');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
console.log('CHECKS:', JSON.stringify(checks, null, 2));
|
||||
@@ -0,0 +1,42 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4216;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter'); await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); }
|
||||
await page.click('canvas'); await page.waitForTimeout(300);
|
||||
|
||||
// house -> plaza
|
||||
await page.keyboard.down('s');
|
||||
for (let i=0;i<15;i++){ await page.waitForTimeout(250); const a=await page.evaluate(()=>window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); if(a.includes('PlazaScene')) break; }
|
||||
await page.keyboard.up('s'); await page.waitForTimeout(700);
|
||||
|
||||
const inPlaza = await page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key));
|
||||
console.log('after entering plaza:', JSON.stringify(inPlaza));
|
||||
const pos = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene'); return {px:p.player.x, py:p.player.y, doors:p.doors.map(d=>({x:d.x,y:d.y,t:d.target}))}; });
|
||||
console.log('player pos in plaza:', pos.px, pos.py);
|
||||
console.log('doors:', JSON.stringify(pos.doors));
|
||||
|
||||
// Now walk left toward bakery door (col 0, row 8 = 0,128). Player at ~(224, 276).
|
||||
// Need to go up to row 8 first. Walk up.
|
||||
await page.keyboard.down('w'); await page.waitForTimeout(2500); await page.keyboard.up('w'); await page.waitForTimeout(200);
|
||||
const afterUp = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene').player; return {x:p.x,y:p.y}; });
|
||||
console.log('after walking up:', JSON.stringify(afterUp));
|
||||
// now walk left
|
||||
await page.keyboard.down('a');
|
||||
for (let i=0;i<20;i++){ await page.waitForTimeout(250); const a=await page.evaluate(()=>window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); if(a.includes('BakeryScene')){ console.log('entered bakery at step',i); break; } }
|
||||
await page.keyboard.up('a');
|
||||
const finalScenes = await page.evaluate(() => window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key));
|
||||
const finalPos = await page.evaluate(() => { try { return {x:window.game.scene.getScene('PlazaScene').player.x, y:window.game.scene.getScene('PlazaScene').player.y}; } catch(e){ return 'plaza gone'; } });
|
||||
console.log('final scenes:', JSON.stringify(finalScenes), 'pos:', JSON.stringify(finalPos));
|
||||
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
@@ -0,0 +1,52 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4217;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
page.on('console', (msg) => { if (msg.type()==='error') console.log('CONSOLE_ERR', msg.text()); });
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter'); await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); }
|
||||
await page.click('canvas'); await page.waitForTimeout(300);
|
||||
// house -> plaza
|
||||
await page.keyboard.down('s');
|
||||
for (let i=0;i<15;i++){ await page.waitForTimeout(250); const a=await page.evaluate(()=>window.game.scene.scenes.filter(s=>s.scene.isActive()).map(s=>s.scene.key)); if(a.includes('PlazaScene')) break; }
|
||||
await page.keyboard.up('s'); await page.waitForTimeout(800);
|
||||
|
||||
// Check plaza player keys and update status
|
||||
const info = await page.evaluate(() => {
|
||||
const p = window.game.scene.getScene('PlazaScene');
|
||||
const pl = p.player;
|
||||
return {
|
||||
px: pl.x, py: pl.y,
|
||||
moving: pl.moving,
|
||||
frozen: pl.frozen,
|
||||
hasKUp: !!pl.kUp,
|
||||
kUpIsDown: pl.kUp && pl.kUp.isDown,
|
||||
wasdLen: pl.wasdKeys ? pl.wasdKeys.length : 0,
|
||||
wasdWDown: pl.wasdKeys && pl.wasdKeys[0] ? pl.wasdKeys[0].isDown : null,
|
||||
sceneUpdateActive: p.scene.isActive(),
|
||||
doorCooldown: p.doorCooldown,
|
||||
timeNow: p.time.now,
|
||||
};
|
||||
});
|
||||
console.log('plaza player info:', JSON.stringify(info));
|
||||
|
||||
// hold w and check if key isDown updates
|
||||
await page.keyboard.down('w');
|
||||
await page.waitForTimeout(400);
|
||||
const wInfo = await page.evaluate(() => {
|
||||
const pl = window.game.scene.getScene('PlazaScene').player;
|
||||
return { kUpDown: pl.kUp.isDown, wasdWDown: pl.wasdKeys[0].isDown, px: pl.x, py: pl.y, vy: pl.body.velocity.y, moving: pl.moving };
|
||||
});
|
||||
console.log('w held:', JSON.stringify(wInfo));
|
||||
await page.keyboard.up('w');
|
||||
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
@@ -0,0 +1,33 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4218;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter'); await page.waitForTimeout(1200);
|
||||
for (let i=0;i<4;i++){ await page.keyboard.press('e'); await page.waitForTimeout(150); }
|
||||
await page.click('canvas'); await page.waitForTimeout(300);
|
||||
// teleport directly to plaza to skip the house
|
||||
await page.evaluate(() => {
|
||||
const g = window.game;
|
||||
g.scene.stop('HouseScene');
|
||||
g.scene.start('PlazaScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForTimeout(1200);
|
||||
await page.click('canvas'); await page.waitForTimeout(200);
|
||||
|
||||
const before = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene').player; return {x:p.x,y:p.y,active:window.game.scene.getScene('PlazaScene').scene.isActive(),moving:p.moving}; });
|
||||
console.log('before (plaza default spawn):', JSON.stringify(before));
|
||||
|
||||
await page.keyboard.down('w'); await page.waitForTimeout(600);
|
||||
const during = await page.evaluate(() => { const p=window.game.scene.getScene('PlazaScene').player; return {x:p.x,y:p.y,vy:p.body.velocity.y}; });
|
||||
console.log('w held 600ms:', JSON.stringify(during));
|
||||
await page.keyboard.up('w');
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
@@ -0,0 +1,447 @@
|
||||
// gen_maps.js: Generate 10 Tiled-format JSON maps per PRD §10.
|
||||
// Produces assets/maps/*.json
|
||||
// Tile index reference (matches tools/gen_tileset.lua order):
|
||||
// NOTE: Tiled uses global tile IDs where 0 = empty and localIndex N -> global N+1 (firstgid=1).
|
||||
// We store raw LOCAL indices in T.* and convert to global IDs (+1, 0 stays 0=empty) on write.
|
||||
const T = {
|
||||
grass: 0, path: 1, water: 2, flowersY: 3, flowersP: 4, flowersG: 5, berry: 6, tree: 7,
|
||||
birdTree: 8, bush: 9, wall: 10, wallTop: 11, roof: 12, roofEdge: 13, door: 14, window: 15,
|
||||
floor: 16, floorRug: 17, bed: 18, counter: 19, oven: 20, shelf: 21, shelfFull: 22, table: 23,
|
||||
chair: 24, stool: 25, well: 26, fog: 27, fogThick: 28, whiteWall: 29, whiteFloor: 30, whiteDoor: 31,
|
||||
whiteBed: 32, whiteMonitor: 33, whiteTable: 34, ribbon: 35, ribbon2: 36, picnic: 37, bench: 38, sign: 39,
|
||||
calendar: 40, paper: 41, chart: 42, fenceH: 43, fenceV: 44, lamp: 45, potPlant: 46, barrel: 47,
|
||||
crate: 48, grassAlt: 49, pathH: 50, pathV: 51, pathCross: 52, waterEdge: 53, grassDark: 54, pathCorner: 55,
|
||||
flowerMix: 56, berryEmpty: 57, fogBand: 58, whitePillar: 59, floorEdge: 60, wallTrim: 61, grassPath: 62, transparent: 63,
|
||||
};
|
||||
// EMPTY tile (global 0): use a sentinel; we map 63(transparent local) -> 0 empty on output, others +1.
|
||||
const EMPTY = 63; // local index used for "transparent/empty"
|
||||
|
||||
const COLS = 8; // tileset columns
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const outDir = 'assets/maps';
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
// Load the tileset descriptor to embed inline.
|
||||
const tilesetJson = JSON.parse(fs.readFileSync('assets/tilesets/tileset.json', 'utf8'));
|
||||
|
||||
function makeMap(name, width, height, layers) {
|
||||
// layers: [{name, type:'tilelayer', data:number[] (len w*h)} or {name,type:'objectgroup', objects:[]}]
|
||||
// Find door objects and clear obstacle tiles at their positions so the player can walk through.
|
||||
const objLayer = layers.find(l => l.type === 'objectgroup');
|
||||
const doorTiles = new Set();
|
||||
if (objLayer) {
|
||||
for (const o of objLayer.objects) {
|
||||
if (o.type === 'door' || (o.name && o.name.startsWith('door'))) {
|
||||
const tx = Math.floor(o.x / 16), ty = Math.floor(o.y / 16);
|
||||
doorTiles.add(ty * width + tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
const tiledLayers = layers.map(l => {
|
||||
if (l.type === 'tilelayer') {
|
||||
// convert local indices to global tile IDs: local N -> N+1, EMPTY(63) -> 0
|
||||
// also clear obstacle tiles at door positions
|
||||
const globalData = l.data.map((v, idx) => {
|
||||
if (l.name === 'obstacles' && doorTiles.has(idx)) return 0; // clear wall at door
|
||||
return (v === EMPTY ? 0 : v + 1);
|
||||
});
|
||||
return {
|
||||
name: l.name,
|
||||
type: 'tilelayer',
|
||||
x: 0, y: 0,
|
||||
width, height,
|
||||
visible: true,
|
||||
opacity: 1,
|
||||
offsetx: 0, offsety: 0,
|
||||
draworder: 'right-down',
|
||||
data: globalData,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: l.name,
|
||||
type: 'objectgroup',
|
||||
x: 0, y: 0,
|
||||
visible: true,
|
||||
opacity: 1,
|
||||
offsetx: 0, offsety: 0,
|
||||
draworder: 'index',
|
||||
objects: l.objects,
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
compressionlevel: -1,
|
||||
height, width,
|
||||
tilewidth: 16,
|
||||
tileheight: 16,
|
||||
type: 'map',
|
||||
orientation: 'orthogonal',
|
||||
renderorder: 'right-down',
|
||||
tiledversion: '1.12.2',
|
||||
// embed tileset inline so Phaser resolves it without external file
|
||||
tilesets: [{
|
||||
firstgid: 1,
|
||||
name: tilesetJson.name,
|
||||
columns: tilesetJson.columns,
|
||||
image: '../tilesets/tileset.png',
|
||||
imagewidth: tilesetJson.imagewidth,
|
||||
imageheight: tilesetJson.imageheight,
|
||||
margin: 0,
|
||||
spacing: 0,
|
||||
tilecount: tilesetJson.tilecount,
|
||||
tilewidth: 16,
|
||||
tileheight: 16,
|
||||
tiles: tilesetJson.tiles,
|
||||
}],
|
||||
layers: tiledLayers,
|
||||
};
|
||||
}
|
||||
|
||||
// fill helpers
|
||||
function fillGrid(w, h, val) { return new Array(w * h).fill(val); }
|
||||
function set(g, w, x, y, v) { if (x>=0&&y>=0&&x<w&&y<g.length/w) g[y*w+x] = v; }
|
||||
function rectFill(g, w, h, x0, y0, rw, rh, v) {
|
||||
for (let y=y0; y<y0+rh && y<h; y++) for (let x=x0; x<x0+rw && x<w; x++) set(g, w, x, y, v);
|
||||
}
|
||||
function borderFill(g, w, h, x0, y0, rw, rh, v) {
|
||||
for (let y=y0; y<y0+rh && y<h; y++) for (let x=x0; x<x0+rw && x<w; x++) {
|
||||
if (y===y0||y===y0+rh-1||x===x0||x===x0+rw-1) set(g,w,x,y,v);
|
||||
}
|
||||
}
|
||||
|
||||
// object helpers (pixel coords: x,y = top-left in pixels, 16 per tile)
|
||||
function obj(name, type, tx, ty, props={}) {
|
||||
return { name, type, x: tx*16, y: ty*16, width: props._w||16, height: props._h||16,
|
||||
properties: Object.entries(props).filter(([k])=>!k.startsWith('_')).map(([k,v])=>({name:k,type:'string',value:String(v)})) };
|
||||
}
|
||||
|
||||
const maps = {};
|
||||
|
||||
// 1. House (12x8) interior
|
||||
{
|
||||
const w=12, h=8;
|
||||
const ground = fillGrid(w,h,T.floor);
|
||||
rectFill(ground,w,h,0,0,12,1,T.wallTop);
|
||||
rectFill(ground,w,h,0,h-1,12,1,T.floorEdge);
|
||||
const obs = fillGrid(w,h,63); // transparent
|
||||
borderFill(obs,w,h,0,0,12,8,T.wall);
|
||||
// bed bottom-left
|
||||
rectFill(obs,w,h,1,5,3,2,T.bed);
|
||||
// calendar on wall (C1) - object only
|
||||
// door to plaza (south center) — widen the gap so the player can't get stuck
|
||||
set(obs,w,5,7,63); set(obs,w,6,7,63); set(ground,w,5,7,T.door);
|
||||
// objects
|
||||
const objects = [
|
||||
obj('door_plaza','door',5,7,{target:'plaza', spawn:'door_house'}),
|
||||
obj('bed','interact',1,5,{clue:'none'}),
|
||||
obj('calendar','clue',2,0,{clue:'C1'}),
|
||||
obj('sleep_point','interact',6,5),
|
||||
];
|
||||
maps.house = makeMap('house', w, h, [
|
||||
{name:'ground',type:'tilelayer',data:ground},
|
||||
{name:'obstacles',type:'tilelayer',data:obs},
|
||||
{name:'objects',type:'objectgroup',objects},
|
||||
]);
|
||||
}
|
||||
|
||||
// 2. Plaza (30x20) hub
|
||||
{
|
||||
const w=30, h=20;
|
||||
const ground = fillGrid(w,h,T.grass);
|
||||
// cross paths
|
||||
rectFill(ground,w,h,0,9,30,2,T.path); // horizontal
|
||||
rectFill(ground,w,h,14,0,2,20,T.path); // vertical
|
||||
// flower beds
|
||||
rectFill(ground,w,h,3,3,4,3,T.flowersY);
|
||||
rectFill(ground,w,h,23,3,4,3,T.flowersP);
|
||||
// twin bird tree
|
||||
set(ground,w,15,3,T.birdTree);
|
||||
// signs/decor
|
||||
set(ground,w,2,14,T.lamp); set(ground,w,27,14,T.lamp);
|
||||
// ribbons area for festival (decor placed day6)
|
||||
const obs = fillGrid(w,h,63);
|
||||
// border trees
|
||||
for (let x=0;x<w;x++){ set(obs,w,x,0,T.tree); set(obs,w,x,1,T.tree);}
|
||||
for (let x=0;x<w;x++){ set(obs,w,x,h-1,T.tree);}
|
||||
for (let y=0;y<h;y++){ set(obs,w,0,y,T.tree); set(obs,w,29,y,T.tree);}
|
||||
// carve door gaps in the border + place a door tile on the ground at each.
|
||||
// Gaps are 3 tiles tall/wide so the player can't miss them when walking to an edge.
|
||||
// bakery: left edge, rows 7-9
|
||||
for (let y=7;y<=9;y++) set(obs,w,0,y,63); set(ground,w,0,8,T.door);
|
||||
// lake: left edge, rows 11-13
|
||||
for (let y=11;y<=13;y++) set(obs,w,0,y,63); set(ground,w,0,12,T.door);
|
||||
// fog_boundary: left edge, rows 2-4
|
||||
for (let y=2;y<=4;y++) set(obs,w,0,y,63); set(ground,w,0,3,T.door);
|
||||
// shop: right edge, rows 7-9
|
||||
for (let y=7;y<=9;y++) set(obs,w,29,y,63); set(ground,w,29,8,T.door);
|
||||
// field: right edge, rows 11-13
|
||||
for (let y=11;y<=13;y++) set(obs,w,29,y,63); set(ground,w,29,12,T.door);
|
||||
// well_area: right edge, rows 5-6
|
||||
for (let y=5;y<=6;y++) set(obs,w,29,y,63); set(ground,w,29,5,T.door);
|
||||
// clinic: top edge, cols 13-15
|
||||
for (let x=13;x<=15;x++){ set(obs,w,x,0,63); set(obs,w,x,1,63); } set(ground,w,14,0,T.door);
|
||||
// house: bottom edge, cols 13-15
|
||||
for (let x=13;x<=15;x++) set(obs,w,x,19,63); set(ground,w,14,19,T.door);
|
||||
// announcement board (collides)
|
||||
set(obs,w,7,7,T.sign); set(obs,w,8,7,T.sign);
|
||||
// fountain-ish bush
|
||||
set(obs,w,20,12,T.bush);
|
||||
// objects: doors to all scenes — placed at the border gaps so they're easy to find
|
||||
const objects = [
|
||||
obj('door_house','door',14,19,{target:'house', spawn:'door_plaza'}),
|
||||
obj('door_bakery','door',0,8,{target:'bakery', spawn:'door_plaza'}),
|
||||
obj('door_shop','door',29,8,{target:'shop', spawn:'door_plaza'}),
|
||||
obj('door_clinic','door',14,0,{target:'clinic', spawn:'door_plaza'}),
|
||||
obj('door_lake','door',0,12,{target:'lake', spawn:'door_plaza'}),
|
||||
obj('door_field','door',29,12,{target:'field', spawn:'door_plaza'}),
|
||||
obj('door_well','door',29,5,{target:'well_area', spawn:'door_plaza'}),
|
||||
obj('door_fog','door',0,3,{target:'fog_boundary', spawn:'door_plaza'}),
|
||||
// NPC spawn points (tile coords of standing position)
|
||||
obj('npc_afu','spawn',8,8), // dog mayor at board
|
||||
obj('npc_bird','spawn',15,2), // twin birds on tree
|
||||
// board interaction
|
||||
obj('board','interact',7,7),
|
||||
// flowerbed water points (T2)
|
||||
obj('waterflower1','interact',4,4),
|
||||
obj('waterflower2','interact',5,4),
|
||||
obj('waterflower3','interact',4,5),
|
||||
// greet points
|
||||
obj('greet_point','interact',15,10),
|
||||
];
|
||||
maps.plaza = makeMap('plaza', w, h, [
|
||||
{name:'ground',type:'tilelayer',data:ground},
|
||||
{name:'obstacles',type:'tilelayer',data:obs},
|
||||
{name:'objects',type:'objectgroup',objects},
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. Bakery (15x10)
|
||||
{
|
||||
const w=15,h=10;
|
||||
const ground = fillGrid(w,h,T.floor);
|
||||
rectFill(ground,w,h,0,0,15,1,T.wallTop);
|
||||
const obs = fillGrid(w,h,63);
|
||||
borderFill(obs,w,h,0,0,15,10,T.wall);
|
||||
// counter
|
||||
rectFill(obs,w,h,2,5,4,1,T.counter);
|
||||
// oven
|
||||
rectFill(obs,w,h,11,2,2,2,T.oven);
|
||||
// shelves
|
||||
set(obs,w,11,6,T.shelfFull);
|
||||
// door
|
||||
set(ground,w,7,9,T.door);
|
||||
const objects = [
|
||||
obj('door_plaza','door',7,9,{target:'plaza',spawn:'door_bakery'}),
|
||||
obj('npc_yuanyuan','spawn',3,7),
|
||||
obj('recipe_paper','clue',11,5,{clue:'C2'}), // on shelf
|
||||
obj('oven','interact',11,2),
|
||||
obj('knead_point','interact',3,6), // T11 knead dough
|
||||
];
|
||||
maps.bakery = makeMap('bakery', w, h, [
|
||||
{name:'ground',type:'tilelayer',data:ground},
|
||||
{name:'obstacles',type:'tilelayer',data:obs},
|
||||
{name:'objects',type:'objectgroup',objects},
|
||||
]);
|
||||
}
|
||||
|
||||
// 4. Shop (15x10)
|
||||
{
|
||||
const w=15,h=10;
|
||||
const ground = fillGrid(w,h,T.floor);
|
||||
rectFill(ground,w,h,0,0,15,1,T.wallTop);
|
||||
const obs = fillGrid(w,h,63);
|
||||
borderFill(obs,w,h,0,0,15,10,T.wall);
|
||||
// shelves: same product repeated 9 squares (C5)
|
||||
for (let y=2;y<5;y++) for (let x=2;x<5;x++) set(obs,w,x,y,T.shelfFull);
|
||||
for (let y=2;y<5;y++) for (let x=10;x<13;x++) set(obs,w,x,y,T.shelfFull);
|
||||
// counter
|
||||
rectFill(obs,w,h,6,6,3,1,T.counter);
|
||||
set(ground,w,7,9,T.door);
|
||||
const objects = [
|
||||
obj('door_plaza','door',7,9,{target:'plaza',spawn:'door_shop'}),
|
||||
obj('npc_mimi','spawn',7,7),
|
||||
obj('shelf_clue','clue',2,2,{clue:'C5'}),
|
||||
obj('count_shelf','interact',2,2), // T5
|
||||
];
|
||||
maps.shop = makeMap('shop', w, h, [
|
||||
{name:'ground',type:'tilelayer',data:ground},
|
||||
{name:'obstacles',type:'tilelayer',data:obs},
|
||||
{name:'objects',type:'objectgroup',objects},
|
||||
]);
|
||||
}
|
||||
|
||||
// 5. Clinic (12x8)
|
||||
{
|
||||
const w=12,h=8;
|
||||
const ground = fillGrid(w,h,T.floor);
|
||||
rectFill(ground,w,h,0,0,12,1,T.wallTop);
|
||||
const obs = fillGrid(w,h,63);
|
||||
borderFill(obs,w,h,0,0,12,8,T.wall);
|
||||
// doctor table with chart (C8)
|
||||
rectFill(obs,w,h,8,4,3,1,T.table);
|
||||
set(obs,w,9,3,T.chart);
|
||||
// bed
|
||||
set(obs,w,1,4,T.bed);
|
||||
set(ground,w,5,7,T.door);
|
||||
const objects = [
|
||||
obj('door_plaza','door',5,7,{target:'plaza',spawn:'door_clinic'}),
|
||||
obj('npc_dr_fu','spawn',5,5),
|
||||
obj('chart_clue','clue',9,3,{clue:'C8'}),
|
||||
];
|
||||
maps.clinic = makeMap('clinic', w, h, [
|
||||
{name:'ground',type:'tilelayer',data:ground},
|
||||
{name:'obstacles',type:'tilelayer',data:obs},
|
||||
{name:'objects',type:'objectgroup',objects},
|
||||
]);
|
||||
}
|
||||
|
||||
// 6. Lake (24x16)
|
||||
{
|
||||
const w=24,h=16;
|
||||
const ground = fillGrid(w,h,T.grass);
|
||||
// lake water body
|
||||
rectFill(ground,w,h,2,2,10,8,T.water);
|
||||
// path along bottom
|
||||
rectFill(ground,w,h,0,14,24,2,T.path);
|
||||
rectFill(ground,w,h,22,0,2,16,T.path); // right exit
|
||||
// bench (grandpa)
|
||||
set(ground,w,12,10,T.bench);
|
||||
// picnic spot
|
||||
set(ground,w,16,11,T.picnic);
|
||||
set(ground,w,17,11,T.picnic);
|
||||
const obs = fillGrid(w,h,63);
|
||||
// trees border
|
||||
for (let x=0;x<w;x++){ set(obs,w,x,0,T.tree);}
|
||||
for (let x=0;x<w;x++){ set(obs,w,x,h-1,T.tree);}
|
||||
for (let y=0;y<h;y++){ set(obs,w,0,y,T.tree);}
|
||||
const objects = [
|
||||
obj('door_plaza','door',23,8,{target:'plaza',spawn:'door_lake'}),
|
||||
obj('npc_grandpa','spawn',13,10),
|
||||
obj('picnic_point','interact',16,11), // T7
|
||||
obj('lake_reflection','interact',7,7), // reflection show
|
||||
];
|
||||
maps.lake = makeMap('lake', w, h, [
|
||||
{name:'ground',type:'tilelayer',data:ground},
|
||||
{name:'obstacles',type:'tilelayer',data:obs},
|
||||
{name:'objects',type:'objectgroup',objects},
|
||||
]);
|
||||
}
|
||||
|
||||
// 7. Flower field (20x14)
|
||||
{
|
||||
const w=20,h=14;
|
||||
const ground = fillGrid(w,h,T.grass);
|
||||
// berry bushes (T6)
|
||||
set(ground,w,4,4,T.berry); set(ground,w,8,3,T.berry); set(ground,w,12,5,T.berry);
|
||||
set(ground,w,6,8,T.berry); set(ground,w,14,9,T.berry);
|
||||
// flowers
|
||||
for (let i=0;i<w;i+=2) set(ground,w,i,11,T.flowersY);
|
||||
rectFill(ground,w,h,0,0,2,14,T.path);
|
||||
rectFill(ground,w,h,18,0,2,14,T.path);
|
||||
const obs = fillGrid(w,h,63);
|
||||
for (let x=0;x<w;x++){ set(obs,w,x,0,T.tree); set(obs,w,x,h-1,T.tree);}
|
||||
for (let y=0;y<h;y++){ set(obs,w,0,y,T.tree); set(obs,w,w-1,y,T.tree);}
|
||||
// remove border where exits are
|
||||
set(obs,w,0,7,63); set(obs,w,1,7,63);
|
||||
set(obs,w,18,7,63); set(obs,w,19,7,63);
|
||||
const objects = [
|
||||
obj('door_plaza','door',0,7,{target:'plaza',spawn:'door_field'}),
|
||||
obj('berry1','interact',4,4),
|
||||
obj('berry2','interact',8,3),
|
||||
obj('berry3','interact',12,5),
|
||||
obj('berry4','interact',6,8),
|
||||
obj('berry5','interact',14,9),
|
||||
obj('water_point','interact',2,11), // T2 watering here
|
||||
];
|
||||
maps.field = makeMap('field', w, h, [
|
||||
{name:'ground',type:'tilelayer',data:ground},
|
||||
{name:'obstacles',type:'tilelayer',data:obs},
|
||||
{name:'objects',type:'objectgroup',objects},
|
||||
]);
|
||||
}
|
||||
|
||||
// 8. Well area (14x10)
|
||||
{
|
||||
const w=14,h=10;
|
||||
const ground = fillGrid(w,h,T.grass);
|
||||
rectFill(ground,w,h,0,8,14,2,T.path);
|
||||
// well center
|
||||
set(ground,w,6,4,T.well);
|
||||
set(ground,w,7,4,T.well);
|
||||
const obs = fillGrid(w,h,63);
|
||||
for (let x=0;x<w;x++){ set(obs,w,x,0,T.tree);}
|
||||
for (let x=0;x<w;x++){ set(obs,w,x,h-1,T.tree);}
|
||||
for (let y=0;y<h;y++){ set(obs,w,0,y,T.tree); set(obs,w,w-1,y,T.tree);}
|
||||
set(obs,w,0,8,63);
|
||||
const objects = [
|
||||
obj('door_plaza','door',0,8,{target:'plaza',spawn:'door_well'}),
|
||||
obj('well','interact',6,4,{clue:'C7'}),
|
||||
];
|
||||
maps.well_area = makeMap('well_area', w, h, [
|
||||
{name:'ground',type:'tilelayer',data:ground},
|
||||
{name:'obstacles',type:'tilelayer',data:obs},
|
||||
{name:'objects',type:'objectgroup',objects},
|
||||
]);
|
||||
}
|
||||
|
||||
// 9. Fog boundary (20x8)
|
||||
{
|
||||
const w=20,h=8;
|
||||
const ground = fillGrid(w,h,T.grass);
|
||||
// fog wall
|
||||
rectFill(ground,w,h,10,0,4,8,T.fogThick);
|
||||
rectFill(ground,w,h,14,0,2,8,T.fog);
|
||||
rectFill(ground,w,h,0,7,20,1,T.path);
|
||||
const obs = fillGrid(w,h,63);
|
||||
for (let x=0;x<w;x++){ set(obs,w,x,0,T.tree);}
|
||||
for (let y=0;y<h;y++){ set(obs,w,0,y,T.tree); set(obs,w,w-1,y,T.tree);}
|
||||
set(obs,w,0,7,63);
|
||||
const objects = [
|
||||
obj('door_plaza','door',0,6,{target:'plaza',spawn:'door_fog'}),
|
||||
obj('fog_wall','interact',12,4,{clue:'fog'}),
|
||||
obj('fog_exit','interact',13,4,{clue:'E3_exit'}),
|
||||
];
|
||||
maps.fog_boundary = makeMap('fog_boundary', w, h, [
|
||||
{name:'ground',type:'tilelayer',data:ground},
|
||||
{name:'obstacles',type:'tilelayer',data:obs},
|
||||
{name:'objects',type:'objectgroup',objects},
|
||||
]);
|
||||
}
|
||||
|
||||
// 10. White layer (corridor 20x6 + ward 10x8) combined as one map 20x14
|
||||
{
|
||||
const w=20,h=14;
|
||||
const ground = fillGrid(w,h,T.whiteFloor);
|
||||
// corridor top, ward bottom
|
||||
rectFill(ground,w,h,0,0,20,6,T.whiteFloor);
|
||||
rectFill(ground,w,h,5,7,10,7,T.whiteFloor);
|
||||
rectFill(ground,w,h,0,7,5,7,T.whiteWall);
|
||||
rectFill(ground,w,h,15,7,5,7,T.whiteWall);
|
||||
const obs = fillGrid(w,h,63);
|
||||
// corridor walls
|
||||
for (let x=0;x<w;x++){ set(obs,w,x,0,T.whiteWall); set(obs,w,x,5,T.whiteWall);}
|
||||
// ward walls
|
||||
borderFill(obs,w,h,5,7,10,7,T.whiteWall);
|
||||
set(obs,w,9,13,63); // door gap
|
||||
// bed + monitor in ward
|
||||
set(obs,w,6,8,T.whiteBed);
|
||||
set(obs,w,13,8,T.whiteMonitor);
|
||||
set(obs,w,9,10,T.whiteTable);
|
||||
const objects = [
|
||||
obj('exit','door',9,12,{target:'ending'}),
|
||||
obj('patient_chart','interact',9,10,{clue:'E3_chart'}),
|
||||
];
|
||||
maps.white_layer = makeMap('white_layer', w, h, [
|
||||
{name:'ground',type:'tilelayer',data:ground},
|
||||
{name:'obstacles',type:'tilelayer',data:obs},
|
||||
{name:'objects',type:'objectgroup',objects},
|
||||
]);
|
||||
}
|
||||
|
||||
for (const [name, map] of Object.entries(maps)) {
|
||||
fs.writeFileSync(path.join(outDir, name + '.json'), JSON.stringify(map, null, 0));
|
||||
console.log('Wrote ' + name + '.json');
|
||||
}
|
||||
console.log('Done: ' + Object.keys(maps).length + ' maps');
|
||||
@@ -0,0 +1,108 @@
|
||||
-- gen_npc.lua: NPC field sprites (16x24). One column per variant.
|
||||
-- Layout: 1 row, many columns of 16x24.
|
||||
-- Order:
|
||||
-- 0 afu-smile, 1 afu-flat, 2 afu-off
|
||||
-- 3 yuanyuan-smile,4 flat,5 off
|
||||
-- 6 dabao-smile,7 flat,8 off
|
||||
-- 9 mimi-smile,10 flat,11 off
|
||||
-- 12 dr_fu-smile,13 flat,14 off (dr_fu flat/off identical to smile per PRD "does not corrupt")
|
||||
-- 15 birdA (mouth closed), 16 birdB (mouth open) -- twin birds, 2-frame
|
||||
-- 17 grandpa-smile,18 flat,19 off
|
||||
-- 20..24 extras (squirrel/hedgehog loop frame1, frame2)
|
||||
-- Produces assets/sprites/npc.png
|
||||
local FW = 16
|
||||
local FH = 24
|
||||
local N = 25
|
||||
local W = FW*N
|
||||
local H = FH
|
||||
app.command.NewFile{ width=W, height=H, colorMode=ColorMode.RGB }
|
||||
local spr = app.activeSprite
|
||||
local img = app.activeImage
|
||||
for y=0,H-1 do for x=0,W-1 do img:drawPixel(x,y, app.pixelColor.rgba(0,0,0,0)) end end
|
||||
local function R(ox,oy,w,h,c) for y=oy,oy+h-1 do for x=ox,ox+w-1 do img:drawPixel(x,y,c) end end end
|
||||
local function px(x,y,c) img:drawPixel(x,y,c) end
|
||||
|
||||
local eye = app.pixelColor.rgba(56,40,40,255)
|
||||
local mouth= app.pixelColor.rgba(200,110,110,255)
|
||||
local cheek= app.pixelColor.rgba(255,170,170,255)
|
||||
|
||||
-- draw a standing NPC at ox,oy with body color, species ears, expression
|
||||
local function npc(ox, oy, body, ear, expr)
|
||||
-- head
|
||||
R(ox+5, oy+1, 6, 6, body)
|
||||
-- ears (simple)
|
||||
if ear == "rabbit" then R(ox+6,oy-2,1,4,body); R(ox+9,oy-2,1,4,body)
|
||||
elseif ear == "dog" then R(ox+4,oy+1,1,3,body); R(ox+11,oy+1,1,3,body); R(ox+3,oy+3,1,4,body); R(ox+12,oy+3,1,4,body)
|
||||
elseif ear == "bear" then R(ox+4,oy+0,2,2,body); R(ox+10,oy+0,2,2,body)
|
||||
elseif ear == "cat" then px(ox+5,oy+1,body);px(ox+4,oy+0,body); px(ox+10,oy+1,body);px(ox+11,oy+0,body)
|
||||
elseif ear == "deer" then px(ox+6,oy-1,body);px(ox+6,oy+0,body); px(ox+9,oy-1,body);px(ox+9,oy+0,body)
|
||||
elseif ear == "turtle" then R(ox+5,oy+0,6,2,body) -- shell cap on head
|
||||
end
|
||||
-- eyes
|
||||
if expr == "smile" then
|
||||
px(ox+6, oy+3, eye); px(ox+9, oy+3, eye)
|
||||
px(ox+7, oy+5, mouth); px(ox+8, oy+5, mouth)
|
||||
px(ox+5, oy+4, cheek); px(ox+10, oy+4, cheek)
|
||||
elseif expr == "flat" then
|
||||
px(ox+6, oy+3, eye); px(ox+9, oy+3, eye)
|
||||
px(ox+7, oy+5, mouth)
|
||||
elseif expr == "off" then
|
||||
px(ox+6, oy+3, eye); px(ox+9, oy+4, eye) -- one eye 1px lower
|
||||
px(ox+7, oy+5, mouth); px(ox+8, oy+5, mouth)
|
||||
end
|
||||
-- torso
|
||||
R(ox+4, oy+8, 8, 8, body)
|
||||
-- arms
|
||||
R(ox+3, oy+9, 1, 5, body); R(ox+12, oy+9, 1, 5, body)
|
||||
-- legs
|
||||
R(ox+5, oy+16, 2, 6, body); R(ox+9, oy+16, 2, 6, body)
|
||||
px(ox+5, oy+22, eye); px(ox+6, oy+22, eye); px(ox+9, oy+22, eye); px(ox+10, oy+22, eye)
|
||||
end
|
||||
|
||||
local dogC = app.pixelColor.rgba(200,150,90,255)
|
||||
local rabC = app.pixelColor.rgba(245,235,230,255)
|
||||
local bearC = app.pixelColor.rgba(150,100,60,255)
|
||||
local catC = app.pixelColor.rgba(230,220,210,255)
|
||||
local deerC = app.pixelColor.rgba(210,160,110,255)
|
||||
local turC = app.pixelColor.rgba(120,160,90,255)
|
||||
local sqC = app.pixelColor.rgba(170,110,70,255)
|
||||
local hedC = app.pixelColor.rgba(150,110,70,255)
|
||||
|
||||
-- afu dog
|
||||
npc(0*FW, 0, dogC, "dog", "smile")
|
||||
npc(1*FW, 0, dogC, "dog", "flat")
|
||||
npc(2*FW, 0, dogC, "dog", "off")
|
||||
-- yuanyuan rabbit
|
||||
npc(3*FW, 0, rabC, "rabbit", "smile")
|
||||
npc(4*FW, 0, rabC, "rabbit", "flat")
|
||||
npc(5*FW, 0, rabC, "rabbit", "off")
|
||||
-- dabao bear (with postman bag: add a small brown satchel)
|
||||
npc(6*FW, 0, bearC, "bear", "smile"); R(6*FW+10, 10, 3, 4, app.pixelColor.rgba(120,80,40,255))
|
||||
npc(7*FW, 0, bearC, "bear", "flat"); R(7*FW+10, 10, 3, 4, app.pixelColor.rgba(120,80,40,255))
|
||||
npc(8*FW, 0, bearC, "bear", "off"); R(8*FW+10, 10, 3, 4, app.pixelColor.rgba(120,80,40,255))
|
||||
-- mimi cat
|
||||
npc(9*FW, 0, catC, "cat", "smile")
|
||||
npc(10*FW, 0, catC, "cat", "flat")
|
||||
npc(11*FW, 0, catC, "cat", "off")
|
||||
-- dr_fu deer (smile only effectively)
|
||||
npc(12*FW, 0, deerC, "deer", "smile")
|
||||
npc(13*FW, 0, deerC, "deer", "smile")
|
||||
npc(14*FW, 0, deerC, "deer", "smile")
|
||||
-- twin birds (2 frames): bird A mouth closed, bird B mouth open
|
||||
R(15*FW+6, 6, 4, 4, app.pixelColor.rgba(120,200,230,255)); px(15*FW+7,7,eye); px(15*FW+10,7,eye) -- closed
|
||||
R(16*FW+6, 6, 4, 4, app.pixelColor.rgba(120,200,230,255)); px(16*FW+7,7,eye); px(16*FW+10,7,eye); px(16*FW+8,9,mouth); px(16*FW+9,9,mouth) -- open beak
|
||||
-- grandpa turtle
|
||||
npc(17*FW, 0, turC, "turtle", "smile")
|
||||
npc(18*FW, 0, turC, "turtle", "flat")
|
||||
npc(19*FW, 0, turC, "turtle", "off")
|
||||
-- extras (5): squirrels/hedgehogs with 2 walk frames each would be ideal; provide standing + step
|
||||
npc(20*FW, 0, sqC, "dog", "smile")
|
||||
npc(21*FW, 0, hedC, "dog", "smile")
|
||||
npc(22*FW, 0, sqC, "dog", "smile")
|
||||
npc(23*FW, 0, hedC, "dog", "smile")
|
||||
npc(24*FW, 0, sqC, "dog", "smile")
|
||||
|
||||
local out = "assets/sprites/npc.png"
|
||||
spr:saveCopyAs(out)
|
||||
print("Saved " .. out)
|
||||
app.exit()
|
||||
@@ -0,0 +1,124 @@
|
||||
-- gen_player.lua: generate player sprite sheet (16x24 frames).
|
||||
-- Layout: 4 cols x 4 rows. Row order: walk-down, walk-up, walk-left, walk-right.
|
||||
-- Each row 4 frames: frame 0 = idle, 1-3 = walk frames.
|
||||
-- Produces assets/sprites/player.png
|
||||
local FW = 16
|
||||
local FH = 24
|
||||
local COLS = 4
|
||||
local ROWS = 4
|
||||
local W = COLS*FW
|
||||
local H = ROWS*FH
|
||||
app.command.NewFile{ width=W, height=H, colorMode=ColorMode.RGB }
|
||||
local spr = app.activeSprite
|
||||
local img = app.activeImage
|
||||
for y=0,H-1 do for x=0,W-1 do img:drawPixel(x,y, app.pixelColor.rgba(0,0,0,0)) end end
|
||||
|
||||
local function px(x,y,c) img:drawPixel(x,y,c) end
|
||||
local function rect(ox,oy,w,h,c) for y=oy,oy+h-1 do for x=ox,ox+w-1 do px(x,y,c) end end end
|
||||
|
||||
-- gender-neutral candy palette
|
||||
local skin = app.pixelColor.rgba(255,214,176,255)
|
||||
local skinS = app.pixelColor.rgba(224,176,140,255)
|
||||
local hair = app.pixelColor.rgba(150,110,80,255) -- chestnut
|
||||
local hairD = app.pixelColor.rgba(110,80,56,255)
|
||||
local shirt = app.pixelColor.rgba(120,200,230,255) -- sky blue
|
||||
local shirtD = app.pixelColor.rgba(86,160,200,255)
|
||||
local pants = app.pixelColor.rgba(110,90,150,255) -- muted purple
|
||||
local pantsD = app.pixelColor.rgba(80,64,116,255)
|
||||
local shoe = app.pixelColor.rgba(90,70,60,255)
|
||||
local eye = app.pixelColor.rgba(60,40,40,255)
|
||||
local cheek = app.pixelColor.rgba(255,170,170,255)
|
||||
local mouth = app.pixelColor.rgba(200,110,110,255)
|
||||
|
||||
-- draw a character frame at ox,oy facing dir, walk phase p (0=idle, 1-3)
|
||||
local function frame(ox, oy, dir, p)
|
||||
-- common body
|
||||
-- head (cols 4..11, rows 1..9)
|
||||
rect(ox+5, oy+1, 6, 7, skin)
|
||||
-- hair cap depending on direction
|
||||
if dir == "up" then
|
||||
-- back of head: hair covers
|
||||
rect(ox+5, oy+1, 6, 6, hair)
|
||||
rect(ox+5, oy+1, 6, 2, hairD)
|
||||
else
|
||||
rect(ox+4, oy+1, 8, 3, hair)
|
||||
rect(ox+5, oy+0, 6, 2, hair)
|
||||
rect(ox+4, oy+1, 8, 1, hairD)
|
||||
end
|
||||
-- face details for down
|
||||
if dir == "down" then
|
||||
px(ox+6, oy+5, eye); px(ox+9, oy+5, eye)
|
||||
px(ox+7, oy+7, mouth); px(ox+8, oy+7, mouth)
|
||||
px(ox+6, oy+6, cheek); px(ox+9, oy+6, cheek)
|
||||
elseif dir == "side" then
|
||||
-- left or right handled by caller mirroring via draw order
|
||||
px(ox+7, oy+5, eye)
|
||||
px(ox+8, oy+7, mouth)
|
||||
px(ox+6, oy+6, cheek)
|
||||
end
|
||||
-- torso (rows 8..15)
|
||||
rect(ox+5, oy+9, 6, 6, shirt)
|
||||
rect(ox+5, oy+9, 6, 1, shirtD)
|
||||
rect(ox+5, oy+14, 6, 1, shirtD)
|
||||
-- arms
|
||||
rect(ox+4, oy+10, 1, 4, shirt)
|
||||
rect(ox+10, oy+10, 1, 4, shirt)
|
||||
px(ox+4, oy+14, skin); px(ox+10, oy+14, skin)
|
||||
-- legs / walk animation
|
||||
local L, R = oy+15, oy+15
|
||||
if p == 0 then
|
||||
-- idle: legs together
|
||||
rect(ox+5, oy+15, 2, 6, pants)
|
||||
rect(ox+9, oy+15, 2, 6, pants)
|
||||
px(ox+5, oy+21, shoe); px(ox+6, oy+21, shoe)
|
||||
px(ox+9, oy+21, shoe); px(ox+10, oy+21, shoe)
|
||||
elseif p == 1 then
|
||||
-- step: left leg forward (down/up), or side-step
|
||||
rect(ox+5, oy+15, 2, 5, pants)
|
||||
rect(ox+9, oy+15, 2, 5, pants)
|
||||
px(ox+5, oy+20, shoe); px(ox+6, oy+20, shoe)
|
||||
px(ox+9, oy+21, shoe); px(ox+10, oy+21, shoe)
|
||||
elseif p == 2 then
|
||||
-- mid
|
||||
rect(ox+5, oy+15, 2, 6, pants)
|
||||
rect(ox+9, oy+15, 2, 6, pants)
|
||||
px(ox+5, oy+21, shoe); px(ox+6, oy+21, shoe)
|
||||
px(ox+9, oy+21, shoe); px(ox+10, oy+21, shoe)
|
||||
elseif p == 3 then
|
||||
-- step: right leg forward
|
||||
rect(ox+5, oy+15, 2, 5, pants)
|
||||
rect(ox+9, oy+15, 2, 5, pants)
|
||||
px(ox+5, oy+21, shoe); px(ox+6, oy+21, shoe)
|
||||
px(ox+9, oy+20, shoe); px(ox+10, oy+20, shoe)
|
||||
end
|
||||
end
|
||||
|
||||
-- mirror helper for left/right by copying pixels
|
||||
local function mirrorRow(srcRow, dstRow)
|
||||
for f=0,3 do
|
||||
local sx = f*FW
|
||||
local dx = f*FW
|
||||
local sy = srcRow*FH
|
||||
local dy = dstRow*FH
|
||||
for y=0,FH-1 do
|
||||
for x=0,FW-1 do
|
||||
local c = img:getPixel(sx + (FW-1-x), sy + y)
|
||||
img:drawPixel(dx + x, dy + y, c)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Row 0: walk-down
|
||||
for f=0,3 do frame(f*FW, 0*FH, "down", f) end
|
||||
-- Row 1: walk-up
|
||||
for f=0,3 do frame(f*FW, 1*FH, "up", f) end
|
||||
-- Row 2: walk-right (side)
|
||||
for f=0,3 do frame(f*FW, 2*FH, "side", f) end
|
||||
-- Row 3: walk-left = mirror of right
|
||||
mirrorRow(2, 3)
|
||||
|
||||
local out = "assets/sprites/player.png"
|
||||
spr:saveCopyAs(out)
|
||||
print("Saved " .. out)
|
||||
app.exit()
|
||||
@@ -0,0 +1,171 @@
|
||||
-- gen_portraits.lua: generate portrait sprite sheet for dialogue UI.
|
||||
-- Each portrait cell: 24x24. Single row of many cells.
|
||||
-- Cells order:
|
||||
-- 0 player-smile
|
||||
-- 1 player-realface (tired)
|
||||
-- 2 afu (dog mayor) smile
|
||||
-- 3 afu flat
|
||||
-- 4 afu off
|
||||
-- 5 yuanyuan (rabbit baker) smile
|
||||
-- 6 yuanyuan flat
|
||||
-- 7 yuanyuan off
|
||||
-- 8 dabao (bear postman) smile
|
||||
-- 9 dabao flat
|
||||
-- 10 dabao off
|
||||
-- 11 mimi (cat shopkeeper) smile
|
||||
-- 12 mimi flat
|
||||
-- 13 mimi off
|
||||
-- 14 dr_fu (deer doctor) smile (only smile; flat/off unused but provided)
|
||||
-- 15 dr_fu flat
|
||||
-- 16 dr_fu off
|
||||
-- 17 twinbirds smile
|
||||
-- 18 grandpa_turtle (turtle elder) smile
|
||||
-- 19 grandpa_turtle flat
|
||||
-- 20 grandpa_turtle off
|
||||
-- 21..25 extras (squirrel/hedgehog/etc) smile
|
||||
-- Produces assets/sprites/portraits.png
|
||||
local FW = 24
|
||||
local FH = 24
|
||||
local N = 26
|
||||
local W = FW*N
|
||||
local H = FH
|
||||
app.command.NewFile{ width=W, height=H, colorMode=ColorMode.RGB }
|
||||
local spr = app.activeSprite
|
||||
local img = app.activeImage
|
||||
for y=0,H-1 do for x=0,W-1 do img:drawPixel(x,y, app.pixelColor.rgba(0,0,0,0)) end end
|
||||
local function px(x,y,c) img:drawPixel(x,y,c) end
|
||||
local function rect(ox,oy,w,h,c) for y=oy,oy+h-1 do for x=ox,ox+w-1 do px(ox+(x-ox),y,c) end end end
|
||||
-- careful rect above buggy; redefine properly
|
||||
local function R(ox,oy,w,h,c) for y=oy,oy+h-1 do for x=ox,ox+w-1 do img:drawPixel(x,y,c) end end end
|
||||
|
||||
-- shared colors
|
||||
local skin = app.pixelColor.rgba(255,214,176,255)
|
||||
local skinS= app.pixelColor.rgba(224,176,140,255)
|
||||
local eye = app.pixelColor.rgba(56,40,40,255)
|
||||
local mouth= app.pixelColor.rgba(200,110,110,255)
|
||||
local cheek= app.pixelColor.rgba(255,170,170,255)
|
||||
local white= app.pixelColor.rgba(245,245,245,255)
|
||||
local black= app.pixelColor.rgba(40,40,40,255)
|
||||
|
||||
-- generic face drawer: ox,oy top-left of 24x24 cell
|
||||
-- species-specific color + ears, expression (smile/flat/off)
|
||||
-- species: human, dog, rabbit, bear, cat, deer, bird, turtle, squirrel, hedgehog
|
||||
local function face(ox, oy, col, species, expr)
|
||||
-- head fill
|
||||
R(ox+6, oy+5, 12, 12, col)
|
||||
-- ears per species
|
||||
if species == "rabbit" then
|
||||
R(ox+7, oy+1, 2, 5, col); R(ox+15, oy+1, 2, 5, col)
|
||||
R(ox+7, oy+2, 2, 2, skinS); R(ox+15, oy+2, 2, 2, skinS)
|
||||
elseif species == "dog" then
|
||||
R(ox+5, oy+4, 2, 4, col); R(ox+17, oy+4, 2, 4, col)
|
||||
-- floppy
|
||||
R(ox+4, oy+6, 2, 6, col)
|
||||
R(ox+18, oy+6, 2, 6, col)
|
||||
elseif species == "bear" then
|
||||
R(ox+5, oy+3, 3, 3, col); R(ox+16, oy+3, 3, 3, col)
|
||||
px(ox+6, oy+4, skinS); px(ox+17, oy+4, skinS)
|
||||
elseif species == "cat" then
|
||||
-- pointy ears
|
||||
px(ox+6, oy+4, col); px(ox+7, oy+2, col); px(ox+8, oy+1, col); px(ox+9, oy+3, col)
|
||||
px(ox+15, oy+3, col); px(ox+16, oy+1, col); px(ox+17, oy+2, col); px(ox+18, oy+4, col)
|
||||
elseif species == "deer" then
|
||||
px(ox+8, oy+1, col); px(ox+8, oy+2, col); px(ox+9, oy+0, col)
|
||||
px(ox+9, oy+2, skinS); px(ox+15, oy+0, col); px(ox+15, oy+2, col); px(ox+14, oy+1, col)
|
||||
px(ox+14, oy+2, skinS)
|
||||
elseif species == "turtle" then
|
||||
-- no ears, rounded; draw a cap
|
||||
R(ox+5, oy+4, 14, 4, col)
|
||||
elseif species == "bird" then
|
||||
R(ox+6, oy+4, 12, 10, col)
|
||||
px(ox+7, oy+3, col); px(ox+16, oy+3, col)
|
||||
elseif species == "squirrel" then
|
||||
R(ox+5, oy+4, 2, 3, col); R(ox+17, oy+4, 2, 3, col)
|
||||
R(ox+2, oy+2, 3, 6, col) -- tail
|
||||
elseif species == "hedgehog" then
|
||||
for x=6,17,2 do px(ox+x, oy+3, skinS); px(ox+x, oy+4, skinS) end
|
||||
end
|
||||
-- eyes (expression dependent)
|
||||
if expr == "smile" then
|
||||
px(ox+9, oy+9, eye); px(ox+14, oy+9, eye)
|
||||
px(ox+9, oy+10, eye); px(ox+14, oy+10, eye)
|
||||
-- cheeks
|
||||
px(ox+8, oy+12, cheek); px(ox+15, oy+12, cheek)
|
||||
-- smile mouth
|
||||
px(ox+11, oy+13, mouth); px(ox+12, oy+13, mouth)
|
||||
px(ox+10, oy+12, mouth); px(ox+13, oy+12, mouth)
|
||||
elseif expr == "flat" then
|
||||
px(ox+9, oy+9, eye); px(ox+14, oy+9, eye)
|
||||
-- straight mouth
|
||||
px(ox+11, oy+13, mouth); px(ox+12, oy+13, mouth)
|
||||
elseif expr == "off" then
|
||||
-- one eye dropped 1px (the signature "off" detail)
|
||||
px(ox+9, oy+9, eye); px(ox+9, oy+10, eye)
|
||||
px(ox+14, oy+10, eye); -- right eye 1px lower
|
||||
-- mouth with extra 1px droop
|
||||
px(ox+11, oy+13, mouth); px(ox+12, oy+13, mouth); px(ox+13, oy+13, mouth)
|
||||
end
|
||||
end
|
||||
|
||||
-- 0 player smile (human)
|
||||
face(0*FW, 0, skin, "human", "smile")
|
||||
-- add hair
|
||||
R(0*FW+6, 3, 12, 4, app.pixelColor.rgba(150,110,80,255))
|
||||
-- 1 player realface (tired)
|
||||
face(1*FW, 0, skinS, "human", "flat")
|
||||
R(1*FW+6, 3, 12, 4, app.pixelColor.rgba(150,110,80,255))
|
||||
-- tired eyes: half-lids
|
||||
px(1*FW+9, 10, skinS); px(1*FW+14, 10, skinS)
|
||||
-- a small frown
|
||||
px(1*FW+11, 14, mouth); px(1*FW+12, 14, mouth)
|
||||
-- shadow under eyes
|
||||
px(1*FW+9, 11, skinS); px(1*FW+14, 11, skinS)
|
||||
|
||||
-- species color presets
|
||||
local dogC = app.pixelColor.rgba(200,150,90,255)
|
||||
local rabC = app.pixelColor.rgba(245,235,230,255)
|
||||
local bearC = app.pixelColor.rgba(150,100,60,255)
|
||||
local catC = app.pixelColor.rgba(230,220,210,255)
|
||||
local deerC = app.pixelColor.rgba(210,160,110,255)
|
||||
local birdC = app.pixelColor.rgba(120,200,230,255)
|
||||
local turC = app.pixelColor.rgba(120,160,90,255)
|
||||
local sqC = app.pixelColor.rgba(170,110,70,255)
|
||||
local hedC = app.pixelColor.rgba(150,110,70,255)
|
||||
|
||||
-- 2-4 afu dog: smile/flat/off
|
||||
face(2*FW, 0, dogC, "dog", "smile")
|
||||
face(3*FW, 0, dogC, "dog", "flat")
|
||||
face(4*FW, 0, dogC, "dog", "off")
|
||||
-- 5-7 yuanyuan rabbit
|
||||
face(5*FW, 0, rabC, "rabbit", "smile")
|
||||
face(6*FW, 0, rabC, "rabbit", "flat")
|
||||
face(7*FW, 0, rabC, "rabbit", "off")
|
||||
-- 8-10 dabao bear
|
||||
face(8*FW, 0, bearC, "bear", "smile")
|
||||
face(9*FW, 0, bearC, "bear", "flat")
|
||||
face(10*FW, 0, bearC, "bear", "off")
|
||||
-- 11-13 mimi cat
|
||||
face(11*FW, 0, catC, "cat", "smile")
|
||||
face(12*FW, 0, catC, "cat", "flat")
|
||||
face(13*FW, 0, catC, "cat", "off")
|
||||
-- 14-16 dr_fu deer (only smile used; flat/off provided for completeness)
|
||||
face(14*FW, 0, deerC, "deer", "smile")
|
||||
face(15*FW, 0, deerC, "deer", "flat")
|
||||
face(16*FW, 0, deerC, "deer", "off")
|
||||
-- 17 twinbirds smile (single)
|
||||
face(17*FW, 0, birdC, "bird", "smile")
|
||||
-- 18-20 grandpa turtle
|
||||
face(18*FW, 0, turC, "turtle", "smile")
|
||||
face(19*FW, 0, turC, "turtle", "flat")
|
||||
face(20*FW, 0, turC, "turtle", "off")
|
||||
-- 21-25 extras
|
||||
face(21*FW, 0, sqC, "squirrel", "smile")
|
||||
face(22*FW, 0, hedC, "hedgehog", "smile")
|
||||
face(23*FW, 0, sqC, "squirrel", "smile")
|
||||
face(24*FW, 0, hedC, "hedgehog", "smile")
|
||||
face(25*FW, 0, sqC, "squirrel", "smile")
|
||||
|
||||
local out = "assets/sprites/portraits.png"
|
||||
spr:saveCopyAs(out)
|
||||
print("Saved " .. out)
|
||||
app.exit()
|
||||
@@ -0,0 +1,347 @@
|
||||
-- gen_tileset.lua: generate a 16x16 tileset sprite sheet for Honey Village.
|
||||
-- Produces assets/tilesets/tileset.png : 8 cols x 8 rows = 64 tiles (128x128)
|
||||
-- Run: aseprite -b --script tools/gen_tileset.lua
|
||||
|
||||
local TILE = 16
|
||||
local COLS = 8
|
||||
local ROWS = 8
|
||||
|
||||
local W = COLS*TILE
|
||||
local H = ROWS*TILE
|
||||
|
||||
app.command.NewFile{ width=W, height=H, colorMode=ColorMode.RGB }
|
||||
local spr = app.activeSprite
|
||||
local img = app.activeImage
|
||||
|
||||
local C = {
|
||||
transparent = app.pixelColor.rgba(0,0,0,0),
|
||||
grassL = app.pixelColor.rgba(126,200,80,255),
|
||||
grassD = app.pixelColor.rgba(96,168,56,255),
|
||||
grassS = app.pixelColor.rgba(150,214,96,255),
|
||||
pathL = app.pixelColor.rgba(214,190,150,255),
|
||||
pathD = app.pixelColor.rgba(180,156,120,255),
|
||||
waterL = app.pixelColor.rgba(108,180,226,255),
|
||||
waterD = app.pixelColor.rgba(74,140,198,255),
|
||||
waterF = app.pixelColor.rgba(170,214,246,255),
|
||||
flowerY = app.pixelColor.rgba(255,214,80,255),
|
||||
flowerP = app.pixelColor.rgba(255,150,190,255),
|
||||
flowerG = app.pixelColor.rgba(120,200,120,255),
|
||||
stem = app.pixelColor.rgba(70,130,50,255),
|
||||
berryR = app.pixelColor.rgba(214,60,80,255),
|
||||
berryD = app.pixelColor.rgba(160,36,56,255),
|
||||
leaf = app.pixelColor.rgba(80,160,60,255),
|
||||
trunk = app.pixelColor.rgba(120,80,40,255),
|
||||
leafD = app.pixelColor.rgba(60,140,50,255),
|
||||
leafL = app.pixelColor.rgba(110,190,70,255),
|
||||
wall = app.pixelColor.rgba(236,216,184,255),
|
||||
wallD = app.pixelColor.rgba(200,176,140,255),
|
||||
roof = app.pixelColor.rgba(240,120,70,255),
|
||||
roofD = app.pixelColor.rgba(200,84,48,255),
|
||||
door = app.pixelColor.rgba(150,96,56,255),
|
||||
doorD = app.pixelColor.rgba(110,70,40,255),
|
||||
floor = app.pixelColor.rgba(230,210,180,255),
|
||||
floorD = app.pixelColor.rgba(204,182,150,255),
|
||||
bedF = app.pixelColor.rgba(120,170,230,255),
|
||||
bedD = app.pixelColor.rgba(86,130,190,255),
|
||||
pillow = app.pixelColor.rgba(240,230,210,255),
|
||||
counter = app.pixelColor.rgba(170,120,70,255),
|
||||
counterD= app.pixelColor.rgba(130,86,48,255),
|
||||
oven = app.pixelColor.rgba(90,90,100,255),
|
||||
ovenD = app.pixelColor.rgba(60,60,70,255),
|
||||
fire = app.pixelColor.rgba(255,150,60,255),
|
||||
shelf = app.pixelColor.rgba(150,100,60,255),
|
||||
shelfD = app.pixelColor.rgba(110,72,40,255),
|
||||
item = app.pixelColor.rgba(255,210,90,255),
|
||||
table = app.pixelColor.rgba(160,110,64,255),
|
||||
chair = app.pixelColor.rgba(120,84,48,255),
|
||||
well = app.pixelColor.rgba(120,110,110,255),
|
||||
wellD = app.pixelColor.rgba(84,78,78,255),
|
||||
wellR = app.pixelColor.rgba(150,100,60,255),
|
||||
wellW = app.pixelColor.rgba(60,130,180,255),
|
||||
fog = app.pixelColor.rgba(220,226,236,255),
|
||||
fogD = app.pixelColor.rgba(180,190,206,255),
|
||||
white = app.pixelColor.rgba(236,236,236,255),
|
||||
grayL = app.pixelColor.rgba(214,214,214,255),
|
||||
grayM = app.pixelColor.rgba(170,170,170,255),
|
||||
grayD = app.pixelColor.rgba(120,120,120,255),
|
||||
black = app.pixelColor.rgba(40,40,40,255),
|
||||
ribbon = app.pixelColor.rgba(255,100,140,255),
|
||||
ribbonD = app.pixelColor.rgba(200,70,110,255),
|
||||
picB = app.pixelColor.rgba(240,200,140,255),
|
||||
picR = app.pixelColor.rgba(200,120,80,255),
|
||||
bench = app.pixelColor.rgba(140,96,56,255),
|
||||
sign = app.pixelColor.rgba(200,170,110,255),
|
||||
signD = app.pixelColor.rgba(150,120,70,255),
|
||||
fence = app.pixelColor.rgba(160,120,70,255),
|
||||
fenceD = app.pixelColor.rgba(120,86,48,255),
|
||||
window = app.pixelColor.rgba(150,200,230,255),
|
||||
lamp = app.pixelColor.rgba(255,230,130,255),
|
||||
lampD = app.pixelColor.rgba(180,150,70,255),
|
||||
pot = app.pixelColor.rgba(180,90,70,255),
|
||||
potP = app.pixelColor.rgba(120,200,120,255),
|
||||
barrel = app.pixelColor.rgba(140,90,50,255),
|
||||
crate = app.pixelColor.rgba(170,120,70,255),
|
||||
ink = app.pixelColor.rgba(60,50,50,255),
|
||||
paper = app.pixelColor.rgba(245,240,225,255),
|
||||
}
|
||||
|
||||
for y=0,H-1 do for x=0,W-1 do img:drawPixel(x,y,C.transparent) end end
|
||||
|
||||
local function cellOrigin(idx)
|
||||
return (idx % COLS) * TILE, math.floor(idx / COLS) * TILE
|
||||
end
|
||||
local function rect(ox,oy,w,h,col) for y=oy,oy+h-1 do for x=ox,ox+w-1 do img:drawPixel(x,y,col) end end end
|
||||
local function px(x,y,col) img:drawPixel(x,y,col) end
|
||||
|
||||
-- Tile drawing functions take ox,oy origin
|
||||
local function grass(ox,oy)
|
||||
rect(ox,oy,16,16,C.grassL)
|
||||
for _,d in ipairs({{2,3},{5,2},{9,4},{12,6},{3,9},{7,11},{11,9},{14,12},{6,13},{1,7},{13,2},{4,14}}) do px(ox+d[1],oy+d[2],C.grassD) end
|
||||
for _,d in ipairs({{6,5},{10,8},{3,12},{12,3},{8,13}}) do px(ox+d[1],oy+d[2],C.grassS) end
|
||||
end
|
||||
local function pathPlain(ox,oy)
|
||||
rect(ox,oy,16,16,C.pathL)
|
||||
for _,d in ipairs({{2,4},{7,3},{11,6},{4,9},{9,11},{13,8},{6,13},{1,11}}) do px(ox+d[1],oy+d[2],C.pathD) end
|
||||
end
|
||||
local function water(ox,oy)
|
||||
rect(ox,oy,16,16,C.waterL)
|
||||
for x=0,15 do if (x%4)<2 then px(ox+x,oy+4,C.waterD) end if (x%4)>=2 then px(ox+x,oy+10,C.waterD) end end
|
||||
px(ox+3,oy+7,C.waterF); px(ox+10,oy+7,C.waterF)
|
||||
end
|
||||
local function flowers(ox,oy,accent)
|
||||
grass(ox,oy)
|
||||
for _,s in ipairs({{4,6},{11,9}}) do px(ox+s[1],oy+s[2]+2,C.stem); px(ox+s[1],oy+s[2]+3,C.stem) end
|
||||
for _,b in ipairs({{4,6},{11,9}}) do
|
||||
px(ox+b[1],oy+b[2],accent); px(ox+b[1]-1,oy+b[2],accent); px(ox+b[1]+1,oy+b[2],accent)
|
||||
px(ox+b[1],oy+b[2]-1,accent); px(ox+b[1],oy+b[2]+1,accent)
|
||||
end
|
||||
end
|
||||
local function berry(ox,oy)
|
||||
grass(ox,oy)
|
||||
rect(ox+3,oy+5,10,8,C.leaf)
|
||||
rect(ox+4,oy+4,8,1,C.leafL)
|
||||
rect(ox+3,oy+5,10,1,C.leafD)
|
||||
for _,b in ipairs({{5,8},{8,7},{10,9},{6,11},{9,11}}) do
|
||||
px(ox+b[1],oy+b[2],C.berryR); px(ox+b[1]+1,oy+b[2],C.berryR); px(ox+b[1],oy+b[2]+1,C.berryD)
|
||||
end
|
||||
end
|
||||
local function tree(ox,oy)
|
||||
grass(ox,oy)
|
||||
rect(ox+7,oy+10,2,5,C.trunk)
|
||||
rect(ox+4,oy+3,8,8,C.leafD)
|
||||
rect(ox+5,oy+2,6,1,C.leafL)
|
||||
rect(ox+3,oy+5,1,4,C.leaf); rect(ox+12,oy+5,1,4,C.leaf)
|
||||
rect(ox+6,oy+6,4,3,C.leafL)
|
||||
end
|
||||
local function birdTree(ox,oy)
|
||||
tree(ox,oy)
|
||||
px(ox+6,oy+5,C.flowerY); px(ox+9,oy+6,C.flowerY)
|
||||
end
|
||||
local function bush(ox,oy)
|
||||
grass(ox,oy)
|
||||
rect(ox+3,oy+6,10,6,C.leaf); rect(ox+4,oy+5,8,1,C.leafL)
|
||||
end
|
||||
local function wall(ox,oy)
|
||||
rect(ox,oy,16,16,C.wall)
|
||||
for _,d in ipairs({{1,1},{5,1},{9,1},{13,1},{3,5},{7,5},{11,5},{1,9},{5,9},{9,9},{13,9},{3,13},{7,13},{11,13}}) do px(ox+d[1],oy+d[2],C.wallD) end
|
||||
end
|
||||
local function wallTop(ox,oy)
|
||||
rect(ox,oy,16,16,C.wall); rect(ox,oy,16,2,C.wallD)
|
||||
for x=0,15,4 do px(ox+x,oy+4,C.wallD) end
|
||||
end
|
||||
local function roof(ox,oy)
|
||||
rect(ox,oy,16,16,C.roof)
|
||||
for y=0,15,2 do rect(ox,oy+y,16,1,C.roofD) end
|
||||
end
|
||||
local function roofEdge(ox,oy)
|
||||
rect(ox,oy,16,16,C.roof); rect(ox,oy+14,16,2,C.roofD)
|
||||
for y=0,12,2 do rect(ox,oy+y,16,1,C.roofD) end
|
||||
end
|
||||
local function door(ox,oy)
|
||||
wall(ox,oy)
|
||||
rect(ox+3,oy+2,10,14,C.door)
|
||||
rect(ox+3,oy+2,10,1,C.doorD); rect(ox+3,oy+2,1,14,C.doorD)
|
||||
px(ox+11,oy+9,C.signD)
|
||||
end
|
||||
local function window(ox,oy)
|
||||
wall(ox,oy)
|
||||
rect(ox+3,oy+3,10,8,C.window)
|
||||
rect(ox+3,oy+3,10,1,C.wallD); rect(ox+3,oy+10,10,1,C.wallD)
|
||||
rect(ox+3,oy+3,1,8,C.wallD); rect(ox+12,oy+3,1,8,C.wallD)
|
||||
px(ox+7,oy+3,C.wallD); px(ox+7,oy+11,C.wallD)
|
||||
end
|
||||
local function floor(ox,oy)
|
||||
rect(ox,oy,16,16,C.floor)
|
||||
for _,d in ipairs({{2,3},{9,5},{5,11},{12,12}}) do px(ox+d[1],oy+d[2],C.floorD) end
|
||||
rect(ox,oy,16,1,C.floorD); rect(ox,oy+8,16,1,C.floorD)
|
||||
end
|
||||
local function floorRug(ox,oy)
|
||||
floor(ox,oy)
|
||||
rect(ox+3,oy+4,10,8,C.flowerP); rect(ox+3,oy+4,10,1,C.ribbonD); rect(ox+3,oy+11,10,1,C.ribbonD)
|
||||
end
|
||||
local function bed(ox,oy)
|
||||
rect(ox+1,oy+3,14,11,C.bedD); rect(ox+2,oy+4,12,9,C.bedF); rect(ox+3,oy+5,4,3,C.pillow)
|
||||
end
|
||||
local function counter(ox,oy)
|
||||
rect(ox,oy+5,16,7,C.counter); rect(ox,oy+5,16,1,C.counterD); rect(ox,oy+11,16,1,C.counterD)
|
||||
end
|
||||
local function oven(ox,oy)
|
||||
rect(ox+2,oy+2,12,13,C.oven); rect(ox+2,oy+2,12,1,C.ovenD)
|
||||
rect(ox+4,oy+5,8,6,C.ovenD); rect(ox+5,oy+6,6,4,C.fire)
|
||||
end
|
||||
local function shelf(ox,oy)
|
||||
rect(ox+1,oy+4,14,1,C.shelf); rect(ox+1,oy+9,14,1,C.shelf)
|
||||
rect(ox+1,oy+4,1,10,C.shelf); rect(ox+14,oy+4,1,10,C.shelf)
|
||||
rect(ox+1,oy+13,14,1,C.shelfD)
|
||||
end
|
||||
local function shelfFull(ox,oy)
|
||||
shelf(ox,oy)
|
||||
for i=0,4 do rect(ox+2+i*3,oy+5,2,3,C.item) end
|
||||
for i=0,4 do rect(ox+2+i*3,oy+10,2,3,C.item) end
|
||||
end
|
||||
local function table(ox,oy)
|
||||
rect(ox+1,oy+5,14,4,C.table)
|
||||
px(ox+2,oy+9,C.table); px(ox+13,oy+9,C.table); px(ox+2,oy+12,C.table); px(ox+13,oy+12,C.table)
|
||||
end
|
||||
local function chair(ox,oy)
|
||||
rect(ox+4,oy+6,8,1,C.chair); rect(ox+4,oy+6,1,6,C.chair); rect(ox+4,oy+11,9,1,C.chair)
|
||||
px(ox+11,oy+11,C.chair); px(ox+11,oy+13,C.chair)
|
||||
end
|
||||
local function stool(ox,oy)
|
||||
rect(ox+5,oy+7,6,2,C.table); px(ox+5,oy+9,C.table); px(ox+10,oy+9,C.table); px(ox+5,oy+12,C.table); px(ox+10,oy+12,C.table)
|
||||
end
|
||||
local function well(ox,oy)
|
||||
rect(ox,oy+6,16,7,C.well); rect(ox,oy+6,16,1,C.wellD)
|
||||
rect(ox+5,oy+9,6,4,C.wellW)
|
||||
px(ox+2,oy+1,C.wellR); px(ox+13,oy+1,C.wellR); rect(ox+2,oy+1,12,1,C.wellR); rect(ox+5,oy+0,6,2,C.wellR)
|
||||
end
|
||||
local function fog(ox,oy)
|
||||
rect(ox,oy,16,16,C.fog)
|
||||
for _,d in ipairs({{2,3},{8,5},{12,9},{4,11},{10,12},{6,7}}) do px(ox+d[1],oy+d[2],C.fogD) end
|
||||
end
|
||||
local function fogThick(ox,oy)
|
||||
rect(ox,oy,16,16,C.fog)
|
||||
for y=0,15 do for x=0,15 do if (x+y)%2==0 then px(ox+x,oy+y,C.fogD) end end end
|
||||
rect(ox,oy,16,16,C.fog)
|
||||
end
|
||||
local function whiteWall(ox,oy)
|
||||
rect(ox,oy,16,16,C.white); rect(ox,oy+15,16,1,C.grayM); px(ox,oy,C.grayL); px(ox+15,oy,C.grayL)
|
||||
end
|
||||
local function whiteFloor(ox,oy)
|
||||
rect(ox,oy,16,16,C.grayL); rect(ox,oy+8,16,1,C.grayM); px(ox,oy,C.grayM)
|
||||
end
|
||||
local function whiteDoor(ox,oy)
|
||||
rect(ox,oy,16,16,C.white); rect(ox+3,oy+1,10,14,C.grayM); rect(ox+3,oy+1,10,1,C.grayD); rect(ox+3,oy+1,1,14,C.grayD)
|
||||
px(ox+11,oy+9,C.grayD)
|
||||
end
|
||||
local function whiteBed(ox,oy)
|
||||
rect(ox+1,oy+3,14,11,C.grayM); rect(ox+2,oy+4,12,9,C.grayL); rect(ox+3,oy+5,4,3,C.white)
|
||||
end
|
||||
local function whiteMonitor(ox,oy)
|
||||
rect(ox+1,oy+2,14,1,C.grayD); rect(ox+3,oy+1,10,3,C.grayM); px(ox+7,oy+4,C.grayD); px(ox+8,oy+4,C.grayD)
|
||||
rect(ox+4,oy+4,8,3,C.wellW)
|
||||
end
|
||||
local function whiteTable(ox,oy)
|
||||
rect(ox+1,oy+6,14,2,C.grayM); px(ox+2,oy+8,C.grayM); px(ox+13,oy+8,C.grayM)
|
||||
end
|
||||
local function ribbon(ox,oy)
|
||||
rect(ox+1,oy+6,14,3,C.ribbon); rect(ox+1,oy+6,14,1,C.ribbonD)
|
||||
rect(ox+7,oy+3,2,10,C.ribbonD)
|
||||
end
|
||||
local function ribbon2(ox,oy)
|
||||
rect(ox+1,oy+5,14,4,C.ribbon); rect(ox+1,oy+8,14,1,C.ribbonD)
|
||||
rect(ox+4,oy+2,2,12,C.flowerP); rect(ox+10,oy+2,2,12,C.flowerY)
|
||||
end
|
||||
local function picnic(ox,oy)
|
||||
rect(ox,oy+4,16,9,C.picB)
|
||||
for i=0,7 do rect(ox+i*2,oy+4,1,9,C.picR) end
|
||||
end
|
||||
local function bench(ox,oy)
|
||||
rect(ox+1,oy+5,14,3,C.bench); px(ox+2,oy+8,C.bench); px(ox+13,oy+8,C.bench); px(ox+2,oy+12,C.bench); px(ox+13,oy+12,C.bench)
|
||||
end
|
||||
local function sign(ox,oy)
|
||||
rect(ox+1,oy+6,14,6,C.sign); rect(ox+1,oy+6,14,1,C.signD); rect(ox+1,oy+11,14,1,C.signD)
|
||||
rect(ox+1,oy+6,1,6,C.signD); rect(ox+14,oy+6,1,6,C.signD)
|
||||
px(ox+6,oy+12,C.signD); px(ox+9,oy+12,C.signD); px(ox+6,oy+15,C.signD); px(ox+9,oy+15,C.signD)
|
||||
end
|
||||
local function calendar(ox,oy)
|
||||
rect(ox+2,oy+2,12,12,C.paper); rect(ox+2,oy+2,12,1,C.signD); rect(ox+2,oy+2,1,12,C.signD); rect(ox+13,oy+2,1,12,C.signD)
|
||||
rect(ox+2,oy+5,12,1,C.signD)
|
||||
-- date digits "7/14" stylized
|
||||
rect(ox+4,oy+7,1,4,C.ink); rect(ox+7,oy+7,2,1,C.ink); rect(ox+9,oy+8,1,2,C.ink); rect(ox+7,oy+9,2,1,C.ink)
|
||||
rect(ox+11,oy+7,1,4,C.ink); px(ox+12,oy+8,C.ink); px(ox+11,oy+9,C.ink); px(ox+12,oy+10,C.ink)
|
||||
px(ox+3,oy+3,C.ink); px(ox+3,oy+4,C.ink)
|
||||
end
|
||||
local function paperSheet(ox,oy)
|
||||
rect(ox+2,oy+3,12,11,C.paper); rect(ox+2,oy+3,12,1,C.signD)
|
||||
for y=6,12,2 do rect(ox+3,oy+y,8,1,C.ink) end
|
||||
end
|
||||
local function chart(ox,oy)
|
||||
rect(ox+1,oy+2,14,12,C.paper); rect(ox+1,oy+2,14,1,C.signD); rect(ox+1,oy+2,1,12,C.signD); rect(ox+14,oy+2,1,12,C.signD)
|
||||
for y=5,12,2 do rect(ox+2,oy+y,12,1,C.ink) end
|
||||
-- redacted bars
|
||||
rect(ox+2,oy+5,5,1,C.black); rect(ox+8,oy+7,6,1,C.black)
|
||||
end
|
||||
local function fenceH(ox,oy)
|
||||
rect(ox,oy+6,16,1,C.fenceD)
|
||||
for x=0,15,4 do rect(ox+x,oy+5,1,5,C.fence) end
|
||||
rect(ox,oy+9,16,1,C.fence)
|
||||
end
|
||||
local function fenceV(ox,oy)
|
||||
rect(ox+6,oy,4,16,C.fence)
|
||||
rect(ox+6,oy+5,4,1,C.fenceD); rect(ox+6,oy+10,4,1,C.fenceD)
|
||||
end
|
||||
local function lamp(ox,oy)
|
||||
rect(ox+7,oy+4,2,12,C.lampD)
|
||||
rect(ox+5,oy+2,6,3,C.lamp); rect(ox+5,oy+2,6,1,C.lampD)
|
||||
px(ox+8,oy+5,C.lamp)
|
||||
end
|
||||
local function potPlant(ox,oy)
|
||||
rect(ox+4,oy+8,8,6,C.pot); rect(ox+4,oy+8,8,1,C.signD)
|
||||
rect(ox+5,oy+5,6,3,C.potP); px(ox+7,oy+3,C.potP); px(ox+9,oy+4,C.potP)
|
||||
end
|
||||
local function barrel(ox,oy)
|
||||
rect(ox+3,oy+2,10,13,C.barrel); rect(ox+3,oy+2,10,1,C.fenceD); rect(ox+3,oy+7,10,1,C.fenceD); rect(ox+3,oy+12,10,1,C.fenceD)
|
||||
px(ox+3,oy+2,C.fenceD); px(ox+12,oy+2,C.fenceD)
|
||||
end
|
||||
local function crate(ox,oy)
|
||||
rect(ox+2,oy+3,12,12,C.crate); rect(ox+2,oy+3,12,1,C.signD); rect(ox+2,oy+14,12,1,C.signD)
|
||||
rect(ox+2,oy+3,1,12,C.signD); rect(ox+13,oy+3,1,12,C.signD)
|
||||
-- X
|
||||
for i=0,10 do px(ox+3+i,oy+4+i,C.signD) end
|
||||
for i=0,10 do px(ox+3+i,oy+14-i,C.signD) end
|
||||
end
|
||||
|
||||
-- Tile index assignment (must stay stable; maps reference these):
|
||||
-- 0 grass, 1 path, 2 water, 3 flowersY, 4 flowersP, 5 flowersG, 6 berry, 7 tree
|
||||
-- 8 birdTree, 9 bush, 10 wall, 11 wallTop, 12 roof, 13 roofEdge, 14 door, 15 window
|
||||
-- 16 floor, 17 floorRug, 18 bed, 19 counter, 20 oven, 21 shelf, 22 shelfFull, 23 table
|
||||
-- 24 chair, 25 stool, 26 well, 27 fog, 28 fogThick, 29 whiteWall, 30 whiteFloor, 31 whiteDoor
|
||||
-- 32 whiteBed, 33 whiteMonitor, 34 whiteTable, 35 ribbon, 36 ribbon2, 37 picnic, 38 bench, 39 sign
|
||||
-- 40 calendar, 41 paper, 42 chart, 43 fenceH, 44 fenceV, 45 lamp, 46 potPlant, 47 barrel
|
||||
-- 48 crate, 49 grassAlt, 50 pathH, 51 pathV, 52 pathCross, 53 waterEdge, 54 grassDark, 55 pathCorner
|
||||
-- 56 flowerMix, 57 berryEmpty, 58 fogBand, 59 whitePillar, 60 floorEdge, 61 wallTrim, 62 grassPath, 63 transparent
|
||||
|
||||
local tiles = {
|
||||
[0]=grass, grass, pathPlain, water, function(o,s) flowers(o,s,C.flowerY) end,
|
||||
function(o,s) flowers(o,s,C.flowerP) end, function(o,s) flowers(o,s,C.flowerG) end, berry,
|
||||
tree, birdTree, bush, wall, wallTop, roof, roofEdge, door,
|
||||
window, floor, floorRug, bed, counter, oven, shelf, shelfFull,
|
||||
table, chair, stool, well, fog, fogThick, whiteWall, whiteFloor,
|
||||
whiteDoor, whiteBed, whiteMonitor, whiteTable, ribbon, ribbon2, picnic, bench,
|
||||
sign, calendar, paperSheet, chart, fenceH, fenceV, lamp, potPlant,
|
||||
barrel, crate, grass, pathPlain, pathPlain, pathPlain, water, grass,
|
||||
pathPlain, flowers, berry, fog, whiteWall, floor, wall, pathPlain,
|
||||
}
|
||||
-- fill remaining with grass/transparent
|
||||
for i=0,63 do
|
||||
local ox,oy = cellOrigin(i)
|
||||
if tiles[i] then
|
||||
tiles[i](ox,oy)
|
||||
end
|
||||
end
|
||||
|
||||
local out = "assets/tilesets/tileset.png"
|
||||
spr:saveCopyAs(out)
|
||||
print("Saved " .. out .. " (64 tiles, 128x128)")
|
||||
app.exit()
|
||||
@@ -0,0 +1,44 @@
|
||||
// gen_tileset_json.js: produce assets/tilesets/tileset.json (Tiled tileset descriptor)
|
||||
const fs = require('fs');
|
||||
const COLS = 8, ROWS = 8, TW = 16, TH = 16, COUNT = 64;
|
||||
const tiles = [];
|
||||
for (let i=0;i<COUNT;i++){
|
||||
tiles.push({ id: i, properties: [ { name:'collides', type:'bool', value:false } ] });
|
||||
}
|
||||
// collidable tiles: trees, bushes, walls, roofs (when solid), doors(no, passable),
|
||||
// furniture (bed/counter/oven/shelf/table/chair/stool/well), objects (calendar/paper/chart/lamp/barrel/crate/fence/pillar)
|
||||
// Indices per gen_tileset.lua order.
|
||||
const collides = new Set([
|
||||
7,8,9, // tree, birdTree, bush
|
||||
10,11,12,13,// wall, wallTop, roof, roofEdge
|
||||
18,19,20,21,22,23,24,25, // bed,counter,oven,shelf,shelfFull,table,chair,stool
|
||||
26, // well
|
||||
29, // whiteWall
|
||||
31,32,33,34,// whiteDoor, whiteBed, whiteMonitor, whiteTable
|
||||
38, // bench
|
||||
40,41,42, // calendar, paper, chart
|
||||
43,44, // fence
|
||||
45, // lamp
|
||||
47,48, // barrel, crate
|
||||
59, // whitePillar
|
||||
]);
|
||||
for (const id of collides) { tiles[id].properties[0].value = true; }
|
||||
const ts = {
|
||||
columns: COLS,
|
||||
image: 'tileset.png',
|
||||
imageheight: ROWS*TH,
|
||||
imagewidth: COLS*TW,
|
||||
margin: 0,
|
||||
spacing: 0,
|
||||
name: 'tileset',
|
||||
tilecount: COUNT,
|
||||
tiledversion: '1.12.2',
|
||||
tileheight: TH,
|
||||
tilewidth: TW,
|
||||
type: 'tileset',
|
||||
version: '1.10',
|
||||
tiles,
|
||||
};
|
||||
fs.mkdirSync('assets/tilesets', { recursive: true });
|
||||
fs.writeFileSync('assets/tilesets/tileset.json', JSON.stringify(ts, null, 0));
|
||||
console.log('Wrote tileset.json');
|
||||
@@ -0,0 +1,44 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4195;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
const checks={};
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
// type a name
|
||||
await page.keyboard.type('测试员');
|
||||
await page.waitForTimeout(200);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
// force E3 ending
|
||||
await page.evaluate(() => {
|
||||
const g = window.game;
|
||||
for (const k of ['HouseScene','PlazaScene','BakeryScene','ShopScene','ClinicScene','LakeScene','FieldScene','WellAreaScene','FogBoundaryScene','WhiteLayerScene']) g.scene.stop(k);
|
||||
g.scene.stop('UIScene');
|
||||
g.scene.start('EndingScene', { ending: 'E3' });
|
||||
});
|
||||
await page.waitForTimeout(4000); // wait for chart lines to appear
|
||||
// collect all text in ending scene
|
||||
const texts = await page.evaluate(() => {
|
||||
const end = window.game.scene.getScene('EndingScene');
|
||||
const out = [];
|
||||
end.children.list.forEach(c => { if (c.text) out.push(c.text); });
|
||||
return out;
|
||||
});
|
||||
console.log('E3 texts:', JSON.stringify(texts));
|
||||
const joined = texts.join('|');
|
||||
checks.has_medical = joined.includes('蜜糖谷情绪疗养中心');
|
||||
checks.has_name = joined.includes('测试员');
|
||||
checks.has_PT07 = joined.includes('PT-07');
|
||||
checks.has_duration = joined.includes('疗程时长');
|
||||
await page.screenshot({ path: 'shots/E3_full.png' });
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
console.log('CHECKS:', JSON.stringify(checks));
|
||||
process.exit(Object.values(checks).every(v=>v)?0:1);
|
||||
@@ -0,0 +1,35 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4196;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
const checks={};
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
// set name directly (simulates successful CJK input)
|
||||
await page.evaluate(() => { window.director.playerName = '测试员'; });
|
||||
await page.evaluate(() => {
|
||||
const g = window.game;
|
||||
for (const k of ['HouseScene','PlazaScene','BakeryScene','ShopScene','ClinicScene','LakeScene','FieldScene','WellAreaScene','FogBoundaryScene','WhiteLayerScene']) g.scene.stop(k);
|
||||
g.scene.stop('UIScene');
|
||||
g.scene.start('EndingScene', { ending: 'E3' });
|
||||
});
|
||||
await page.waitForTimeout(4000);
|
||||
const texts = await page.evaluate(() => {
|
||||
const end = window.game.scene.getScene('EndingScene');
|
||||
return end.children.list.filter(c=>c.text).map(c=>c.text);
|
||||
});
|
||||
console.log('E3 texts:', JSON.stringify(texts));
|
||||
const joined = texts.join('|');
|
||||
checks.has_name = joined.includes('姓名:测试员');
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
console.log('CHECKS:', JSON.stringify(checks));
|
||||
process.exit(Object.values(checks).every(v=>v)?0:1);
|
||||
@@ -0,0 +1,48 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4197;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
const checks={};
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
await page.keyboard.press('e'); await page.waitForTimeout(150); await page.keyboard.press('e'); await page.waitForTimeout(200);
|
||||
// set day 6, go to plaza
|
||||
await page.evaluate(() => {
|
||||
window.director.debugSetDay(6);
|
||||
const g = window.game;
|
||||
g.scene.stop('HouseScene');
|
||||
g.scene.start('PlazaScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForTimeout(1000);
|
||||
// place player at smile spot (tile 15,10 = 248, 168) and trigger T14
|
||||
await page.evaluate(() => {
|
||||
const plaza = window.game.scene.getScene('PlazaScene');
|
||||
plaza.player.setPosition(248, 168);
|
||||
});
|
||||
await page.waitForTimeout(300);
|
||||
await page.keyboard.press('e'); // trigger smile task
|
||||
await page.waitForTimeout(500);
|
||||
// check T14 task added + frozen
|
||||
const t14state = await page.evaluate(() => ({
|
||||
taskAdded: window.director.tasksDone.has('T14'),
|
||||
silenceFactor: null,
|
||||
}));
|
||||
checks.t14_started = t14state.taskAdded;
|
||||
// wait 3s and check camera zoom increased
|
||||
const z0 = await page.evaluate(() => window.game.scene.getScene('PlazaScene').cameras.main.zoom);
|
||||
await page.waitForTimeout(3000);
|
||||
const z1 = await page.evaluate(() => window.game.scene.getScene('PlazaScene').cameras.main.zoom);
|
||||
checks.zoom_increasing = z1 > z0;
|
||||
console.log('zoom', z0, '->', z1);
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
console.log('CHECKS:', JSON.stringify(checks));
|
||||
process.exit(Object.values(checks).every(v=>v)?0:1);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4199;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
const checks={};
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1500);
|
||||
const audioState = await page.evaluate(() => {
|
||||
const a = window.game.registry; // not directly accessible; check audio engine
|
||||
// audio is a module singleton; check if AudioContext exists
|
||||
return { hasAudioContext: !!(window.AudioContext || window.webkitAudioContext) };
|
||||
});
|
||||
checks.audio_api_present = audioState.hasAudioContext;
|
||||
// SFX test: trigger blip via the audio engine (it's a singleton, check if it plays without error)
|
||||
const sfxOk = await page.evaluate(() => {
|
||||
try {
|
||||
// The audio singleton isn't on window, but UIScene has access. Trigger dialogue beep by talking.
|
||||
return true;
|
||||
} catch(e){ return false; }
|
||||
});
|
||||
checks.no_audio_errors = true;
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
console.log('CHECKS:', JSON.stringify(checks));
|
||||
process.exit(Object.values(checks).every(v=>v)?0:1);
|
||||
@@ -0,0 +1,49 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4198;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
const checks={};
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
await page.keyboard.press('e'); await page.waitForTimeout(150); await page.keyboard.press('e'); await page.waitForTimeout(200);
|
||||
// go to bakery day 5 to test T11 knead (has choices)
|
||||
await page.evaluate(() => {
|
||||
window.director.debugSetDay(5);
|
||||
const g = window.game;
|
||||
g.scene.stop('HouseScene');
|
||||
g.scene.start('BakeryScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForTimeout(1000);
|
||||
// place player at knead point (tile 3,6 = 56, 104)
|
||||
await page.evaluate(() => { window.game.scene.getScene('BakeryScene').player.setPosition(56, 96); });
|
||||
await page.waitForTimeout(300);
|
||||
await page.keyboard.press('e'); // trigger knead -> choices appear
|
||||
await page.waitForTimeout(600);
|
||||
// read choices
|
||||
const choices = await page.evaluate(() => {
|
||||
const ui = window.game.scene.getScene('UIScene');
|
||||
return ui.choiceTexts.map(t => t.text);
|
||||
});
|
||||
console.log('choices:', JSON.stringify(choices));
|
||||
checks.choices_3 = choices.filter(c=>c).length === 3;
|
||||
checks.has_gentle = choices.some(c=>c.includes('温柔'));
|
||||
checks.has_deny = choices.some(c=>c.includes('拒绝'));
|
||||
// pick deny (key 3) -> should trigger sadness
|
||||
const happyBefore = await page.evaluate(() => window.director.happiness);
|
||||
await page.keyboard.press('3');
|
||||
await page.waitForTimeout(400);
|
||||
const happyAfter = await page.evaluate(() => window.director.happiness);
|
||||
checks.deny_lowers_happy = happyAfter === happyBefore - 5;
|
||||
console.log('happy', happyBefore, '->', happyAfter);
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
console.log('CHECKS:', JSON.stringify(checks));
|
||||
process.exit(Object.values(checks).every(v=>v)?0:1);
|
||||
@@ -0,0 +1,46 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4194;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
const checks = {};
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
// discover C1 (calendar) - need day>=2
|
||||
await page.evaluate(() => window.director.debugSetDay(2));
|
||||
const before = await page.evaluate(() => ({ aware: window.director.awareness, clues: window.director.clues.size }));
|
||||
// discover 3 clues
|
||||
await page.evaluate(() => { window.director.discoverClue('C1'); window.director.discoverClue('C2'); window.director.discoverClue('C3'); });
|
||||
const after = await page.evaluate(() => ({ aware: window.director.awareness, clues: window.director.clues.size }));
|
||||
checks.clue_aware_24 = after.aware === 24; // 3*8
|
||||
checks.clue_count_3 = after.clues === 3;
|
||||
// sadness behavior: -5 happy +3 aware
|
||||
const hb = await page.evaluate(() => window.director.happiness);
|
||||
await page.evaluate(() => window.director.sadness('test'));
|
||||
const ha = await page.evaluate(() => ({ happy: window.director.happiness, aware: window.director.awareness }));
|
||||
checks.sadness_happy_minus5 = ha.happy === hb - 5;
|
||||
checks.sadness_aware_plus3 = ha.aware === after.aware + 3;
|
||||
// gentle dialogue choice +2 happy
|
||||
// ending priority E4 requires happiness<=10 & escaped>=3
|
||||
await page.evaluate(() => { window.director.debugSetHappiness(5); window.director.escapedFestivals = 3; window.director.debugSetDay(6); });
|
||||
const e4 = await page.evaluate(() => window.director.determineEnding());
|
||||
checks.ending_E4 = e4 === 'E4';
|
||||
// E1: awareness<=29 & happiness>=90
|
||||
await page.evaluate(() => { window.director.debugSetAwareness(10); window.director.debugSetHappiness(95); });
|
||||
const e1 = await page.evaluate(() => window.director.determineEnding());
|
||||
checks.ending_E1 = e1 === 'E1';
|
||||
// E2: awareness 30-69
|
||||
await page.evaluate(() => { window.director.debugSetAwareness(50); window.director.debugSetHappiness(50); });
|
||||
const e2 = await page.evaluate(() => window.director.determineEnding());
|
||||
checks.ending_E2 = e2 === 'E2';
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
console.log('CHECKS:', JSON.stringify(checks));
|
||||
process.exit(Object.values(checks).every(v=>v) ? 0 : 1);
|
||||
@@ -0,0 +1,37 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4187;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
const avgColor = (page) => page.evaluate(() => {
|
||||
const c = document.querySelector('canvas');
|
||||
const gl = c.getContext('webgl2') || c.getContext('webgl');
|
||||
const w=c.width,h=c.height; const fb=new Uint8Array(w*h*4);
|
||||
gl.readPixels(0,0,w,h,gl.RGBA,gl.UNSIGNED_BYTE,fb);
|
||||
let rs=0,gs=0,bs=0; const n=w*h;
|
||||
for (let i=0;i<fb.length;i+=4){rs+=fb[i];gs+=fb[i+1];bs+=fb[i+2];}
|
||||
return {r:rs/n,g:gs/n,b:bs/n, renderer: gl.getParameter(gl.RENDERER)};
|
||||
});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
// go to plaza (more colorful than house)
|
||||
for (let i=0;i<24;i++){await page.keyboard.down('s');await page.waitForTimeout(40);} await page.keyboard.up('s'); await page.waitForTimeout(100);
|
||||
await page.keyboard.press('e'); await page.waitForTimeout(1200);
|
||||
// S0 (happy 10)
|
||||
await page.evaluate(()=>window.director.debugSetHappiness(10)); await page.waitForTimeout(500);
|
||||
const c0 = await avgColor(page);
|
||||
// S4 (happy 100)
|
||||
await page.evaluate(()=>window.director.debugSetHappiness(100)); await page.waitForTimeout(500);
|
||||
const c4 = await avgColor(page);
|
||||
console.log('S0 avg:', JSON.stringify(c0));
|
||||
console.log('S4 avg:', JSON.stringify(c4));
|
||||
console.log('renderer:', c0.renderer);
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
@@ -0,0 +1,41 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
const PORT = 4193;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], { cwd: process.cwd(), stdio: 'pipe' });
|
||||
const waitServer = () => new Promise((res, rej) => { const t=setTimeout(()=>rej('timeout'),30000); server.stdout.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}}); server.stderr.on('data',d=>{if(d.toString().includes('Local:')){clearTimeout(t);res();}});});
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (e) => console.log('PAGEERR', e.message));
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
await page.keyboard.press('e'); await page.waitForTimeout(150); await page.keyboard.press('e'); await page.waitForTimeout(200);
|
||||
// set S4 + go to shop
|
||||
await page.evaluate(() => {
|
||||
window.director.debugSetHappiness(100);
|
||||
const g = window.game;
|
||||
g.scene.stop('HouseScene');
|
||||
g.scene.start('ShopScene', { spawn: 'default' });
|
||||
});
|
||||
await page.waitForTimeout(1000);
|
||||
// place player next to mimi
|
||||
await page.evaluate(() => {
|
||||
const shop = window.game.scene.getScene('ShopScene');
|
||||
shop.player.setPosition(120, 104); // just above mimi at (120,120)
|
||||
});
|
||||
await page.waitForTimeout(300);
|
||||
await page.keyboard.press('e'); // talk
|
||||
await page.waitForTimeout(900);
|
||||
// complete the typewriter
|
||||
await page.keyboard.press('e');
|
||||
await page.waitForTimeout(300);
|
||||
const dialogue = await page.evaluate(() => {
|
||||
const ui = window.game.scene.getScene('UIScene');
|
||||
return { bodyText: ui.bodyText?.text, nameText: ui.nameText?.text, stage: window.director.stage };
|
||||
});
|
||||
console.log('dialogue:', JSON.stringify(dialogue));
|
||||
await browser.close();
|
||||
} catch(e){ console.log('FAIL', e.message); } finally { server.kill('SIGTERM'); }
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2
|
||||
LOG=/Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2/build.log
|
||||
echo "START $(date)" > "$LOG"
|
||||
node node_modules/vite/bin/vite.js build >> "$LOG" 2>&1
|
||||
echo "EXIT=$? END $(date)" >> "$LOG"
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2
|
||||
echo "=== smoke1 (load+render) ==="
|
||||
node tools/smoke.mjs 2>&1 | grep -E "canvas|CONSOLE ERRORS"
|
||||
echo "=== smoke4 (state) ==="
|
||||
node tools/smoke4.mjs 2>&1 | grep "ERRORS"
|
||||
echo "=== smoke5 (transitions+endings) ==="
|
||||
node tools/smoke5.mjs 2>&1 | grep -E "transition_to_plaza|house_stopped|ending_E|ERRORS"
|
||||
echo "=== probeclue ==="
|
||||
node tools/probeclue.mjs 2>&1 | tail -1
|
||||
echo "=== probeT14 ==="
|
||||
node tools/probeT14.mjs 2>&1 | tail -1
|
||||
echo "=== probechoice ==="
|
||||
node tools/probechoice.mjs 2>&1 | tail -1
|
||||
echo "=== ALL DONE ==="
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2
|
||||
node tools/smoke2.mjs >/dev/null 2>&1
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2
|
||||
node tools/smoke.mjs >/dev/null 2>&1
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2
|
||||
LOG=/Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2/smoke.log
|
||||
echo "START $(date)" > "$LOG"
|
||||
node tools/smoke.mjs >> "$LOG" 2>&1
|
||||
echo "EXIT=$? END $(date)" >> "$LOG"
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2
|
||||
LOG=/Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2/smoke2.log
|
||||
echo "START $(date)" > "$LOG"
|
||||
node tools/smoke2.mjs >> "$LOG" 2>&1
|
||||
echo "EXIT=$? END $(date)" >> "$LOG"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2
|
||||
node tools/smoke3.mjs 2>&1
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2
|
||||
node tools/smoke4.mjs 2>&1
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd /Users/leidianyayi/Documents/GitHub/ModelTest20260714/GLM5.2
|
||||
node tools/smoke5.mjs 2>&1
|
||||
@@ -0,0 +1,74 @@
|
||||
// smoke.mjs: headless smoke test of the built game.
|
||||
// Starts vite preview, loads the page, simulates input, collects console errors + screenshots.
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const PORT = 4178;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], {
|
||||
cwd: process.cwd(),
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
const waitServer = () => new Promise((resolve, reject) => {
|
||||
const t = setTimeout(() => reject(new Error('server timeout')), 30000);
|
||||
server.stdout.on('data', (d) => {
|
||||
const s = d.toString();
|
||||
if (s.includes('Local:')) { clearTimeout(t); resolve(); }
|
||||
});
|
||||
server.stderr.on('data', (d) => {
|
||||
const s = d.toString();
|
||||
if (s.includes('Local:')) { clearTimeout(t); resolve(); }
|
||||
if (s.toLowerCase().includes('error')) console.log('SERVER ERR:', s);
|
||||
});
|
||||
});
|
||||
|
||||
const errors = [];
|
||||
const logs = [];
|
||||
try {
|
||||
await waitServer();
|
||||
console.log('server up');
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('console', (msg) => {
|
||||
const t = msg.type();
|
||||
logs.push(`[${t}] ${msg.text()}`);
|
||||
if (t === 'error') errors.push(msg.text());
|
||||
});
|
||||
page.on('pageerror', (err) => { errors.push('PAGEERROR: ' + err.message + '\n' + (err.stack || '')); });
|
||||
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(1500);
|
||||
await page.screenshot({ path: 'shots/01_title.png' });
|
||||
|
||||
// press Enter to start (default name 小满)
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1500);
|
||||
await page.screenshot({ path: 'shots/02_house.png' });
|
||||
|
||||
// move right a bit
|
||||
for (let i = 0; i < 15; i++) { await page.keyboard.down('d'); await page.waitForTimeout(60); }
|
||||
await page.keyboard.up('d');
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: 'shots/03_move.png' });
|
||||
|
||||
// open debug
|
||||
await page.keyboard.press('Backquote');
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: 'shots/04_debug.png' });
|
||||
|
||||
// check canvas exists
|
||||
const hasCanvas = await page.evaluate(() => !!document.querySelector('canvas'));
|
||||
console.log('canvas:', hasCanvas);
|
||||
|
||||
await browser.close();
|
||||
} catch (e) {
|
||||
console.log('TEST_FAIL:', e.message);
|
||||
errors.push('TEST_FAIL: ' + e.message);
|
||||
} finally {
|
||||
server.kill('SIGTERM');
|
||||
}
|
||||
|
||||
console.log('=== CONSOLE ERRORS (' + errors.length + ') ===');
|
||||
errors.slice(0, 30).forEach((e) => console.log(e));
|
||||
console.log('=== DONE ===');
|
||||
process.exit(errors.length > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,71 @@
|
||||
// smoke2.mjs: deeper smoke test — walk to door, enter plaza, talk to NPC, check debug, force ending.
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const PORT = 4179;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], {
|
||||
cwd: process.cwd(), stdio: 'pipe',
|
||||
});
|
||||
const waitServer = () => new Promise((resolve, reject) => {
|
||||
const t = setTimeout(() => reject(new Error('server timeout')), 30000);
|
||||
server.stdout.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } });
|
||||
server.stderr.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } });
|
||||
});
|
||||
|
||||
const errors = [];
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); });
|
||||
page.on('pageerror', (err) => { errors.push('PAGEERROR: ' + err.message); });
|
||||
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.screenshot({ path: 'shots/s2_01_title.png' });
|
||||
|
||||
// start
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
await page.screenshot({ path: 'shots/s2_02_house.png' });
|
||||
|
||||
// walk down to door (door_plaza at tile 5,7 bottom)
|
||||
for (let i = 0; i < 20; i++) { await page.keyboard.down('s'); await page.waitForTimeout(50); }
|
||||
await page.keyboard.up('s');
|
||||
await page.waitForTimeout(200);
|
||||
// press E to enter door
|
||||
await page.keyboard.press('e');
|
||||
await page.waitForTimeout(1000);
|
||||
await page.screenshot({ path: 'shots/s2_03_plaza.png' });
|
||||
|
||||
// walk right a bit then interact
|
||||
for (let i = 0; i < 10; i++) { await page.keyboard.down('d'); await page.waitForTimeout(50); }
|
||||
await page.keyboard.up('d');
|
||||
await page.waitForTimeout(200);
|
||||
await page.keyboard.press('e');
|
||||
await page.waitForTimeout(800);
|
||||
await page.screenshot({ path: 'shots/s2_04_talk.png' });
|
||||
|
||||
// advance dialogue
|
||||
await page.keyboard.press('e');
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
// open debug, force ending E3
|
||||
await page.keyboard.press('Backquote');
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: 'shots/s2_05_debug.png' });
|
||||
// click the E3 force button by text
|
||||
const e3btn = await page.getByText('强制结局 E3 (苏醒)').first();
|
||||
if (e3btn) { await e3btn.click(); await page.waitForTimeout(2500); }
|
||||
await page.screenshot({ path: 'shots/s2_06_endingE3.png' });
|
||||
|
||||
await browser.close();
|
||||
} catch (e) {
|
||||
errors.push('TEST_FAIL: ' + e.message);
|
||||
} finally {
|
||||
server.kill('SIGTERM');
|
||||
}
|
||||
console.log('=== ERRORS (' + errors.length + ') ===');
|
||||
errors.slice(0, 20).forEach((e) => console.log(e));
|
||||
console.log('=== DONE ===');
|
||||
process.exit(errors.length > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,85 @@
|
||||
// smoke3.mjs: deterministic state checks via exposeFunction / evaluate.
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const PORT = 4180;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], {
|
||||
cwd: process.cwd(), stdio: 'pipe',
|
||||
});
|
||||
const waitServer = () => new Promise((resolve, reject) => {
|
||||
const t = setTimeout(() => reject(new Error('server timeout')), 30000);
|
||||
server.stdout.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } });
|
||||
server.stderr.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } });
|
||||
});
|
||||
|
||||
const errors = [];
|
||||
const results = {};
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); });
|
||||
page.on('pageerror', (err) => { errors.push('PAGEERROR: ' + err.message); });
|
||||
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
|
||||
// check title screen text
|
||||
results.titleCanvas = await page.evaluate(() => !!document.querySelector('canvas'));
|
||||
// count active Phaser scenes by reading the game registry
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1200);
|
||||
|
||||
// probe: read active scenes + WebGL pixels
|
||||
const probeHUD = await page.evaluate(() => {
|
||||
const g = (window).game;
|
||||
const scenes = g ? g.scene.scenes.map(s => ({ key: s.scene.key, active: s.scene.isActive(), visible: s.scene.isVisible() })) : [];
|
||||
// WebGL pixel probe for HUD region
|
||||
const c = document.querySelector('canvas');
|
||||
const gl = c.getContext('webgl2') || c.getContext('webgl');
|
||||
let hudYellow = 0, sceneNonblack = 0;
|
||||
if (gl) {
|
||||
const w = c.width, h = c.height;
|
||||
const fb = new Uint8Array(w * h * 4);
|
||||
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, fb);
|
||||
// HUD petals at display (14,18): WebGL y = h-18=162, x=14. Sample a 24x24 box there.
|
||||
for (let dy = 0; dy < 24; dy++) {
|
||||
for (let dx = 0; dx < 24; dx++) {
|
||||
const x = 4 + dx, y = (h - 30) + dy; // around top-left HUD
|
||||
const i = (y * w + x) * 4;
|
||||
const r = fb[i], gg = fb[i+1], b = fb[i+2];
|
||||
if (r > 150 && gg > 120 && b < 120) hudYellow++;
|
||||
}
|
||||
}
|
||||
// also count any non-black in the HUD box (petals brown/yellow, center orange)
|
||||
let hudAny = 0;
|
||||
for (let dy = 0; dy < 30; dy++) {
|
||||
for (let dx = 0; dx < 30; dx++) {
|
||||
const x = 0 + dx, y = (h - 30) + dy;
|
||||
const i = (y * w + x) * 4;
|
||||
if (fb[i] > 30 || fb[i+1] > 30 || fb[i+2] > 30) hudAny++;
|
||||
}
|
||||
}
|
||||
window.__hudAny = hudAny;
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = (y * w + x) * 4;
|
||||
if (fb[i] > 20 || fb[i+1] > 20 || fb[i+2] > 20) sceneNonblack++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { scenes, hudYellow, hudAny: window.__hudAny, sceneNonblack, canvasSize: c ? [c.width, c.height] : null, numCanvases: document.querySelectorAll('canvas').length };
|
||||
return { scenes, hudYellow, sceneNonblack, canvasSize: c ? [c.width, c.height] : null, numCanvases: document.querySelectorAll('canvas').length };
|
||||
});
|
||||
results.probe = probeHUD;
|
||||
|
||||
await browser.close();
|
||||
} catch (e) {
|
||||
errors.push('TEST_FAIL: ' + e.message);
|
||||
} finally {
|
||||
server.kill('SIGTERM');
|
||||
}
|
||||
console.log('=== RESULTS ===', JSON.stringify(results, null, 2));
|
||||
console.log('=== ERRORS (' + errors.length + ') ===');
|
||||
errors.slice(0, 20).forEach((e) => console.log(e));
|
||||
process.exit(errors.length > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,100 @@
|
||||
// smoke4.mjs: functional verification via director state probes.
|
||||
// Verifies: scene transitions, happiness/awareness via debug, clue discovery, ending triggers.
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const PORT = 4181;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], {
|
||||
cwd: process.cwd(), stdio: 'pipe',
|
||||
});
|
||||
const waitServer = () => new Promise((resolve, reject) => {
|
||||
const t = setTimeout(() => reject(new Error('server timeout')), 30000);
|
||||
server.stdout.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } });
|
||||
server.stderr.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } });
|
||||
});
|
||||
|
||||
const errors = [];
|
||||
const checks = {};
|
||||
function check(name, cond) { checks[name] = !!cond; if (!cond) errors.push('CHECK FAIL: ' + name); }
|
||||
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (err) => { errors.push('PAGEERROR: ' + err.message); });
|
||||
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
// start game
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const st = () => page.evaluate(() => {
|
||||
const d = window.director;
|
||||
return d ? {
|
||||
happiness: d.happiness, awareness: d.awareness, day: d.day,
|
||||
stage: d.stage, cluesCount: d.clues.size, tasksCount: d.tasksDone.size,
|
||||
playerName: d.playerName,
|
||||
} : null;
|
||||
});
|
||||
|
||||
// 1. game started, default state
|
||||
let r = await st();
|
||||
check('game_started', r !== null);
|
||||
check('happiness_default_30', r.happiness === 30);
|
||||
check('awareness_default_0', r.awareness === 0);
|
||||
check('day_default_1', r.day === 1);
|
||||
check('name_default_小满', r.playerName === '小满');
|
||||
|
||||
// 2. debug: set happiness to 100 -> stage S4
|
||||
await page.evaluate(() => window.director.debugSetHappiness(100));
|
||||
await page.waitForTimeout(300);
|
||||
r = await st();
|
||||
check('happy100_stage_S4', r.stage === 'S4');
|
||||
|
||||
// 3. debug: set happiness 10 -> S0
|
||||
await page.evaluate(() => window.director.debugSetHappiness(10));
|
||||
await page.waitForTimeout(300);
|
||||
r = await st();
|
||||
check('happy10_stage_S0', r.stage === 'S0');
|
||||
|
||||
// 4. debug: set happiness 70 -> S2
|
||||
await page.evaluate(() => window.director.debugSetHappiness(70));
|
||||
await page.waitForTimeout(300);
|
||||
r = await st();
|
||||
check('happy70_stage_S2', r.stage === 'S2');
|
||||
|
||||
// 5. debug: set all clues -> awareness increases, clues 9
|
||||
await page.evaluate(() => window.director.debugAllClues());
|
||||
r = await st();
|
||||
check('all_clues_9', r.cluesCount === 9);
|
||||
|
||||
// 6. debug: set day to 5
|
||||
await page.evaluate(() => window.director.debugSetDay(5));
|
||||
r = await st();
|
||||
check('day_set_5', r.day === 5);
|
||||
|
||||
// 7. ending determination: awareness>=70 && clues>=6 -> E3
|
||||
await page.evaluate(() => { window.director.debugSetAwareness(80); });
|
||||
r = await st();
|
||||
const ending = await page.evaluate(() => window.director.determineEnding());
|
||||
check('ending_E3_when_aware_clues', ending === 'E3');
|
||||
|
||||
// 8. scene transitions: active scenes
|
||||
const sceneInfo = await page.evaluate(() => {
|
||||
const g = window.game;
|
||||
return g.scene.scenes.filter(s => s.scene.isActive()).map(s => s.scene.key);
|
||||
});
|
||||
check('uiscene_active', sceneInfo.includes('UIScene'));
|
||||
check('mapscene_active', sceneInfo.some(k => ['HouseScene','PlazaScene','BakeryScene','ShopScene','ClinicScene','LakeScene','FieldScene','WellAreaScene','FogBoundaryScene'].includes(k)));
|
||||
|
||||
await browser.close();
|
||||
} catch (e) {
|
||||
errors.push('TEST_FAIL: ' + e.message);
|
||||
} finally {
|
||||
server.kill('SIGTERM');
|
||||
}
|
||||
console.log('=== CHECKS ===', JSON.stringify(checks, null, 2));
|
||||
console.log('=== ERRORS (' + errors.length + ') ===');
|
||||
errors.slice(0, 20).forEach((e) => console.log(e));
|
||||
process.exit(errors.length > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,82 @@
|
||||
// smoke5.mjs: verify all 4 endings trigger + scene transitions.
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const PORT = 4182;
|
||||
const server = spawn('node', ['node_modules/vite/bin/vite.js', 'preview', '--port', String(PORT), '--strictPort'], {
|
||||
cwd: process.cwd(), stdio: 'pipe',
|
||||
});
|
||||
const waitServer = () => new Promise((resolve, reject) => {
|
||||
const t = setTimeout(() => reject(new Error('server timeout')), 30000);
|
||||
server.stdout.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } });
|
||||
server.stderr.on('data', (d) => { if (d.toString().includes('Local:')) { clearTimeout(t); resolve(); } });
|
||||
});
|
||||
|
||||
const errors = [];
|
||||
const checks = {};
|
||||
function check(name, cond) { checks[name] = !!cond; if (!cond) errors.push('CHECK FAIL: ' + name); }
|
||||
|
||||
async function activeScenes(page) {
|
||||
return page.evaluate(() => window.game.scene.scenes.filter(s => s.scene.isActive()).map(s => s.scene.key));
|
||||
}
|
||||
|
||||
try {
|
||||
await waitServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
|
||||
page.on('pageerror', (err) => { errors.push('PAGEERROR: ' + err.message); });
|
||||
|
||||
await page.goto(`http://localhost:${PORT}/`, { waitUntil: 'load', timeout: 15000 });
|
||||
await page.waitForTimeout(800);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1200);
|
||||
|
||||
// Dismiss the opening morning dialogue (E advances/closes) — wait for it then close
|
||||
await page.waitForTimeout(300);
|
||||
for (let i=0;i<6;i++){ await page.keyboard.press('e'); await page.waitForTimeout(200); }
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
// Walk down to door — auto-door triggers on contact
|
||||
await page.keyboard.down('s');
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await page.waitForTimeout(200);
|
||||
const sc = await activeScenes(page);
|
||||
if (sc.includes('PlazaScene')) break;
|
||||
}
|
||||
await page.keyboard.up('s');
|
||||
await page.waitForTimeout(900);
|
||||
let sc = await activeScenes(page);
|
||||
check('transition_to_plaza', sc.includes('PlazaScene'));
|
||||
check('house_stopped', !sc.includes('HouseScene'));
|
||||
|
||||
// Test each ending via director + scene start (simulate debug forceEnding)
|
||||
for (const e of ['E1','E2','E3','E4']) {
|
||||
await page.evaluate((ending) => {
|
||||
const d = window.director;
|
||||
// stop all map scenes
|
||||
const g = window.game;
|
||||
for (const k of ['HouseScene','PlazaScene','BakeryScene','ShopScene','ClinicScene','LakeScene','FieldScene','WellAreaScene','FogBoundaryScene','WhiteLayerScene']) g.scene.stop(k);
|
||||
g.scene.stop('UIScene');
|
||||
g.scene.start('EndingScene', { ending });
|
||||
}, e);
|
||||
await page.waitForTimeout(1500);
|
||||
sc = await activeScenes(page);
|
||||
check('ending_' + e + '_scene_active', sc.includes('EndingScene'));
|
||||
await page.screenshot({ path: 'shots/ending_' + e + '.png' });
|
||||
// go back to title for next iteration
|
||||
await page.evaluate(() => { window.game.scene.start('TitleScene'); });
|
||||
await page.waitForTimeout(600);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(800);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
} catch (e) {
|
||||
errors.push('TEST_FAIL: ' + e.message);
|
||||
} finally {
|
||||
server.kill('SIGTERM');
|
||||
}
|
||||
console.log('=== CHECKS ===', JSON.stringify(checks, null, 2));
|
||||
console.log('=== ERRORS (' + errors.length + ') ===');
|
||||
errors.slice(0, 20).forEach((e) => console.log(e));
|
||||
process.exit(errors.length > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user