feat(cheap-worker): 非遗品类件③few-shot正例+rubric金标锚定回填+骨架import护栏 (W-GENRE)
③ game-runtime/games/_fewshot-feiyi/:few-shot 过门正例(木版年画作坊,源=n=5 小批第 3 款 p11d-print):attempts=1/¥2.65/九门 A-I 全绿/丰富度 9-11;范例签名机器校验门 (tools.mjs check)蒸馏时复跑 PASS;README 带来历/复验配方/漂移纪律(设计档 §3⑥ 「必须同改」清单成员)。 ④ 锚定回填 heritage.rubric.json:通用 11+品类 5 合评、两分母分开(M3 judge,容差 ±1/组)——正例 通用 10/11(L2 6/6·L3 2/3·L4 2/2)+品类 5/5;薄反例(骨架原样) 通用 6/11+品类 3/5(节律张力/技艺成长 false=骨架无 combo/解锁,判罚与轻起点定位 一致)——评分尺判别力坐实。 ② 骨架 game-logic.js 头注加 import 完整性护栏:n=5 首款 p11d-porc 失败根因 = agent 改写时 import 列表漏 FIRST_READY_DELAY_MS、裸标识符 esbuild 当全局放行、 运行时点开工即 ReferenceError 被 L1 隔离→永卡菜单、E/G/H 三挂(check/build 双门 拦不住的盲区;node headless 探针定位)。护栏注释进克隆起点后 3 连过佐证。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c18e7f5324
commit
39cc2c09ba
@ -45,8 +45,17 @@
|
|||||||
],
|
],
|
||||||
"groupMaxes": { "L2": 3, "L3": 1, "L4": 1 },
|
"groupMaxes": { "L2": 3, "L3": 1, "L4": 1 },
|
||||||
"anchors": {
|
"anchors": {
|
||||||
"_note": "金标锚定(SoT §4 规范三:≥1 正例 + ≥1 薄反例;rubric 文本/评分 prompt 变更须金标复验,单款分组小计漂移 >±1 回退)。待首次金标复验回填。",
|
"_note": "金标锚定(SoT §4 规范三:≥1 正例 + ≥1 薄反例;rubric 文本/评分 prompt 变更须金标复验,单款分组小计漂移 >±1 回退)。口径 = 通用底座 11 条 + 本品类扩展 5 条一次合评、两分母分开(MiniMax-M3 judge,单次锚定、容差 ±1/组);2026-07-02 首评回填。",
|
||||||
"positive": null,
|
"positive": {
|
||||||
"thinNegative": null
|
"sample": "games/_fewshot-feiyi(源=生成局 p11d-print,木版年画作坊)",
|
||||||
|
"base": { "score": 10, "max": 11, "groups": { "L2": { "score": 6, "max": 6 }, "L3": { "score": 2, "max": 3 }, "L4": { "score": 2, "max": 2 } } },
|
||||||
|
"genre": { "score": 5, "max": 5, "groups": { "L2": { "score": 3, "max": 3 }, "L3": { "score": 1, "max": 1 }, "L4": { "score": 1, "max": 1 } } }
|
||||||
|
},
|
||||||
|
"thinNegative": {
|
||||||
|
"sample": "games/_template-feiyi 骨架原样实例(轻起点,天然薄)",
|
||||||
|
"base": { "score": 6, "max": 11, "groups": { "L2": { "score": 4, "max": 6 }, "L3": { "score": 0, "max": 3 }, "L4": { "score": 2, "max": 2 } } },
|
||||||
|
"genre": { "score": 3, "max": 5, "groups": { "L2": { "score": 2, "max": 3 }, "L3": { "score": 0, "max": 1 }, "L4": { "score": 1, "max": 1 } },
|
||||||
|
"perItem": { "工序链成立": true, "节律张力": false, "技艺成长可见": false, "作品可展示": true, "文化氛围一致": true } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
game-runtime/games/_fewshot-feiyi/README.md
Normal file
33
game-runtime/games/_fewshot-feiyi/README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# _fewshot-feiyi — 非遗品类 few-shot 过门正例(木版年画作坊)
|
||||||
|
|
||||||
|
这是非遗品类的 **few-shot 完整正例**(W-GENRE 四件套之③):一款由生成 agent 在 `_template-feiyi` 工序节拍骨架上做出来、**真过九门**的完整游戏,示范"轻骨架之上把 brief 做丰富"长什么样。生成 agent 需要完整范例时 read_file 这里的 `src/game-logic.js`(prompt 先读手册第 6 步与 `.agents/skills/heritage-game-design.md` §8 都指向此处)。
|
||||||
|
|
||||||
|
## 来历(可复核)
|
||||||
|
|
||||||
|
- **brief**:「木版年画作坊:刻版、上墨、铺纸、拓印,赶在年前印出最多年画;印得多解锁新的吉祥图样」。
|
||||||
|
- **生成局**:`p11d-print`(便宜档进程内路,MiniMax-M3,attempts=1 一次成型,¥2.65,335s;W-GENRE 非遗品类件 n=5 小批第 3 款)。
|
||||||
|
- **九门**:A–I 全绿(verdict = `games/_wg1-gen/p11d-print/evidence/verdict.json`,生成机器留档;批产记录见品类件工单回执)。
|
||||||
|
- **丰富度**(纯 LLM 验证 agent,非阻塞):9/11——L2 5/6 / L3 2/3 / L4 2/2;未命中仅「情感锚 / 放置回归」。
|
||||||
|
- **范例签名机器校验门**:`tools.mjs check` PASS(API 静态门 / 形状门 / 红线全绿;蒸馏时复跑过)。
|
||||||
|
|
||||||
|
## 它示范了什么(读 game-logic.js 时看这些)
|
||||||
|
|
||||||
|
- **换手艺不重写循环**:工序链(刻版→上墨→铺纸→拓印)与作品表(福字/门神/财神/麒麟/童子抱鱼)全落在 core.js 参数空间;工序节拍结构(就绪→窗口内点击→出成品→回炉)原样保留。
|
||||||
|
- **在循环上加机制**:正窗暴击 + 连击、攒工钱解锁新图样(本品类成长轴 = 工艺步骤/图样解锁)、结算屏摆出本局印成的年画(炫耀时刻)。
|
||||||
|
- **取证契约不破**:`_forensicsView().state()` 的 `targets`(occupied=当前就绪工位)与 `score`(单调上升)语义未动——这是过九门的命脉。
|
||||||
|
|
||||||
|
## 复验配方(怀疑漂移时跑)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在 game-runtime/ 下:骨架 clone 一个临时局,覆盖本正例 L3,过机器门 + 九门
|
||||||
|
node tools/amodel-gen/tools.mjs scaffold-saa p11d-fewshot-recheck _template-feiyi
|
||||||
|
cp games/_fewshot-feiyi/src/*.js games/amgen-p11d-fewshot-recheck/src/
|
||||||
|
node -e "import('./tools/amodel-gen/tools.mjs').then(m=>process.exit(m.check('p11d-fewshot-recheck').ok?0:1))"
|
||||||
|
node tools/amodel-gen/tools.mjs build-saa p11d-fewshot-recheck && node tools/amodel-gen/tools.mjs stage p11d-fewshot-recheck
|
||||||
|
cp ../cheap-worker/fixtures/golden-specs/heritage-craft.play-spec.json games/_wg1-gen/p11d-fewshot-recheck/play-spec.json
|
||||||
|
bash games/_wg1-gen/_shared/serve-and-play.sh p11d-fewshot-recheck
|
||||||
|
```
|
||||||
|
|
||||||
|
## 漂移纪律(设计档 §3⑥/§6:per-genre 正例是「必须同改」清单成员)
|
||||||
|
|
||||||
|
L2 插件 API 改签名时,本正例随 `_template-feiyi`、prompt、skill **同批同改**并复跑上面的复验配方——正例签名与现契约不一致 = 下一代脏数据源,宁可删除不可留漂。
|
||||||
73
game-runtime/games/_fewshot-feiyi/src/assets.js
Normal file
73
game-runtime/games/_fewshot-feiyi/src/assets.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* assets.js — 资产装载器(宿主层:允许浏览器原生 Image/fetch;逻辑层 core.js 不得 import 本文件)
|
||||||
|
* owner:_template 克隆起点 | 消费方:game.js(init 时 await loadAssets)/ render.js(若用图则取)
|
||||||
|
*
|
||||||
|
* 【本款现状:无外部资产(纯程序化图形)】
|
||||||
|
* 「点圆得分」全用 g 原生绘制(圆/矩形/文字),不需任何 .jpg/.mp3,故 IMAGE_FILES 为空。
|
||||||
|
* 但**声明模式的结构必须留全**——这是克隆起点的意义:加美术时只需往 IMAGE_FILES 添键,
|
||||||
|
* render 里用 assets.images.<键> 取图,装载/容错链条已就位,不必重搭。
|
||||||
|
*
|
||||||
|
* 【容错铁律(发行版红线 §7-5)】
|
||||||
|
* 任一资产缺失/加载失败 → 该键置 null 并记入 missing,游戏照常启动(render 走程序化回退),
|
||||||
|
* 绝不因资产把游戏带崩。本款无资产 → loadAssets 容错返回空集(images:{}, missing:[], loaded:0, total:0)。
|
||||||
|
* node 环境无 Image → 同样安全返回空集(import-safe)。
|
||||||
|
* 音频文件不在此装载(需用户手势后解码,归 audio-director 懒加载;本款无音频)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图像资产键 → 文件名(与 assets/manifest.json 对应)。
|
||||||
|
* 【在此声明图片资产键→文件名】本款无美术故为空对象 {}。
|
||||||
|
* 加图示例:`itemSoda: 'item-soda.jpg'`,然后 render 里用 `assets.images.itemSoda`(键名间接引用,不写死文件名)。
|
||||||
|
* @type {Record<string, string>}
|
||||||
|
*/
|
||||||
|
export const IMAGE_FILES = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 装载全部图像资产(并行;单个失败不连坐)。本款 IMAGE_FILES 为空 → 返回空集。
|
||||||
|
* @param {string} [baseUrl] 资产根(缺省 './assets/')
|
||||||
|
* @returns {Promise<{ images: Record<string, HTMLImageElement|null>, missing: string[], loaded: number, total: number }>}
|
||||||
|
*/
|
||||||
|
export function loadAssets(baseUrl) {
|
||||||
|
const root = baseUrl == null ? './assets/' : baseUrl;
|
||||||
|
const keys = Object.keys(IMAGE_FILES);
|
||||||
|
/** @type {Record<string, HTMLImageElement|null>} */
|
||||||
|
const images = {};
|
||||||
|
/** @type {string[]} */
|
||||||
|
const missing = [];
|
||||||
|
|
||||||
|
/** 装载单图(失败 resolve null,不 reject——容错降级)。@param {string} key */
|
||||||
|
function loadOne(key) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 浏览器环境守卫:node 侧 import 本模块不应触发 Image(import-safe)。
|
||||||
|
if (typeof Image === 'undefined') {
|
||||||
|
images[key] = null;
|
||||||
|
missing.push(key);
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
images[key] = img;
|
||||||
|
resolve(img);
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
// 资产缺失:记录并降级(渲染层走程序化回退),错误路径留痕。
|
||||||
|
console.warn('[_template-assets] 资产加载失败(已降级程序化回退):' + IMAGE_FILES[key]);
|
||||||
|
images[key] = null;
|
||||||
|
missing.push(key);
|
||||||
|
resolve(null);
|
||||||
|
};
|
||||||
|
img.src = root + IMAGE_FILES[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空集时 Promise.all([]) 立即 resolve → 返回空容错结构(本款常态)。
|
||||||
|
return Promise.all(keys.map(loadOne)).then(() => ({
|
||||||
|
images,
|
||||||
|
missing,
|
||||||
|
loaded: keys.length - missing.length,
|
||||||
|
total: keys.length,
|
||||||
|
}));
|
||||||
|
}
|
||||||
179
game-runtime/games/_fewshot-feiyi/src/core.js
Normal file
179
game-runtime/games/_fewshot-feiyi/src/core.js
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* core.js — 【L3 纯逻辑】木版年画作坊 · 工序节拍玩法数值与判定
|
||||||
|
*
|
||||||
|
* 本款 = 非遗「工序节拍」品类(template-feiyi)+ 传统技艺「木版年画」。
|
||||||
|
* 一件年画 = 一条 4 道工序链:刻版 → 上墨 → 铺纸 → 拓印;在火候窗口内点工位
|
||||||
|
* 完成该道工序;过窗回炉;整条链走完 = 印出一张年画;60s 限时赶工结算。
|
||||||
|
* 印满阈值解锁新图样(财神/麒麟/童子)——「印得多解锁新的吉祥图样」的核心钩子。
|
||||||
|
*
|
||||||
|
* 工序名取自木版年画通识工序词;**不编造具体非遗项目的失实细节**。
|
||||||
|
*
|
||||||
|
* 作品字段(公开契约,render / game-logic / 取证共用):
|
||||||
|
* work = { id, name, bonus, stepIndex, stepState:'crafting'|'ready', stateSinceMs,
|
||||||
|
* craftMs, windowMs, glyph: '福'|'门神'|... }
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
* 【多样性参数空间 · 木版年画作坊】
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/** 一局开工时长(ms):年前赶工 60s。 */
|
||||||
|
export const ROUND_MS = 60000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工序链(木版年画 4 道):名字 + 颜色 + 完成分。
|
||||||
|
* 颜色用于程序化绘制工位物件(刻刀/墨辊/宣纸/棕刷的视觉锚)。
|
||||||
|
* 顺序:刻版→上墨→铺纸→拓印,越接近成品越值钱(后道高于前道)。
|
||||||
|
*/
|
||||||
|
export const CRAFT_CHAIN = [
|
||||||
|
{ name: '刻版', color: '#5a3a24', points: 5, tool: '刻刀', desc: '雕梨木板' },
|
||||||
|
{ name: '上墨', color: '#c8261a', points: 7, tool: '墨辊', desc: '朱砂上墨' },
|
||||||
|
{ name: '铺纸', color: '#f3e8c2', points: 9, tool: '宣纸', desc: '覆纸上版' },
|
||||||
|
{ name: '拓印', color: '#1f4e7a', points: 12, tool: '棕刷', desc: '按压转印' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作品表(吉祥图样):印出一张年画;bonus = 成品奖励分(≈单道工序分的 3–4 倍)。
|
||||||
|
* 收集向 = 凑齐一套解锁「图鉴」成就感。
|
||||||
|
*
|
||||||
|
* `unlockAtWage` 字段:达成该累计工钱后该图样解锁(传 -1 = 默认解锁)。
|
||||||
|
* 默认解锁 3 件,工钱达 80 解锁第 4 件「麒麟」,达 160 解锁第 5 件「童子」。
|
||||||
|
* ——「印得多解锁新的吉祥图样」的核心钩子落在这里。
|
||||||
|
*/
|
||||||
|
export const WORKS = [
|
||||||
|
{ name: '福字', glyph: '福', bgColor: '#c8261a', bonus: 26, unlockAtWage: 0 },
|
||||||
|
{ name: '门神', glyph: '门', bgColor: '#1f4e7a', bonus: 32, unlockAtWage: 0 },
|
||||||
|
{ name: '财神', glyph: '财', bgColor: '#caa84a', bonus: 38, unlockAtWage: 0 },
|
||||||
|
{ name: '麒麟', glyph: '麒', bgColor: '#5a3a24', bonus: 44, unlockAtWage: 80 },
|
||||||
|
{ name: '童子抱鱼', glyph: '童', bgColor: '#7a9b3e', bonus: 50, unlockAtWage: 160 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 解锁的初始默认工资阈值(在本局内)——所有工资 < 阈值的图样在图鉴里置灰挂锁。 */
|
||||||
|
export const UNLOCK_PROGRESS_TOTAL = 200; // 「印得多解锁新的吉祥图样」封顶参考
|
||||||
|
|
||||||
|
/** 工序就绪间隔区间(ms):上一道完成后,下一道多久「就绪」。 */
|
||||||
|
export const CRAFT_MIN_MS = 650;
|
||||||
|
export const CRAFT_MAX_MS = 1050;
|
||||||
|
/** 开局首道工序就绪延迟(ms):留一拍让玩家看清案台。 */
|
||||||
|
export const FIRST_READY_DELAY_MS = 700;
|
||||||
|
|
||||||
|
/** 火候窗口区间(ms):工序就绪后须在窗口内点掉,过窗回炉(不判死)。 */
|
||||||
|
export const WINDOW_MIN_MS = 5000;
|
||||||
|
export const WINDOW_MAX_MS = 8000;
|
||||||
|
|
||||||
|
/** 正窗暴击(基础分之上加成):窗口前 40% 出手 → 加成倍率(双层奖励之技巧分)。 */
|
||||||
|
export const CRIT_RATIO = 0.4;
|
||||||
|
export const CRIT_MULT = 1.6;
|
||||||
|
|
||||||
|
/** 回炉冷却(ms):过窗后该道重新加工的耗时(损时间不清进度)。 */
|
||||||
|
export const REDO_MS = 1100;
|
||||||
|
|
||||||
|
/** 工位点按半径(手指友好,别小于 ~44)。 */
|
||||||
|
export const STATION_TAP_RADIUS = 58;
|
||||||
|
|
||||||
|
/** 工位布局比例(读 viewport 算具体像素,别硬编码 390×844):案台一横排。 */
|
||||||
|
export const STATION_ROW_Y_RATIO = [0.62];
|
||||||
|
export const STATION_MARGIN_X_RATIO = 0.14;
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
* 【工序循环判定 · 结构层 · 一般不动】
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/** 受控随机区间 [lo,hi):入参 rng 走 ctx.random,纯确定性。 */
|
||||||
|
function rngRange(rng, lo, hi) { return lo + rng() * (hi - lo); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 算工位中心点(每道工序一个工位;读 viewport,不硬编码)。
|
||||||
|
* @returns {{x:number,y:number,station:number}[]} 与 CRAFT_CHAIN 等长
|
||||||
|
*/
|
||||||
|
export function computeStations(viewW, viewH, count) {
|
||||||
|
const n = count == null ? CRAFT_CHAIN.length : count;
|
||||||
|
const rows = STATION_ROW_Y_RATIO.length;
|
||||||
|
const perRow = Math.ceil(n / rows);
|
||||||
|
const left = viewW * STATION_MARGIN_X_RATIO;
|
||||||
|
const right = viewW * (1 - STATION_MARGIN_X_RATIO);
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const row = Math.floor(i / perRow);
|
||||||
|
const col = i % perRow;
|
||||||
|
const inRow = Math.min(perRow, n - row * perRow);
|
||||||
|
const x = inRow === 1 ? (left + right) / 2 : left + (col * (right - left)) / (inRow - 1);
|
||||||
|
out.push({ station: i, x: Math.round(x), y: Math.round(viewH * STATION_ROW_Y_RATIO[Math.min(row, rows - 1)]) });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取当前工钱下可印的作品表(过滤未解锁的)。 */
|
||||||
|
export function availableWorks(wage, works) {
|
||||||
|
const list = works || WORKS;
|
||||||
|
return list.filter((w) => wage >= (w.unlockAtWage != null ? w.unlockAtWage : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开一件新作品(从已解锁图样中轮换)。 */
|
||||||
|
export function makeWork(rng, nowMs, idSeq, firstDelayMs, wage, works) {
|
||||||
|
const pool = availableWorks(wage, works);
|
||||||
|
const list = pool.length > 0 ? pool : (works || WORKS);
|
||||||
|
const w = list[idSeq % list.length];
|
||||||
|
return {
|
||||||
|
id: idSeq,
|
||||||
|
name: w.name,
|
||||||
|
glyph: w.glyph,
|
||||||
|
bgColor: w.bgColor,
|
||||||
|
bonus: w.bonus,
|
||||||
|
stepIndex: 0,
|
||||||
|
stepState: 'crafting',
|
||||||
|
stateSinceMs: nowMs,
|
||||||
|
craftMs: firstDelayMs != null ? firstDelayMs : Math.round(rngRange(rng, CRAFT_MIN_MS, CRAFT_MAX_MS)),
|
||||||
|
windowMs: Math.round(rngRange(rng, WINDOW_MIN_MS, WINDOW_MAX_MS)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加工态是否已到就绪时刻(crafting 满 craftMs → 该转 ready)。 */
|
||||||
|
export function shouldBecomeReady(work, nowMs) {
|
||||||
|
return !!work && work.stepState === 'crafting' && (nowMs - work.stateSinceMs) >= work.craftMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 就绪态是否已过火候窗口(ready 超 windowMs → 该回炉)。 */
|
||||||
|
export function isWindowExpired(work, nowMs) {
|
||||||
|
return !!work && work.stepState === 'ready' && (nowMs - work.stateSinceMs) > work.windowMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 火候窗口剩余比例 [0,1](1=刚就绪,0=已过窗;非 ready 返 0)。 */
|
||||||
|
export function windowRatio(work, nowMs) {
|
||||||
|
if (!work || work.stepState !== 'ready' || !work.windowMs) return 0;
|
||||||
|
const waited = nowMs - work.stateSinceMs;
|
||||||
|
return Math.max(0, Math.min(1, (work.windowMs - waited) / work.windowMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前工序此刻是否可点(= ready 且窗口未过)。 */
|
||||||
|
export function isStepTappable(work, nowMs) {
|
||||||
|
return !!work && work.stepState === 'ready' && (nowMs - work.stateSinceMs) <= work.windowMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点 (px,py) 是否命中工位(圆心 + 半径)。 */
|
||||||
|
export function hitStation(px, py, st) {
|
||||||
|
if (!st) return false;
|
||||||
|
const dx = px - st.x, dy = py - st.y;
|
||||||
|
return dx * dx + dy * dy <= STATION_TAP_RADIUS * STATION_TAP_RADIUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 本道工序完成分(读 CRAFT_CHAIN;越界容错返 0)。 */
|
||||||
|
export function stepPoints(stepIndex) {
|
||||||
|
const s = CRAFT_CHAIN[stepIndex];
|
||||||
|
return s ? s.points : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否为正窗暴击(在窗口前 CRIT_RATIO 比例内出手)。 */
|
||||||
|
export function isCritWindow(work, nowMs) {
|
||||||
|
if (!work || work.stepState !== 'ready' || !work.windowMs) return false;
|
||||||
|
const waited = nowMs - work.stateSinceMs;
|
||||||
|
return waited <= work.windowMs * CRIT_RATIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解锁进度(0..1)——本局累计工资 / UNLOCK_PROGRESS_TOTAL,供图鉴用。 */
|
||||||
|
export function unlockProgress(wage) {
|
||||||
|
if (UNLOCK_PROGRESS_TOTAL <= 0) return 1;
|
||||||
|
return Math.max(0, Math.min(1, wage / UNLOCK_PROGRESS_TOTAL));
|
||||||
|
}
|
||||||
331
game-runtime/games/_fewshot-feiyi/src/game-logic.js
Normal file
331
game-runtime/games/_fewshot-feiyi/src/game-logic.js
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
/**
|
||||||
|
* game-logic.js — 【L3 游戏本体】木版年画作坊 · 工序节拍 + 暴击 combo + 吉祥图样解锁
|
||||||
|
*
|
||||||
|
* 玩法核心(60s 赶工):
|
||||||
|
* · 一件年画 = 4 道工序链:刻版 → 上墨 → 铺纸 → 拓印
|
||||||
|
* · 工序依次就绪(craftMs 加工后转 ready),火候窗口(windowMs)内点掉=完成
|
||||||
|
* · 过窗回炉(REDO_MS 冷却后该道重做,损时间不清进度)
|
||||||
|
* · 整条链走完 = 印出一张年画(bonus 加分 + 庆祝)
|
||||||
|
* · **正窗暴击**:窗口前 40% 出手 → 工钱 ×CRIT_MULT,连续正窗叠 combo
|
||||||
|
* · **吉祥图样解锁**:累计工钱达标(80/160)→ 麒麟/童子抱鱼入图鉴可印
|
||||||
|
* · 60s 到点切 over 结算,再开一印
|
||||||
|
*
|
||||||
|
* 已在 _forensicsView.state 实时暴露工资 + combo + 暴击 + 解锁图样列表,
|
||||||
|
* 满足九门 H 进展门(score 单调上升)与 E_live(targets 含就绪工位)。
|
||||||
|
*
|
||||||
|
* 入参(L1 已摊平):createGame({ plugins, bundle, viewport })
|
||||||
|
* 受控面 ctx 在 init(boot) 的 boot.ctx。
|
||||||
|
* 返回:init/update/render/destroy + handleTap + _forensicsView。
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ROUND_MS, CRAFT_CHAIN, FIRST_READY_DELAY_MS, REDO_MS,
|
||||||
|
WORKS, UNLOCK_PROGRESS_TOTAL, CRIT_MULT,
|
||||||
|
computeStations, makeWork, shouldBecomeReady, isWindowExpired,
|
||||||
|
isStepTappable, hitStation, stepPoints, isCritWindow,
|
||||||
|
} from './core.js';
|
||||||
|
import { renderMenu, renderPlay, renderOver } from './render.js';
|
||||||
|
import { loadAssets } from './assets.js';
|
||||||
|
|
||||||
|
/* ── 场景名(作坊三幕:开张 → 开工 → 收工)── */
|
||||||
|
const SCENE_MENU = 'menu';
|
||||||
|
const SCENE_PLAY = 'play';
|
||||||
|
const SCENE_OVER = 'over';
|
||||||
|
|
||||||
|
/** 飘字生命与上飘速度——纯表现,随意调。 */
|
||||||
|
const FLOAT_LIFE_S = 0.95;
|
||||||
|
const FLOAT_RISE_PXPS = 72;
|
||||||
|
/** combo 高亮飘字延长生命。 */
|
||||||
|
const COMBO_FLOAT_LIFE_S = 1.3;
|
||||||
|
/** 呼吸周期 220ms(就绪工位的脉冲)——用内部累计毫秒算 0..1,避免依赖裸 performance.now。 */
|
||||||
|
const PULSE_PERIOD_MS = 220;
|
||||||
|
|
||||||
|
export function createGame({ plugins, bundle, viewport }) {
|
||||||
|
// 用 6 件:场景机 / 计分 / UI / 计时 / 音效 / 粒子(节拍 + 出彩)。
|
||||||
|
const { sceneFsm, sessionScore, hudUi, timerScheduler, audioMusic, juice } = plugins;
|
||||||
|
|
||||||
|
const VW = (viewport && viewport.w) || 390;
|
||||||
|
const VH = (viewport && viewport.h) || 844;
|
||||||
|
|
||||||
|
/* ── init(boot) 内填充 ── */
|
||||||
|
let boot = null;
|
||||||
|
let ctx = null;
|
||||||
|
let assets = null;
|
||||||
|
let inited = false;
|
||||||
|
|
||||||
|
/* ── 实例私有运行态 ── */
|
||||||
|
let stations = computeStations(VW, VH); // 工位中心(读 viewport,定一次)
|
||||||
|
let work = null; // 当前作品
|
||||||
|
let floats = []; // 飘字
|
||||||
|
let elapsedMs = 0; // play 内累计毫秒
|
||||||
|
let nextWorkId = 0; // 作品序号
|
||||||
|
let wage = 0; // 当日工钱(随完成工序上升)
|
||||||
|
let worksDone = []; // 本局印出的作品名(结算 + 取证)
|
||||||
|
let redos = 0; // 回炉次数
|
||||||
|
let roundTimer = null; // 60s 倒计时句柄
|
||||||
|
|
||||||
|
/* ── 双层奖励 / 正窗暴击 / combo ── */
|
||||||
|
let combo = 0; // 连续正窗次数
|
||||||
|
let bestCombo = 0; // 本局最高 combo
|
||||||
|
let totalCrits = 0; // 本局暴击总次数
|
||||||
|
let lastCrit = false; // 上一次出手是否暴击(取证可见)
|
||||||
|
|
||||||
|
/* ── 命中区(菜单/结算按钮):render 回调实时算并写回 ── */
|
||||||
|
let menuStartBtn = null;
|
||||||
|
let overRestartBtn = null;
|
||||||
|
|
||||||
|
/* ── 取证度量 ── */
|
||||||
|
const measures = { frames: 0, taps: 0, steps: 0, works: 0, redos: 0, crits: 0 };
|
||||||
|
|
||||||
|
/** 受控随机 [0,1):经 boot.ctx.random。 */
|
||||||
|
function rng() { return ctx ? ctx.random.next() : 0; }
|
||||||
|
|
||||||
|
/** 进 play:重置开工态、起 60s 倒计时。 */
|
||||||
|
function enterPlay() {
|
||||||
|
sessionScore.reset();
|
||||||
|
floats = [];
|
||||||
|
elapsedMs = 0;
|
||||||
|
wage = 0; worksDone = []; redos = 0;
|
||||||
|
combo = 0; bestCombo = 0; totalCrits = 0; lastCrit = false;
|
||||||
|
measures.steps = 0; measures.works = 0; measures.redos = 0; measures.crits = 0;
|
||||||
|
nextWorkId = 0;
|
||||||
|
work = makeWork(rng, 0, nextWorkId++, FIRST_READY_DELAY_MS, wage, WORKS);
|
||||||
|
if (roundTimer != null) { try { timerScheduler.cancel(roundTimer); } catch (_) { /* 幂等 */ } roundTimer = null; }
|
||||||
|
roundTimer = timerScheduler.after(ROUND_MS, () => {
|
||||||
|
sessionScore.lose();
|
||||||
|
sceneFsm.transition(SCENE_OVER);
|
||||||
|
if (ctx) ctx.log('scene', 'over 收工', { score: sessionScore.getScore(), wage, works: worksDone.length, redos, bestCombo });
|
||||||
|
});
|
||||||
|
sceneFsm.transition(SCENE_PLAY);
|
||||||
|
if (ctx) ctx.log('scene', 'play 开工', { roundMs: ROUND_MS, chain: CRAFT_CHAIN.map((s) => s.name).join('→') });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** L3 输入入口:L1 在 pointerdown 时直达调用。 */
|
||||||
|
function handleTap(x, y) {
|
||||||
|
measures.taps += 1;
|
||||||
|
const cur = sceneFsm.current();
|
||||||
|
if (cur === SCENE_MENU) {
|
||||||
|
if (menuStartBtn && hudUi.pointInRect(x, y, menuStartBtn)) enterPlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cur === SCENE_OVER) {
|
||||||
|
if (overRestartBtn && hudUi.pointInRect(x, y, overRestartBtn)) enterPlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cur === SCENE_PLAY) {
|
||||||
|
// 工序核心:点中就绪工位 = 完成该道(1 步成序,自动验收据此涨分)。
|
||||||
|
if (!work || !isStepTappable(work, elapsedMs)) return;
|
||||||
|
const st = stations[work.stepIndex];
|
||||||
|
if (st && hitStation(x, y, st)) completeStep(st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 完成当前工序:基础分 + 暴击判定 + combo + 飘字 + 粒子 + 音效。 */
|
||||||
|
function completeStep(st) {
|
||||||
|
measures.steps += 1;
|
||||||
|
const base = stepPoints(work.stepIndex);
|
||||||
|
const crit = isCritWindow(work, elapsedMs);
|
||||||
|
lastCrit = crit;
|
||||||
|
let amount = base;
|
||||||
|
if (crit) {
|
||||||
|
amount = Math.round(base * CRIT_MULT);
|
||||||
|
totalCrits += 1; measures.crits += 1;
|
||||||
|
combo += 1; if (combo > bestCombo) bestCombo = combo;
|
||||||
|
} else {
|
||||||
|
combo = 0;
|
||||||
|
}
|
||||||
|
// combo 额外加成(2 连起每连 +10%,叠到原数上,封顶 1.5x)
|
||||||
|
if (combo >= 2) {
|
||||||
|
const bonus = Math.min(0.5, (combo - 1) * 0.1);
|
||||||
|
amount = Math.round(amount * (1 + bonus));
|
||||||
|
}
|
||||||
|
wage += amount;
|
||||||
|
sessionScore.add(amount); // 计分即工钱(score 单调上升 → H 进展门)
|
||||||
|
// 飘字
|
||||||
|
const life = crit ? COMBO_FLOAT_LIFE_S : FLOAT_LIFE_S;
|
||||||
|
if (crit) {
|
||||||
|
floats.push({ x: st.x, y: st.y - 70, age: 0, life, amount, crit: true });
|
||||||
|
if (combo >= 2) {
|
||||||
|
floats.push({ x: st.x, y: st.y - 110, age: 0, life: COMBO_FLOAT_LIFE_S, amount: combo, combo: true });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
floats.push({ x: st.x, y: st.y - 70, age: 0, life, amount });
|
||||||
|
}
|
||||||
|
if (juice) { try { juice.burst(st.x, st.y - 30, crit ? { count: 18 } : undefined); } catch (_) { /* 容错 */ } }
|
||||||
|
if (audioMusic) { try { audioMusic.playSfx(crit ? 'chime' : 'click'); } catch (_) { /* 容错 */ } }
|
||||||
|
if (ctx) ctx.log('step', '工序完成', { step: CRAFT_CHAIN[work.stepIndex].name, base, crit, combo, amount, total: wage });
|
||||||
|
|
||||||
|
if (work.stepIndex + 1 >= CRAFT_CHAIN.length) {
|
||||||
|
finishWork();
|
||||||
|
} else {
|
||||||
|
work.stepIndex += 1;
|
||||||
|
work.stepState = 'crafting';
|
||||||
|
work.stateSinceMs = elapsedMs;
|
||||||
|
const fresh = makeWork(rng, elapsedMs, work.id, undefined, wage, WORKS);
|
||||||
|
work.craftMs = fresh.craftMs;
|
||||||
|
work.windowMs = fresh.windowMs;
|
||||||
|
work.name = fresh.name; work.glyph = fresh.glyph; work.bgColor = fresh.bgColor; work.bonus = fresh.bonus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 出成品:成品奖励 + 庆祝 + 记入当日作品,开下一件。 */
|
||||||
|
function finishWork() {
|
||||||
|
measures.works += 1;
|
||||||
|
wage += work.bonus;
|
||||||
|
sessionScore.add(work.bonus);
|
||||||
|
worksDone.push(work.name);
|
||||||
|
floats.push({ x: VW / 2, y: VH * 0.36, age: 0, life: FLOAT_LIFE_S * 1.5, amount: work.bonus, big: work.name });
|
||||||
|
if (juice) { try { juice.burst(VW / 2, VH * 0.4, { count: 26 }); } catch (_) { /* 容错 */ } }
|
||||||
|
if (audioMusic) { try { audioMusic.playSfx('win'); } catch (_) { /* 容错 */ } }
|
||||||
|
if (ctx) ctx.log('work', '印出年画', { name: work.name, glyph: work.glyph, bonus: work.bonus, works: worksDone.length });
|
||||||
|
work = makeWork(rng, elapsedMs, nextWorkId++, undefined, wage, WORKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 过窗回炉:该道重做 + 轻屏震 + 闷响 + combo 断。 */
|
||||||
|
function redoStep() {
|
||||||
|
redos += 1; measures.redos += 1;
|
||||||
|
work.stepState = 'crafting';
|
||||||
|
work.stateSinceMs = elapsedMs;
|
||||||
|
work.craftMs = REDO_MS;
|
||||||
|
combo = 0; // 回炉断 combo
|
||||||
|
if (juice) { try { juice.shakeScreen(0.18, 4); } catch (_) { /* 容错 */ } }
|
||||||
|
if (audioMusic) { try { audioMusic.playSfx('thud'); } catch (_) { /* 容错 */ } }
|
||||||
|
if (ctx) ctx.log('redo', '火候过了回炉', { step: CRAFT_CHAIN[work.stepIndex].name, redos });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工序状态推进:加工满时长 → 就绪;就绪过窗 → 回炉。 */
|
||||||
|
function updateWork() {
|
||||||
|
if (!work) return;
|
||||||
|
if (shouldBecomeReady(work, elapsedMs)) {
|
||||||
|
work.stepState = 'ready';
|
||||||
|
work.stateSinceMs = elapsedMs;
|
||||||
|
if (audioMusic) { try { audioMusic.playSfx('blip'); } catch (_) { /* 容错 */ } }
|
||||||
|
} else if (isWindowExpired(work, elapsedMs)) {
|
||||||
|
redoStep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 飘字上飘 + 淡出。 */
|
||||||
|
function updateFloats(dt) {
|
||||||
|
for (let i = floats.length - 1; i >= 0; i--) {
|
||||||
|
const f = floats[i];
|
||||||
|
f.age += dt; f.y -= FLOAT_RISE_PXPS * dt;
|
||||||
|
if (f.age >= f.life) floats.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 呼吸相位 0..1(基于 elapsedMs 算 sin 输入,纯确定性)——给 renderPlay 让就绪工位脉冲。 */
|
||||||
|
function pulse01() {
|
||||||
|
return 0.5 + 0.5 * Math.sin((elapsedMs / PULSE_PERIOD_MS) * Math.PI * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 三场景注册(每场景各调一次 define)。 */
|
||||||
|
function defineScenes() {
|
||||||
|
sceneFsm.define(SCENE_MENU, {
|
||||||
|
onEnter() { floats = []; menuStartBtn = null; },
|
||||||
|
render(g) { menuStartBtn = renderMenu(g, hudUi, { vw: VW, vh: VH }).startBtn; },
|
||||||
|
});
|
||||||
|
sceneFsm.define(SCENE_PLAY, {
|
||||||
|
render(g) {
|
||||||
|
renderPlay(g, hudUi, {
|
||||||
|
vw: VW, vh: VH, stations, work, floats,
|
||||||
|
wage, works: worksDone.length, nowMs: elapsedMs,
|
||||||
|
remainMs: Math.max(0, ROUND_MS - elapsedMs),
|
||||||
|
pulse01: pulse01(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
sceneFsm.define(SCENE_OVER, {
|
||||||
|
onEnter() { if (roundTimer != null) { try { timerScheduler.cancel(roundTimer); } catch (_) { /* 幂等 */ } roundTimer = null; } },
|
||||||
|
render(g) {
|
||||||
|
overRestartBtn = renderOver(g, hudUi, {
|
||||||
|
vw: VW, vh: VH, wage, worksDone, redos, best: sessionScore.getBest(),
|
||||||
|
}).restartBtn;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ════════════ GameInstance 五法 ════════════ */
|
||||||
|
const instance = {
|
||||||
|
async init(b) {
|
||||||
|
boot = b;
|
||||||
|
ctx = b.ctx;
|
||||||
|
assets = await loadAssets('./assets/');
|
||||||
|
stations = computeStations(VW, VH);
|
||||||
|
defineScenes();
|
||||||
|
sceneFsm.start(SCENE_MENU);
|
||||||
|
inited = true;
|
||||||
|
ctx.log('init', '年画作坊就绪 menu', { vw: VW, vh: VH, steps: CRAFT_CHAIN.length });
|
||||||
|
},
|
||||||
|
|
||||||
|
update(dt) {
|
||||||
|
if (!inited) return;
|
||||||
|
measures.frames += 1;
|
||||||
|
if (bundle && typeof bundle.tick === 'function') bundle.tick(dt);
|
||||||
|
sceneFsm.update(dt);
|
||||||
|
if (sceneFsm.current() === SCENE_PLAY) {
|
||||||
|
elapsedMs += dt * 1000;
|
||||||
|
updateWork();
|
||||||
|
updateFloats(dt);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render(g) {
|
||||||
|
if (!inited) return;
|
||||||
|
sceneFsm.render(g);
|
||||||
|
if (juice && typeof juice.render === 'function') {
|
||||||
|
try { juice.render(g); } catch (_) { /* 容错 */ }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTap,
|
||||||
|
destroy() {
|
||||||
|
if (roundTimer != null) { try { timerScheduler.cancel(roundTimer); } catch (_) { /* 幂等 */ } roundTimer = null; }
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取证契约:实时算 phase/score/targets/combo/解锁等。
|
||||||
|
* ★ 必须在函数体内实时算,不能闭包冻结(否则 host 读 boot 快照=永 menu)。
|
||||||
|
* targets = 每工位一项;当前工序在窗口内可点 → occupied:true,
|
||||||
|
* 其余 false(自动验收 driver 据此驱动游戏)。
|
||||||
|
*/
|
||||||
|
_forensicsView() {
|
||||||
|
return {
|
||||||
|
state() {
|
||||||
|
const cur = sceneFsm ? sceneFsm.current() : null;
|
||||||
|
// 实时算 targets(每工位)
|
||||||
|
const targets = [];
|
||||||
|
for (let i = 0; i < stations.length; i++) {
|
||||||
|
const st = stations[i];
|
||||||
|
let occupied = false;
|
||||||
|
if (cur === SCENE_PLAY && work && i === work.stepIndex && isStepTappable(work, elapsedMs)) {
|
||||||
|
occupied = true;
|
||||||
|
}
|
||||||
|
targets.push({ x: st.x, y: st.y, occupied });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
phase: cur,
|
||||||
|
score: sessionScore ? sessionScore.getScore() : wage,
|
||||||
|
targets,
|
||||||
|
wage,
|
||||||
|
works: worksDone.length,
|
||||||
|
redos,
|
||||||
|
combo,
|
||||||
|
bestCombo,
|
||||||
|
crits: totalCrits,
|
||||||
|
lastCrit,
|
||||||
|
nextStep: work ? (CRAFT_CHAIN[work.stepIndex] && CRAFT_CHAIN[work.stepIndex].name) : null,
|
||||||
|
unlocked: (WORKS || []).filter((w) => wage >= (w.unlockAtWage || 0)).map((w) => w.name),
|
||||||
|
remainMs: Math.max(0, ROUND_MS - elapsedMs),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
measures() {
|
||||||
|
return Object.assign({}, measures, { score: sessionScore ? sessionScore.getScore() : wage, wage, works: worksDone.length, redos, combo, bestCombo, crits: totalCrits });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
379
game-runtime/games/_fewshot-feiyi/src/render.js
Normal file
379
game-runtime/games/_fewshot-feiyi/src/render.js
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
/**
|
||||||
|
* render.js — 【L3 表现层】木版年画作坊 · 绘制
|
||||||
|
* 一律画在入参 g(= boot.mainContext)。本款全程序化绘制(无外部美术)。
|
||||||
|
*
|
||||||
|
* 美术风格:「纸艺高对比」—— 宣纸白底 + 朱红/靛蓝/藤黄(年画调性);
|
||||||
|
* 案台面 = 一张长宣纸;工位 = 各色墨印/工具;成品出炉展示朱红方框大字。
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { CRAFT_CHAIN, WORKS, UNLOCK_PROGRESS_TOTAL } from './core.js';
|
||||||
|
|
||||||
|
/* ── 配色(年画调性:朱砂红 + 靛蓝 + 藤黄 + 宣纸米白)── */
|
||||||
|
const C = {
|
||||||
|
// 背景
|
||||||
|
bgWall: '#2b1810', // 深木墙
|
||||||
|
bgWallAccent: '#3d261a',
|
||||||
|
lattice: '#5c3a26',
|
||||||
|
// 案台(宣纸)
|
||||||
|
paper: '#f3e8c2', // 宣纸米白
|
||||||
|
paperEdge: '#d8c896',
|
||||||
|
paperShadow: 'rgba(0,0,0,0.35)',
|
||||||
|
// 文本
|
||||||
|
text: '#241208', // 在宣纸上的字
|
||||||
|
textLight: '#fbeec2', // 在深底上的字
|
||||||
|
textDim: '#8c6a4a',
|
||||||
|
// 状态
|
||||||
|
ready: '#1f7a3a', // 就绪:墨绿
|
||||||
|
readyGlow: '#7ed957',
|
||||||
|
warn: '#caa84a',
|
||||||
|
danger: '#c8261a',
|
||||||
|
done: '#7a9b3e', // 完成:藤黄绿
|
||||||
|
pending: 'rgba(60,40,20,0.45)',
|
||||||
|
// 朱砂红(年画主色)
|
||||||
|
vermilion: '#c8261a',
|
||||||
|
vermilionDark: '#7a1810',
|
||||||
|
// 靛蓝
|
||||||
|
indigo: '#1f4e7a',
|
||||||
|
// 装饰
|
||||||
|
accentGold: '#caa84a',
|
||||||
|
panel: 'rgba(0,0,0,0.55)',
|
||||||
|
panelLight: 'rgba(255,236,194,0.92)',
|
||||||
|
stroke: 'rgba(255,236,194,0.35)',
|
||||||
|
strokeDark: 'rgba(36,18,8,0.55)',
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/** 铺底:深木墙 + 木格窗 + 牌匾(年画坊名)。 */
|
||||||
|
function drawBackdrop(g, VW, VH) {
|
||||||
|
// 深木墙
|
||||||
|
g.fillStyle = C.bgWall; g.fillRect(0, 0, VW, VH * 0.50);
|
||||||
|
g.fillStyle = C.bgWallAccent; g.fillRect(0, VH * 0.50, VW, VH * 0.50);
|
||||||
|
// 横梁
|
||||||
|
g.fillStyle = '#1a0e08'; g.fillRect(0, 0, VW, 8);
|
||||||
|
g.fillStyle = '#1a0e08'; g.fillRect(0, VH * 0.50 - 4, VW, 8);
|
||||||
|
// 两扇木格窗
|
||||||
|
const winY = 70, winH = 90, winW = 72;
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
const wx = VW * (0.22 + i * 0.56) - winW / 2;
|
||||||
|
g.fillStyle = '#160c06'; g.fillRect(wx, winY, winW, winH);
|
||||||
|
g.strokeStyle = C.lattice; g.lineWidth = 2.5; g.strokeRect(wx, winY, winW, winH);
|
||||||
|
g.beginPath();
|
||||||
|
g.moveTo(wx + winW / 2, winY); g.lineTo(wx + winW / 2, winY + winH);
|
||||||
|
g.moveTo(wx, winY + winH / 2); g.lineTo(wx + winW, winY + winH / 2);
|
||||||
|
g.stroke();
|
||||||
|
}
|
||||||
|
// 牌匾(年画坊)—— 标题叠在 renderMenu 里画
|
||||||
|
g.fillStyle = C.vermilion;
|
||||||
|
g.fillRect(VW / 2 - 110, 22, 220, 38);
|
||||||
|
g.strokeStyle = C.vermilionDark; g.lineWidth = 3;
|
||||||
|
g.strokeRect(VW / 2 - 110, 22, 220, 38);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一张长宣纸案台。 */
|
||||||
|
function drawBench(g, VW, VH) {
|
||||||
|
// 案台阴影
|
||||||
|
g.fillStyle = C.paperShadow;
|
||||||
|
g.fillRect(VW * 0.04, VH * 0.565, VW * 0.92, VH * 0.18);
|
||||||
|
// 宣纸
|
||||||
|
g.fillStyle = C.paper;
|
||||||
|
g.fillRect(VW * 0.06, VH * 0.555, VW * 0.88, VH * 0.16);
|
||||||
|
// 纸边毛笔感(浅描边)
|
||||||
|
g.strokeStyle = C.paperEdge; g.lineWidth = 2;
|
||||||
|
g.strokeRect(VW * 0.06, VH * 0.555, VW * 0.88, VH * 0.16);
|
||||||
|
// 案台桌脚暗影
|
||||||
|
g.fillStyle = '#1a0e08';
|
||||||
|
g.fillRect(0, VH * 0.715, VW, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一个工位:工具/物件图 + 状态圈 + 工序名。
|
||||||
|
* 状态: 'pending' | 'crafting' | 'ready' | 'done'
|
||||||
|
* 呼吸脉冲用入参 pulse01(0..1)驱动,避免依赖裸 performance.now。
|
||||||
|
*/
|
||||||
|
function drawStation(g, st, step, state, ratio, pulse01) {
|
||||||
|
// 工位阴影
|
||||||
|
g.beginPath(); g.ellipse(st.x, st.y + 38, 42, 10, 0, 0, Math.PI * 2);
|
||||||
|
g.fillStyle = C.paperShadow; g.fill();
|
||||||
|
// 工位物件(按工序绘制不同工具)
|
||||||
|
drawTool(g, st.x, st.y, step, state);
|
||||||
|
// 状态圈
|
||||||
|
if (state === 'ready') {
|
||||||
|
// 就绪:高亮绿圈 + 呼吸
|
||||||
|
const pulse = 0.85 + 0.15 * (pulse01 != null ? pulse01 : 0.5);
|
||||||
|
g.beginPath(); g.arc(st.x, st.y, 40 * pulse, 0, Math.PI * 2);
|
||||||
|
g.strokeStyle = C.readyGlow; g.lineWidth = 4; g.stroke();
|
||||||
|
g.beginPath(); g.arc(st.x, st.y, 46, 0, Math.PI * 2);
|
||||||
|
g.strokeStyle = C.ready; g.lineWidth = 2; g.stroke();
|
||||||
|
// 火候窗口条
|
||||||
|
const barW = 70, barH = 8, bx = st.x - barW / 2, by = st.y - 62;
|
||||||
|
g.fillStyle = 'rgba(36,18,8,0.55)'; g.fillRect(bx, by, barW, barH);
|
||||||
|
g.fillStyle = ratio > 0.5 ? C.readyGlow : (ratio > 0.25 ? C.warn : C.danger);
|
||||||
|
g.fillRect(bx, by, barW * ratio, barH);
|
||||||
|
g.strokeStyle = C.strokeDark; g.lineWidth = 1; g.strokeRect(bx, by, barW, barH);
|
||||||
|
} else if (state === 'crafting') {
|
||||||
|
// 加工中:虚线黄圈
|
||||||
|
g.setLineDash([5, 5]); g.beginPath(); g.arc(st.x, st.y, 38, 0, Math.PI * 2);
|
||||||
|
g.strokeStyle = C.warn; g.lineWidth = 3; g.stroke(); g.setLineDash([]);
|
||||||
|
} else if (state === 'done') {
|
||||||
|
g.beginPath(); g.arc(st.x, st.y, 36, 0, Math.PI * 2);
|
||||||
|
g.strokeStyle = C.done; g.lineWidth = 3; g.stroke();
|
||||||
|
// 打勾
|
||||||
|
g.beginPath();
|
||||||
|
g.moveTo(st.x - 12, st.y);
|
||||||
|
g.lineTo(st.x - 4, st.y + 9);
|
||||||
|
g.lineTo(st.x + 14, st.y - 10);
|
||||||
|
g.strokeStyle = C.done; g.lineWidth = 4; g.stroke();
|
||||||
|
} else {
|
||||||
|
// pending:虚线灰圈
|
||||||
|
g.setLineDash([3, 5]); g.beginPath(); g.arc(st.x, st.y, 36, 0, Math.PI * 2);
|
||||||
|
g.strokeStyle = C.pending; g.lineWidth = 2; g.stroke(); g.setLineDash([]);
|
||||||
|
}
|
||||||
|
// 工序名
|
||||||
|
g.font = '700 14px "PingFang SC", "Microsoft YaHei", sans-serif';
|
||||||
|
g.textAlign = 'center'; g.textBaseline = 'middle';
|
||||||
|
g.fillStyle = state === 'pending' ? C.textDim : C.text;
|
||||||
|
g.fillText(step.name, st.x, st.y + 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工位上的工具/物件(每道工序不同,程序化绘制)。 */
|
||||||
|
function drawTool(g, x, y, step, state) {
|
||||||
|
const col = state === 'pending' ? C.pending : step.color;
|
||||||
|
g.save();
|
||||||
|
g.translate(x, y);
|
||||||
|
const idx = step.name;
|
||||||
|
if (idx === '刻版') {
|
||||||
|
// 木版 + 刻线
|
||||||
|
g.fillStyle = '#7a4a26';
|
||||||
|
g.fillRect(-22, -10, 44, 22);
|
||||||
|
g.fillStyle = col;
|
||||||
|
g.fillRect(-22, -10, 44, 4);
|
||||||
|
g.strokeStyle = col; g.lineWidth = 2;
|
||||||
|
g.beginPath(); g.moveTo(-14, -2); g.lineTo(-6, 6); g.stroke();
|
||||||
|
g.beginPath(); g.moveTo(0, -4); g.lineTo(8, 4); g.stroke();
|
||||||
|
g.beginPath(); g.moveTo(-10, 6); g.lineTo(10, -2); g.stroke();
|
||||||
|
// 刻刀(右上)
|
||||||
|
g.fillStyle = '#444';
|
||||||
|
g.fillRect(14, -22, 4, 14);
|
||||||
|
g.fillStyle = '#888';
|
||||||
|
g.beginPath();
|
||||||
|
g.moveTo(16, -8); g.lineTo(13, -2); g.lineTo(19, -2); g.closePath();
|
||||||
|
g.fill();
|
||||||
|
} else if (idx === '上墨') {
|
||||||
|
// 墨辊
|
||||||
|
g.save();
|
||||||
|
g.rotate(-0.3);
|
||||||
|
g.fillStyle = '#5a3a24';
|
||||||
|
g.fillRect(-24, -4, 48, 8);
|
||||||
|
g.fillStyle = col;
|
||||||
|
g.fillRect(-24, -4, 48, 4);
|
||||||
|
g.fillStyle = '#7a4a26';
|
||||||
|
g.fillRect(-30, -3, 6, 6);
|
||||||
|
g.fillRect(24, -3, 6, 6);
|
||||||
|
g.restore();
|
||||||
|
} else if (idx === '铺纸') {
|
||||||
|
// 宣纸方块
|
||||||
|
g.fillStyle = col;
|
||||||
|
g.fillRect(-20, -16, 40, 32);
|
||||||
|
g.strokeStyle = C.strokeDark; g.lineWidth = 1.5;
|
||||||
|
g.strokeRect(-20, -16, 40, 32);
|
||||||
|
g.beginPath();
|
||||||
|
g.moveTo(-20, 0); g.lineTo(20, 0);
|
||||||
|
g.strokeStyle = C.paperEdge; g.lineWidth = 1; g.stroke();
|
||||||
|
} else if (idx === '拓印') {
|
||||||
|
// 棕刷 + 版
|
||||||
|
g.fillStyle = '#7a4a26';
|
||||||
|
g.fillRect(-22, -4, 44, 16);
|
||||||
|
g.fillStyle = col;
|
||||||
|
g.fillRect(-22, -4, 44, 4);
|
||||||
|
g.strokeStyle = col; g.lineWidth = 1.5;
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const px = -18 + i * 9;
|
||||||
|
g.beginPath(); g.moveTo(px, 12); g.lineTo(px + 2, 18); g.stroke();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
g.beginPath(); g.arc(0, 0, 22, 0, Math.PI * 2);
|
||||||
|
g.fillStyle = col; g.fill();
|
||||||
|
}
|
||||||
|
g.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 顶部 HUD:工钱 + 成品数 + 倒计时。 */
|
||||||
|
function drawHud(g, hud, info) {
|
||||||
|
hud.drawPanel(g, { x: 8, y: 76, w: info.vw - 16, h: 52 }, { fill: C.panel, stroke: C.stroke, lineWidth: 2, radius: 12 });
|
||||||
|
hud.drawText(g, '工钱 ' + info.wage, 22, 102, { font: '700 22px "PingFang SC",sans-serif', color: C.accentGold, align: 'left', baseline: 'middle' });
|
||||||
|
hud.drawText(g, '年画 ' + info.works, info.vw / 2, 102, { font: '600 18px "PingFang SC",sans-serif', color: C.textLight, align: 'center', baseline: 'middle' });
|
||||||
|
const sec = Math.max(0, Math.ceil(info.remainMs / 1000));
|
||||||
|
const tcol = info.remainMs < 10000 ? C.danger : C.textLight;
|
||||||
|
hud.drawText(g, '剩余 ' + sec + 's', info.vw - 22, 102, { font: '700 20px "PingFang SC",sans-serif', color: tcol, align: 'right', baseline: 'middle' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前作品条 + 图样大字符(让玩家看到在印什么)。 */
|
||||||
|
function drawWorkLine(g, hud, view) {
|
||||||
|
if (!view.work) return;
|
||||||
|
const idx = view.work.stepIndex;
|
||||||
|
const total = CRAFT_CHAIN.length;
|
||||||
|
const stepName = CRAFT_CHAIN[idx] ? CRAFT_CHAIN[idx].name : '';
|
||||||
|
hud.drawPanel(g, { x: view.vw * 0.10, y: view.vh * 0.42, w: view.vw * 0.80, h: 36 },
|
||||||
|
{ fill: C.panelLight, stroke: C.strokeDark, lineWidth: 1.5, radius: 8 });
|
||||||
|
hud.drawText(g, '在印:' + view.work.name + ' · ' + stepName + '(' + (idx + 1) + '/' + total + ')',
|
||||||
|
view.vw / 2, view.vh * 0.42 + 18, { font: '700 16px "PingFang SC",sans-serif', color: C.text, align: 'center', baseline: 'middle' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 图鉴条(底部):已解锁/未解锁的图样预览 —— 「印得多解锁新吉祥图样」的视觉钩子。 */
|
||||||
|
function drawCodex(g, hud, view) {
|
||||||
|
const yTop = view.vh * 0.78;
|
||||||
|
// 浅纸面
|
||||||
|
hud.drawPanel(g, { x: view.vw * 0.06, y: yTop, w: view.vw * 0.88, h: view.vh * 0.16 },
|
||||||
|
{ fill: C.paper, stroke: C.strokeDark, lineWidth: 1.5, radius: 10 });
|
||||||
|
// "图鉴"小标
|
||||||
|
hud.drawText(g, '图鉴', view.vw * 0.10, yTop + 14, { font: '700 12px "PingFang SC",sans-serif', color: C.text, align: 'left', baseline: 'middle' });
|
||||||
|
// 解锁进度条
|
||||||
|
const pbW = view.vw * 0.55;
|
||||||
|
hud.drawBar(g, { x: view.vw * 0.18, y: yTop + 8, w: pbW, h: 8 }, Math.min(1, view.wage / UNLOCK_PROGRESS_TOTAL),
|
||||||
|
{ bg: 'rgba(0,0,0,0.25)', fill: C.vermilion, stroke: C.strokeDark, radius: 4 });
|
||||||
|
hud.drawText(g, view.wage + ' / ' + UNLOCK_PROGRESS_TOTAL, view.vw * 0.18 + pbW + 6, yTop + 12, { font: '600 11px sans-serif', color: C.textDim, align: 'left', baseline: 'middle' });
|
||||||
|
// 图样格(每件一格:大字 + 解锁/未解锁底色)
|
||||||
|
const cellW = 44, cellH = 56;
|
||||||
|
const cellY = yTop + 28;
|
||||||
|
const startX = view.vw * 0.18;
|
||||||
|
for (let i = 0; i < WORKS.length; i++) {
|
||||||
|
const w = WORKS[i];
|
||||||
|
const cx = startX + i * (cellW + 4);
|
||||||
|
const unlocked = view.wage >= (w.unlockAtWage || 0);
|
||||||
|
// 格背景
|
||||||
|
hud.drawPanel(g, { x: cx, y: cellY, w: cellW, h: cellH },
|
||||||
|
{ fill: unlocked ? w.bgColor : 'rgba(60,40,20,0.18)', stroke: unlocked ? w.bgColor : C.textDim, lineWidth: 1.5, radius: 6 });
|
||||||
|
if (unlocked) {
|
||||||
|
// 大字
|
||||||
|
g.fillStyle = '#fbeec2';
|
||||||
|
g.font = '900 26px "STKaiti", "KaiTi", "PingFang SC", serif';
|
||||||
|
g.textAlign = 'center'; g.textBaseline = 'middle';
|
||||||
|
g.fillText(w.glyph, cx + cellW / 2, cellY + 24);
|
||||||
|
hud.drawText(g, w.name, cx + cellW / 2, cellY + cellH - 6, { font: '600 10px "PingFang SC",sans-serif', color: '#fbeec2', align: 'center', baseline: 'middle' });
|
||||||
|
} else {
|
||||||
|
// 锁图标(用文字符号)
|
||||||
|
g.fillStyle = C.textDim;
|
||||||
|
g.font = '700 18px sans-serif';
|
||||||
|
g.textAlign = 'center'; g.textBaseline = 'middle';
|
||||||
|
g.fillText('LOCK', cx + cellW / 2, cellY + 22);
|
||||||
|
hud.drawText(g, w.unlockAtWage + '钱', cx + cellW / 2, cellY + cellH - 6, { font: '600 10px "PingFang SC",sans-serif', color: C.textDim, align: 'center', baseline: 'middle' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 完成飘字。 */
|
||||||
|
function drawFloats(g, floats) {
|
||||||
|
for (const f of floats) {
|
||||||
|
const alpha = Math.max(0, 1 - f.age / f.life);
|
||||||
|
if (alpha <= 0) continue;
|
||||||
|
g.save(); g.globalAlpha = alpha;
|
||||||
|
g.textAlign = 'center'; g.textBaseline = 'middle';
|
||||||
|
if (f.combo) {
|
||||||
|
g.font = '900 26px "PingFang SC",sans-serif';
|
||||||
|
g.fillStyle = C.accentGold; g.strokeStyle = '#241208'; g.lineWidth = 4;
|
||||||
|
g.strokeText('连击 ×' + f.amount, f.x, f.y);
|
||||||
|
g.fillText('连击 ×' + f.amount, f.x, f.y);
|
||||||
|
} else if (f.crit) {
|
||||||
|
g.font = '900 28px "PingFang SC",sans-serif';
|
||||||
|
g.fillStyle = C.vermilion; g.strokeStyle = '#241208'; g.lineWidth = 4;
|
||||||
|
g.strokeText('正窗! +' + f.amount, f.x, f.y);
|
||||||
|
g.fillText('正窗! +' + f.amount, f.x, f.y);
|
||||||
|
} else if (f.big) {
|
||||||
|
g.font = '900 30px "STKaiti","KaiTi",serif';
|
||||||
|
g.fillStyle = C.danger; g.strokeStyle = '#241208'; g.lineWidth = 5;
|
||||||
|
g.strokeText(f.big + ' 印成! +' + f.amount, f.x, f.y);
|
||||||
|
g.fillText(f.big + ' 印成! +' + f.amount, f.x, f.y);
|
||||||
|
} else {
|
||||||
|
g.font = '800 24px "PingFang SC",sans-serif';
|
||||||
|
g.fillStyle = C.accentGold; g.strokeStyle = '#241208'; g.lineWidth = 4;
|
||||||
|
g.strokeText('+' + f.amount, f.x, f.y);
|
||||||
|
g.fillText('+' + f.amount, f.x, f.y);
|
||||||
|
}
|
||||||
|
g.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ────────── 场景绘制入口 ────────── */
|
||||||
|
|
||||||
|
/** menu:牌匾标题 + 开工按钮。 */
|
||||||
|
export function renderMenu(g, hud, view) {
|
||||||
|
drawBackdrop(g, view.vw, view.vh);
|
||||||
|
// 暗化
|
||||||
|
g.fillStyle = 'rgba(15,8,4,0.55)'; g.fillRect(0, 0, view.vw, view.vh);
|
||||||
|
// 牌匾标题(画在牌匾上)
|
||||||
|
hud.drawText(g, '年画作坊', view.vw / 2, 41, { font: '900 24px "STKaiti","KaiTi",serif', color: C.textLight, align: 'center', baseline: 'middle' });
|
||||||
|
// 大标题
|
||||||
|
hud.drawText(g, '赶在年前', view.vw / 2, view.vh * 0.26, { font: '900 42px "STKaiti","KaiTi",serif', color: C.vermilion, align: 'center', baseline: 'middle' });
|
||||||
|
hud.drawText(g, '印出最多年画', view.vw / 2, view.vh * 0.26 + 56, { font: '900 32px "STKaiti","KaiTi",serif', color: C.accentGold, align: 'center', baseline: 'middle' });
|
||||||
|
// 副标
|
||||||
|
hud.drawText(g, '60s 内刻版→上墨→铺纸→拓印,工钱越多解锁新图样',
|
||||||
|
view.vw / 2, view.vh * 0.45, { font: '14px "PingFang SC",sans-serif', color: C.textLight, align: 'center', baseline: 'middle' });
|
||||||
|
// 开工按钮
|
||||||
|
const w = 240, h = 60;
|
||||||
|
const startBtn = hud.drawButton(g, { x: view.vw / 2 - w / 2, y: view.vh * 0.62 - h / 2, w, h },
|
||||||
|
{ label: '开 印', font: '800 24px "STKaiti","KaiTi",serif', fill: C.vermilion, textColor: C.textLight, radius: 30 });
|
||||||
|
return { startBtn };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* play:背景 + 工位链 + 作品条 + 图鉴 + 飘字 + HUD。
|
||||||
|
* pulse01 = 0..1 的呼吸相位(0..1 循环),由 game-logic 经受控时钟算后传入。
|
||||||
|
*/
|
||||||
|
export function renderPlay(g, hud, view) {
|
||||||
|
const { vw, vh, stations, work, floats, wage, works, remainMs, nowMs, pulse01 } = view;
|
||||||
|
drawBackdrop(g, vw, vh);
|
||||||
|
drawBench(g, vw, vh);
|
||||||
|
drawWorkLine(g, hud, view);
|
||||||
|
// 工位
|
||||||
|
for (const st of stations) {
|
||||||
|
const step = CRAFT_CHAIN[st.station] || CRAFT_CHAIN[0];
|
||||||
|
let state = 'pending';
|
||||||
|
let ratio = 0;
|
||||||
|
if (work) {
|
||||||
|
if (st.station < work.stepIndex) state = 'done';
|
||||||
|
else if (st.station === work.stepIndex) {
|
||||||
|
if (work.stepState === 'ready') {
|
||||||
|
const waited = nowMs - work.stateSinceMs;
|
||||||
|
ratio = Math.max(0, Math.min(1, (work.windowMs - waited) / work.windowMs));
|
||||||
|
state = 'ready';
|
||||||
|
} else {
|
||||||
|
state = 'crafting';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawStation(g, st, step, state, ratio, pulse01);
|
||||||
|
}
|
||||||
|
drawFloats(g, floats);
|
||||||
|
drawHud(g, hud, { vw, vh, wage, works, remainMs });
|
||||||
|
drawCodex(g, hud, { vw, vh, wage });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** over:今日工钱 + 出炉作品展示 + 图鉴进度 + 最佳纪录 + 再开按钮。 */
|
||||||
|
export function renderOver(g, hud, view) {
|
||||||
|
drawBackdrop(g, view.vw, view.vh);
|
||||||
|
g.fillStyle = 'rgba(15,8,4,0.60)'; g.fillRect(0, 0, view.vw, view.vh);
|
||||||
|
// 标题
|
||||||
|
hud.drawText(g, '收 工 咯', view.vw / 2, view.vh * 0.16, { font: '900 36px "STKaiti","KaiTi",serif', color: C.vermilion, align: 'center', baseline: 'middle' });
|
||||||
|
hud.drawText(g, '工钱 ' + view.wage, view.vw / 2, view.vh * 0.26, { font: '900 50px "STKaiti","KaiTi",serif', color: C.accentGold, align: 'center', baseline: 'middle' });
|
||||||
|
// 成品展示条
|
||||||
|
const itemY = view.vh * 0.36;
|
||||||
|
hud.drawPanel(g, { x: view.vw * 0.08, y: itemY, w: view.vw * 0.84, h: 64 },
|
||||||
|
{ fill: C.paper, stroke: C.strokeDark, lineWidth: 2, radius: 10 });
|
||||||
|
const names = (view.worksDone && view.worksDone.length) ? view.worksDone.join(' · ') : '今日没印出年画';
|
||||||
|
hud.drawText(g, '印出:' + names, view.vw / 2, itemY + 22, { font: '700 16px "PingFang SC",sans-serif', color: C.text, align: 'center', baseline: 'middle' });
|
||||||
|
hud.drawText(g, '成品 ' + (view.worksDone ? view.worksDone.length : 0) + ' 回炉 ' + view.redos,
|
||||||
|
view.vw / 2, itemY + 46, { font: '600 15px "PingFang SC",sans-serif', color: C.textDim, align: 'center', baseline: 'middle' });
|
||||||
|
// 最佳纪录
|
||||||
|
hud.drawText(g, '最佳工钱 ' + view.best, view.vw / 2, view.vh * 0.50, { font: '700 18px "PingFang SC",sans-serif', color: C.accentGold, align: 'center', baseline: 'middle' });
|
||||||
|
// 提示
|
||||||
|
hud.drawText(g, '印得越多,「麒麟」「童子抱鱼」会陆续解锁哦~',
|
||||||
|
view.vw / 2, view.vh * 0.56, { font: '13px "PingFang SC",sans-serif', color: C.textLight, align: 'center', baseline: 'middle' });
|
||||||
|
// 再开按钮
|
||||||
|
const w = 240, h = 60;
|
||||||
|
const restartBtn = hud.drawButton(g, { x: view.vw / 2 - w / 2, y: view.vh * 0.70 - h / 2, w, h },
|
||||||
|
{ label: '再开一印', font: '800 24px "STKaiti","KaiTi",serif', fill: C.vermilion, textColor: C.textLight, radius: 30 });
|
||||||
|
return { restartBtn };
|
||||||
|
}
|
||||||
@ -17,6 +17,8 @@
|
|||||||
* · 换手艺/工序/作品/难度 → 改 core.js 顶部的多样性参数空间(CRAFT_CHAIN、WORKS、节奏常量)
|
* · 换手艺/工序/作品/难度 → 改 core.js 顶部的多样性参数空间(CRAFT_CHAIN、WORKS、节奏常量)
|
||||||
* · 画面观感(案台长啥样、工位长啥样、配色)→ 改 render.js
|
* · 画面观感(案台长啥样、工位长啥样、配色)→ 改 render.js
|
||||||
* · 想加机制(正窗暴击 combo、解锁新作品、图鉴)→ 在循环里加,但**别破坏 targets/score 契约**
|
* · 想加机制(正窗暴击 combo、解锁新作品、图鉴)→ 在循环里加,但**别破坏 targets/score 契约**
|
||||||
|
* · ⚠ 改写本文件时 **import 列表必须与你用到的 core.js 导出一一对齐**:漏 import 的裸标识符
|
||||||
|
* check/build 都拦不住(esbuild 当全局放行),运行时点一下就 ReferenceError 被隔离 → 永卡菜单、九门全挂。
|
||||||
*
|
*
|
||||||
* 入参(L1 已摊平):createGame({ plugins, bundle, viewport });受控面 ctx 在 init(boot) 的 boot.ctx。
|
* 入参(L1 已摊平):createGame({ plugins, bundle, viewport });受控面 ctx 在 init(boot) 的 boot.ctx。
|
||||||
* 返回 GameInstance:init/update/render/destroy + handleTap + _forensicsView。
|
* 返回 GameInstance:init/update/render/destroy + handleTap + _forensicsView。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user