Initial import of HEROLL source and design documents

This commit is contained in:
toshiocn
2026-10-26 16:05:39 +08:00
commit fdb55672c1
1846 changed files with 290692 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
import http from 'node:http';
import fs from 'node:fs/promises';
import fsSync from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { chromium } from 'file:///C:/Users/toshio/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright/index.mjs';
const here = path.dirname(fileURLToPath(import.meta.url));
const workspace = path.resolve(here, '../../..');
const previewDir = path.resolve(here, '../preview');
const qaPath = path.resolve(here, '../qa-results.json');
const screenshotScreens = ['board','battle','gear','shop','dig','bubble','revive','victory'];
const expectedScreens = ['board','dice','third-die','battle','gear','shop','tile-install','chest','campfire','smithy','blessing','dig','legendary','lotto','blackjack','claw','carriage','dice-machine','bubble','revive','victory','defeat','pause'];
const mime = new Map([
['.html','text/html; charset=utf-8'],['.css','text/css; charset=utf-8'],['.js','text/javascript; charset=utf-8'],
['.json','application/json; charset=utf-8'],['.png','image/png'],['.jpg','image/jpeg'],['.jpeg','image/jpeg']
]);
const server = http.createServer(async (req,res) => {
try {
const url = new URL(req.url, 'http://127.0.0.1');
const decoded = decodeURIComponent(url.pathname);
const relative = decoded === '/' ? 'design/ue-ui/prototype/index.html' : decoded.replace(/^\//,'');
let target = path.resolve(workspace, relative);
if (!target.startsWith(workspace)) throw new Error('Path outside workspace');
const stat = await fs.stat(target);
if (stat.isDirectory()) target = path.join(target, 'index.html');
const data = await fs.readFile(target);
res.writeHead(200, {'Content-Type': mime.get(path.extname(target).toLowerCase()) || 'application/octet-stream','Cache-Control':'no-store'});
res.end(data);
} catch {
res.writeHead(404, {'Content-Type':'text/plain; charset=utf-8'});
res.end('Not found');
}
});
await new Promise(resolve => server.listen(0,'127.0.0.1',resolve));
const port = server.address().port;
const candidates = [
'C:/Program Files/Google/Chrome/Application/chrome.exe',
'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
'C:/Program Files/Microsoft/Edge/Application/msedge.exe'
];
const executablePath = candidates.find(p => fsSync.existsSync(p));
if (!executablePath) throw new Error('No local Chrome or Edge executable found.');
await fs.mkdir(previewDir,{recursive:true});
const browser = await chromium.launch({headless:true,executablePath});
const context = await browser.newContext({viewport:{width:750,height:1334},deviceScaleFactor:1});
const page = await context.newPage();
const requestFailures = [];
const pageErrors = [];
page.on('requestfailed',r => requestFailures.push({url:r.url(),error:r.failure()?.errorText || 'unknown'}));
page.on('pageerror',e => pageErrors.push(String(e)));
const results = [];
try {
for (const id of screenshotScreens) {
await page.goto(`http://127.0.0.1:${port}/design/ue-ui/prototype/?screen=${id}&capture=1`,{waitUntil:'networkidle'});
await page.waitForFunction(() => [...document.images].every(img => img.complete));
const metrics = await page.evaluate(() => {
const phone=document.querySelector('#phone');
const rect=phone.getBoundingClientRect();
const broken=[...document.images].filter(img=>!img.naturalWidth).map(img=>img.getAttribute('src'));
return {width:rect.width,height:rect.height,imageCount:document.images.length,broken};
});
await page.screenshot({path:path.join(previewDir,`${id}.png`),fullPage:false});
results.push({screen:id,...metrics,screenshot:`preview/${id}.png`});
}
await page.goto(`http://127.0.0.1:${port}/design/ue-ui/prototype/?screen=board&capture=1`,{waitUntil:'networkidle'});
const coverage = await page.evaluate(async ids => {
const rows=[];
for (const id of ids) {
window.HEROLL_UI.selectScreen(id);
await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)));
const root=document.querySelector('.game-screen');
const imgs=[...document.images];
rows.push({id,rendered:Boolean(root),images:imgs.length,broken:imgs.filter(i=>!i.naturalWidth).map(i=>i.getAttribute('src')),primary:document.querySelector('.primary,.result-button')?.textContent?.trim() || ''});
}
return rows;
}, expectedScreens);
const qa = {
generatedAt:new Date().toISOString(),
target:{width:750,height:1334,aspect:'9:16 portrait'},
browser:{executablePath},
summary:{
expectedScreenCount:expectedScreens.length,
renderedScreenCount:coverage.filter(r=>r.rendered).length,
screenshotCount:results.length,
brokenImageCount:[...results.flatMap(r=>r.broken),...coverage.flatMap(r=>r.broken)].length,
requestFailureCount:requestFailures.length,
pageErrorCount:pageErrors.length,
dimensionsPass:results.every(r=>r.width===750 && r.height===1334)
},
screenshots:results,
coverage,
requestFailures,
pageErrors
};
qa.summary.pass = qa.summary.renderedScreenCount===qa.summary.expectedScreenCount && qa.summary.brokenImageCount===0 && qa.summary.requestFailureCount===0 && qa.summary.pageErrorCount===0 && qa.summary.dimensionsPass;
await fs.writeFile(qaPath,JSON.stringify(qa,null,2),'utf8');
console.log(JSON.stringify(qa.summary,null,2));
if (!qa.summary.pass) process.exitCode=1;
} finally {
await browser.close();
await new Promise(resolve=>server.close(resolve));
}