/** * 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)); }