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
+120
View File
@@ -0,0 +1,120 @@
import assert from "node:assert/strict";
import { chromium } from "file:///C:/Users/toshio/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright/index.mjs";
const BASE = process.env.HEROLL_MVP_URL ?? "http://127.0.0.1:4173/mvp/";
const browser = await chromium.launch({
headless: true,
executablePath: "C:/Program Files/Google/Chrome/Application/chrome.exe",
});
const context = await browser.newContext({ viewport: { width: 758, height: 1352 }, deviceScaleFactor: 1 });
const page = await context.newPage();
const errors = [];
const failed = [];
page.on("console", (message) => { if (message.type() === "error") errors.push(message.text()); });
page.on("pageerror", (error) => errors.push(error.message));
page.on("requestfailed", (request) => failed.push(`${request.url()} ${request.failure()?.errorText ?? ""}`));
page.on("response", (response) => { if (response.status() >= 400) errors.push(`${response.status()} ${response.url()}`); });
const box = async (selector) => {
const rect = await page.locator(selector).boundingBox();
assert.ok(rect, `缺少布局节点 ${selector}`);
return rect;
};
try {
await page.goto(BASE, { waitUntil: "networkidle" });
await page.locator("#seed-input").fill("ORIGINAL-RESTORATION-001");
await page.locator("#new-run-button").click();
await page.locator("#game-screen.active").waitFor();
assert.equal(await page.locator(".board-tile").count(), 40, "原作细格环形路径应为40个实际地块");
assert.equal(await page.locator(".board-tile.logical").count(), 40, "每个画面格都必须是独立逻辑落点");
assert.equal(await page.locator("#gear-slots button").count(), 4, "底部固定四装备槽");
assert.equal(await page.locator("#auto-indicator").textContent(), "AUTO");
assert.equal(await page.locator("#roll-button").textContent(), "ROLL");
assert.equal(await page.locator("#board-ring").evaluate((node) => getComputedStyle(node).transform), "none", "棋盘必须固定,不得随玩家位置旋转");
const clockwise = await page.evaluate(() => {
const next = document.querySelector('[data-visual-index="1"]').getBoundingClientRect();
const previous = document.querySelector('[data-visual-index="39"]').getBoundingClientRect();
return { nextX: next.x, previousX: previous.x };
});
assert.ok(clockwise.nextX < clockwise.previousX, "玩家应从底部 START 沿左侧顺时针移动");
assert.ok(await page.locator(".board-tile.logical.battle .tile-art").count() > 0, "怪物地块必须显示原作1至4骷髅图标");
assert.ok(await page.locator(".board-tile.logical.good .tile-art").count() > 0, "事件地块必须显示原作事件图标");
const battleTiles = await page.locator(".board-tile.logical.battle").evaluateAll((nodes) => nodes.map((node) => ({ label: node.getAttribute("aria-label"), src: node.querySelector("img")?.getAttribute("src") })));
assert.ok(battleTiles.every(({ label, src }) => /[1-4]怪物地块/.test(label) && /icon_NormalEnemy_[1-4]/.test(src)), "骷髅图标必须与怪物数量一一对应");
assert.match(await page.locator(".board-tile.logical.good .tile-art").first().getAttribute("src"), /Installtile_/, "事件格应使用 newtile.csv 指定的 Installtile 原作图标");
assert.equal(await page.locator(".board-tile.miniboss .tile-art").count(), 1, "必须显示原作小Boss地块");
assert.equal(await page.locator(".board-tile.infected_boss .tile-art").count(), 1, "必须显示原作污染Boss地块");
const app = await box("#app");
const resource = await box(".resource-chip");
const auto = await box("#auto-indicator");
const meter = await box("#floor-meter");
const boss = await box("#boss-figure");
const gear = await box("#gear-slots");
const roll = await box("#roll-button");
const potion = await box("#potion-button");
assert.ok(resource.x / app.width < 0.04 && resource.y / app.height < 0.08, "点数条应位于左上");
assert.ok(auto.x / app.width > 0.70 && meter.x / app.width > 0.90, "AUTO 与 Boss Gauge 应位于右上/右侧");
assert.ok(Math.abs((boss.x + boss.width / 2) / app.width - 0.5) < 0.02, "Boss 应保持棋盘中心");
assert.ok(gear.y / app.height > 0.76 && roll.y / app.height > 0.90, "装备槽与主操作键应位于底部操作区");
assert.ok(potion.x / app.width > 0.84, "药水入口应位于右下");
const sourceStyles = await page.evaluate(() => ({
hero: getComputedStyle(document.querySelector("#hero-marker")).backgroundImage,
boss: getComputedStyle(document.querySelector("#boss-figure")).backgroundImage,
tile: getComputedStyle(document.querySelector(".board-tile.empty")).backgroundImage,
}));
assert.match(sourceStyles.hero, /knight_m_idle_anim_f0/, "棋子必须使用逆向导出的原作骑士帧");
assert.match(sourceStyles.boss, /001_boss_idle_2/, "中心 Boss 必须使用原作 boss_0 帧");
assert.match(sourceStyles.tile, /tile_normal/, "棋盘格必须使用原作 Tile 纹理");
await page.locator("#roll-button").click();
await page.waitForTimeout(160);
assert.ok(await page.locator("#game-screen.showing-dice").count(), "掷骰阶段必须保持独立演出态");
assert.equal(await page.locator(".die-face").count(), 2, "必须显示红/蓝两枚原作骰面");
const exactSum = Number(await page.locator("#dice-result").getAttribute("data-sum"));
assert.equal(Number(await page.locator(".dice-sum").textContent()), exactSum, "骰子总点数必须明确显示");
assert.equal(await page.locator("#board-view").evaluate((node) => getComputedStyle(node).display), "block", "掷骰时仍显示棋盘");
assert.equal(await page.locator("#battle-view").evaluate((node) => getComputedStyle(node).display), "none", "骰子演出结束前不得抢先切战斗");
const diceStyles = await page.locator(".die-face").evaluateAll((nodes) => nodes.map((node) => getComputedStyle(node).getPropertyValue("--die-image")));
assert.match(diceStyles[0], /dice_red_[1-6]/);
assert.match(diceStyles[1], /dice_blue_[1-6]/);
await page.waitForTimeout(1050);
assert.equal(await page.locator("#game-screen.showing-dice").count(), 0, "骰子演出应在落点结算前结束");
await page.locator("#game-screen.animating-move").waitFor({ state: "visible" });
assert.equal(await page.locator("#modal-layer .modal").count(), 0, "逐格移动完成前不得弹出地块结算");
const runningFrame = await page.locator("#hero-marker.running").evaluate((node) => getComputedStyle(node).backgroundImage);
assert.match(runningFrame, /knight_m_run_anim_f[0-3]/, "逐格移动必须使用原作骑士跑步帧");
await page.locator("#game-screen.animating-move").waitFor({ state: "hidden", timeout: 6000 });
assert.equal(Number(await page.locator(".board-tile.current").getAttribute("data-visual-index")), exactSum, "从START时ROLL点数必须等于实际移动格数");
assert.equal(await page.locator("#board-ring").evaluate((node) => getComputedStyle(node).transform), "none", "移动结束后棋盘仍必须固定");
await page.locator("#debug-toggle").click();
await page.locator("button[data-debug=battle]").click();
await page.locator("#battle-view.active").waitFor();
await page.locator("#battle-announcer.show").waitFor();
assert.match(await page.locator("#battle-announcer").textContent(), /BATTLE/);
assert.equal(await page.locator("#battle-versus").count(), 1, "战斗顶部应显示双方生命对比条");
const battleStyles = await page.evaluate(() => ({
player: getComputedStyle(document.querySelector(".player-silhouette")).backgroundImage,
enemy: getComputedStyle(document.querySelector(".enemy.sword .pixel-fighter")).backgroundImage,
}));
assert.match(battleStyles.player, /knight_m_idle_anim_f[0-3]/, "战斗角色必须沿用原作红披风骑士帧");
assert.match(battleStyles.enemy, /m_004/, "第一层剑士型敌人必须使用 enemy_sword_0 的原作纹理");
await page.locator(".damage-number").first().waitFor({ state: "attached", timeout: 5000 });
await page.locator("button[data-debug=merchant]").click();
await page.locator(".shop-modal").waitFor();
assert.equal(await page.locator(".shop-section-title").allTextContents().then((values) => values.join("/")), "一般商品/特别商品");
assert.equal(await page.locator(".shop-offer").count(), 6, "原作商店应显示 3+3 商品");
const shopBackground = await page.locator(".shop-modal").evaluate((node) => getComputedStyle(node).backgroundImage);
assert.match(shopBackground, /ingame_shop_bg/, "商店必须使用原作棚架面板");
assert.deepEqual(errors, [], `浏览器错误:\n${errors.join("\n")}`);
assert.deepEqual(failed, [], `资源加载失败:\n${failed.join("\n")}`);
console.log("PASS original restoration: layout → source assets → dice hold → combat sprites → shop structure");
} finally {
await browser.close();
}