③ 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>
180 lines
8.3 KiB
JavaScript
180 lines
8.3 KiB
JavaScript
/**
|
||
* 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));
|
||
}
|