# saolei **Repository Path**: lp9906/saolei ## Basic Information - **Project Name**: saolei - **Description**: 基于 TapTap 小游戏平台、使用 JavaScript Canvas 2D 实现的经典扫雷游戏。采用事件驱动和插件架构设计,通过 Plugin、Page、Layout 三层机制实现模块化开发。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-08-18 - **Last Updated**: 2026-09-02 ## Categories & Tags **Categories**: games **Tags**: taptap, vite-native-ts, game, Minesweeper, Canvas ## README # 扫雷(Minesweeper) 基于 TapTap H5 小游戏平台、使用 **TypeScript + Vite + Canvas 2D** 实现的经典扫雷游戏。采用事件驱动和插件架构设计,通过 Plugin、Page、Layout 三层机制实现模块化开发,内置棋盘缩放/平移、长按滑动扫雷、辅助功能(快速清除/自动标雷)与自定义难度。 --- ## 项目结构 ``` saolei/ ├── index.html # Vite 入口(#gameCanvas + module script) ├── game.json # TapTap 游戏清单(appId、横屏、无状态栏) ├── project.config.json # TapTap 项目配置 ├── package.json # scripts:dev / build / preview / typecheck / package ├── vite.config.ts # 构建配置(outDir=dist,入口产物固定名 game.js,base="./") ├── tsconfig.json # TS 严格模式配置 ├── scripts/package.mjs # 零依赖打包脚本(dist+配置 → packages/saolei_v1.x.x.zip) ├── scripts/scan-normalize.mjs # 规范化自动扫描(npm run scan:命名/体积/模式残留检查) ├── src/ │ ├── main.ts # 启动入口(try/catch + 全局错误捕获) │ ├── settings.ts # 会话级设置(快速清除 / 自动标雷 / 滑动扫雷、缩放平移视图记忆、自定义难度记忆,跨重开保留) │ ├── core/ # SLEvent.ts、SLGame.ts │ ├── plugins/ # SLPlayer、SLConfig、SLRender、SLTexturePlugin、SLSavePlugin │ │ └── control/ # 控制类插件:SLControl、SLClickRegion、SLGestureControl、 │ │ # SLDigControl、SLZoomControl、SLMoveControl、SLSlideSweepControl │ ├── textures/ # 纹理系统:SLTexture(核心)、DrawUtil(绘制原语)、 │ │ │ # Textures(注册表:kind → 工厂) │ │ └── factories/ # 纹理工厂拆分:SLTextureFactoryBase(基类:kind/build/主题访问)、 │ │ # CellTextures(单元格 7 类)、StatusBarTextures(状态栏 6 类)、 │ │ # HomeTextures(首页按钮)、TextTexture(文本) │ ├── audio/ # WebAudio 音频分层:SLAudioEngine(基础工具:上下文/输出链/单音合成/节流)、 │ │ # SLAudio(音效层:开启格子/失败/成功/按钮,柔和包络) │ ├── taptap/ # TapTap 平台封装:Environment(环境判断)、Leaderboard(官方排行榜)、 │ │ # Save(云存档)、Updater(版本更新检查,window.tap 环境判断) │ ├── saves/ # 存档与记录:SaveBase(基类接口)、LocalStorageSave(本地存储)、 │ │ # Records(通关记录 + 排行榜记录:条目/得分标准/辅助标记/排序与存档替换) │ ├── layouts/ # SLBaseLayout、SLBackgroundLayout、SLHomeTitleLayout(首页标题)、 │ │ # SLHomeButtonLayout、 │ │ # SLHomeCustomButtonLayout / SLHomeRecordsButtonLayout / SLHomeRankingButtonLayout、 │ │ # SLVersionLabelLayout(首页版本号)、 │ │ # SLBoardCellLayout、SLBoardStatusBarLayout、SLBaseListLayout(记录列表基类)、 │ │ # SLWinRecordsListLayout、SLRankingListLayout │ │ └── statusbar/ # 状态栏子项拆分:SLStatusBarItem(统一接口+基类)、雷数/计时面板、 │ │ # 模式切换、重开笑脸、设置按钮、设置面板(内部设置行:开关/主题/音量/ │ │ # 返回首页;说明 tip 封装为 SLSettingHelpTipItem) │ ├── pages/ # SLBasePage、SLBaseListPage(记录列表页基类)、SLHomePage、SLBoardPage、 │ │ # SLWinRecordsPage、SLRankingPage │ │ └── board/ # 棋盘页拆分:SLBoardView(几何/缩放/平移)、SLBoardRenderer(动态纹理渲染) │ ├── logic/ # 对局逻辑拆分:SLBoardLogic(编排:棋盘/翻开/flood fill/胜利检测)、 │ │ # SLMineField(雷盘生成/相邻计数)、SLBoardTimer(计时)、 │ │ # SLBoardAssist(快速清除/自动标雷)、SLWinRecorder(胜利结算记录) │ ├── types/ # SLTypes.ts(接口与事件)、TapTapTypes.ts(SDK/窗口尺寸类型) │ └── utils/ # ObjectUtil、ColorUtil、LogUtil、DomUtil、MathUtil ├── dist/ # Vite 构建产物(gitignore) ├── packages/ # 发布 zip(gitignore) ├── images/ # 截图与录屏素材 ├── images_ai/ # AI 生成图标提示词(icon.txt) └── LICENSE # MIT ``` ## 技术选型 | 类别 | 选型 | |------|------| | 语言 | TypeScript(`strict` + `noUnusedLocals` 等严格检查) | | 构建 | Vite(`vite build` → `dist/`,入口产物固定名 `game.js`,`base:"./"` 适配子路径托管) | | 运行平台 | TapTap H5 小游戏(**H5 原生 API**:`window`/`document`/Canvas;可选接入 `window.tap` SDK——存在时启用 TapTap 云存档与 **TapTap 官方排行榜**(`getLeaderboardManager`),否则浏览器 localStorage + 本地榜单) | | 渲染 | Canvas 2D | | 打包 | `npm run package` → `packages/saolei_v1.x.x.zip`(解压后仅一个 `saolei/` 文件夹,含入口 `index.html`) | | 开发服务器 | `vite dev`(本地热更新) | --- ## 核心功能 - **插件化架构**:`SLPlayer`(游戏循环)、`SLConfig`(窗口管理)、`SLRender`(渲染)、`SLTexturePlugin`(纹理缓存)、`SLSavePlugin`(全局设置存档)、`SLControl`(事件适配与分发)、`SLGestureControl`(手势识别)、`SLDigControl`(排雷控制)、`SLZoomControl`(缩放控制)、`SLMoveControl`(移动控制)、`SLSlideSweepControl`(滑动排雷控制) - **双事件总线**:`SLGame.event`(系统级事件)与 `SLGame.pageEvent`(页面级事件)分离,页面切换时一键销毁页面事件 - **页面/布局渲染树**:`SLPage` 包含多个 `SLLayout`,每帧由 `SLRender` 驱动绘制 - **经典扫雷逻辑**:地雷生成、相邻计数、栈式展开(flood fill)、胜利/失败判定;**雷盘懒初始化**——进入棋局不布雷,**首次翻开格子时才生成**,且**首开格及其 8 邻居排除布雷**(经典安全开局,避免开局踩雷;雷数接近满盘时退化为仅首开格非雷) - **Canvas 2D 渲染**:单元格凸起/扁平/红色背景三种样式,覆盖 6 种状态;地雷为炸弹图标(渐变弹体 + 高光 + 引线),失败时错误标记格叠加红叉 - **顶部状态栏**(56px):剩余雷数面板(LCD 风格)、计时面板、**模式切换按钮**(扫雷/标记纯图标,**屏幕居中**)、**重开笑脸按钮**(**圆形按钮,左侧雷数面板旁**——与居中模式按钮拉开距离防误触;点击重新开始当前难度;**可在设置面板内开关显示**,默认开启)、**设置按钮**(齿轮) - **设置面板**:`快速清除`(点击数字格且相邻标记数≥数字时,自动展开其余未开格)、`自动标雷`(数字格未开格数=剩余雷数时自动标记;翻开/新增标记后对相邻数字格**复查**并**级联**触发,不漏标)、`滑动扫雷`(长按数字格滑动连扫的开关)三项开关 + **主题下拉框**(经典/Win10)+ **音量滑块**(点击轨道任意位置调节 0~100%,百分比实时显示,随存档保存)+ **返回首页**操作;**设置项帮助**:每项名称左侧**叹号圆形图标**,点击展开**带主题背景的说明 tip 弹窗**(面板下方,标题 + 自动折行正文,解释辅助功能的能力与用法、主题与音量的含义;再点叹号/点 tip/操作其它行关闭);**默认全部开启**,**会话级持久化**(重开棋盘不重置) - **自定义难度**:首页"自定义"按钮输入宽度(列)/高度(行)/雷数(校验:列/行 ≥2、雷数 1~格子数-1,无尺寸上限——支持 300×300 等超大棋盘);**使用过的配置记入全局设置并随存档保存**——输入默认值回填上次配置、按钮副标题展示当前配置(`行×列 · 雷数`,与其他难度按钮同格式) - **通关记录**:胜利时自动记录(**难度/用时/辅助功能使用标记/通关时间**,最新在前,最多保留 20 条,随存档保存);首页"通关记录"按钮进入记录页,**拖动滚动浏览**(右侧滚动条指示位置),右上角返回首页;**辅助功能使用判定锚定"打开单元格触发事件"**——只要某次打开/展开经由辅助功能路径执行即记录,flag 位表示:`0x1` 快速清除(chord 展开)、`0x10` 自动标雷(实际标记)、`0x100` 滑动扫雷(滑动驱动翻开/chord),可组合;记录行展示辅助标记文本(无辅助显示"无") - **排行榜**:胜利时同一局按**得分标准**入榜——**每个雷 10 分,每使用一个辅助功能减 1 分**(辅助功能默认全部开启,关闭辅助通关可得满分;得分下限钳制 0);**TapTap 环境只保留 TapTap 官方排行榜**——胜利后自动提交云榜(不再维护本地榜),首页排行榜按钮副标题展示"TapTap 官方榜",排行榜页拉取**云榜前 10 名 + 我的排名**展示(行内名次/昵称/得分,**前三名名次与得分金色高亮、我的排名行背景高亮**),头部 **"▶ TapTap 官方排行榜"按钮**拉起 SDK 官方排行榜面板(**打开失败时按钮变红提示"打开失败(错误码),点击重试"**);仅无 TapTap 环境(H5)降级展示本地榜单(得分降序/同分用时升序,最多 20 条,随存档保存);样式与通关记录页一致(共用 `SLBaseListPage` + `SLBaseListLayout` 基类)。**排行榜接入前置条件**:① 基础库 **≥ 1.8.0**(低版本自动降级本地榜);② 后台创建并启用排行榜,`src/taptap/Leaderboard.ts` 顶部 `LEADERBOARD_ID` 已配置为实际 ID(当前 `r196zs8wp2729mhgl9`;更换榜单时同步修改);③ 后台勾选 **"好友关系"隐私权限**(小游戏管理->基础配置->协议配置->小游戏隐私保护指引->增加信息类型)——排行榜接口依赖该权限,缺失时 `openLeaderboard`/榜单查询回调失败码 - **全局主题(经典 / Win10)**:**两套主题**——`src/theme/Themes.ts` 集中定义视觉 token(页面背景/状态栏/LCD 面板/单元格/数字配色/设置面板/开关/滚动条):**经典**(灰 3D 凸起单元格 + 深色 LCD 面板 + 红字计时)、**Win10**(浅灰蓝扁平单元格 + 浅色面板 + 蓝色点缀,参考 Win10 扫雷);棋盘页设置面板内 **"主题"下拉框**切换(展开选项列表,选中项高亮 ✓),**随存档保存**;纹理 key 自动含主题名(切换即重建缓存),棋盘动态纹理签名含主题维度(切换即时整盘重绘),状态栏/背景/滚动条每帧读主题即时生效;记录类页面(通关记录/排行榜)保持独立深色设计不随主题 - **合成音效**:**纯 WebAudio 实时合成**(无音频资源),音色偏柔和(正弦/三角波 + 快速起音 + 指数衰减 + 低音量,不刺耳)——**开启格子**轻微咔哒(短促高频)、**失败**登登登(三音下行)、**成功**登等(两音上行长收尾)、**按钮触发**咔哒(短促中频,首页/状态栏/记录页按钮);**音量可调**:设置面板滑块(0~100%,点击轨道调节,百分比显示),实际音量 = 基础音量 × 全局倍率 × 10 并**限幅 0.95**(响度接近满量程且无削波失真,滑块全程线性),随存档保存;**首次播放零延迟**——启动时 `warmup()` 预创建 AudioContext、首个手势内 resume 解锁自动播放策略、首声等待 resume 完成再调度(不丢音);**连发不破音**——主增益 + 动态压缩器(叠加自动压限防削波)+ 同音效节流(滑动扫雷逐格连发丢弃极短间隔重复)+ 活动音效数上限;环境不支持/合成失败时静默 - **棋盘缩放/平移**:缩放倍数锚定为**屏幕宽度展示的单元格个数**(1 倍=横向 5 格,10 倍=横向 50 格,**默认 4 倍=横向 20 格**),格尺寸 = 宽度 ÷ (5 × 倍数),**与棋盘尺寸无关**(棋盘不再按尺寸计算适配缩放);超宽/过高棋盘初始显示左上角,单指/双指拖动平移(**边界钳制**保留 20px 安全边距;**棋盘任一出超出显示区域即可平移,与缩放倍数无关**——1 倍缩放下的大棋盘同样可拖动);缩放后细条**只读滚动条**展示显示范围(棋盘超出显示区域时显示,滑块位置按**实际平移行程**换算,行程=棋盘−窗口+2×边距,与边界钳制范围一致,全程移动不失真) - **全局设置存档**:`SLSavePlugin` 自动存档全局设置(辅助功能开关 + 缩放平移记忆 + **自定义难度配置** + **通关记录** + **排行榜**)——**环境判断**:`isTapTapEnvironment()`(`src/taptap/Environment.ts`)→ TapTap 云存档(`taptap/Save.ts`),否则浏览器 localStorage(`LocalStorageSave`),后端均实现 `SaveBase` 基类接口(save/load/clear);**TapTap 后端三级冗余**:云端槽位(create/update/download)+ 本地文件(每次保存必写)+ localStorage 兜底(与 H5 共用 key)——**保存**写本地文件 + 双写 localStorage + 上传云(云失败仅日志不阻断);**加载**云端优先(跨设备最新)→ 云失败回退本地文件 → 再回退 localStorage,云故障/未登录/后台未开云存档时数据仍可恢复,不再"存档失效";**加载**:启动时异步读取,版本校验后合并进设置——**字段级校验钳制**(开关须 boolean、音量 0~1、缩放 1~10、平移比例 0~1,类型非法/越界/非有限值回退默认;自定义难度钳制校验、通关记录与排行榜记录非法项过滤并按序重排)并通知棋盘页重应用视图记忆;**保存**:设置变更防抖(300ms)自动保存、页面切换与页面隐藏(pagehide/切后台)兜底保存 - **主菜单**:**两栏布局**(3 行 × 2 列,按钮宽 = 屏宽 × 0.42、栏间距 0.05 屏宽,高 1.7 份屏高)——四档难度按钮(简单/中等/困难/自定义)+ **通关记录** + **排行榜** 共 6 个按钮铺满屏 - **鼠标滚轮缩放**:桌面端滚轮(含触控板双指滚动/捏合,Ctrl+滚轮)与双指捏合走**同一缩放管线**,行为一致(1~10 倍钳制、**以手势位置为缩放锚点**——捏合以两指中点、滚轮以鼠标位置为轴,超大棋盘(如 300×300)缩放不锚定左上角、展示只读滚动条;滚轮上滚/张开手指=放大=倍数减小) - **长按交互**:长按 500ms(单指/多指静止),动作与当前模式**互换**(排雷模式长按=标记,标记模式长按=排雷);长按触发后不再产生 click;捏合移动不误触长按 - **滑动扫雷**:长按**已翻开数字格**进入滑动扫雷模式,手指滑动到哪个格子即触发排雷动作(未开格直接翻开、数字格触发快速清除 chord——**相邻标记数≥数字**才展开其余未开格,判定逻辑与快速清除一致:标记不足时周围仍有未知雷,绝不展开);**无去重缓存**——翻开/展开天然幂等(重复经过为空操作),判定被拦截的数字格每次经过都重新评估,随滑动展开/自动标雷满足判定后立即触发;进入的同时 press 也触发一次排雷(起始数字格同样执行 chord);松开手指结束;可在设置面板开关,长按其余格子仍为动作互换 - **缩放/平移记忆**:缩放倍数**全局存储**(1~10 倍,锚点为横向单元格个数,与棋盘尺寸无关,跨棋盘格大小天然一致),返回首页/切换棋盘后恢复;平移按**实际行程比例**换算(0~1:0=起始端可见、1=末端可见,行程=棋盘−窗口+2×边距),重开棋盘/页面切换后恢复(适配不同尺寸棋盘,越界自动钳制) - **触控优化**:`preventDefault` + 时间窗抑制浏览器合成鼠标事件(防双击)、点击回调延迟一帧防页面切换事件穿透;`touchcancel`/窗口失焦时**彻底清理触摸会话状态**(含滑动扫雷会话),避免残留导致后续触发失效 - **纹理系统**:**全部可复用 canvas 渲染代码封装为纹理**(`src/textures/`:绘制原语 `DrawUtil` + 定义注册表 `Textures`),组成方式两种——**canvas JS**(单元格各状态/首页按钮/状态栏面板·齿轮·笑脸·模式按钮·开关·设置面板等)或**图片资源**(纹理定义支持 image 字段,URL 或经 vite 引入的打包资源);**文本同样纹理化**(`text` 纹理按 text+font+color 缓存、measureText 测量宽度,每帧 drawImage 而非 fillText 重栅格化);`SLTexturePlugin` 统一构建与缓存(静态纹理按 key 参数化缓存、棋盘为**动态纹理**按**可见区域 + 单元格状态指纹 FNV-1a 聚合签名**检测变化后重建——只渲染屏幕可见部分,纹理尺寸 ≤ 窗口,棋盘超出 Canvas 尺寸/面积上限时画布也不会失效;指纹为紧凑数值位编码,逐格累加哈希,无每帧大字符串拼接),**布局不再持有单独渲染缓存**;仅全宽背景/滚动条等**动态几何**保持直接绘制 - **页面级区域分派**:`SLClickRegion` 插件维护每页 1 个点击区域对象,触控按坐标解析分派(雷区 O(1) 编号定位、按钮/状态栏矩形分派) --- ## 对象模型 ### 核心类 | 类 | 说明 | |----|------| | `SLGame` | 核心游戏类。创建 Canvas(`#gameCanvas`)与 2D 上下文,持有 `event`(系统总线)和 `pageEvent`(页面总线),管理插件生命周期,提供 `resetPageEvents()` | | `SLEvent` | 泛型事件总线。支持 `register`(覆盖注册,同一事件名仅保留一个回调)、`listen`(追加多监听)、`unlisten`(按回调移除监听,页面/布局销毁时清理)、`remove`、`trigger` | | `gameSettings` | 会话级设置对象(快速清除/自动标雷/滑动扫雷),跨棋盘重开保留 | | `boardViewMemory` | 会话级视图记忆(单元格尺寸锚点 + 平移比例),返回首页/切换棋盘后恢复 | | `SLTexture` | 纹理核心。离屏画布 + 定义(canvas JS 绘制函数或图片资源),由纹理插件按 key 构建缓存,每帧 drawImage 复用 | ### 工具类(静态方法,无实例状态) | 类 | 静态方法 | 说明 | |----|----------|------| | `ObjectUtil` | `clone(obj)` | 浅拷贝对象(`{...obj}`) | | `ColorUtil` | `lightenColor(hex, ratio)` | 将 `#rrggbb` 颜色向白色混合,用于按钮渐变高光 | | `LogUtil` | `logStartupError(message, error)` / `registerGlobalErrorHandlers()` | 错误日志:控制台输出 + 页面红色面板;注册 window error / unhandledrejection 全局捕获 | | `CanvasDrawUtil`(`textures/DrawUtil.ts`) | `roundRectPath` / `drawText` / `drawTextTexture` / `drawRaisedRect` / `drawFlatRect` / `drawFlag` / `drawMine` / `drawCrossMark` / `drawLine` / `fillCircle` | 纹理工具的通用绘制原语:圆角矩形、文本(`drawTextTexture` 以纹理方式绘制)、3D 凸起/扁平矩形、旗帜、炸弹地雷图标、红叉标记、线段(moveTo+lineTo+stroke)、实心圆(arc+fill) | | `MathUtil`(`utils/MathUtil.ts`) | `minMax(value, min, max)` / `inRange(value, min, max)` | 数值工具:钳制数值到 `[min, max]` 区间(缩放倍数/平移比例/音量/滚动范围等双侧钳制统一封装);闭区间命中判定(`a ≥ min && a ≤ max`,状态栏/按钮/列表区域命中统一封装) | | `DomUtil` | `getWindowInfo` / `createCanvas` / `resizeCanvas` | H5 原生:窗口尺寸、画布获取与窗口同步 | ### 插件(Plugin,实现 `SLPluginI`) | 插件 | 职责 | |------|------| | `SLPlayer` | 游戏循环驱动。`window.requestAnimationFrame` 递归触发 `loop` 事件,`stop()` 返回 Promise 优雅停止 | | `SLConfig` | 窗口配置。监听 `window.resize`,维护 `windowSize` 并触发 `onWindowSizeChange` | | `SLRender` | 渲染引擎。监听 `loop` 清屏 + 绘制当前 Page;监听 `gotoPage` 切换页面(旧页 `destroy()` + `resetPageEvents()`);提供 `getCurrentPage` 查询当前页面 | | `SLTexturePlugin` | 纹理插件。统一构建与缓存纹理(**canvas JS 或图片资源**两种组成方式,key 含参数缓存复用);棋盘为**动态纹理**(**可见区域 + 单元格状态指纹 FNV-1a 聚合签名**检测变化后重建,纹理尺寸 ≤ 窗口,画布不超限,每帧一次 drawImage);页面销毁时清理动态缓存 | | `SLSavePlugin` | 存档插件。全局设置(辅助功能开关 + 缩放平移记忆 + 自定义难度配置 + **通关记录 + 排行榜**)自动存档:`window.tap` 存在 → TapTap 云存档,否则 localStorage(均实现 `SaveBase` 接口);注册时异步加载合并(版本校验、字段级钳制、通关记录/排行榜非法项过滤重排,`onSaveLoaded` 通知棋盘页重应用视图记忆);设置变更防抖保存 + 页面切换/页面隐藏兜底保存 | | `SLControl` | 触控事件适配与分发。H5 原生 touch/mouse/wheel 监听(鼠标模拟触摸)、原始事件归一化(画布坐标)并分发 `onTouchStart/Move/End/Cancel`/`onWheel`;防双击/防穿透。**不含任何业务/识别/分发逻辑** | | `SLClickRegion` | 页面点击区域插件。维护页面级点击区域注册表(每页仅注册一个,覆盖式,`onClick`/`offClick` 页面事件桥接),消费手势识别结果 `onClick/onLongPress` 经单解析器按坐标 O(1) 分派到目标布局;页面切换经 `pageEventReset` 重注册 | | `SLGestureControl` | 手势识别。消费原始触摸事件,按规则识别并分发抽象手势:click(短按松开)/长按(超阈值立即触发且不再 click)/移动(距离超阈值)/捏合/平移;滑动扫雷会话路由;touchcancel 会话清理 | | `SLDigControl` | 排雷控制。消费 `onCellAction`(单元格布局转发),决策翻开/标记/快速清除 chord 与长按滑动扫雷/动作互换 | | `SLZoomControl` | 缩放控制。消费 `onPinchStart/onPinchScale/onWheel`,对当前棋盘页应用缩放(捏合与滚轮同一管线,1~10 倍钳制、**以手势位置(捏合中点/鼠标)为缩放锚点**) | | `SLMoveControl` | 移动控制。消费 `onPan`,对当前棋盘页应用平移(未缩放不平移、边界钳制) | | `SLSlideSweepControl` | 滑动排雷控制。消费 `onSlideSweepMove`,滑动经过的格子逐格触发排雷(数字格按标记数≥数字判定展开、未开格翻开);**无去重缓存,翻开/展开天然幂等,判定拦截的数字格每次经过重新评估**,松手结束 | ### 排行榜服务(`taptap/Leaderboard.ts`) | 服务 | 说明 | |------|------| | `TapTapLeaderboardService` | TapTap 官方排行榜封装。静态方法:`isAvailable()`(`window.tap.getLeaderboardManager` 存在性判断,**基础库 ≥ 1.8.0**)、`submitScore(score)`、`openLeaderboard()`(拉起官方排行榜 UI 面板,**失败返回 SDK 错误码**供 UI 提示诊断)、`getMyRank()`、`getTopScores(limit)`;管理器实例**全局缓存**(文档约定单例);排行榜 ID 由顶部 `LEADERBOARD_ID` 常量配置。**所有调用失败不抛异常**,无 TapTap 环境时排行榜页自动降级本地榜单 | ### 游戏逻辑 | 类 | 说明 | |----|------| | `SLBoardLogic` | 对局判断类(**编排**)。持有棋盘与对局状态,按职责组合拆分出的子类:**`SLMineField`**(`logic/SLMineField.ts`——雷盘懒初始化:进入棋局不布雷、首次翻开时生成并排除首开格及邻居,避免开局踩雷;相邻雷计数)、**`SLBoardTimer`**(`logic/SLBoardTimer.ts`——首开启动/终局冻结计时)、**`SLBoardAssist`**(`logic/SLBoardAssist.ts`——快速清除 chord 判定+展开、自动标雷级联复查,经 `SLBoardAssistHost` 接口协作)、**`SLWinRecorder`**(`logic/SLWinRecorder.ts`——O(1) 胜利检测后的结算:通关记录 + 排行榜得分 + 存档通知,经 `SLWinRecorderHost` 接口协作);自身保留翻开/栈式 flood fill、标记(`mode` 扫雷/标记互换)、剩余雷数、全部揭示与 **O(1) 胜利检测**(未开安全格计数,替代全盘扫描——超大棋盘 300×300 每格翻开/滑动扫雷不再长帧卡顿);**辅助功能使用跟踪**(锚定打开单元格触发事件:快速清除 0x1/自动标雷 0x10/滑动扫雷 0x100) | ### 页面(Page,实现 `SLPageI`) | 页面 | 说明 | |------|------| | `SLBasePage` | 抽象页面基类。持有 `children`(`SLLayout[]`),负责子布局注册与遍历渲染 | | `SLBaseListPage` | 记录类列表页面基类(通关记录/排行榜共用)。深色背景 + 列表布局,页面级点击区域仅命中返回按钮 | | `SLHomePage` | 主菜单页面。**两栏布局**(3 行 × 2 列)按 `HOME_DIFFICULTIES` 配置生成 4 个难度按钮:简单/中等/困难 + **自定义**(`SLHomeCustomButtonLayout` 输入宽/高/雷数)+ **通关记录**按钮(进入记录页,副标题展示通关次数)+ **排行榜**按钮(进入排行榜页,副标题展示最高分) | | `SLBoardPage` | 游戏面板页面(组合编排)。创建 `SLBoardLogic` 与全部单元格 + 状态栏,注册页面级点击区域;几何/缩放/平移由 **`SLBoardView`**(`pages/board/`)管理(1~10 倍、边界钳制;**棋盘超出显示区域即可平移,与缩放倍数无关**),棋盘渲染由 **`SLBoardRenderer`** 承担(可见区域动态纹理 + 只读滚动条);destroy 时移除本页注册的系统监听(onWindowSizeChange/onSaveLoaded) | | `SLWinRecordsPage` | 通关记录页面。滚动区域浏览通关记录(最新在前,随存档保存),右上角返回首页;列表布局监听 `onPan` 拖动滚动 | | `SLRankingPage` | 排行榜页面。**TapTap 环境只保留官方排行榜**:进入页面异步拉取云榜前 10 名 + 我的排名(加载中/空态/未上榜均有文案),头部官方排行榜按钮拉起 SDK 面板(打开失败按钮变红提示可重试);仅无 TapTap 环境降级展示本地榜单;样式与通关记录页一致(共用 `SLBaseListPage`) | ### 布局(Layout,继承 `SLBaseLayout` 实现 `SLLayoutI`) | 布局 | 说明 | |------|------| | `SLBaseLayout` | 布局基类。**不持有离屏渲染缓存**,每帧直接绘制内容;可复用图形由纹理插件缓存统一管理,棋盘整盘由动态纹理缓存(内容签名检测变化后重建);窗口变化以 **listen 追加式**注册(多布局并存互不覆盖),**destroy() 按回调移除**(页面切换时由页面统一调用) | | `SLBackgroundLayout` | 全屏纯色背景(默认 `#cccccc`),可参数化 | | `SLHomeButtonLayout` | 主菜单按钮。圆角渐变 + 阴影 + 难度配色,**两栏布局**(column 0/1 决定左右栏,行位置由 topRatio 决定),副标题展示规格与雷数(子类可覆盖:自定义难度展示配置、通关记录展示通关次数、排行榜展示最高分) | | `SLBoardCellLayout` | 扫雷单元格。单击/长按转发系统事件 `onCellAction`(排雷控制决策),已开格统一支持 chord;提供**状态指纹**(紧凑数值位编码,供棋盘动态纹理签名聚合);由 SLBoardPage 统一绘制进棋盘级缓存画布 | | `SLBoardStatusBarLayout` | 顶部状态栏(**组合编排**)。底色/边框直接绘制,其余全部子项拆分封装为独立 class,以 **`SLStatusBarItemI` 统一接口**管理(矩形命中/点击消费/绘制/窗口重排,按注册顺序统一分派):雷数/计时 LCD 面板(`SLStatusPanelItem` 基类)、模式切换(`SLModeToggleItem`)、重开笑脸(`SLRestartButtonItem`)、设置按钮(`SLSettingsButtonItem`)、设置面板(`SLSettingsPanelItem`,内部设置行以 `SLSettingRowI` 统一管理:开关行/主题下拉行/音量行/返回首页行,自持展开/下拉/帮助 tip 状态) | | `SLBaseListLayout` | 记录类列表布局基类(通关记录/排行榜共用,视觉与交互风格统一)。深色半透明面板 + 标题 + 返回按钮 + 分隔线 + 滚动区域(onPan 拖动、内容钳制)+ 只读滚动条;支持**标题下方额外区域**(子类头部按钮,列表滚动区自动下移);行内容由子类 `renderRow` 提供 | | `SLWinRecordsListLayout` | 通关记录列表(继承 `SLBaseListLayout`)。行内难度/用时/辅助标记/通关时间 | | `SLRankingListLayout` | 排行榜列表(继承 `SLBaseListLayout`)。**TapTap 云榜为主**(行数据由页面注入:名次/昵称或难度/得分/副行文案,我的排名行背景高亮),头部官方排行榜按钮(TapTap 环境显示,点击拉起 SDK 面板);无 TapTap 环境时由 `buildLocalRows()` 降级组装本地榜单行 | ### 单元格数据(SLBoardCellI) | 字段 | 类型 | 说明 | |------|------|------| | `row` / `col` | `number` | 行/列坐标 | | `isOpen` | `boolean` | 是否已翻开 | | `type` | `'mine' \| 'empty' \| 'number'` | 类型 | | `isFlagged` | `boolean` | 是否标记 | | `isFailed` | `boolean` | 是否踩雷失败 | | `adjMines` | `number` | 相邻雷数(type='number' 时有效) | --- ## 事件系统 ### 系统级事件(`SLGame.event`)— 随插件生命周期常驻 | 事件名 | 注册方 | 参数 | 说明 | |--------|--------|------|------| | `run` / `stop` | SLPlayer | 无 | 启动/停止游戏循环 | | `loop` | SLRender | 无 | 每帧触发,驱动渲染 | | `onWindowSizeChange` | SLRender、页面与布局(listen 追加式,互不覆盖) | `TapTapWindowsSizeInfo` | 窗口尺寸变化通知 | | `getWindowSize` | SLConfig | 无 → `{windowWidth, windowHeight}` | 获取当前窗口尺寸 | | `gotoPage` | SLRender | `SLPageI` | 切换当前页面 | | `pageEventReset` | SLControl | 无 | 页面事件总线被重置,SLControl 重注册 | | `getCurrentPage` | SLRender | 无 → `SLPageI \| null` | 获取当前页面(控制插件定位棋盘页) | | `onTouchStart` / `onTouchMove` / `onTouchEnd` | SLGestureControl | `RawTouchInfo` | 原始触摸事件(画布坐标归一化,SLControl 适配分发) | | `onTouchCancel` | SLGestureControl | — | 原始触摸取消(touchcancel/窗口失焦),识别器清理会话 | | `onClick` / `onLongPress` | SLControl | `(pos)` | 手势识别结果;SLControl 经页面级点击区域分派到目标布局 | | `onCellAction` | SLDigControl | `(row, col, longPress)` | 单元格动作(单元格布局转发;排雷控制决策) | | `onSlideSweepStart` | SLGestureControl + SLSlideSweepControl | `(row, col)` | 长按已翻开数字格进入滑动扫雷模式(多监听) | | `onSlideSweepMove` | SLSlideSweepControl | `(x, y)` | 滑动扫雷中手指画布坐标,逐格触发排雷动作 | | `onSlideSweepEnd` | SLSlideSweepControl | — | 松开手指结束滑动扫雷 | | `onMoveStart` / `onMove` / `onMoveEnd` | —(分发用) | `(touchLevel, pos)` | 移动手势(移动距离超过移动判定阈值进入/结束) | | `onPinchStart` / `onPinchScale(ratio)` / `onPinchEnd` | SLZoomControl | — | 双指捏合缩放(鼠标滚轮复用同一管线,滚轮每次滚动独立基准) | | `onPan` | SLMoveControl | `(dx, dy)` | 单指/双指中点拖动平移 | | `onWheel` | SLZoomControl | `(deltaY, ctrlKey)` | 鼠标滚轮(SLControl 已归一化为像素,钳制与缩放由 SLZoomControl 处理) | | `getTexture` | SLTexturePlugin | `(kind, params?)` | 获取纹理(canvas JS 或图片资源,按 key 缓存复用) | | `dynamicTexture` | SLTexturePlugin | `(owner, w, h, signature, build)` | 动态纹理缓存:内容签名变化时重建画布(棋盘整盘) | | `clearDynamicTexture` | SLTexturePlugin | `(owner)` | 清除动态纹理缓存(页面销毁时) | | `getSaveBackend` | SLSavePlugin | 无 → `SaveBase \| null` | 获取存档后端(window.tap 存在时 TapTap 云存档,否则 localStorage) | | `saveSettings` | SLSavePlugin | 无 | 立即保存全局设置存档 | | `onSettingsChange` | SLSavePlugin | 无 | 全局设置变化通知(防抖自动存档) | | `onSaveLoaded` | SLBoardPage | 无 | 存档加载完成(棋盘页重应用视图记忆) | ### 页面级事件(`SLGame.pageEvent`)— 随页面切换整体销毁 | 事件名 | 注册方 | 参数 | 说明 | |--------|--------|------|------| | `onClick` | SLControl | `(layout, callback, resolver?)` | 页面级点击区域注册;回调 `(x, y, target, longPress?)` | | `offClick` | SLControl | `layout` | 移除点击区域 | | `onCellClick` | SLBoardLogic | `(row, col)` | 单元格翻开(扫雷模式 / 已开格 chord),排雷控制触发 | | `onCellFlag` | SLBoardLogic | `(row, col)` | 单元格标记切换,排雷控制触发 | --- ## 工作流程 ### 1. 游戏启动 ``` main.ts 加载 → 全局错误捕获注册(window error / unhandledrejection) → new SLGame([SLPlayer, SLConfig, SLRender, SLControl, SLDigControl, SLZoomControl, SLMoveControl, SLSlideSweepControl]) → 插件 register → init → gotoPage(SLHomePage) → run() → 启动失败 try/catch → 控制台 + 页面红色面板输出 ``` ### 2. 游戏循环(SLPlayer 驱动) ``` run() → loop() → triggerEvent("loop") → window.requestAnimationFrame(loop) ``` ### 3. 渲染流程 ``` SLRender.render() → renderClear() (clearRect 清空画布) → renderPage()(this.page.render()) → 背景缓存 drawImage → 棋盘动态纹理(可见区域 + 单元格状态指纹 FNV-1a 聚合签名检测变化 → 可见区域重建 → 1 次 drawImage) → 状态栏直绘(含设置面板/滚动条) ``` ### 4. 窗口 resize ``` window.resize → SLConfig.onResize() → triggerEvent("onWindowSizeChange") → 布局/页面更新尺寸、画布尺寸同步 ``` ### 5. 页面切换 ``` triggerEvent("gotoPage", page) → SLRender.gotoPage:旧页 destroy()(移除页面/布局注册的系统监听 + 清除动态纹理缓存) → resetPageEvents() → 新页 register() → 替换 ``` ### 6. 手势与点击交互 ``` touchstart / touchmove / touchend(H5 原生) → SLControl 事件适配:原生事件归一化(画布坐标)→ onTouchStart/Move/End/Cancel 分发 → SLGestureControl 手势识别: → 单击(按下松开时长 < 500ms 且未移动):onClick → SLControl pageRegion 解析器按坐标 O(1) 分派 → 延迟一帧回调(防穿透)→ 单元格转发 onCellAction(排雷控制决策) → 长按(按下 > 500ms 且静止):立即触发 onLongPress 且不再产生 click → pageRegion 分派 → 单元格转发 onCellAction → 移动(移动距离 > 25px):onMoveStart/onMove/onMoveEnd 分发;单指/双指位移 → onPan → SLMoveControl 平移(边界钳制) → 双指捏合:两指距离变化 → onPinchScale → SLZoomControl 缩放 1~10 倍(保持平移位置) → 鼠标滚轮:deltaY 归一化(行/页模式折算)→ onWheel → SLZoomControl 与捏合同管线增量缩放(Ctrl+滚轮=触控板捏合) → 长按已开数字格(设置面板开启滑动扫雷):进入滑动扫雷并触发一次排雷(起始格 chord)→ touchmove 路由 onSlideSweepMove → 逐格触发排雷(数字格按标记数≥数字快速清除 / 未开格翻开,每格一次)→ touchend 结束 → 浏览器合成鼠标事件:preventDefault 抑制(防双击) ``` ### 7. 单元格点击流程 ``` 用户点击单元格(SLControl 识别 → pageRegion 分派 → onCellAction → SLDigControl 决策) → 未开格:按模式触发 onCellClick(翻开,flood fill)/ onCellFlag(标记) → 已开数字格:触发 onCellClick → 快速清除 chord(相邻标记数≥数字时展开其余未开格) → 翻开后:自动标雷复查本轮新开格及其相邻已开数字格(未开格数=剩余雷数 → 自动标记,级联触发);手动新增标记后同样复查相邻数字格 → 滑动扫雷:长按已开数字格进入(SLSlideSweepControl 会话),滑动经过的未开格直接翻开、数字格按快速清除基础逻辑(相邻标记数≥数字)展开其余未开格,每格仅触发一次,松开手指结束 → 下一帧棋盘缓存重建,渲染反映状态变化 ``` --- ## 开发 ```bash npm install # 安装依赖 npm run dev # 本地开发(vite,http://localhost:5173) npm run typecheck # TypeScript 严格检查(tsc --noEmit) npm run build # 构建到 dist/ npm run preview # 预览构建产物 npm run package # 构建 + 打包(dist+配置 → packages/saolei_v1.x.x.zip) ``` ## 发布 1. 升版:更新 `package.json` 的 `version`(如 1.0.30) 2. `npm run package` 生成 `packages/saolei_v1.x.x.zip` 3. 上传至 TapTap **H5 小游戏**类型;包结构:解压后仅一个 `saolei/` 文件夹,内含入口 `index.html` + `game.js` + `game.json` + `project.config.json` ## 开发状态 - 基础框架(双事件总线、插件、渲染、触控):已完成 - 主菜单(四档难度,含自定义输入):已完成 - 游戏逻辑(雷生成、flood fill、胜负、标记、计时):已完成 - 棋盘懒初始化(进入棋局不布雷、首次翻开才生成,首开格及 8 邻居排除布雷避免开局踩雷):已完成 - 顶部状态栏(模式切换、笑脸、雷数、计时、设置按钮):已完成 - 设置面板(快速清除、自动标雷、滑动扫雷,默认全部开启,会话级持久化;返回首页):已完成 - 棋盘安全边距(四周 20px 留白,缩放后平移钳制同样保留,避免操作贴近屏幕边缘):已完成 - 主菜单按钮间距(纵向 3.5% 屏高,互不紧贴):已完成 - 棋盘缩放(1~10 倍,锚点为横向单元格个数,默认 4 倍=20 格)/ 平移(边界钳制)/ 只读滚动条(滑块按实际平移行程换算,全程移动不失真):已完成 - 拖动越界不累积(越界偏移实时钳制,回拖立即响应,缩放后保持在有效范围):已完成 - 鼠标滚轮缩放(与捏合同管线,含触控板 Ctrl+滚轮):已完成 - 超宽/过高棋盘初始显示左上角,可平移查看:已完成 - 自动标雷(翻开/新增标记后复查相邻数字格,级联触发不漏标):已完成 - 长按交互(500ms,动作与模式互换,多指支持):已完成 - 滑动扫雷(长按数字格进入,滑动逐格排雷,数字格按标记数≥数字判定展开、未开格翻开,无去重缓存天然幂等、判定拦截格每次经过重新评估,松手结束,可在设置面板开关;touchcancel 会话清理防残留失效):已完成 - 缩放/平移记忆(全局存储倍数 1~10,锚点为横向单元格个数,跨棋盘格大小天然一致):已完成 - 全局设置存档(SaveBase 基类接口 + LocalStorage/TapTap 云存档后端,window.tap 环境判断;启动加载合并、设置变更防抖保存、页面切换/隐藏兜底保存;视图记忆晚到重应用):已完成 - 自定义难度记忆(使用过的自定义配置记入全局设置并随存档保存;首页按钮回填输入默认值、副标题展示当前配置):已完成 - 通关记录(胜利自动收集:难度/用时/辅助功能使用标记/通关时间,flag 位 0x1/0x10/0x100 锚定打开单元格触发事件;最新在前最多 20 条,随存档保存;首页按钮 + 记录页拖动滚动浏览 + 返回首页):已完成 - 排行榜(胜利同局按得分标准入榜:每个雷 10 分、每个辅助功能减 1 分,得分降序/同分用时升序,最多 20 条随存档保存;首页两栏按钮 + 排行榜页拖动滚动浏览,前三名金色高亮;通关记录与排行榜共用 SLBaseListPage/SLBaseListLayout 基类样式一致):已完成 - TapTap 官方排行榜(TapTapLeaderboardService 封装提交/打开 UI/我的排名/前 N 名,环境判断失败静默;排行榜页云榜优先 + 官方按钮,无 tap 环境降级本地榜;胜利自动提交得分,LEADERBOARD_ID 常量配置):已完成 - 首页两栏布局(3 行 × 2 列:简单/中等/困难/自定义/通关记录/排行榜 6 按钮):已完成 - 合成音效(纯 WebAudio 实时合成:开启格子咔哒/失败登登登/成功登等/按钮咔哒,正弦/三角波 + 指数衰减柔和包络,随用户手势解锁):已完成 - 音量调节(设置面板滑块 0~100%,点击轨道调节 + 百分比显示,全局音量倍率随存档保存):已完成 - 控制插件拆分(SLControl 仅事件适配与分发;手势识别/排雷/缩放/移动/滑动排雷独立插件,分层事件流):已完成 - 纹理系统(全部可复用 canvas 渲染封装为纹理:绘制原语 drawUtil + 单元格/按钮/状态栏视觉纹理 + **文本纹理**,canvas JS 或图片资源两种组成方式,纹理插件统一缓存,棋盘可见区域动态纹理(画布不超限),移除布局级渲染缓存):已完成 - 渲染缓存(棋盘单画布 + 内容签名)与低开销渲染:已完成 - 页面级区域分派(触控 O(1) 定位):已完成 - Vite + TypeScript 工程化与打包发布脚本:已完成 --- ## 变更记录 ### v1.0.79 - **调整**:首页布局——按钮行整体下移(行位置 0.5/3.5/6.5 → **1.8/4.8/7.8** 份)为标题让出空间;标题「扫雷」改为**卡通醒目风格**:字号随屏宽(390px ≈ 51px,钳制 36~56px)+ 投影浮起 + **粗圆角描边**(lineJoin=round)+ 主题亮色主体,配色随全局主题新增 token `homeTitleFill`/`homeTitleOutline`(经典=暖黄 `#ffd54f`+深棕 `#a05a00`,与笑脸同色系;Win10=亮蓝 `#5aa9ff`+深蓝 `#0e3e8f`) ### v1.0.78 - **新增**:首页顶部居中展示游戏标题 **「扫雷」**(`SLHomeTitleLayout`,`layouts/`,30px 粗体深色,位于第一行按钮上方,不参与点击区域解析) ### v1.0.77 - **新增**:**首页显示当前版本**——新增 `SLVersionLabelLayout`(`layouts/`),首页底部居中展示 `v{version}`,版本号由构建时从 `package.json` 注入(`resolveJsonModule`),随每次升版自动更新;仅展示,不参与点击区域解析 - **新增**:**TapTap 环境启动更新检查**——`TapTapUpdater`(`taptap/Updater.ts`)启动时经 `tap.getUpdateManager()` 注册更新监听:`onCheckForUpdate` 记录检查结果(有无新版本);`onUpdateReady` 新版本下载完成后弹窗「更新提示 / 新版本已经准备好,是否重启应用?」(`tap.showModal`),确认后 `applyUpdate()` 强制重启使用新版本;`onUpdateFailed` 记录下载失败警告;检查/下载由 TapTap 客户端每次启动自动触发;非 TapTap 环境 / 基础库未提供更新能力时静默跳过;`TapTapTypes` 补充 `TapTapUpdateManager` 类型与 SDK `getUpdateManager`/`showModal` 声明 ### v1.0.76 - **新增**:**规范化自动扫描工具** `scripts/scan-normalize.mjs`(`npm run scan`)——自动识别并扫描 src 的规范性问题:① 子目录文件大驼峰命名;② 导出类 SL 前缀、导出接口 I 后缀(工具类/外部 SDK 形状/参数包豁免);③ 超过 300 行的文件列为拆分候选警告;④ 模式残留(旧模块路径、直接 `window.tap`、内联 `arcTo`、嵌套 Math 钳制、内联闭区间命中判定) - **规范化**:`BoardView`→**`SLBoardView`**、`BoardRenderer`→**`SLBoardRenderer`**(类 + 文件 + 宿主接口 `SLBoardViewHostI`/`SLBoardRendererHostI`) - **规范化**:接口统一 I 后缀——`RankRow→SLRankRowI`、`HomeDifficulty→SLHomeDifficultyI`、`PageRegion→SLPageRegionI`、`SaveFile→SLSaveFileI`、`LeaderboardResult→SLLeaderboardResultI`、`ListLayoutHost→SLListLayoutHostI`、`SLBoardAssistHost→SLBoardAssistHostI`、`SLWinRecorderHost→SLWinRecorderHostI`、`WinRecord→SLWinRecordI`、`RankRecord→SLRankRecordI` - **拆分**:`SLHomePage` 的三个按钮布局类迁出至 `layouts/`(`SLHomeCustomButtonLayout` / `SLHomeRecordsButtonLayout` / `SLHomeRankingButtonLayout`),页面保留难度配置与组合 - **拆分**:设置面板说明 tip 封装为 **`SLSettingHelpTipItem`**(`layouts/statusbar/`,内容状态/命中/折行绘制自持),`SLSettingsPanelItem` 减至 300 行内 - **说明**:`SLGestureControl`(317 行)为手势识别核心状态机(历经多轮缺陷修复),暂不拆分,扫描保留警告 ### v1.0.75 - **重构**:`DrawUtil` 新增基础绘制原语并替换内联块——`drawLine`(moveTo+lineTo+stroke 封装:分隔线/旗杆/红叉/笑脸 X 眼与嘴)、`fillCircle`(arc+fill 封装:音量圆点/齿轮螺孔/笑脸点眼/开关滑块/地雷弹体火花高光) - **重构**:`MathUtil` 新增 **`inRange(value, min, max)`** 闭区间命中判定(`a ≥ b && a ≤ c`),替换全部矩形命中(状态栏子项/面板/tip/主题下拉选项/返回按钮/官方按钮/首页按钮) - **重构**:`SLBaseListLayout.renderContent` 拆分为子函数(`renderPanelBackground`/`renderTitle`/`renderBackButton`/`renderTitleDivider`/`renderListArea`/`renderEmptyState`/`renderRows`/`renderScrollbar`),绘制顺序与行为不变 - **重构**:**`SLBoardLogic` 拆分为不同 class 文件**——`SLMineField`(雷盘生成/相邻计数)、`SLBoardTimer`(计时)、`SLBoardAssist`(快速清除/自动标雷,经 `SLBoardAssistHost` 接口协作)、`SLWinRecorder`(胜利结算记录,经 `SLWinRecorderHost` 接口协作),`SLBoardLogic` 保留编排与翻开/flood fill/胜利检测,公开 API 不变(`startTime`/`elapsedSeconds` 改为委托 getter) - **重构**:**`Textures.ts` 拆分为不同 class 文件 + 基类**——`factories/SLTextureFactoryBase`(基类:静态 `kind` 注册键 + `build(params)` 契约 + 主题访问),15 个纹理各封装为独立 class(`CellTextures` 7 类 / `StatusBarTextures` 6 类 / `HomeTextures` 1 类 / `TextTexture` 1 类),`Textures.ts` 瘦身为注册表统一装配 - **重构**:`types/SLGame.ts` 更名 **`types/SLTypes.ts`**(文件结尾 Types),`TapTapTypes.ts` 迁回 **`types/`** 目录 - **重构**:`records.ts` 更名并迁移至 **`src/saves/Records.ts`**(通关记录 + 排行榜记录随存档域收拢) ### v1.0.74 - **重构**:`ClickRegion` 重命名为 **`SLClickRegion`**(`src/plugins/control/SLClickRegion.ts`),与其他 SL 前缀插件命名一致 - **重构**:**taptap 相关代码全部收拢至 `src/taptap/` 目录**——`TapTapTypes.ts`(原 `types/taptap.ts`:SDK/窗口尺寸类型)、`Environment.ts`(新增:`isTapTapEnvironment()`/`getTapTapSDK()` 集中管理 `window.tap` 访问,云存档/排行榜/存档插件共用)、`Leaderboard.ts`(原 `leaderboard/TapTapLeaderboard.ts`)、`Save.ts`(原 `saves/TapTapSave.ts`);原 `src/leaderboard/` 目录移除 - **重构**:src 子目录文件全部按**大驼峰命名**——`types/sl_game.ts → SLGame.ts`、`textures/drawUtil.ts → DrawUtil.ts`、`textures/textures.ts → Textures.ts`、`theme/themes.ts → Themes.ts`(根目录 `main.ts`/`settings.ts`/`records.ts` 等保持小写) - **重构**:**圆角矩形路径封装**——内联 `moveTo/arcTo/closePath` 块(状态栏主题下拉框、设置面板帮助 tip、记录列表返回按钮、排行榜官方按钮)统一替换为 `CanvasDrawUtil.roundRectPath` - **重构**:新增 **`MathUtil.minMax(value, min, max)`**(`src/utils/MathUtil.ts`)并替换全部双侧钳制(缩放倍数/平移边界/平移比例/音量/滚轮增量/滚动范围/自定义难度雷数/副标题字号) ### v1.0.73 - **重构**:音频拆分为**基础工具与音效**两层——`SLAudioEngine`(`src/audio/SLAudioEngine.ts`)承担全部 WebAudio 基础设施(上下文生命周期/主增益+压缩器输出链/单音合成/同音效节流/活动音效上限/首声等待 resume),`SLAudio`(`src/audio/SLAudio.ts`)只定义游戏音色(开启格子/失败/胜利/按钮的波形音高时长响度)与节流策略,外部引用(`slAudio`)不变,行为零变化 - **重构**:状态栏子项拆分封装为独立 class,以 **interface 统一管理**——`SLBoardStatusBarLayout` 瘦身为组合编排(底色/边框 + 按注册顺序统一分派点击/遍历绘制/窗口重排);子项位于 `src/layouts/statusbar/`:`SLStatusBarItem`(统一接口 `SLStatusBarItemI` + 基类)、`SLStatusPanelItem` 基类(雷数/计时面板)、`SLModeToggleItem`、`SLRestartButtonItem`、`SLSettingsButtonItem`、`SLSettingsPanelItem`(设置行以 `SLSettingRowI` 统一管理:`SLSettingSwitchRowItem`/`SLSettingThemeRowItem`/`SLSettingVolumeRowItem`/`SLSettingHomeRowItem`,面板自持展开/主题下拉/帮助 tip 状态);`SL_STATUS_BAR_HEIGHT` 移至子项模块并转发,既有引用不变 - **修复**(重构伴随):设置面板说明 tip 弹窗画在面板下方、原实现未纳入命中范围,重构后点击 tip 区域可正常关闭(面板命中判定扩展至展开时的 tip 弹窗区域) ### v1.0.72 - **修复**:移动棋盘(平移)完毕后,首次单击/长按(翻开、标记、快速清除、滑动扫雷)触发时画面抖动一次(每次平移后仅首操作触发)——根因:棋盘动态纹理缓存签名只含**窗口钳制后的可见区域**(`visibleLeft/Top/vw/vh`)与**可见格状态指纹**,不含**棋盘内容偏移**(`boardLeft/boardTop`)与格尺寸:大棋盘平移时可见区域不变、全闭格指纹相同(或平移不足一格时可见格集合不变),签名不变导致纹理**过期不重建**,画面停在旧偏移处;首个改变格子内容的操作(翻开/标雷/快速清除/滑动扫雷)强制重建,整盘**一次性跳变到当前几何**形成抖动。现签名加入**棋盘偏移与格尺寸**,任何平移/缩放逐帧重建纹理,内容始终与几何一致,消除该抖动 ### v1.0.71 - **修复**:拖动平移开始瞬间画面沿移动方向跳变抖动(方向一致、每次拖动会话仅首次位移触发、再次拖动再次触发,300×300 大棋盘明显)——根因:进入移动模式的**首次 `onPan` 以会话起点为增量基准**,把"从按下到跨过移动阈值(25px)的累计位移"一次性应用。现进入移动模式时**平移增量基准重置为当前位置**,阈值内不移动、超过后按增量平滑跟随;同时消除由此引发的长按/滑动扫雷误触(视图跳变导致命中格子偏移) ### v1.0.70 - **修复**:长按/点击触发瞬间画面抖动、滑动扫雷误触其它格子——两处根因:① 云存档加载完成(可能耗时数秒)时 `onSaveLoaded` **无条件恢复视图记忆**,正在操作的玩家视图跳变、命中坐标错位;现**仅当玩家未手动调整视图且尚未开局时恢复**(缩放/平移后或首开后跳过);② 每次翻开/滑动扫雷逐格执行 `checkWin()` **全盘扫描**(300×300 每格 9 万次循环)造成长帧卡顿抖动;现改为**未开安全格计数 O(1) 胜利判定** ### v1.0.69 - **调整**:重开按钮与模式切换按钮由圆形改回**正方形**(36×36,经典主题 3D 凸起、Win10 扁平)——保持方形观感但不再是 48×36 长方形;布局位置不变(重开左侧、切换居中) ### v1.0.68 - **调整**:状态栏布局防误触——**重开笑脸按钮移到左侧**(剩余雷数面板旁),**模式切换按钮保持屏幕居中**,两者间距拉开;两个按钮均改为**圆形**(主题色圆底 + 描边,替代原方形凸起) - **新增**:**重开按钮显示开关**——设置面板新增"重开按钮"开关行(默认开启,随存档保存),关闭后状态栏不显示笑脸且点击无效;带叹号帮助说明 ### v1.0.67 - **新增**:设置面板**设置项帮助提示**——快速清除/自动标雷/滑动扫雷/主题/音量每项名称左侧新增**叹号圆形图标**(配色随主题),点击展开**带主题背景的说明 tip 弹窗**(面板下方,标题 + 自动折行正文,解释辅助功能能力与用法、主题与音量含义);再点叹号切换/关闭,点 tip 或操作其它设置行自动关闭;设置行名称与音量轨道相应右移 ### v1.0.66 - **配置**:`LEADERBOARD_ID` 更新为 `r196zs8wp2729mhgl9`(原 `gecwjebfp6rvc7y6qw` 榜单配置异常,已在后台更换榜单) ### v1.0.65 - **修复**:主题下拉框展开时文字重叠——选项行背景透明且绘制顺序在音量行之前(音量行文字盖在选项文字上)。现选项列表**最后绘制浮在最上层**,每行先填面板同色背景遮挡下层内容(选中项再叠加微亮),文字不再重叠 ### v1.0.64 - **修复**:超大棋盘(如自定义 300×300)缩放中心固定到左上角——原实现缩放只改倍数、平移偏移不变,棋盘超出窗口时钳制逻辑使左上角成为锚点。现缩放**以手势位置为锚点**:`onPinchScale` 携带捏合中点、`onWheel` 携带鼠标位置,`BoardView.setZoom` 调整平移偏移保持锚点下的棋盘内容不动(贴边时由边界钳制自然回弹);小棋盘居中缩放行为不变 - **修正**:README 自定义难度范围描述与实际一致(列/行 ≥2、雷数 1~格子数-1,无尺寸上限) ### v1.0.63 - **新增**:**全局主题系统(经典 / Win10)**——`src/theme/themes.ts` 集中定义视觉 token(页面背景/状态栏/LCD 面板/单元格/数字配色/设置面板/开关/滚动条);棋盘页设置面板新增 **"主题"下拉框**(展开选项列表、选中项 ✓ 高亮),切换即时生效并**随存档保存**(存档字段校验支持 theme) - **调整**:纹理 key 含主题名(切换自动重建缓存);棋盘动态纹理签名含主题维度(切换整盘即时重绘);首页/棋盘背景、状态栏、滚动条每帧读主题;记录类页面(通关记录/排行榜)保持独立深色设计 ### v1.0.62 - **修复**:升版后存档失效——根因是 TapTap 云存档链路单点:某次保存后云上传失败(未登录/后台未开启云存档/网络)或下次启动云列表拉取失败时,存档直接"丢失"。`TapTapSave` 改为**三级冗余**:保存写本地文件(必写)+ 双写 localStorage(与 H5 共用 key)+ 上传云端(失败仅日志不阻断);加载**云端优先 → 失败回退本地文件 → 再回退 localStorage**,任一链路可用即可恢复存档 ### v1.0.61 - **修复**:音频首次加载延时——启动时 `warmup()` 预创建 AudioContext(消除首次播放同步创建耗时)+ 首个 `pointerdown` 手势预热 resume(解锁自动播放策略);首声若上下文仍 suspended,等待 `resume()` 完成再调度(首声不丢不延迟) - **修复**:音效连发破音——所有音效汇入**主增益 + 动态压缩器**(多声叠加超阈值自动压限防削波);**同音效节流**(dig 35ms / button 30ms,滑动扫雷逐格连发等高频场景丢弃极短间隔重复);**活动音效数上限**(8 个,超出丢弃新声,压缩器之外的最后防线) ### v1.0.60 - **修复**:H5 场景加载旧版脚本问题——构建产物 `game.js` 为固定文件名(无 hash),浏览器可能缓存旧 JS(如仍含已移除的调试面板);`index.html` 增加 `no-cache` 缓存控制头,确保加载最新版本 ### v1.0.59 - **移除**:游戏内排行榜调试面板与逐步调试日志(`leaderboardDebugLogs`/`logDebug`)——功能验证通过后清理,恢复干净界面;保留打开失败按钮错误码提示与失败 console 日志 ### v1.0.58 - **配置**:`LEADERBOARD_ID` 替换为实际创建的排行榜 ID `gecwjebfp6rvc7y6qw`(此前为示例值 `weekly_challenge_01`,后台未创建导致四个排行榜接口全部 SDK 失败) ### v1.0.57 - **修复**:游戏内排行榜调试面板**日志自动折行完整显示**(不再 46 字符截断)——按面板宽度实测等宽字符数换行,SDK 失败的错误码与 message 全文可见;总行数超限时丢弃最旧日志 - **增强**:SDK 失败日志补全——message 为空时显示"(无消息)",并附排查提示(①后台"好友关系"权限 ②LEADERBOARD_ID/榜单创建 ③基础库≥1.8.0 ④网络/登录态);面板宽度自适应(≤420px) ### v1.0.56 - **新增**:**游戏内排行榜调试面板**——排行榜页右下角实时渲染排行榜每步操作日志(环境检查/获取管理器/提交分数/打开排行榜/我的排名/前 N 名:时间、成败、SDK 错误码、数据摘要、耗时;成功绿色/失败红色,最近 10 条),无需开发者工具即可在真机画面直接诊断排行榜问题 - **调整**:`TapTapLeaderboardService` 全部操作逐步埋点日志(`leaderboardDebugLogs` 模块级数组,最多保留 40 条,同时输出控制台),进入排行榜页时记录环境与 LEADERBOARD_ID 上下文 ### v1.0.55 - **修复**:打开 TapTap 官方排行榜失败提示携带 **SDK 错误码**(`⚠ 打开失败(码),点击重试`),按官方文档透传 `onFailure(code, message)`,便于诊断"好友关系"权限未勾选 / 排行榜未创建 / 网络等问题 - **调整**:`TapTapLeaderboardService` 管理器实例**全局缓存**(官方文档约定:多次调用 `getLeaderboardManager()` 返回同一实例);低版本基础库(< 1.8.0)兼容处理——无 `getLeaderboardManager` 时视为不可用,排行榜页自动降级本地榜 - **补充**:类型声明补全官方文档方法 `loadPlayerCenteredScores`(相近成绩查询,预留);README 记录排行榜接入前置条件(基础库 ≥ 1.8.0、后台创建排行榜并配置 ID、"好友关系"隐私权限) ### v1.0.54 - **调整**:TapTap 环境**只保留 TapTap 官方排行榜**——胜利时仅提交云榜(不再维护本地榜),首页排行榜按钮副标题展示"TapTap 官方榜"(不读本地数据),排行榜页仅展示云榜(前 10 名 + 我的排名);本地榜仅在无 TapTap 环境(H5)降级使用 - **修复**:打开 TapTap 官方排行榜失败时**可见报错**——按钮变红提示"⚠ 打开失败,点击重试"(重试成功后恢复金色),不再静默失败 ### v1.0.53 - **新增**:**TapTap 官方排行榜**接入——`TapTapLeaderboardService` 封装(`submitScores` / `openLeaderboard` / `loadCurrentPlayerLeaderboardScore` / `loadLeaderboardScores`,环境判断 `window.tap.getLeaderboardManager`,失败静默不阻塞游戏);排行榜 ID 由 `src/leaderboard/TapTapLeaderboard.ts` 顶部 `LEADERBOARD_ID` 常量配置(需替换为后台创建的榜单 ID) - **调整**:排行榜页以**云榜为主**——TapTap 环境异步拉取**前 10 名 + 我的排名**(加载中/空态/未上榜均有文案),头部 **"▶ TapTap 官方排行榜"按钮**拉起 SDK 官方面板;无 TapTap 环境降级展示本地榜单(随存档保存) - **新增**:胜利时自动提交得分到 TapTap 云榜(得分标准:每个雷 10 分,每使用一个辅助功能减 1 分;失败仅日志) - **优化**:`SLBaseListLayout` 支持标题下方额外区域(官方按钮占位,列表滚动区自动下移);排行榜行支持昵称/我的排名高亮/未上榜占位 ### v1.0.52 - **新增**:排行榜——胜利时按**得分标准**入榜(每个雷 10 分,每使用一个辅助功能减 1 分,下限钳制 0),按得分降序/同分用时升序排列,最多保留 20 条,**随存档保存**(存档结构新增可选 `rankings` 字段,旧存档兼容不丢数据) - **新增**:首页"排行榜"按钮 + 排行榜页面(行内名次/难度/得分/用时/辅助标记,前三名金色高亮,拖动滚动浏览) - **调整**:首页按钮由单列 5 个改为**两栏布局**(3 行 × 2 列,按钮宽 = 屏宽 × 0.42),通关记录 + 排行榜按钮位于第三行 - **优化**:通关记录与排行榜页面**样式统一**——抽取 `SLBaseListPage` / `SLBaseListLayout` 基类(深色面板/标题/返回按钮/分隔线/滚动条/拖动滚动共用),两个列表页保持同一视觉与交互风格 ### v1.0.51 - **修复**:页面/布局系统监听从不清理(`SLEvent` 新增 `unlisten` 按回调移除;`SLBoardPage` 的 `onSaveLoaded`/`onWindowSizeChange`、`SLWinRecordsListLayout` 的 `onPan`、`SLBaseLayout` 的 `onWindowSizeChange` 均在页面 `destroy()` 时移除,防止反复进出页面累积监听与对象引用泄漏) - **修复**:`onWindowSizeChange` 互相覆盖(布局与棋盘页由覆盖式 `registerEvent` 改为 `listenEvent` 追加式,resize 后状态栏等全部布局正确重排,不再只有最后注册者生效) - **修复**:1 倍缩放下大棋盘无法平移且无滚动条(平移与滚动条显示条件由"缩放倍数 > 1"改为"棋盘是否超出显示区域",与缩放倍数解耦) - **修复**:存档加载无字段级校验(开关/音量/缩放/平移比例逐字段类型 + 范围钳制,非法值回退默认;`applyCustomDifficulty` 防 NaN/Infinity 污染) - **优化**:棋盘动态纹理签名由每帧拼接大字符串改为单元格状态指纹(紧凑数值位编码)逐格累加 FNV-1a 哈希,消除每帧数十 KB 临时字符串分配 - **优化**:踩雷时不再与翻开音效同帧重叠(目标格为雷时跳过 dig 音效,仅播失败音) --- ## AI 协作 本项目在开发过程中由 AI 辅助编码完成: - **模型**:DeepSeek v4 Flash(`deepseek-v4-flash`) - **开发框架**:DeepSeek Harness(`deepseek-harness`) ### AI 处理流程 每个迭代周期的标准流程(按序执行,缺一不可): 1. **处理**:按需求修改代码(修复/优化/新功能),`npm run typecheck` 通过 2. **更新 MD**:同步更新 `README.md`(功能/架构/事件表变更 + 变更记录条目) 3. **升版 Package Version**:更新 `package.json` 的 `version`(如 1.0.51) 4. **pkg zip**:执行 `npm run package` 生成 `packages/saolei_v{version}.zip` 5. **commit**:`git add` 全部改动并提交(提交信息注明变更内容与版本号)