将分层反馈强化版提升为正式 main,统一协作入口并移除外部构建路径

This commit is contained in:
toshiocn
2026-10-23 11:04:37 +08:00
parent 83e2e40269
commit a156e8b5c6
173 changed files with 1181 additions and 498 deletions
+9
View File
@@ -0,0 +1,9 @@
# 协作约定
- 仓库:NumberSwipe_New;正式发布分支:main。
- 唯一微信主工程:wechat-campaign-200-r2/。修改、构建和验收以该目录为准。
- 先读取该目录 README.md 与 release-info.json。不要从旧工作副本覆盖主版本。
- 开工先检查 git status 与远端差异;不要覆盖其他协作者未提交的改动。新功能从最新 main 建立 codex/主题 分支,完成验证后合并。
- 使用 npm ci 与 npm run check;提交必要源码、构建产物及 package-lock.json。发布使用 npm run package。
- 禁止新增本机绝对路径或跨仓库依赖;资源路径相对当前模块定位。保留关卡、playOrder、上游快照和存档兼容。
- campaign-200-r2/ 是网页来源;历史发布包与 archive/ 不代表当前微信主版本。公司原仓库不作为本仓库推送目标。
+3 -102
View File
@@ -1,104 +1,5 @@
# NumberSwipe_New · 数字一笔画衍生项目
# NumberSwipe_New
本仓库管理 NumberSwipe-Variants 下的衍生玩法、关卡、生成工具和设计资料。
正式分支:main。唯一微信主工程:[wechat-campaign-200-r2](wechat-campaign-200-r2/README.md)。当前为分层反馈、音乐与流动指路主版本,版本以 [release-info.json](wechat-campaign-200-r2/release-info.json) 为准。
## 最新微信美术版(2026-10-22)
- 源码及微信开发者工具导入目录: [wechat-campaign-200-r2](wechat-campaign-200-r2/)。
- 最新独立包:[明亮教具室·过关重音修复版](releases/NumberSwipe-campaign-200-r2-明亮教具室-过关重音修复版-20261022.zip)。
- 包含明亮教具室美术、字体资源、指路表现、循环教学及庆祝效果;网页版本入口见下方。
## 网页版:campaign-200-r2
- 试玩入口:[200 关评分修订版](campaign-200-r2/index.html),下载后可直接打开。
- 版本说明与校验方式:[最新版 README](campaign-200-r2/README.md)。
- 关卡评分:[SCORES.html](campaign-200-r2/SCORES.html)。
- `campaign-200`、`campaign-50` 和 `archive` 保留为历史版本及来源资料。
- 根目录 `index.html` 是早期五玩法验证入口,最新版请使用上方入口。
以下保留早期玩法验证说明。
---
# 一笔路径 · 第二轮玩法验证
按第一轮试玩反馈,当前只保留五个入口,每个玩法8关:入门、普通、进阶、难,各两关,共40关。
## 开始试玩
双击 `index.html`。各玩法 `modes/<id>/play.html` 都是可独立运行的离线单文件,所有关卡开放。首页也可直接进入各玩法的设计评审与答案图。
| 入口 | 核心区别 |
|---|---|
| 方向通道 | 横线只通左右,竖线只通上下;四种拐角连接不同两条边,都可以反向经过 |
| 双色交替 | 每步蓝→橙→蓝;大格整块一步,走过后仍保留原颜色 |
| 永久墙壁 | 共边上的黑黄墙整局不变,无开关,必须绕行 |
| 步进开关门 | 只有一道门;每落一格切换开/关,必须安排到门边的步数 |
| 六边融合:双色与墙 | 六向邻接同时满足换颜色与不穿墙,进阶和难关拓宽到三行 |
相对步差、先后链、到达窗口和路径开门已从试玩入口退出,保存在 `archive/v1/`,不参与当前生成、构建及验收。
## 方向图示
图标是通道形状,不是单向箭头:
- `─`:左 ↔ 右。
- `│`:上 ↔ 下。
- `└`:上 ↔ 右。
- `┌`:右 ↔ 下。
- `┐`:下 ↔ 左。
- `┘`:左 ↔ 上。
试玩用Canvas绘制清晰的线条通道,不依赖系统字体箭头。点击条件标签可高亮对应格。通道格不能做起点或终点,离开该格时检查进出边。
## 双色图示
蓝格带○,橙格带△;颜色为主要信息,符号辅助区分。蓝→橙→蓝是每关常驻说明。经过的格显示步号,底色仍是原来的蓝或橙;不能把已走格统一染成另一个颜色。
## 永久墙与步进门
黑黄粗线画在两格真实共边上,是永久墙,不占一个格,也不会切换。
步进门没有开关格,也没有钥匙:
1. 空盘门关。
2. 落下起点算第一格,门变开。
3. 落下第二格门变关,第三格再开,依此交替。
4. 移动之前用当前门状态判断能否穿过;落格后再切换。
5. 不落格、点击按钮或原地等待不会切换;撤销按剩余路径恢复状态。
盘面门口显示“开/关”,条件栏同时显示“再落一格就关/开”。开门是两段绿色边框之间的缺口,关门是连续阻挡。
## 操作与设计资料
点按或拖动相邻格追加,松手可继续;点已走格回退,也可撤销和重开。固定数字与通道条件画错会标红,但允许保留路径再修正;永久墙、关闭门和同色移动直接阻止追加。指路不会沿错误前缀继续。
每个文件夹含 `levels.json`、独立 `rules.mjs` / `ui.mjs`、离线 `play.html`、`DESIGN.html` / `DESIGN.md` / `design.json`,以及8张原始盘面和8张作者解SVG。
设计资料记录作者解、关键分叉、诱惑路线及每项新机制解除后的完整反例。试玩包只有规则和原始关卡,运行时求解,不读取设计资料答案。进度按玩法分别存档。
## 构建与检查
Node.js 22+,本次使用24.21.0;无需安装第三方依赖。
```powershell
node --test tests/*.test.mjs
node tools/validate.mjs
node tools/build.mjs
node tools/design-pages.mjs
node tools/isolation.mjs
```
`node tools/generate.mjs` 使用固定种子,会覆盖当前五个玩法的关卡与设计资料;重生成后必须重新构建、检查。历史四个玩法不会重新加入入口。
当前40/40关完整有序解唯一,每项通道要求、颜色机制、永久墙及步进门逐项必要;解除任意一项均至少两解。除教学起点外,辅助数字逐项检查必要性。
27项规则与离线页面脚本测试通过。浏览器工具禁止file://地址,因此尚未完成实际浏览器像素与真机触感验收;DOM/Canvas测试不能替代这些检查。难度仍需新玩家校准。
原NumberSwipe项目保持只读,全部修改在同级NumberSwipe-Variants目录。原项目91个受检查文件的内容及清单保持一致,详见 `reports/ISOLATION.md`。
## 对比试玩
建议先玩方向通道、双色交替,再玩永久墙与步进门,最后玩六边融合。各玩法按四档顺序推进;观察是否能只看盘面说明就复述规则。
在“记录本关体验”填口述推理,导出本关JSON。观察者用 `reports/playtest-template.csv` 补充第一次错误、回退、提示、思考时间和边界可读性。尤其比较墙与门是否产生不同决策,以及六边融合是否仍主要靠顺手试画。
克隆整个仓库后进入主工程运行 npm ci、npm run check。微信开发者工具可导入仓库根目录或主工程。协作前读取 [AGENTS.md](AGENTS.md)。发布包位于 releases/;网页来源和历史版本保留在 campaign-200-r2、campaign-200、campaign-50、archive。
+2 -1
View File
@@ -23,5 +23,6 @@
"appid": "wx7a14d0d70b5dd13d",
"editorSetting": {},
"miniprogramRoot": "wechat-campaign-200-r2/",
"projectname": "NumberSwipe-campaign-200-r2"
"projectname": "NumberSwipe_New-main",
"description": "正式主版本:分层反馈、音乐与流动指路"
}
Binary file not shown.
+3
View File
@@ -1,2 +1,5 @@
.font-sources/
.font-tools/
node_modules/
*.log
project.private.config.json
+12 -144
View File
@@ -1,155 +1,23 @@
# 当前交接补充(2026-10-22)
# NumberSwipe 正式主版本
主版本权威目录:`G:\项目\NumberSwipe-Variants\wechat-campaign-200-r2`。当前包:`releases/NumberSwipe-campaign-200-r2-明亮教具室-续玩选关教学流畅版-20261022.zip`;版本标记:`2026-10-22-resume-chapter-lessons`。以下2026-10-20的“两边同步”“最新包”与教学速度描述仅作历史记录,不覆盖本段及当前README。
本目录是唯一正式微信主工程,属于 NumberSwipe_New 仓库 main 分支。2026-10-23 已将分层反馈、音乐与流动指路强化版提升为主版本。版本以 release-info.json 为准。
本轮改动:`src/resume.mjs`处理续玩选关与路线校验;`src/level-select.mjs`处理章节布局和三种通关状态;`build.mjs`在适配层注入启动/退出存档,保持四份上游快照原样;`src/main.mjs`缓存教学示范棋盘和指路静态界面;`src/hints.mjs`与棋盘适配缓存提示几何。教程缓存不得影响玩家状态,输入、尺寸、规则页变化必须使对应缓存失效。后台返回也要进入未完成关卡,不停在选关页。
## 开发与验收
存档键 `variant:campaign-200-r2:v1:resume` 不取代原通关键。重玩已通关关卡不能覆盖未完成进度。源码及交付版属于main,勿覆盖 `G:\项目\NumberSwipe\.worktrees\layered-feedback`。工作暂存目录 `G:\项目\NumberSwipe\wechat-campaign-200-r2-resume-select` 只用于本轮构建验证。
验收:50项模块测试与200关模拟通关;四档选关按钮边界;浏览器200关填色采样;教学平均绘制1.63→0.72ms/帧、357/360帧复用。真机性能尚未确认。按README所列命令构建/验证。
---
# NumberSwipe 主程交接
更新时间:2026-10-20(北京时间)。面向接任主程;先读本文件、同目录 README.md,再查源码。当前任务已完成,没有正在等待执行的开发指令;下一步根据用户真机反馈继续调整。
## 1. 当前版本和目录
| 用途 | 目录或入口 |
| --- | --- |
| 本会话微信开发工作副本 | `G:\项目\NumberSwipe\wechat-campaign-200-r2` |
| 已同步的微信项目副本 | `G:\项目\NumberSwipe-Variants\wechat-campaign-200-r2` |
| 200关评分修订版网页源项目 | `G:\项目\NumberSwipe-Variants\campaign-200-r2` |
| 微信导入入口 | 微信项目中的 `project.config.json`,运行产物为 `game.js` |
| 网页试玩入口 | `G:\项目\NumberSwipe-Variants\campaign-200-r2\index.html` |
| 旧版,避免误改 | `NumberSwipe-Variants\campaign-200`、`campaign-50`(实际53关) |
两个微信目录不是自动同步关系。本会话在 NumberSwipe 下的微信子项目修改、验证后,再复制指定修改文件到 Variants 对应目录。继续开发前对比两边的源码和时间,避免覆盖其他会话的新改动。原 NumberSwipe 其他模式、旧网页 DEMO 不在本次修改范围。
微信版关卡来源:`scored-2026-10-15-r2`,200关、20章,每章10关。微信适配版本标记:`2026-10-19-animated-lessons-one-second`。不得从同级 `wechat` 或其他模式的微信目录混入关卡。
本次交接核对时,两边 `game.js` 及下述最新压缩包内的 `game.js` SHA-256 相同:
```text
4069ac9b0f38d42196a544f008988808c227653abb0bdb8e326aa7adcfb861a5
```
这只是交接时点的快照;之后有新改动时应重新核对。
## 2. 最新交付与用户要求
最新微信包:
`G:\项目\NumberSwipe-Variants\releases\NumberSwipe-campaign-200-r2-高亮指路-保留玩家填色-20261020.zip`
同名副本也在 `G:\项目\NumberSwipe\releases`。更早的“动画教学”包较慢,不能当最新版。压缩包已经构建,导入微信开发者工具后重新编译并生成真机预览;旧二维码可能加载旧代码。本会话未上传或发布小游戏。
最新两项用户要求已实施:
1. 新机制说明必须使用真实游戏关卡画面,文案与动画联合解释,不再只有文字。
2. 用户认为逐格停顿效率低,要求整段约1秒、连续流畅;当前 `duration:1000` 指整段,不是每格1秒。结束停留,点击“重播”再播放。
不要恢复此前每拍1300毫秒、移动450毫秒后停顿的实现,也不要未经用户要求放慢动画。错误反馈和门状态变化仍保留;是否足够看清需真机反馈。
目录里还包含通关操作栏修订:完成后底部由“下一关”独占,替换重开/撤销/指路;最后一关为“返回选关”。反馈、操作栏与棋盘分区,出现按钮不能挪动或遮盖棋盘。该修订见当前源码、README 和 validation,不要用旧文件覆盖。
## 3. 源码定位及修改方式
| 文件 | 职责 |
| --- | --- |
| `src/main.mjs` | 微信 Canvas 页面、触摸路由、规则面板、演示布局、动画绘制、前后台处理 |
| `src/copy.mjs` | 最新十类教学及组合规则文案;保留具体动作、奇偶例子,不退回旧网页术语 |
| `src/hints.mjs` | 分段提示判定、生命周期和独立透明层光效 |
| `src/lessons.mjs` | 按当前机制选择真实教学关、通过求解器取得演示路线、错误例子、回退、1秒时间轴 |
| `src/platform.mjs` | wx 到上游 UI 所需接口的适配、PACK、存储和 Canvas |
| `src/audio.mjs` | 微信原生音频播放器,静音、后台停止、试听及错误诊断 |
| `audio-assets.mjs` | 生成本地 WAV 资源 |
| `build.mjs` | esbuild 打包,注入微信适配及演示绘制接口 |
| `src/upstream/*` | 网页 r2 的四个原始快照;来源及哈希见 source-manifest.json |
保持 `src/upstream/levels.json`、`rules.mjs`、`ui.mjs`、`audio.mjs` 快照原样。微信改动放适配层,必要的上游 UI 替换通过 build.mjs 注入;不要手工修改压缩后的 game.js。
教学实现细节:
- 共十类:基础、墙、横通道、竖通道、拐角、双色、奇偶、节奏门、六边形、无起点。
- 首次教学及手动规则页都使用实际教学关。组合关按出现的机制拆段,不展示当前正式关的完整答案。
- 演示复用上游盘面绘制;build.mjs 导出 `renderLesson`,暂存并恢复绘制状态,用独立 Canvas 输出。`previewOnly` 阻止教学绘制更新实际游戏 UI、报错、操作按钮等。
- 墙/同色显示红色阻挡线,方向/奇偶显示错误及回退。门展示切换、穿过保持开启、撤销穿门。
- `lessonFrame` 使用连续进度,main.mjs 绘制移动光圈和连接段;结束保持结果。翻段/重播重新计时;后台停止绘制,返回重新演示。
- 不修改玩家路线、存档、提示次数、音效记录;不要为了教学调用实际玩家输入函数。
- 只读诊断:`GameGlobal.numberSwipeR2.snapshot()`、`.buttons()`。可查真实游戏状态、音频状态及当前教学。
## 4. 已确认的玩法与表现约束
- 每格仅走一次,共边移动、不可斜走;固定数字表示第几步到达。无计时、体力和分数。
- 松手续画。拖动时只有回到紧邻的上一格才撤销一步,碰其他已走格不撤销;撤销按钮退一步,重开清空。
- 方向格是明确的通道:lr 横向、du 纵向,ru/dr/dl/lu 指定两侧拐角,可反向通过,不能作为端点。图标居中固定,线从图标穿过,正确亮起、错误变红,均不位移。
- 双色仅用蓝/橙区分,连续两格必须异色。不要恢复三角/圆形叠加标识;内部 category 名字仍可能为 circle/triangle,不等于需要画形状。
- 大格/异形整块一步,不画内部拼接缝;双色、奇偶不要求必须带大格。
- 奇偶按访问步数判定。避免同一格叠加奇偶和方向图标。
- 永久墙不能通过。节奏门空盘关闭,起点算一步,每进入一格切换一次;移动前检查门状态。合法穿过后保持开启,撤销穿门那一步后恢复切换,重开重置。
- 六边形共边六邻接,可以混合其他已支持机制;关卡应宽向展开,避免全部是纵向蛇形单路线。
- 指路现为高对比分段提示(白色/深色双描边与流动光点),只展示最早未完成的一段。节点为起点、数字点、方向点、奇偶点、终点,按正确解排序;正确到达段末后消失,再点才提示下一段。暂不接广告。提示使用独立透明 hintCanvas,清空或擦除只影响提示层;已走格多边形和实际连接段让出给原棋盘,保留整格填色及数字、方向反馈。不要在主 Canvas 上使用 destination-out 清除玩家绘制。
- 不恢复已被用户否决的相对步差、先后链、到达窗口、开关钥匙开门。保留的是永久墙和节奏门。
- 玩法可以混合,但新机制首次出现需极简教学;用户不需要专门的大格教学。
- 沿用原项目白底、紫色路线及既有 UI;不要重新发明一套皮肤。
音效:普通移动沿当前音色每步升一个半音;撤销/重开后步数决定音高。其他五类反馈音保持既有听感。微信使用随包41个 WAV(36个移动音+五类反馈)及 `wx.createInnerAudioContext`,不依赖浏览器振荡器。静音/进入后台时停止播放和延迟音效。
## 5. 构建、测试与实际验证边界
在当前微信项目目录执行:
需要 Node.js 22+、npm。首次克隆后在本目录运行:
```powershell
Set-Location 'G:\项目\NumberSwipe\wechat-campaign-200-r2'
node build.mjs
node --test hints.test.mjs lessons.test.mjs audio-copy.test.mjs
node smoke.mjs
npm ci
npm run check
npm run package
```
本机 Node:`C:\Program Files\nodejs\node.exe`。build.mjs 当前硬编码 esbuild 位置为 `G:/项目/NumberSwipe/modes/01-number-stroke/node_modules/esbuild/lib/main.js`,迁移电脑需调整依赖路径。播放器无第三方运行时依赖。
已报告通过:
- lessons.test.mjs:12项,包括十类教学、200关机制映射及1秒连续时间轴/结束停留。
- audio-copy.test.mjs:6项,原生音频、静音/后台、音高、资源和文案。
- smoke.mjs:实际 game.js 在模拟 wx/Canvas 中的200关唯一解、触摸通关、撤销/重开/提示、存档和前后台;教学状态隔离与窄屏分页。
- 当前 validation.json 另记录通关按钮布局及4种手机尺寸检查。该内容可能来自其他会话的更新,以当前文件为准。
不要把 Node 模拟器、假 Canvas 和边界断言当作真实屏幕截图或真机验收。本会话未验证手机实际帧率、1秒内的错误提示可读性、真实字体布局及音效听感;需要开发者工具和手机预览确认。文档交接本身未重新运行全套游戏测试。
smoke.mjs 会校验上游路径和快照哈希,并覆写 validation.json。移到新机器时缺少上游目录会导致来源检查失败,不能直接删除该检查掩盖问题。
## 6. 长线关卡设计背景(调整关卡时才需展开)
用户反馈原200关重复、后半程太简单、最大18格、六边形多为单调竖向路径。r2 是评分后重排并重制的版本,最大36格,不能误把旧200关当当前数据。评分仅是推理负担代理,不能当真人难度或通关率。
用户主观锚点对应旧版编号:
- 需要提示/否则思考很久:73、76、77、80、85、88、144。
- 略难、应在100关以后经常出现:149、158;比需要提示简单明显。
- 喜欢但不能过多:无固定起点的111(12-1)、136(14-6)。此前端口17851/17581口述有歧义,用户明确确认关卡号码和无起点特征;不要据端口推翻反馈。
r2 无起点关序号为45、65、105、145、185。网页设计资料、评分表、关卡映射及作者证明在 `campaign-200-r2` 的 DESIGN/SCORES/DIFFICULTY 页面、design.json 和 reports 下。作者解不应打包给玩家;运行判定用规则求解。
此前局域网约定:17850=53关,17851=旧200关,17852=r2评分修订200关。服务可能已停止或换址,先查实际监听和返回版本,不能凭端口断言当前内容。微信包与这些网页服务相互独立。
## 7. 接手时的行动顺序与注意事项
1. 确认用户在修改微信版还是网页版,阅读本文件及当前 README,核对两个微信目录是否仍一致。
2. 读取当前源码及 Git 状态;原工作区存在其他会话未提交文件,不清理、不回滚。用户已授权的范围内直接完成工作,不重复请求确认。
3. 若继续调教学,先看 lessons.mjs/main.mjs/copy.mjs,再看 build.mjs 的 renderLesson 注入。保持上游快照、200关排列和存档键 `variant:campaign-200-r2:v1`。
4. 修改后构建和运行与改动相关的检查,再同步微信副本、生成带新版本名的独立包。不要默认两个目录有自动镜像。
5. 本环境 Variants 目录不在默认可写根内,写入需通过工具申请 require_escalated;读取允许。不要绕过权限。
当前 package.ps1 可生成仅含运行必需文件的独立导入包。重新打包时需明确包含 game.js/game.json/project.config.json/audio 和所需开发文件,排除作者解、研究资料及无关项目;按 project.config.json 的上传忽略配置复核资源。上传发布前沿用用户当前授权范围,不假定本交接授予上传授权。
给新会话的起始消息可直接用:
> 请接任 NumberSwipe 主程,先读取 G:\项目\NumberSwipe\wechat-campaign-200-r2\HANDOFF.md 和同目录 README.md,核对当前文件及另一个微信副本后,再按我的新需求继续。不要覆盖其他会话更新,也不要重新生成或打乱当前200关。
微信开发者工具导入本目录,或导入仓库根目录(根配置已指向这里)。构建使用本目录锁定的 esbuild,无需原 NumberSwipe 目录。烟测使用仓库内相邻 campaign-200-r2 的上游文件及快照哈希;移动整个仓库后仍有效。字体、音效、音乐与上游快照均随仓库保存。现成字体可直接构建;仅重新制作字体时另需 Python fonttools 和字体原始文件(见 build-fonts.py)。
## 协作
从最新 main 创建 codex/主题 分支,在本目录修改、构建、测试,通过评审后合并 main。每次发布更新 release-info.json,运行打包命令;输出 ../releases/ 下对应版本名。不要把独立旧副本当作主目录,不要将本机绝对路径或跨仓库依赖写入脚本。网页 campaign-200-r2 是关卡来源及历史网页版本,不是本微信主版本。
当前版本保留200关、原存档兼容、分层音效、BGM、振动设置、三列选关及流动指路。真机听感、振动与性能仍需微信预览验收。
提升前基线:Git 标签 backup/pre-layered-main-20261023。历史开发记录见 OPTIMIZATION.md,其中独立实验版描述仅适用于提升前。
+73
View File
@@ -0,0 +1,73 @@
## 本轮更新:音乐与流动指路(2026-10-22)
- 每页15关,3列×5行(末页5关左对齐);完成数字后绘制圆润勾。320×568仍保持44px以上格子。
- 选关按钮与指路启用分别播放新短音;选关、游戏各有一首原创16秒稀疏木琴循环,低音量独立播放器,不占用四路反馈池。
- 游戏右上角统一进入设置,只有音乐、音效、振动三个独立存储开关;旧减少动态存储不再限制本版表现。
- 数字节点经实际操作测试确认:小幅回弹、中震、checkpoint-N音;终点只用complete和弦,无节点叠加。方向格步号缩小保留线路。
- 指路箭头沿完整分段保留位置;前进一步旧箭头180ms冲向终点,新箭头65px/s出发;不按剩余路线长度重置相位。
- 后台、音频中断停止音乐和反馈;恢复后按场景播放。原版声音资源及上一版包保留。
验证:72项模块测试、200关模拟触摸通过;浏览器375宽与320×568排版、200/200关提示前后及撤销重画的玩家填色像素检查通过。iPhone/Android真机音量、触觉及帧率待验收。
当前独立包:`NumberSwipe-campaign-200-r2-分层反馈二版-音乐与流动指路-20261022.zip`。主版与历史包保持原样。
# 分层反馈独立版验收记录(2026-10-22)
当前主版本保持不变。本目录是独立开发副本,版本标记为 `experimental-layered-feedback`,基于 `2026-10-22-resume-chapter-lessons`。不回写 Variants 主目录,也不覆盖其他工作树及历史发布包。
## 使用与验证
解压独立包后导入微信开发者工具,重新编译并生成预览二维码。源代码目录运行:
```powershell
node build.mjs
node --test feedback.test.mjs resume-select.test.mjs bright-classroom.test.mjs hints.test.mjs lessons.test.mjs audio-copy.test.mjs
node smoke.mjs
./package.ps1
```
浏览器验收页:
```powershell
$env:NUMBERSWIPE_QA_PORT='17861'
node qa/serve.mjs
```
打开 `http://127.0.0.1:17861/qa/bright-classroom.html`,使用“反馈对照”选择原版/强化版。此页不发起触觉;原音效保存在 `audio-original/`,哈希记录在 `qa/original-audio-sha256.json`。正式设置只增加“振动”开关,没有强度滑杆。
## 实现与参数
| 内容 | 实现 | 验收依据 |
| --- | --- | --- |
| 实际操作事件 | `layered-feedback-build.mjs` 在追加、回退、错误及完成处注入事件;每次原生触摸回调的插值批次统一提交声音和最高级触觉 | 高速单次移动跨三块得到 move、move、complete;序号与步号递增 |
| 数字节点 | 固定数字大于1、步号匹配且现有规则无冲突;终点只发 complete | 合法但偏离唯一解的移动仍按普通事件处理,不用答案判断反馈 |
| 回退确认 | `backtrack.mjs` 对原命中多边形并集的外边界计算距离,内缩 `min(3,0.06u)`;剔除异形内部拼接边 | 方形、凹口、六边与真实拖动测试;起手和前进命中未扩大,按钮撤销即时 |
| 普通落格 | 120ms,最低缩放0.985;新桥100ms展开;数字、通道、墙门、命中轮廓不缩放 | `landingScale` 边界测试和原有200关几何回归 |
| 数字节点 | 回弹最高1.035,260ms结束;原局部彩带最多两组;回退节点取消其彩带 | 多个节点先到达、后绘制的测试;通关清空未结束节点 |
| 完成 | 即时完成章;庆祝沿用原2.6秒规模;实际 onPlay 在120ms内作为起点,否则锁定逻辑起点 | 迟到回调不重启;音频中断、后台、换关取消旧演出 |
| 音频 | 四路:三路移动、一路节点/确认;普通音超过20次/秒合并到最新步号;通关停掉残留音 | 最新步号、四路停止、静音取消、回调代际测试 |
| 触觉 | 独立键 `number-swipe:haptics:v1`,默认开;普通轻震70ms限频;节点中震替代轻震;完成中震后90ms轻震 | 关闭、预览、旧任务、迟到失败回调、短震降级测试;从不调用长震 |
| 选关 | 始终每行3关,每章10关;通关绿色、当前紫色 | 四档手机尺寸与三列断言;截图 `qa/layered-feedback-three-columns.png` |
集中参数在 `src/feedback-params.mjs`。反馈调度和生命周期在 `src/feedback.mjs`,触觉适配在 `src/haptics.mjs`。四份上游快照未修改;所有规则、关卡、顺序、续玩存档、提示和手动下一关继续保留。减少动态关闭块体缩放和彩带运动,音效和振动分别控制。
重开、换关、选关退出、后台和音频中断会取消旧音频、触觉延迟任务与表现事件。硬件不支持强度时只降级为一次短震;不可用静默跳过。一次原生触摸回调产生多个插值事件时,视觉事件逐个保留,音频与触觉采用最高优先级,不补播过期的低级触觉。
## 证据与结果
| 证据 | 结论 | 复现入口 |
| --- | --- | --- |
| 67项模块测试通过 | 事件优先级、回退确认、触觉代际、原音效哈希、音画同步与已有模块回归通过 | 上述 `node --test` 命令 |
| 200关模拟触摸通过 | 关卡唯一解、合法移动、撤销重开、存档与分段指路未破坏 | `node smoke.mjs` / `validation.json` |
| 浏览器200关稳定画面填色采样通过 | 开启提示和撤销重画后,玩家填色保持一致 | `qa/hints.html` |
| 强化移动音第1/36步 RMS 约1503/555 | 高步号能量降低,36步逐半音关系仍在 | `feedback.test.mjs`、`audio-copy.test.mjs` |
| 主版源文件基线逐项哈希一致 | 独立开发没有改写当前主版 | 工作区同级 `wechat-campaign-200-r2-layered-feedback-baseline.json` |
## 真机验收:待完成
| 设备 | 状态 | 仍需确认 |
| --- | --- | --- |
| iPhone | 未进行 | 轻/中震差别、90ms组合舒适度、高步号音色、密集关跟手、实际音画同步与帧率 |
| Android | 未进行 | 强度不支持时的短震降级、连续震动舒适度、同上音效与性能项目 |
浏览器和模拟器结果不作为真机通过。本轮没有可用的iPhone/Android设备会话,没有上传或发布小游戏。
+11 -125
View File
@@ -1,135 +1,21 @@
# 主版本:续玩、章节选关与教学动画优化(2026-10-22)
# NumberSwipe 正式主版本
主版本源码以 `G:\项目\NumberSwipe-Variants\wechat-campaign-200-r2` 为准;本轮基于“过关重音修复版”开发。`codex/layered-feedback` 开发分支及其工作树不参与此次同步。版本标记见 `release-info.json`。
本目录是唯一正式微信主工程,属于 NumberSwipe_New 仓库 main 分支。2026-10-23 已将分层反馈、音乐与流动指路强化版提升为主版本。版本以 release-info.json 为准。
- 启动直接进入上次未完成关卡,恢复已画路线;即使退出前停在选关页,冷启动与后台返回均回到未完成关。画线、撤销、重开实时保存,进入后台再补存。完成一关后下次进入下一未完成关;重玩已通关关卡不会覆盖原来的未完成存档。
- 选关每页一章、10关,增加留白;已通关为绿色并带勾,当前关紫色,其他关奶油色。自动定位当前章节,保留全部200关可选。320×568等四档尺寸均检查触摸区域和安全区。
- 新机制教学按“教学页、路线、画布尺寸”缓存示范棋盘,移动光圈仍连续绘制;重播、分段及结束后停留循环保持。指路额外缓存圆滑路径与静态界面,降低反复曲线计算和拖尾阴影开销。玩家填色、规则标注及门状态继续实时更新。
## 开发与验收
发布包:`../releases/NumberSwipe-campaign-200-r2-明亮教具室-续玩选关教学流畅版-20261022.zip`。直接解压导入,重新编译并生成预览。尚未上传发布。
验证命令:
需要 Node.js 22+、npm。首次克隆后在本目录运行:
```powershell
node build.mjs
node --test resume-select.test.mjs bright-classroom.test.mjs hints.test.mjs lessons.test.mjs audio-copy.test.mjs
node smoke.mjs
./package.ps1
npm ci
npm run check
npm run package
```
50项模块测试、200关模拟通关及稳定画面下的200关浏览器填色采样对比通过。浏览器教学对比:375×812、DPR 2、3轮各120帧,平均1.63→0.72 ms/frame,357/360帧复用示范棋盘;数据见 `qa/lesson-performance-20261022.json`,属于本机浏览器绘制耗时,不等于微信手机帧率。微信真机仍需验收退出再进、字体、教学流畅度。
微信开发者工具导入本目录,或导入仓库根目录(根配置已指向这里)。构建使用本目录锁定的 esbuild,无需原 NumberSwipe 目录。烟测使用仓库内相邻 campaign-200-r2 的上游文件及快照哈希;移动整个仓库后仍有效。字体、音效、音乐与上游快照均随仓库保存。现成字体可直接构建;仅重新制作字体时另需 Python fonttools 和字体原始文件(见 build-fonts.py)。
新增存档:`variant:campaign-200-r2:v1:resume`,保留原通关和设置存档键。损坏或过期路线安全回到对应关卡空盘;所有关卡通关后保留可重玩关卡。`node qa/serve.mjs` 提供本机预览,默认端口17859,可用 `NUMBERSWIPE_QA_PORT` 覆盖。
## 协作
以下为历史版本说明,不作为当前发布包依据。
从最新 main 创建 codex/主题 分支,在本目录修改、构建、测试,通过评审后合并 main。每次发布更新 release-info.json,运行打包命令;输出 ../releases/ 下对应版本名。不要把独立旧副本当作主目录,不要将本机绝对路径或跨仓库依赖写入脚本。网页 campaign-200-r2 是关卡来源及历史网页版本,不是本微信主版本。
---
2026-10-22 数字和六边形优化:通道一位数按格宽36%、两位数按30%绘制,上限分别28/25逻辑像素;调整通道及数字牌间距,小屏按比例缩放以避免越界。双色玩法移除圆/三角角标,数字在方块中央,保留奇偶规则文字。六边形厚度1.2–2.2像素,投影透明度0.09,减小偏移、倒角暗部及暖色外晕。发布包:releases/NumberSwipe-campaign-200-r2-明亮教具室-大数字双色轻六边版-20261022.zip。
2026-10-22 指路重新设计:参考任天堂官方《灵活脑学校 一起伸展大脑》玩法画面中的奶油色承托、珊瑚橙强调和清楚的方向标识,结合本游戏圆润积木材质,自行程序化绘制奶油色圆边+浅珊瑚色教学引导带。三个圆角方向标沿圆弧移动,带短柔光尾;终点有贴纸式目标环;减少动态保持静态方向。已走方块、规则文字继续避让。参考图片仅用于分析,未作为游戏资源。连接桥没有改动。
参考:https://www.nintendo.com/hk/switch/azlsa/stretch/index.html
官方示例:https://www.nintendo.com/hk/switch/azlsa/assets/img/stretch/genre5-img3.png
当前包:releases/NumberSwipe-campaign-200-r2-明亮教具室-教具指路设计版-20261022.zip。
2026-10-22 更新:连接桥缩短到格子边缘,仅保留1.2逻辑像素衔接;指路合并连续单元路径,并用二次曲线采样圆滑转角。光点及渐隐拖尾沿同一条曲线按弧长向目标移动,过弯保持平稳速度。减少动态模式保持静态指引。新增圆弧拼接与断段隔离回归测试。当前包:releases/NumberSwipe-campaign-200-r2-明亮教具室-短桥圆弧流光版-20261022.zip。
本轮连接桥与指路精修(2026-10-21):连接桥去掉端部封口、端部倒角和实体厚边,只有侧面保留柔和倒角,两端延伸并渐隐到已走块体内。指路改为暖金色光带、浅金高光、移动光点和方向箭头;减少动态模式显示静态虚线及箭头。提示继续独立绘制并避让已走方块和规则文字。
验收:34项模块测试通过;200关模拟微信触摸回归通过;浏览器200关提示前后及撤销重画的玩家填色像素一致。新增截图:qa/bright-classroom-bridge-v4.png、qa/bright-classroom-hint-v4.png。发布包:releases/NumberSwipe-campaign-200-r2-明亮教具室-连接桥与指路精修版-20261021.zip(位于项目根目录)。
# 数字一笔 · campaign-200-r2 微信小游戏
## 明亮教具室程序化美术(2026-10-21)
### 投影、数字与按钮精修(v3,当前版本)
根据参考图再次区分两类效果:棋块外沿使用黄棕色近边晕染+短投影,底盘则没有实体下沿,只由实际面轮廓alpha投出软阴影。底盘暗部改为淡暖色,避免灰黑底沿看起来像第二块垫子。
数字以真实字形边界定位;原生Canvas若只返回文字宽度,会通过一次离屏alpha测量获得字形边界。普通格数字在视觉中心,大块/异形数字位于左上占用单元区域,输入中心与规则不变。白字增加短暗边和柔光,深色数字使用淡渐变与固定微颗粒;生成缓存不作为外部图片资源。
按钮重新调整实体图标轮廓、视觉尺寸与文字留白,短标签使用随包的站酷快乐体改名子集 `NumberSwipePlayful.ttf`,字体许可见 `fonts/zcoolkuaile-OFL.txt`。规则和正文继续用Noto Sans SC。当前有5个字体子集、3份许可;字体源及输出哈希见manifest。
33项模块测试、200关模拟触摸与200关浏览器提示像素检查通过。新版包为 `../releases/NumberSwipe-campaign-200-r2-明亮教具室-投影字效精修版-20261021.zip`;微信手机的显示、字体和生成材质耗时仍需新包真机预览。
以下为历史修订记录。
### 参考图圆润材质修订(v2)
按用户随后提供的效果图重做主游戏区:浅紫 `#8178F4`、暖奶油 `#F9EDD5`、金黄背景;移除固定数字框、四角框、双色访问内框和常驻块轮廓线。现在使用 `src/bright-classroom/cushion.mjs` 从几何距离场生成柔光倒角、下缘暗部和软投影,所有缓存像素均在运行时生成,未引入图片资源。按钮使用同一圆润材质和实心玩具图标;桥接缩细,简单盘面按可用面积放大。
本次用户参考图优先于早期报告的薄描边与低渐变建议,对应参数集中在设计令牌 `referenceMatch`。密集盘的小号白数字仍采用较深紫面。规则所需的墙、通道、类别符号继续保留,操作和关卡不变。
新版导入包:`../releases/NumberSwipe-campaign-200-r2-明亮教具室-参考图圆润版-20261021.zip`。以下为第一版说明及历史记录;当前执行30项模块测试。
当前 `game.js` 已应用暖黄背景、奶油舞台、轻厚度块体、紫色路径、程序图标和圆角按钮。方形、大块异形、六边、通道、双色、奇偶与墙门共用新渲染器;规则教学同步换皮。所有游戏美术均由Canvas路径生成,不使用图片资源。字体为随包提供的Nunito数字800与Noto Sans SC中文500/600/700子集,许可证、字集和SHA-256见 `fonts/`。
200关内容、顺序、原规则、微信存档key、逐格回退、撤销、分段提示及手动下一关保持原行为。双色已走保留蓝橙面与圆/三角;提示使用真实共边和块内引导,遮罩避让玩家块面与关键标注。新增选关页“设置”中的减少动态开关,保存于本地;静态页面和无动画的思考阶段停止RAF。
直接导入本目录可运行,或解压 `../releases/NumberSwipe-campaign-200-r2-明亮教具室-20261021.zip` 后导入。包含 `game.js`、两个配置文件、`audio/`、`fonts/`,无需构建npm。不要使用旧二维码判断新皮肤是否生效,应重新编译并生成预览。
开发预览:在本目录执行 `node qa/serve.mjs`,打开 [明亮教具室试玩](http://127.0.0.1:17859/qa/bright-classroom.html),可切换全部200关和五种屏幕尺寸;画前几步/完成本关是验收页工具,正式游戏不提供自动解题按钮。
验证命令:
```powershell
node build.mjs
node --test bright-classroom.test.mjs hints.test.mjs lessons.test.mjs audio-copy.test.mjs
node smoke.mjs
```
29项模块测试通过;模拟微信200关唯一解与触摸通关通过,覆盖教学隔离、四种手机尺寸、减少动态存档、静态停止绘制和字体装载调用。浏览器200关已走格采样像素在提示前后及撤销重画后保持一致。自动检查与浏览器预览不代表微信手机实测;仍需手机验收字体加载、密集关手势、音频与帧率。
美术接入在 `bright-classroom-build.mjs` 与 `src/bright-classroom/`,上游四份快照未修改。重建依赖本仓库的 `docs/design/bright-classroom/tokens.json` 与原型内esbuild安装。字体无需在普通构建时重做;补充字集时用 `build-fonts.py`,读取 `.font-sources/` 的上游字体(构建专用),生成改名子集及manifest。来源:[Nunito](https://github.com/google/fonts/tree/main/ofl/nunito)、[Noto Sans SC](https://github.com/google/fonts/tree/main/ofl/notosanssc),许可随资源保留。
以下为此前版本的变更记录。
本包源于 G:\项目\NumberSwipe-Variants\campaign-200-r2,版本 scored-2026-10-15-r2。完整保留原版 200 关及 playOrder,不混入原 NumberSwipe 项目关卡,也不重新生成关卡。
## 导入
微信开发者工具 → 小游戏 → 导入本目录(含 game.js / game.json / project.config.json)。已经构建,不需要“构建 npm”。项目沿用 Variants 根配置中的小游戏 AppID;需使用有权限的微信账号登录,才能真机预览和上传。本次未执行上传。
内容包含原有基础、大块/异形、墙、通道、双色交替、奇偶、节奏门及六边盘面。保留 20 章顺序、全部选关、可关闭教学规则、手势插值、逐格回退、撤销、重开、指路、通关后手动下一关,以及原版半音音阶音效。音频改用随包提供的41个WAV文件和 wx.createInnerAudioContext 播放,保留半音上行与五类反馈音;关闭音效会立即停止,重新开启会播放试听音。微信版不再依赖浏览器振荡器。
选关界面用分页适配微信 Canvas。反馈通过微信原生输入弹窗填写;导出记录保存到小游戏用户目录,并在支持时打开微信文件分享界面。玩法、判定和盘面绘制仍直接使用 r2 模块,通过轻量宿主接口适配,不加载 WebView。
## 来源与验证
src/upstream 下 levels.json、rules.mjs、ui.mjs、audio.mjs 是上游原文件的完整快照,source-manifest.json 记录原始路径及 SHA-256。构建为 ui.mjs 注入微信宿主,替换音频、规则文案和浏览器下载记录入口。上游四个快照文件保持不变。发布包没有 design.json、作者解及反例文件。
存档 key 延续 variant:campaign-200-r2:v1,保存于微信本地;浏览器和微信存储彼此独立,不会自动迁移网页进度。
validation.json:200/200 唯一解及模拟微信触摸通关,撤销/重开/指路,前后台循环和存档/音效偏好恢复。原 r2 的 8 项测试也全部通过。尚需在微信开发者工具和手机检查显示、音效听感及平台兼容性;模拟验收不等于真机验收。
## 重建
此目录 src 保存完整适配源码。build.mjs 默认引用原 NumberSwipe 工程的 esbuild;如独立移动目录,需要将该 import 改为本机可用的 esbuild。运行 node build.mjs 后重新打包。smoke.mjs 会读 source-manifest.json 指定的上游路径检查哈希,然后对实际 game.js 做 200 关模拟验收。
## 真机音效及文案修订(2026-10-19)
改用微信原生音频播放器与本地音效资源;关闭后再次点“音效开”会播放试听音。新增6项音频/文案回归检查。进后台与系统音频中断时停止音效和延迟播放。错误不再静默忽略,可从只读状态的audio字段查询。
十类教学说明及组合规则采用具体动作与例子,例如“共边”改为“边挨着边,不能斜着走”,“奇偶”说明第1、3、5步与第2、4、6步。复杂规则可以分页阅读。关卡的布局、约束、顺序与原有微信存档保持不变。
修改后需要在开发者工具重新编译并重新生成真机预览,旧二维码可能仍加载旧包。自动测试验证播放调用和非静音音频资源,手机上的实际听感仍需真机确认。
## 通关按钮布局修订
通关后“下一关”独占底部操作栏,替换重开/撤销/指路;最后一关显示“返回选关”。盘面、反馈文字与操作栏固定分区,按钮出现时不挪动盘面。验证320×568、375×667、390×844、414×896四种尺寸下无按钮/盘面遮挡,且能正确跳转。
## 关卡动画教学(2026-10-19)
首次机制教学和手动规则页加入真实教学关卡画面,复用正式盘面绘制与规则判定,不制作独立示意图。文字说明与逐步动画同步,黄色光圈表示演示位置;墙和同色展示红色阻挡线,通道及奇偶展示变红、退回及正确路线。节奏门演示切换、穿过锁定及撤销穿门。支持重播、逐段阅读。组合关按实际出现的机制拆段说明,使用对应极简教学关,避免提前展示正式关解法。
演示独立于玩家路线、提示次数、存档及音效;关闭页面继续原关。后台停止绘制,返回时从头演示。新增 lessons.test.mjs 的11项教学检查;smoke.mjs 检查动画前后路线、错误状态、棋盘坐标和通关状态不变,并验证200关及窄屏规则页。仍需在微信开发者工具和手机检查实际动画与排版。
### 教学动画提速
整段演示固定约1秒,逐帧连续连线,去掉每格停顿与重复静止步骤。演示结束后停留在结果,点击重播可再次观看。
## 高亮分段指路与玩家填色(2026-10-20)
恢复高对比提示:点击“指路”直接显示最早未正确完成的一段,按起点、数字点、方向点、奇偶点和终点分段。提示为半透明白线、深色描边及连续流动白色光点;正确到达段末后消失,再次点击才显示下一段,暂不接广告。
提示使用独立透明 Canvas,每帧只清空提示层,并从中擦除玩家已经走过的格子区域;已画的连接段不再叠加提示。玩家棋盘及整格填色、数字、方向反馈继续由原绘制代码负责,双色关保留原有蓝/橙格色。撤销、重开会按实际路线恢复相应提示。
构建:node build.mjs。检查:node --test hints.test.mjs lessons.test.mjs audio-copy.test.mjs(23项),node smoke.mjs(200关,并比较点击指路前后玩家填色数据)。浏览器像素回归:node qa/serve.mjs 后打开 http://127.0.0.1:17859/qa/hints.html,检查200关前两步的已走格填色采样像素在开启提示、撤销再画前后相同;qa/hint-player-fill.png 为浏览器检查截图,不能作为微信真机截图。独立导入包通过 ./package.ps1 生成,只含运行必需文件。真机显示仍需手机预览确认。
当前版本保留200关、原存档兼容、分层音效、BGM、振动设置、三列选关及流动指路。真机听感、振动与性能仍需微信预览验收。
+16 -5
View File
@@ -1,6 +1,17 @@
import fs from 'node:fs';import path from 'node:path';
// 原版音效保持不变;路径点新增同音色的短C大调庆祝音。
export function buildAudio(root){const out=path.join(root,'audio');fs.mkdirSync(out,{recursive:true});const cues={checkpoint:[[523,.065,0],[784,.38,.05],[392,.25,.065]],correct:[[659,.09,0],[880,.14,.075]],error:[[330,.10,0],[247,.15,.09]],undo:[[587,.07,0],[440,.10,.055]],restart:[[523,.075,0],[392,.09,.065],[523,.11,.14]],complete:[[392,.10,0],[494,.10,.075],[587,.13,.15],[523,.65,.26],[659,.55,.28],[784,.48,.30],[196,.18,0,.28],[261.5,.22,.075,.25],[392,.24,.15,.25]]};for(let n=1;n<=36;n++)cues['move-'+n]=[[523*Math.pow(2,(n-1)/12),.07,0]];
for(const [name,tones]of Object.entries(cues)){const rate=22050,length=Math.ceil((Math.max(...tones.map(([,d,t])=>t+d))+.03)*rate),b=Buffer.alloc(44+length*2);b.write('RIFF');b.writeUInt32LE(b.length-8,4);b.write('WAVEfmt ',8);b.writeUInt32LE(16,16);b.writeUInt16LE(1,20);b.writeUInt16LE(1,22);b.writeUInt32LE(rate,24);b.writeUInt32LE(rate*2,28);b.writeUInt16LE(2,32);b.writeUInt16LE(16,34);b.write('data',36);b.writeUInt32LE(length*2,40);for(let i=0;i<length;i++){const t=i/rate;let sample=0;for(const [f,d,offset,weight=1]of tones){const u=t-offset;if(u>=0&&u<d){const themed=name==='checkpoint'||name==='complete',gain=themed?.20:name==='error'?.24:.32;
const env=u<.008?u/.008:Math.pow(.0001/gain,(u-.008)/(d-.008));
sample+=(Math.sin(2*Math.PI*f*u)+(themed?.08*Math.sin(4*Math.PI*f*u)*Math.exp(-u/.05):0))*gain*env*weight;}}b.writeInt16LE(Math.round(Math.max(-1,Math.min(1,sample))*32767),44+i*2);}fs.writeFileSync(path.join(out,name+'.wav'),b);}}
export function buildAudio(root){
const out=path.join(root,'audio');fs.mkdirSync(out,{recursive:true});
const cues={ui:[[659,.055,0],[784,.06,.025]],hint:[[523,.09,0],[659,.10,.055],[784,.13,.11]],correct:[[659,.11,0]],error:[[330,.09,0],[247,.09,.045]],undo:[[440,.065,0]],restart:[[523,.08,0],[392,.09,.06]],complete:[[261.6256,.96,0,.8],[329.6276,.68,0,.62],[392,.62,0,.48],[523.251,.5,0,.16]],checkpoint:[[523,.14,0]]};
for(let n=1;n<=36;n++){const f=523*Math.pow(2,(n-1)/12);cues['move-'+n]=[[f,.09,0]];cues['checkpoint-'+n]=[[f,.14,0]];}
for(const [name,tones]of Object.entries(cues)){
const rate=22050,length=Math.ceil((Math.max(...tones.map(([,d,t])=>d+t))+.02)*rate),b=Buffer.alloc(44+length*2);
b.write('RIFF');b.writeUInt32LE(b.length-8,4);b.write('WAVEfmt ',8);b.writeUInt32LE(16,16);b.writeUInt16LE(1,20);b.writeUInt16LE(1,22);b.writeUInt32LE(rate,24);b.writeUInt32LE(rate*2,28);b.writeUInt16LE(2,32);b.writeUInt16LE(16,34);b.write('data',36);b.writeUInt32LE(length*2,40);
for(let i=0;i<length;i++){const t=i/rate;let sample=0;for(const [f,d,offset,weight=1]of tones){const u=t-offset;if(u<0||u>=d)continue;
const move=name.startsWith('move-'),node=name.startsWith('checkpoint'),attenuation=1/(1+Math.pow(f/2300,1.35));
const gain=(name==='complete'?.20:node?.38:move?.28:.23)*attenuation*weight;
const tau=name==='complete'?.11:node?.032:move?.022:.025;
const env=(1-Math.exp(-u/(node?.0015:.003)))*Math.exp(-u/tau)*Math.min(1,(d-u)/.008);
sample+=(Math.sin(2*Math.PI*f*u)+.10*Math.exp(-u/.012)*Math.sin(4*Math.PI*f*u))*gain*env;
}b.writeInt16LE(Math.round(Math.max(-1,Math.min(1,sample))*32767),44+i*2);}fs.writeFileSync(path.join(out,name+'.wav'),b);
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import path from 'node:path';import {fileURLToPath} from 'node:url';import {CasualAudio,stopAudio,audioDiagnostics} from './src/audio.mjs';import {tutorials,rulesFor,playerText} from './src/copy.mjs';
const root=path.dirname(fileURLToPath(import.meta.url));
function setup(){const played=[],contexts=[],storage=new Map(),options=[];globalThis.wx={setInnerAudioOption:o=>options.push(o),showToast:()=>{},createInnerAudioContext:()=>{const a={stops:0,stop(){this.stops++;},play(){assert.ok(fs.existsSync(path.join(root,this.src)));played.push(this.src);},onError(fn){this.error=fn;},onPlay(fn){this.playing=fn;}};contexts.push(a);return a;}};const sound=new CasualAudio({}, {getItem:k=>storage.get(k),setItem:(k,v)=>storage.set(k,v)},'sound');return {sound,played,contexts,storage,options};}
test('手机没有浏览器音频接口时,原生播放器仍能播放全部 36 步和六类反馈音',()=>{const h=setup();for(let step=1;step<=36;step++)h.sound.play('move',0,step);for(const kind of ['correct','error','undo','restart','complete','checkpoint'])h.sound.play(kind);assert.equal(h.played.length,42);assert.equal(h.contexts.length,4);assert.ok(h.options.every(o=>o.obeyMuteSwitch===false));assert.ok(h.contexts.every(a=>a.obeyMuteSwitch===false));});
test('手机没有浏览器音频接口时,原生播放器仍能播放全部 36 步和六类反馈音',()=>{const h=setup();for(let step=1;step<=36;step++){h.sound.lastMove=-Infinity;h.sound.play('move',0,step);}for(const kind of ['correct','error','undo','restart','complete','checkpoint']){h.sound.stop();h.sound.play(kind);}assert.equal(h.played.length,42);assert.equal(h.contexts.length,4);assert.ok(h.options.every(o=>o.obeyMuteSwitch===false));assert.ok(h.contexts.every(a=>a.obeyMuteSwitch===false));});
test('关闭音效停止播放;再次开启会试听并保存偏好;后台中断停止所有播放器',()=>{const h=setup();h.sound.play('move');h.sound.toggle();const count=h.played.length;h.sound.play('move');assert.equal(h.played.length,count);assert.equal(h.storage.get('sound'),'off');h.sound.toggle();assert.equal(h.played.at(-1),'audio/correct.wav');assert.equal(h.storage.get('sound'),'on');const stops=h.contexts.map(a=>a.stops);stopAudio();assert.ok(h.contexts.every((a,i)=>a.stops>stops[i]));});
test('延迟通关音在静音或进入后台时取消,不在返回后突然播放',async()=>{const h=setup();h.sound.play('complete',.02);h.sound.toggle();await new Promise(resolve=>setTimeout(resolve,40));assert.equal(h.played.length,0);});
test('音频错误可诊断,不再静默吞掉',()=>{const h=setup();h.sound.unlock();h.contexts[0].error({errCode:10003,errMsg:'test missing audio'});assert.equal(audioDiagnostics.lastError.errCode,10003);});
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More