diff --git a/.planning/phases/10-docs-restructure/10-01-PLAN.md b/.planning/phases/10-docs-restructure/10-01-PLAN.md index c32eaa3..1023be8 100644 --- a/.planning/phases/10-docs-restructure/10-01-PLAN.md +++ b/.planning/phases/10-docs-restructure/10-01-PLAN.md @@ -93,13 +93,13 @@ Output: 已清空的 `docs/`(无旧内容)+ 已清理并注册好的 `docs/. (1) 删除旧内容:删除文件 `docs/index.md`,并递归删除目录 `docs/guide/`、`docs/dev/`、`docs/logger/`、`docs/api/`(连同其中全部文件与子目录,例如 `docs/guide/img/`、`docs/api/*/` 等)。用文件系统删除(如 `Remove-Item -Recurse -Force`),使 git 把这些受跟踪路径记为删除;不要遗留空目录。删除白名单严格限定为上述五个路径,绝不触碰 `docs/intermediate/`、`docs/.vitepress/`。 - (2) 就地编辑 `docs/.vitepress/config.ts`(最小作用域 Edit):从首部静态 import 中删除两条属 API 侧边栏机制的导入——从 `./apiSidebar` 导入的默认值(第 3 行),以及从 `./api` 导入的 `generateSidebar`(第 5 行);删除仅在开发期调用 `generateSidebar` 的 `listenSidebar()` 函数定义及其在 `vite.plugins` 数组中的调用(`MermaidPlugin()` 保留、`optimizeDeps.include` 与 `ssr.noExternal` 保留)。从 `themeConfig.nav` 删除四条指向已删内容的旧入口(主页 / 指南 / API / 错误代码),nav 数组留待 Task 2 写入「中级教程」一条。从 `themeConfig.sidebar` 删除三个指向已删章节的旧键(分别对应 guide、logger、api 三个路径前缀)及其中全部内容。其余配置(`title` / `description` / `base` / `markdown` / `outDir` / `socialLinks` / `search` / `locales`)逐字保留。 + (2) 就地编辑 `docs/.vitepress/config.ts`(最小作用域 Edit):从首部静态 import 中删除两条属 API 侧边栏机制的导入——从 `./apiSidebar` 导入的默认值(第 3 行),以及从 `./api` 导入的 `generateSidebar`(第 5 行);删除仅在开发期调用 `generateSidebar` 的 `listenSidebar()` 函数定义及其在 `vite.plugins` 数组中的调用(`MermaidPlugin()` 保留、`optimizeDeps.include` 与 `ssr.noExternal` 保留);并同步删除此时已成为未使用导入的 `Plugin` 类型——`listenSidebar()` 是它唯一的消费者,故 `docs/.vitepress/config.ts:1` 的 `import { defineConfig, Plugin } from 'vitepress'` 须改为 `import { defineConfig } from 'vitepress'`(否则命中 `docs/**/*.ts` 上 eslint `@typescript-eslint/no-unused-vars`(error),使 lint 失败)。从 `themeConfig.nav` 删除四条指向已删内容的旧入口(主页 / 指南 / API / 错误代码),nav 数组留待 Task 2 写入「中级教程」一条。从 `themeConfig.sidebar` 删除三个指向已删章节的旧键(分别对应 guide、logger、api 三个路径前缀)及其中全部内容。其余配置(`title` / `description` / `base` / `markdown` / `outDir` / `socialLinks` / `search` / `locales`)逐字保留。 (3) 编辑 `docs/.vitepress/api.ts`:在 `generateSidebar()` 入口处加一个「`docs/api` 目录不存在时直接 `return`」的守卫,使 `docs/api/` 被删除后 `tsx docs/.vitepress/init.ts`(以及依赖它的 `docs:dev`)不再因对缺失目录 `readdir` 抛错。守卫用 `node:fs` 的 `existsSync` 判断(dev.md 禁止 try-catch);新增的 `import { existsSync } from 'node:fs';` 放在文件首部既有 import 之后、import 之间不得插注释或空行。**不改动** `generateSidebar()` 的扫描 / 命名 / 排序 / 写文件逻辑(D-03:保留按包生成机制,供后续 API 参考重建)。若工作区残留 gitignored 的 `docs/.vitepress/apiSidebar.ts`,一并删除(已无消费者,且为会被覆盖的生成物)。 完成三步后运行构建命令确认站点通过。删除后 `docs/` 下已无 markdown 页面,VitePress 总会注入虚拟 `404.md`,故零页面时构建仍可成功;本步只需验证「无 import / dead-link 失败」。 - $ErrorActionPreference='Stop'; $gone=@('docs/guide','docs/dev','docs/logger','docs/api','docs/index.md'); foreach($p in $gone){ if(Test-Path $p){ Write-Error "STILL-PRESENT $p" } }; $cfg=[IO.File]::ReadAllText((Resolve-Path 'docs/.vitepress/config.ts')); if($cfg -match "'/guide/'"){ Write-Error "CFG-OLD-GUIDE-KEY" }; if($cfg -match "'/logger/'"){ Write-Error "CFG-OLD-LOGGER-KEY" }; if($cfg -match "'/api/'"){ Write-Error "CFG-OLD-API-KEY" }; if($cfg -match "apiSidebar"){ Write-Error "CFG-APISIDEBAR-IMPORT" }; if($cfg -match "listenSidebar"){ Write-Error "CFG-LISTENSIDEBAR" }; if($cfg -match "generateSidebar"){ Write-Error "CFG-GENERATESIDEBAR" }; $api=[IO.File]::ReadAllText((Resolve-Path 'docs/.vitepress/api.ts')); if($api -notmatch "existsSync"){ Write-Error "API-NO-GUARD" }; $out = cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build" 2>&1; if($LASTEXITCODE -ne 0){ $out | Write-Output; exit 1 }; $txt = $out -join "`n"; if($txt -notmatch 'build complete'){ Write-Error "NO-BUILD-COMPLETE" }; if($txt -match 'dead link'){ Write-Error "DEAD-LINK" }; if($txt -match 'Failed to resolve import'){ Write-Error "IMPORT-FAIL" }; Write-Output 'W0-CLEAR-OK' + $ErrorActionPreference='Stop'; $gone=@('docs/guide','docs/dev','docs/logger','docs/api','docs/index.md'); foreach($p in $gone){ if(Test-Path $p){ Write-Error "STILL-PRESENT $p" } }; $cfg=[IO.File]::ReadAllText((Resolve-Path 'docs/.vitepress/config.ts')); if($cfg -match "'/guide/'"){ Write-Error "CFG-OLD-GUIDE-KEY" }; if($cfg -match "'/logger/'"){ Write-Error "CFG-OLD-LOGGER-KEY" }; if($cfg -match "'/api/'"){ Write-Error "CFG-OLD-API-KEY" }; if($cfg -match "apiSidebar"){ Write-Error "CFG-APISIDEBAR-IMPORT" }; if($cfg -match "listenSidebar"){ Write-Error "CFG-LISTENSIDEBAR" }; if($cfg -match "generateSidebar"){ Write-Error "CFG-GENERATESIDEBAR" }; $api=[IO.File]::ReadAllText((Resolve-Path 'docs/.vitepress/api.ts')); if($api -notmatch "existsSync"){ Write-Error "API-NO-GUARD" }; $ErrorActionPreference='Continue'; $out = cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build" 2>&1; $ec=$LASTEXITCODE; $ErrorActionPreference='Stop'; if($ec -ne 0){ $out | Write-Output; exit 1 }; $txt = ($out | ForEach-Object { $_.ToString() }) -join "`n"; if($txt -notmatch 'build complete'){ Write-Error "NO-BUILD-COMPLETE" }; if($txt -match 'dead link'){ Write-Error "DEAD-LINK" }; if($txt -match 'Failed to resolve import'){ Write-Error "IMPORT-FAIL" }; Write-Output 'W0-CLEAR-OK' non-zero exit, or output contains "STILL-PRESENT", "CFG-OLD-", "CFG-APISIDEBAR-IMPORT", "CFG-LISTENSIDEBAR", "CFG-GENERATESIDEBAR", "API-NO-GUARD", "NO-BUILD-COMPLETE", "DEAD-LINK", or "IMPORT-FAIL", or output lacks "W0-CLEAR-OK" 旧文档树与首页已删除;`config.ts` 不再含旧 nav/sidebar 键与 apiSidebar/api 导入;`api.ts` 含缺失目录守卫;`tsx docs/.vitepress/init.ts` 不抛错且 `docs:build` 打印 build complete、无 dead link / import 错误;输出 W0-CLEAR-OK。 @@ -110,7 +110,7 @@ Output: 已清空的 `docs/`(无旧内容)+ 已清理并注册好的 `docs/. docs/.vitepress/config.ts, docs/intermediate/ 用**就地编辑(Edit,最小作用域)**修改 `docs/.vitepress/config.ts`,只做两处新增,并对 Task 1 已清空的 nav/sidebar 收尾。(1) 在 `nav` 数组写入一条 `{ text: '中级教程', link: '/intermediate/data-vs-render' }`(Task 1 已移除四条旧入口,此处之后 nav 仅此一条)。(2) 在 `sidebar` 对象中新增 `'/intermediate/'` 键,其值为一个 `{ text: '中级教程', collapsed: false, items: [...] }`,`items` 内按 D-20 顺序嵌套四个分组:`概念与导航`(`data-vs-render` / `src-tour` / `code-basics` / `hooks`)、`基础接口`(`data-state-overview` / `hero-attribute` / `hero-items` / `hero-equipment` / `hero-location` / `map-structure` / `map-tiles` / `flags`)、`战斗`(`combat-overview` / `enemy-specials` / `enemy-aura` / `enemy-query` / `enemy-final` / `enemy-calculator` / `enemy-map-damage` / `enemy-combat-script` / `enemy-status-hooks`)、`其他`(`events` / `replay-commands` / `data-plugins`)。每条写为 `{ text: '<已获批显示名>', link: '/intermediate/<文件名>' }`(绝对路径、不带 `.md`,写法与既有条目一致;分组嵌套写法参照 `'/guide/'` 原嵌套形状),显示名沿用此前获批的:`数据端与渲染端` / `src 导览` / `代码基础` / `钩子` / `数据端总览` / `勇士属性` / `勇士道具` / `勇士装备` / `勇士位置` / `地图结构` / `地图图块` / `Flag 变量` / `战斗总览` / `怪物特殊属性` / `光环` / `查询效果` / `最终属性` / `伤害计算` / `地图伤害` / `战斗脚本` / `状态钩子` / `自定义事件` / `录像指令` / `数据插件`(依次对应上述 24 个文件名)。创建目录 `docs/intermediate/`(`New-Item -ItemType Directory -Force -Path docs/intermediate`),本步仅为后续写作计划与本地预览提供落点(git 不跟踪空目录)。新增后运行构建命令确认站点通过(命令先跑 init.ts,init.ts 现已在 `docs/api/` 缺失时安全跳过)。 - $ErrorActionPreference='Stop'; $names=@('data-vs-render','src-tour','code-basics','hooks','data-state-overview','hero-attribute','hero-items','hero-equipment','hero-location','map-structure','map-tiles','flags','combat-overview','enemy-specials','enemy-aura','enemy-query','enemy-final','enemy-calculator','enemy-map-damage','enemy-combat-script','enemy-status-hooks','events','replay-commands','data-plugins'); $cfg=[IO.File]::ReadAllText((Resolve-Path 'docs/.vitepress/config.ts')); if($cfg -notmatch "text: '中级教程'"){ Write-Error "NO-NAV-ENTRY" }; if($cfg -notmatch "link: '/intermediate/data-vs-render'"){ Write-Error "NO-NAV-TARGET" }; if($cfg -notmatch "'/intermediate/'"){ Write-Error "NO-INTERMEDIATE-KEY" }; $targets=[regex]::Matches($cfg,"link:\s*'/intermediate/([a-z-]+)'") | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique; $want=$names | Sort-Object -Unique; if(($targets -join ',') -ne ($want -join ',')){ Write-Error ("SIDEBAR-TARGETS " + ($targets -join ',')) }; foreach($g in @('概念与导航','基础接口','战斗','其他')){ if($cfg -notmatch [regex]::Escape("text: '$g'")){ Write-Error "NO-GROUP $g" } }; if(-not (Test-Path 'docs/intermediate')){ Write-Error "NO-INTERMEDIATE-DIR" }; $out = cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build" 2>&1; if($LASTEXITCODE -ne 0){ $out | Write-Output; exit 1 }; $txt = $out -join "`n"; if($txt -notmatch 'build complete'){ Write-Error "NO-BUILD-COMPLETE" }; if($txt -match 'dead link'){ Write-Error "DEAD-LINK" }; if($txt -match 'Failed to resolve import'){ Write-Error "IMPORT-FAIL" }; Write-Output 'W0-REGISTER-OK' + $ErrorActionPreference='Stop'; $names=@('data-vs-render','src-tour','code-basics','hooks','data-state-overview','hero-attribute','hero-items','hero-equipment','hero-location','map-structure','map-tiles','flags','combat-overview','enemy-specials','enemy-aura','enemy-query','enemy-final','enemy-calculator','enemy-map-damage','enemy-combat-script','enemy-status-hooks','events','replay-commands','data-plugins'); $cfg=[IO.File]::ReadAllText((Resolve-Path 'docs/.vitepress/config.ts')); if($cfg -notmatch "text: '中级教程'"){ Write-Error "NO-NAV-ENTRY" }; if($cfg -notmatch "link: '/intermediate/data-vs-render'"){ Write-Error "NO-NAV-TARGET" }; if($cfg -notmatch "'/intermediate/'"){ Write-Error "NO-INTERMEDIATE-KEY" }; $targets=[regex]::Matches($cfg,"link:\s*'/intermediate/([a-z-]+)'") | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique; $want=$names | Sort-Object -Unique; if(($targets -join ',') -ne ($want -join ',')){ Write-Error ("SIDEBAR-TARGETS " + ($targets -join ',')) }; foreach($g in @('概念与导航','基础接口','战斗','其他')){ if($cfg -notmatch [regex]::Escape("text: '$g'")){ Write-Error "NO-GROUP $g" } }; if(-not (Test-Path 'docs/intermediate')){ Write-Error "NO-INTERMEDIATE-DIR" }; $ErrorActionPreference='Continue'; $out = cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build" 2>&1; $ec=$LASTEXITCODE; $ErrorActionPreference='Stop'; if($ec -ne 0){ $out | Write-Output; exit 1 }; $txt = ($out | ForEach-Object { $_.ToString() }) -join "`n"; if($txt -notmatch 'build complete'){ Write-Error "NO-BUILD-COMPLETE" }; if($txt -match 'dead link'){ Write-Error "DEAD-LINK" }; if($txt -match 'Failed to resolve import'){ Write-Error "IMPORT-FAIL" }; Write-Output 'W0-REGISTER-OK' non-zero exit, or output contains "NO-NAV-ENTRY", "NO-NAV-TARGET", "NO-INTERMEDIATE-KEY", "SIDEBAR-TARGETS", "NO-GROUP", "NO-INTERMEDIATE-DIR", "NO-BUILD-COMPLETE", "DEAD-LINK", or "IMPORT-FAIL", or output lacks "W0-REGISTER-OK" `config.ts` 新增 nav 一条与 `'/intermediate/'` sidebar 键(去重后恰 24 条 link、4 个分组,顺序 4/8/9/3);`docs/intermediate/` 目录存在;`docs:build` 通过并打印 build complete;输出 W0-REGISTER-OK。 diff --git a/.planning/phases/10-docs-restructure/10-02-PLAN.md b/.planning/phases/10-docs-restructure/10-02-PLAN.md index 6a47585..d084f0c 100644 --- a/.planning/phases/10-docs-restructure/10-02-PLAN.md +++ b/.planning/phases/10-docs-restructure/10-02-PLAN.md @@ -51,10 +51,9 @@ Output: 4 个新 markdown 文件(中文正文,英文标识符)。 @.planning/phases/10-docs-restructure/10-RESEARCH.md @.planning/phases/10-docs-restructure/10-PATTERNS.md @dev.md -@docs/guide/quick-start.md -@docs/guide/system.md **写作约定:** +- 旧文档 `docs/guide/quick-start.md` 与 `docs/guide/system.md` 已被 `10-01` 在本批删除(D-04),不再作为 `@` 引用加载(旧路径在 10-01 后不存在,加载会失败)。其页面骨架 / mermaid / 提示容器 / 外链等 analog 片段已内联于上方的 `10-PATTERNS.md`(「通用页面骨架」Analog A/B 及各页 Analog),写作时以 `10-PATTERNS.md` 所载片段为准。 - 文件命名 kebab-case(dev.md:68)、内容 CRLF(dev.md:116);页面骨架为单 `#` 主标题 + `##` 小节(PATTERNS「单 H1」)。 - 每篇 2–5 处跳转链接即可(D-17);**禁止**任何「后续再讲/留到高级教程/届时」式元叙述(D-16);指向渲染端内容时当作指向一个**已存在的平行部分**。 - 本组为阅读顺序最前,**不得**在正文中以 markdown 链接指向尚未写就的 `docs/intermediate/` 其它页面(否则构建报 dead link);确需提及时以代码span形式书写页面名,不加链接。 @@ -81,7 +80,7 @@ Output: 4 个新 markdown 文件(中文正文,英文标识符)。 - `data-vs-render.md`:一级标题「数据端与渲染端」;正文讲「数据端负责算、渲染端负责画」,比喻「棋局/账本」对「棋盘/摄像机」;指明两者分别在 `src/data`、`src/client`;重点讲**录像验证**(只跑数据端);含一张 mermaid 图;**不**出现「渲染端不能反向推送」的表述(留高级)。 - `src-tour.md`:一级标题「src 导览」;详讲 `src/data`、`src/client`、`src/content`、`src/constant.ts`;**一句带过** `src/bootstrap.ts`、`src/data.ts`、`src/package.json`、`src/styles.less`;**不讲** `src/main.ts`、`src/App.vue`、`src/types/`;含一张「该不该改」表。 - 按 D-21 写 `data-vs-render.md`:先给结论(数据端「算」、渲染端「画」),用「棋局/账本」对「棋盘/摄像机」的比喻点明职责边界,说明二者分别在 `src/data` 与 `src/client`,然后重点展开**录像验证只跑数据端**这一好处并给出 mermaid 图(图用 ` ```mermaid ` 代码块,语法照 `docs/guide/quick-start.md:27-31`)。素材:`dev.md:125-158`(双端分离)、`src/data.ts:1-5`、`dev.md:132`(`Mota.r()` 门控)。不要写渲染端反向推送相关结论(D-21)。\n\n按 D-22 写 `src-tour.md`:以目录导览方式详讲 `src/data`(数据端)、`src/client`(渲染端)、`src/content`(内容层)、`src/constant.ts`(常量),素材 `file:line` 见 10-RESEARCH「概念与导航 · src-tour」(如 `src/data/entry.ts:1-32`、`src/data/initialize.ts:1-85`、`src/content/item.ts:1-24`、`src/constant.ts:1-51`);`src/bootstrap.ts`、`src/data.ts`、`src/package.json`、`src/styles.less` 各用一句话带过(一般不需改动);不含 `src/main.ts`、`src/App.vue`、`src/types/`;末尾用一张 markdown 表格回答「该不该改」(列出目录/文件 → 是否通常需要改动)。两篇均遵守写作约定(kebab-case 文件名、CRLF、单 H1、无元叙述、跳转链接适量;本组不链接尚未写就的页面)。接口名/路径须逐字核对源码(RESEARCH Pitfall 5)。 + 按 D-21 写 `data-vs-render.md`:先给结论(数据端「算」、渲染端「画」),用「棋局/账本」对「棋盘/摄像机」的比喻点明职责边界,说明二者分别在 `src/data` 与 `src/client`,然后重点展开**录像验证只跑数据端**这一好处并给出 mermaid 图(图用 ` ```mermaid ` 代码块,语法照 `docs/guide/quick-start.md:27-31`)。该旧文件已由 `10-01` 删除,片段见 `10-PATTERNS.md`。素材:`dev.md:125-158`(双端分离)、`src/data.ts:1-5`、`dev.md:132`(`Mota.r()` 门控)。不要写渲染端反向推送相关结论(D-21)。\n\n按 D-22 写 `src-tour.md`:以目录导览方式详讲 `src/data`(数据端)、`src/client`(渲染端)、`src/content`(内容层)、`src/constant.ts`(常量),素材 `file:line` 见 10-RESEARCH「概念与导航 · src-tour」(如 `src/data/entry.ts:1-32`、`src/data/initialize.ts:1-85`、`src/content/item.ts:1-24`、`src/constant.ts:1-51`);`src/bootstrap.ts`、`src/data.ts`、`src/package.json`、`src/styles.less` 各用一句话带过(一般不需改动);不含 `src/main.ts`、`src/App.vue`、`src/types/`;末尾用一张 markdown 表格回答「该不该改」(列出目录/文件 → 是否通常需要改动)。两篇均遵守写作约定(kebab-case 文件名、CRLF、单 H1、无元叙述、跳转链接适量;本组不链接尚未写就的页面)。接口名/路径须逐字核对源码(RESEARCH Pitfall 5)。 $ErrorActionPreference='Stop'; $files=@('docs/intermediate/data-vs-render.md','docs/intermediate/src-tour.md'); foreach($p in $files){ if(-not(Test-Path $p)){ Write-Error "MISSING $p" }; $raw=[IO.File]::ReadAllText((Resolve-Path $p)); if($raw -match "(? non-zero exit, or output contains "MISSING", "LF-ONLY", "H1-COUNT", or "NO-MERMAID" @@ -96,7 +95,7 @@ Output: 4 个新 markdown 文件(中文正文,英文标识符)。 - `code-basics.md`:一级标题;按 ①简明语法 ②模块与引入 ③类型入门 顺序;类型部分把**泛型比喻成「类型的函数」**;讲 `interface` → `implements` 约定(**建议但非强制**);结尾给出外链 `https://h5mota.com/bbs/thread/?tid=1018&p=3#p41`。 - `hooks.md`:一级标题;讲钩子是什么;列常用钩子族一览;含**「需求 → 钩子」对照表**(核心);讲 `addHook` / `removeHook`;`hook/initialize.ts` 一笔带过;标明哪些是 async 钩子;用一句话说明「钩子可能被非本对象触发」。 - 按 D-23 写 `code-basics.md`:三段式——①简明语法(变量、函数、分支等基础,可参照 `docs/guide/system.md:160-191` 的模块化示例风格);②模块与引入(`import`/`export`,用 ` ```ts ` 代码块);③类型入门(基础类型 → 接口 → 泛型;把**泛型比作「类型的函数」**,讲 `interface` → `implements` 是**建议但非强制**的约定)。结尾以一句自然引导给出**唯一获批外链** `https://h5mota.com/bbs/thread/?tid=1018&p=3#p41`(D-23;不得新增其它外链)。本页无源码依赖。\n\n按 D-24 写 `hooks.md`:先从概念讲「钩子是什么」,再列**常用钩子族一览**(族名清单的 `file:line` 见 10-RESEARCH「概念与导航 · hooks」,如 `IHeroAttributeHooks`、`IHeroLocationHooks`、`IObjectMoverHooks`、`ICombatFlowHooks`、`IMapLayerHooks` 等),核心是**「需求 → 钩子」对照表**(markdown 表格,左列需求、右列对应钩子/族);然后讲 `addHook` / `removeHook` 的用法(顶层接口名,D-15;机制接口在 `packages/common/src/hook.ts:23-52`);`hook/initialize.ts` 一句带过(注册点,`src/data/hook/initialize.ts:17-26`);标注哪些钩子族是 async;末尾一句说明「钩子可能被非本对象触发」(多主对象时才需判断)。两篇遵守写作约定;`hooks` 的对照表若需引用后续页面,用代码span写页面名而不加链接。 + 按 D-23 写 `code-basics.md`:三段式——①简明语法(变量、函数、分支等基础,可参照 `docs/guide/system.md:160-191`(该旧文件已由 `10-01` 删除,片段见 `10-PATTERNS.md`)的模块化示例风格);②模块与引入(`import`/`export`,用 ` ```ts ` 代码块);③类型入门(基础类型 → 接口 → 泛型;把**泛型比作「类型的函数」**,讲 `interface` → `implements` 是**建议但非强制**的约定)。结尾以一句自然引导给出**唯一获批外链** `https://h5mota.com/bbs/thread/?tid=1018&p=3#p41`(D-23;不得新增其它外链)。本页无源码依赖。\n\n按 D-24 写 `hooks.md`:先从概念讲「钩子是什么」,再列**常用钩子族一览**(族名清单的 `file:line` 见 10-RESEARCH「概念与导航 · hooks」,如 `IHeroAttributeHooks`、`IHeroLocationHooks`、`IObjectMoverHooks`、`ICombatFlowHooks`、`IMapLayerHooks` 等),核心是**「需求 → 钩子」对照表**(markdown 表格,左列需求、右列对应钩子/族);然后讲 `addHook` / `removeHook` 的用法(顶层接口名,D-15;机制接口在 `packages/common/src/hook.ts:23-52`);`hook/initialize.ts` 一句带过(注册点,`src/data/hook/initialize.ts:17-26`);标注哪些钩子族是 async;末尾一句说明「钩子可能被非本对象触发」(多主对象时才需判断)。两篇遵守写作约定;`hooks` 的对照表若需引用后续页面,用代码span写页面名而不加链接。 $ErrorActionPreference='Stop'; $files=@('docs/intermediate/code-basics.md','docs/intermediate/hooks.md'); foreach($p in $files){ if(-not(Test-Path $p)){ Write-Error "MISSING $p" }; $raw=[IO.File]::ReadAllText((Resolve-Path $p)); if($raw -match "(? non-zero exit, or output contains "MISSING", "LF-ONLY", "H1-COUNT", "NO-APPROVED-LINK", or "NO-ADDHOOK" diff --git a/.planning/phases/10-docs-restructure/10-03-PLAN.md b/.planning/phases/10-docs-restructure/10-03-PLAN.md index e84e170..5c44854 100644 --- a/.planning/phases/10-docs-restructure/10-03-PLAN.md +++ b/.planning/phases/10-docs-restructure/10-03-PLAN.md @@ -59,9 +59,9 @@ Output: 8 个新 markdown 文件。 @.planning/phases/10-docs-restructure/10-CONTEXT.md @.planning/phases/10-docs-restructure/10-RESEARCH.md @.planning/phases/10-docs-restructure/10-PATTERNS.md -@docs/guide/implements/special.md **写作约定:** +- 旧文档 `docs/guide/implements/special.md` 已被 `10-01` 在本批删除(D-04),不再作为 `@` 引用加载(旧路径在 10-01 后不存在,加载会失败)。其 Analog C 骨架片段(`// [!code ++]` 行高亮、`{起-止}` 区块高亮、how-to 叙述、代码块语言标注)已内联于上方的 `10-PATTERNS.md`,写作时以该文件所载片段为准。 - 骨架统一照 Analog C(`docs/guide/implements/special.md`):`# 标题` → 引言 → `## 小节` → ` ```ts ` 代码块;标新增行用 `// [!code ++]`,标改动区块用 ` ```ts {起-止} `。**只借结构、不抄旧接口名**。 - 代码块内每处接口名/字段名必须按 10-RESEARCH「基础接口」表的 `file:line` 逐字核对(RESEARCH Pitfall 5)。 - kebab-case 文件名、CRLF、单 H1、无元叙述(D-16);跳转链接 2–5 处/篇(D-17)。 diff --git a/.planning/phases/10-docs-restructure/10-04-PLAN.md b/.planning/phases/10-docs-restructure/10-04-PLAN.md index 5d5c253..8a294af 100644 --- a/.planning/phases/10-docs-restructure/10-04-PLAN.md +++ b/.planning/phases/10-docs-restructure/10-04-PLAN.md @@ -61,10 +61,9 @@ Output: 9 个新 markdown 文件。 @.planning/phases/10-docs-restructure/10-CONTEXT.md @.planning/phases/10-docs-restructure/10-RESEARCH.md @.planning/phases/10-docs-restructure/10-PATTERNS.md -@docs/guide/implements/special.md -@docs/dev/enemy/enemy-combat-flow.md **写作约定与前置:** +- 旧文档 `docs/guide/implements/special.md` 与 `docs/dev/enemy/enemy-combat-flow.md` 已被 `10-01` 在本批删除(D-04),不再作为 `@` 引用加载(旧路径在 10-01 后不存在,加载会失败)。其骨架 / 战斗流程 analog 片段已内联于上方的 `10-PATTERNS.md`,写作时以该文件所载片段为准。 - 骨架照 Analog C(`docs/guide/implements/special.md`):`# 标题` → 引言 → `## 小节` → 代码块;`// [!code ++]` 标新增行,` ```ts {起-止} ` 标改动区块。**只借结构、不抄旧接口名**(旧文档对应旧接口)。 - 代码块内每处接口名/字段名按 10-RESEARCH「战斗」表 `file:line` 逐字核对(RESEARCH Pitfall 5)。 - kebab-case、CRLF、单 H1、无元叙述(D-16);跳转链接 2–5 处/篇。 @@ -94,7 +93,7 @@ Output: 9 个新 markdown 文件。 - `enemy-specials`:特殊属性是什么、在数据里的样子、两类定义(无参 `defineNonePropertySpecial` / 有参 `defineCommonSerializableSpecial`);注册在 `src/data/enemy/special.ts`;如何自定义新属性(步骤);「属性实现位置」对照表;多个示例(有参/无参、`name`/`desc` 用字符串与函数);编辑器识别约定(`// {code} - {name}`、`/** {code} - {name} */`、`ISpecial{code}Value`、`#region` 不能删);怪物比较简单说。 - `enemy-aura`:先讲优先级(高优先级可影响低优先级、反之不行;与查询共用优先级);内置光环(普通 23 / 支援 24 / 特殊光环 26);运作方式(转换器 + 视图并注册);四种内置范围只列名;多个自定义示例(基础光环「减少周围怪物金币与经验」+ 高级特殊光环「给周围怪物上光环、优先级高于特殊光环」);讲清 `apply` 与 `applySpecial` 差别。 - 按 D-33/D-34/D-35 撰写。`combat-overview` 素材:`packages-user/data-system/src/combat/types.ts:847-901`(`ICombatFlow`)、同文件:747-755(`IEnemyContext.buildup()` 四阶段文档)、`src/data/enemy/initialize.ts:27-70`(注册与装配全流程)、`packages-user/data-state/src/core.ts:79-83,123-127`(L2 装配);mermaid 图照 D-33 流程,写法参照 `docs/guide/quick-start.md:27-31`。`enemy-specials` 素材:`data-base/src/enemy/types.ts:31-75,164-248`(`ISpecial`/`IReadonlyEnemy`/`IEnemyManager`/`SpecialCreation`)、`data-base/src/enemy/special.ts:136-150`(两类定义函数)、`src/data/enemy/special.ts:1-477`(内置注册;注释清单:72-84;编辑器识别注释:8-14,98,100,473)、`src/data/enemy/comparer.ts:1-31`(怪物比较)。`enemy-aura` 素材:`src/data/enemy/aura.ts:1-301`(`CommonAura` code 23:29-119;`GuardAura` code 24:125-184;`SpecialAura` code 26:190-299;`SpecialAuraModifier`:206-242;`priority` 实测值 CommonAura=10、GuardAura=5、SpecialAura=20,见 aura.ts:50,147,249)、`data-system/src/combat/types.ts:99-191`(`IAuraView`/`IEnemyAuraView`/`IAuraConverter`/`IEnemySpecialModifier`)、注册 `registerAuraConverter`:638、全局光环 `addAura`:727、范围类 `FullRange`/`RectRange`/`ManhattanRange`/`RayRange` 来自 `@motajs/common`。遵守写作约定。 + 按 D-33/D-34/D-35 撰写。`combat-overview` 素材:`packages-user/data-system/src/combat/types.ts:847-901`(`ICombatFlow`)、同文件:747-755(`IEnemyContext.buildup()` 四阶段文档)、`src/data/enemy/initialize.ts:27-70`(注册与装配全流程)、`packages-user/data-state/src/core.ts:79-83,123-127`(L2 装配);mermaid 图照 D-33 流程,写法参照 `docs/guide/quick-start.md:27-31`(该旧文件已由 `10-01` 删除,片段见 `10-PATTERNS.md`)。`enemy-specials` 素材:`data-base/src/enemy/types.ts:31-75,164-248`(`ISpecial`/`IReadonlyEnemy`/`IEnemyManager`/`SpecialCreation`)、`data-base/src/enemy/special.ts:136-150`(两类定义函数)、`src/data/enemy/special.ts:1-477`(内置注册;注释清单:72-84;编辑器识别注释:8-14,98,100,473)、`src/data/enemy/comparer.ts:1-31`(怪物比较)。`enemy-aura` 素材:`src/data/enemy/aura.ts:1-301`(`CommonAura` code 23:29-119;`GuardAura` code 24:125-184;`SpecialAura` code 26:190-299;`SpecialAuraModifier`:206-242;`priority` 实测值 CommonAura=10、GuardAura=5、SpecialAura=20,见 aura.ts:50,147,249)、`data-system/src/combat/types.ts:99-191`(`IAuraView`/`IEnemyAuraView`/`IAuraConverter`/`IEnemySpecialModifier`)、注册 `registerAuraConverter`:638、全局光环 `addAura`:727、范围类 `FullRange`/`RectRange`/`ManhattanRange`/`RayRange` 来自 `@motajs/common`。遵守写作约定。 $ErrorActionPreference='Stop'; $files=@('docs/intermediate/combat-overview.md','docs/intermediate/enemy-specials.md','docs/intermediate/enemy-aura.md'); foreach($p in $files){ if(-not(Test-Path $p)){ Write-Error "MISSING $p" }; $raw=[IO.File]::ReadAllText((Resolve-Path $p)); if($raw -match "(? non-zero exit, or output contains "MISSING", "LF-ONLY", "H1-COUNT", "NO-MERMAID", or "NO-APPLYSPECIAL" diff --git a/.planning/phases/10-docs-restructure/10-05-PLAN.md b/.planning/phases/10-docs-restructure/10-05-PLAN.md index eb12410..9f3a714 100644 --- a/.planning/phases/10-docs-restructure/10-05-PLAN.md +++ b/.planning/phases/10-docs-restructure/10-05-PLAN.md @@ -48,10 +48,9 @@ Output: 3 个新 markdown 文件。 @.planning/phases/10-docs-restructure/10-CONTEXT.md @.planning/phases/10-docs-restructure/10-RESEARCH.md @.planning/phases/10-docs-restructure/10-PATTERNS.md -@docs/guide/implements/special.md -@docs/dev/replay/replay-system.md **写作约定与前置:** +- 旧文档 `docs/guide/implements/special.md` 与 `docs/dev/replay/replay-system.md` 已被 `10-01` 在本批删除(D-04),不再作为 `@` 引用加载(旧路径在 10-01 后不存在,加载会失败)。其骨架 / 「核心概念定义」组织方式的 analog 片段已内联于上方的 `10-PATTERNS.md`,写作时以该文件所载片段为准。 - 骨架照 Analog C(`special.md`);`replay-commands` 的组织方式可借 `docs/dev/replay/replay-system.md:29-40` 的「核心概念定义」分节写法,但**不沿用**其「每节都是 `#`」风格(本批用单 H1 + `##`)。 - 代码块接口名/字段按 10-RESEARCH「其他」表 `file:line` 逐字核对(RESEARCH Pitfall 5)。 - kebab-case、CRLF、单 H1、无元叙述(D-16);跳转链接 2–5 处/篇。 diff --git a/.planning/phases/10-docs-restructure/10-06-PLAN.md b/.planning/phases/10-docs-restructure/10-06-PLAN.md index feb7445..058b7fd 100644 --- a/.planning/phases/10-docs-restructure/10-06-PLAN.md +++ b/.planning/phases/10-docs-restructure/10-06-PLAN.md @@ -68,10 +68,10 @@ Output: 验收摘要 `10-06-SUMMARY.md`;站点构建产物 `public/_docs`(gi Task 1: 全量构建 + 静态验收核对 - docs/.vitepress/config.ts + docs/.vitepress/config.ts(只读核对;本计划不改动仓库文件,frontmatter files_modified 为空,产物仅 SUMMARY) 先跑全量构建,再做静态核对。(1) 构建:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"`,确认打印 build complete、无 dead link、无 `Failed to resolve import`,且产物目录 `public/_docs` 已生成;旧内容已于 10-01 随本批删除,本批 dead-link 数应为 0。(2) 旧内容删除基线:确认 `docs/guide`、`docs/dev`、`docs/logger`、`docs/api`、`docs/index.md` 均不存在(首页 `docs/index.md` 本批**有意缺省**,不作为缺页问题)。(3) 静态核对 24 个文件:`docs/intermediate/` 下恰存在 D-20 的 24 个文件名(`data-vs-render`/`src-tour`/`code-basics`/`hooks`/`data-state-overview`/`hero-attribute`/`hero-items`/`hero-equipment`/`hero-location`/`map-structure`/`map-tiles`/`flags`/`combat-overview`/`enemy-specials`/`enemy-aura`/`enemy-query`/`enemy-final`/`enemy-calculator`/`enemy-map-damage`/`enemy-combat-script`/`enemy-status-hooks`/`events`/`replay-commands`/`data-plugins`,各加 `.md`);每个文件 CRLF、单 H1。(4) 核对 `config.ts` 的 `'/intermediate/'` 键:去重后恰含 24 个 `/intermediate/<名>` 目标,且与 24 个文件名一一对应(导航入口 `中级教程` 复用其中一个目标,去重后仍为 24)。(5) 外链白名单:全 24 篇内出现的外链仅为 `https://h5mota.com/bbs/thread/?tid=1018&p=3#p41`(若文本中还出现其它 `http`,逐一报告供用户判断,不擅自删改)。发现任何缺页/错链只报告,不擅自补写。 - $ErrorActionPreference='Stop'; $names=@('data-vs-render','src-tour','code-basics','hooks','data-state-overview','hero-attribute','hero-items','hero-equipment','hero-location','map-structure','map-tiles','flags','combat-overview','enemy-specials','enemy-aura','enemy-query','enemy-final','enemy-calculator','enemy-map-damage','enemy-combat-script','enemy-status-hooks','events','replay-commands','data-plugins'); $missing=@(); foreach($n in $names){ $p="docs/intermediate/$n.md"; if(-not(Test-Path $p)){ $missing+=$p; continue }; $raw=[IO.File]::ReadAllText((Resolve-Path $p)); if($raw -match "(?&1; if($LASTEXITCODE -ne 0){ $out | Write-Output; exit 1 }; $txt = $out -join "`n"; if($txt -notmatch 'build complete'){ Write-Error "NO-BUILD-COMPLETE" }; if($txt -match 'dead link'){ Write-Error "DEAD-LINK" }; if($txt -match 'Failed to resolve import'){ Write-Error "IMPORT-FAIL" }; Write-Output 'ACCEPTANCE-OK' + $ErrorActionPreference='Stop'; $names=@('data-vs-render','src-tour','code-basics','hooks','data-state-overview','hero-attribute','hero-items','hero-equipment','hero-location','map-structure','map-tiles','flags','combat-overview','enemy-specials','enemy-aura','enemy-query','enemy-final','enemy-calculator','enemy-map-damage','enemy-combat-script','enemy-status-hooks','events','replay-commands','data-plugins'); $missing=@(); foreach($n in $names){ $p="docs/intermediate/$n.md"; if(-not(Test-Path $p)){ $missing+=$p; continue }; $raw=[IO.File]::ReadAllText((Resolve-Path $p)); if($raw -match "(?&1; $ec=$LASTEXITCODE; $ErrorActionPreference='Stop'; if($ec -ne 0){ $out | Write-Output; exit 1 }; $txt = ($out | ForEach-Object { $_.ToString() }) -join "`n"; if($txt -notmatch 'build complete'){ Write-Error "NO-BUILD-COMPLETE" }; if($txt -match 'dead link'){ Write-Error "DEAD-LINK" }; if($txt -match 'Failed to resolve import'){ Write-Error "IMPORT-FAIL" }; Write-Output 'ACCEPTANCE-OK' non-zero exit, or output contains "LF-ONLY", "H1-COUNT", "MISSING:", "OLD-CONTENT-PRESENT", "SIDEBAR-TARGETS", "EXT-LINK-NOT-APPROVED", "NO-BUILD-COMPLETE", "DEAD-LINK", or "IMPORT-FAIL"; or output lacks "ACCEPTANCE-OK" 旧内容删除基线成立(guide/dev/logger/api 与首页均不存在);构建通过且产物生成、dead-link 为 0;24 个文件齐全、CRLF、单 H1;sidebar 去重后恰 24 个目标并与文件一一对应;无非白名单外链;输出 ACCEPTANCE-OK。 diff --git a/.planning/phases/10-docs-restructure/10-CONTEXT.md b/.planning/phases/10-docs-restructure/10-CONTEXT.md index cbd1287..00b0a01 100644 --- a/.planning/phases/10-docs-restructure/10-CONTEXT.md +++ b/.planning/phases/10-docs-restructure/10-CONTEXT.md @@ -85,30 +85,81 @@ **逐篇内容** -- **D-21(概念·`data-vs-render`)** 数据端负责「算」、渲染端负责「画」;比喻「棋局 / 账本」对「棋盘 / 摄像机」;两者分别在 `src/data`、`src/client`;只讲好处,重点**录像验证**(只跑数据端);附 mermaid 图;**不提**「渲染端不能反向推送」(留高级);不提「后续再讲」。 -- **D-22(概念·`src-tour`)** 详讲 `src/data`、`src/client`、`src/content`、`src/constant.ts`;**一句带过** `src/bootstrap.ts`、`src/data.ts`(数据端入口、需外部 bootstrap)、`src/package.json`、`src/styles.less`(一般不需要动);**不讲** `src/main.ts`、`src/App.vue`、`src/types/`(均已弃用);含一张「该不该改」表。 -- **D-23(概念·`code-basics`)** 顺序 ①简明语法 ②模块与引入 ③类型入门;类型把**泛型比喻成「类型的函数」**;讲 `interface` → `implements` 约定(**建议但非强制**,不遵循也不会出问题);三者都大概讲;结尾给 JS/TS 教程链接 `https://h5mota.com/bbs/thread/?tid=1018&p=3#p41`(该教程针对本引擎设计、更适合本引擎作者)。 -- **D-24(概念·`hooks`)** 钩子是什么;常用钩子族一览;**需求 → 钩子对照表**(核心);`addHook` / `removeHook`;`hook/initialize.ts` 一笔带过;async 钩子只提哪些是;「钩子可能被非本对象触发」简说一句(多主对象时才需要判断,一般不会串)。 -- **D-25(基础·`data-state-overview`)** 数据端主对象 `state` 是访问一切入口;一张表列常用对象与获取方式:`hero` / `maps` / `flags` / `enemyManager` / `enemyContext` / `combat` / `eventSystem` / `pathfinding` / `tileStore` / `itemStore` / `mapStore` / `eventStore` / `faceManager` / `roleFace` / `replaySystem` / `loader` / `loadManager` / `getDataPlugin(id)` / `emitTip(...)`;按 L0–L3 **轻量分组标注**(分层不影响接口使用,细讲留高级)。放在基础接口组最前。 -- **D-26(基础·`hero-attribute`)** 属性列表;基础 vs 最终属性(`getBaseAttribute` / `getFinalAttribute`);改基础属性 `set` / `add` / `mul` / `div`(经 `getModifiableAttribute()`);**修饰器**:内置 `ValueModifier`(数值加减,`@system/value`)与 `PercentageModifier`(**按基础值算百分比**,`@system/percentage`);`addModifier` / `deleteModifier`;提 `save` 参数——**不进存档时,应由外部其它系统负责分配该修饰器,避免读档后不一致**;`getAttribute()` 只读、`getIsolatedAttribute()` 试算;示例:攻击 +N 永久、防御 −X% 临时;指回 hooks 篇。 -- **D-27(基础·`hero-items`)** `IHeroItems`(`addItem` 可传负数、`Pick` 立刻生效、`getItemState`、`iterateItems`、`useItem` 仅 `Constant` / `Consumable`、`itemCount`);**四种类别区分**:`Constant`(永久)/ `Consumable`(消耗)/ `Pick`(即捡即用)/ `Equipment`(装备,虽是道具但属单独系统);道具效果提一句属**内容层**,并说明道具效果可调用哪些内容(`state`、道具数据);示例:发黄钥匙 / 减道具 / 使用道具。 -- **D-28(基础·`hero-equipment`)** `IHeroEquipment`(`slots` / `canEquipTo` / `equip` 含 `autoUnload` / `unequip` / `equipped` / `getEquipped` / `getEquips`);`IHeroEquipsStore`(`add` / `delete` / `get` / `instancesOf` / `instances` / `count`);`IEquipmentState` 的 `value` / `percentage`(本质是修饰器);`compareEquip` **简单讲**;**排序器不讲**(高级);以「会用常用接口」为界。 -- **D-29(基础·`hero-location`)** 只讲**获取位置**(`hero.location` 的 `x` / `y` / `direction`、`hero.getLocation()`)与**获取楼层**(`floorId` / `map`);移动位置、切换楼层留高级。 -- **D-30(基础·`map-structure`)** `IMapState` 楼层集合(`maps` / `getMap` / `getActiveMap` / `iterateAllMaps`)与**楼层激活**(active);`IGameMap`(`layerList` / `eventLayer` / `tileStore` / 宽高);`IMapLayer`(`alias` 别名、宽高);**事件层本质就是个图层,归本篇**。 -- **D-31(基础·`map-tiles`)** 只讲 `getBlock` / `setBlock` / `removeBlock` / `putMapData` / `getMapData` / `iterateBlocks`;`getLocationData` 属低级需求,**留高级**。 -- **D-32(基础·`flags`)** 全局**可存档**变量系统;**所有接口都讲(含 `occupied`)**:`setField` / `getField` / `getOrInsert` / `getOrInsertComputed` / `deleteField` / `setFieldValue` / `addFieldValue` / `getFieldValue` / `getFieldValueDefaults`;会进存档;**建议**不滥用、不用的变量及时 `deleteField`;示例:记录开关 / 计数。 -- **D-33(战斗·`combat-overview`)** 伤害大致怎么算;流程(基础数据 → 光环 / 查询 / 最终 → 伤害计算 → 战前脚本 → 战斗 → 战后结算 + 状态钩子);mermaid 图;末尾「流程步骤 → 对应文档」对照表;临界值不在本篇提。 -- **D-34(战斗·`enemy-specials`)** 特殊属性是什么、在数据里的样子、两类定义(无参 `defineNonePropertySpecial` / 有参 `defineCommonSerializableSpecial`);`src/data/enemy/special.ts` 注册;**如何自定义新特殊属性**(步骤);**「属性实现位置」对照表**(内置属性各自实现在哪);**多个示例**(有参 / 无参、`name` / `desc` 用字符串与函数);编辑器识别约定(`// {code} - {name}`、`/** {code} - {name} */`、`ISpecial{code}Value`、`#region` 不能删);**怪物比较**简单说、一般不改、真要改参考高级教程。 -- **D-35(战斗·`enemy-aura`)** **先讲优先级**(高优先级可影响低优先级、反之不行;与查询效果共用优先级);内置光环(普通 23 / 支援 24 / 特殊光环 26);运作方式(写「转换器 + 视图」并注册,**不深讲为何**);四种内置范围只列名(`FullRange` / `RectRange` / `ManhattanRange` / `RayRange`);**多个自定义示例**:基础光环(减少周围怪物金币与经验)+ 高级特殊光环(给周围怪物上光环、优先级高于特殊光环);借示例讲清基础光环 vs 特殊光环差距;`apply` 与 `applySpecial` 差别讲清。 -- **D-36(战斗·`enemy-query`)** **新增内容(引擎未内置)**;解决「特殊属性查询外部状态」;常规查询 `CommonQuery`(`registerCommonQueryEffect(code, effect)`,`apply(handler, special, query)`)与特殊查询 `SpecialQuery`(`registerSpecialQueryEffect(effect)`,`for(ctx)` → 修饰器 `shouldQuery` / `add` / `delete` / `modify`);与光环共用优先级;**三个示例**:抱团(范围内每有一个带该属性的怪物就给自身加成)、同化(把范围内怪物特殊属性同化到自身,数值型加算叠加、否则取位置索引最小者)、号角(场上每有一只怪物就给自身加成);说明三者能干什么即可;用**抱团**对比「用光环实现复杂度更高」;设计原因留高级。 -- **D-37(战斗·`enemy-final`)** 最终属性效果:**只影响自身、不依赖也不影响外部环境**的一段收尾调整;内置坚固(3)、模仿(8);接口 `IEnemyFinalEffect`(`priority` + `apply`)、`registerFinalEffect`;**三方对比**(光环:不依赖外部但影响外部;查询:依赖外部仅影响自身;最终:两者皆无);示例:**仿攻**(怪物攻击 = 玩家攻击);`priority` 一句带过;回指 `combat-overview` 流程。 -- **D-38(战斗·`enemy-calculator`)** `IDamageCalculator`(`calculate` → `{ damage, turn }`、`getCriticalLimit`);注册 `useCalculator`;**逐段导读**内置伤害公式并指明「加自己的属性加在哪一步」;**支援怪**较难 → 写「实在理解不了可暂时跳过」;示例:**等差数列属性**(怪物攻击每回合按固定值递增,等差公式)等;核心讲明调整的是 `f(enemy, hero)` 里的 **`f`**;**临界**:定义 + 现有约定(伤害值单调递减;不单调也可、但临界不准)+ 临界值唯一依赖「最后的伤害值」,**不讲二分过程**。 -- **D-39(战斗·`enemy-map-damage`)** 概念(领域 / 夹击 / 阻击 / 激光 / 伏击);三部分(伤害视图 / 转换器 / 合并器,浅讲);注册 `useConverter` / `useReducer`;内置五类表(`code` / 实现位置**写文档时核对源码**);自定义示例:**受地形遮挡的激光**;合并器简单讲(决定一个点受多个地图伤害时取什么伤害类型,深入留高级);**实际结算用户届时补**。 -- **D-40(战斗·`enemy-combat-script`)** 战斗脚本(战前 `before` 返回 `bool` 拦截、战后 `after` 结算,按 `priority`);内置 `MainCombatScript`;注册 `addCombatScript`;**「脚本 vs 钩子」选型表**;战前示例「怪物拥有某特殊属性时无法与其战斗」、战后示例「战后玩家回复固定量生命值」;`priority` 一句。 -- **D-41(战斗·`enemy-status-hooks`)** 状态类属性(中毒 / 衰弱 / 重生)必须挂钩子;三类内置(中毒:`onStepEnd` + `onAfterTeleportTo` **两个钩子**;衰弱:`onAfterCombat` + 修饰器;重生:`onAfterCombat` + `onSetFloor` **跨钩子协同**);注册在 `initialize.ts`;**强调一个状态可能要用多个钩子**;自定义示例「下次战斗伤害乘二」(并说明还要改伤害计算器,串起 `enemy-calculator`);修饰器概念见 `hero-attribute`。 -- **D-42(其他·`events`)** 只讲**如何自定义新事件**(内建函数),不讲原理、不列内建全量;`BuiltInFunction`(`name` + `func(param, env)`);`IBlockEventEnv` 常用字段(其余指向 API);参数接口在 `types.ts`;注册在 `createEventRegistrations()`;内置示例导读;**自定义示例:设置 flag、勇士回血**;编辑器识别方式未设计完 → 文档留余地。 -- **D-43(其他·`replay-commands`)** 录像系统用途 + **核心原则**「玩家直接产生、会影响最终成绩的行为必须计入录像」并解释其含义;**如何新增自定义录像需改 `packages-user` 下的引擎内容**;主动录像 vs 被动录像(`ReplayCommandType.Active` / `Passive`);`@shouldReplay` / `@ignoreReplay` 的功能与检测(`begin` / `end` 区间);**不给示例**,后续其他教程引入自定义录像时直接链接过去。 -- **D-44(其他·`data-plugins`)** 插件是强拓展系统,中级只讲基础、**高级教程详解其工作原理**;`IDataPluginDefine`(`id` / `dependencies` / `awake` / `initialize` / `afterLoad`);时机次序 `awake`(装配)→ `initialize`(**加载前**)→ `afterLoad`(加载后);注册在 `src/data/plugin/list.ts` 的 `dataPlugins`;`state.addSaveableContent`;示例**占位**(待用户提供**较简单**插件,**不用商店插件**);写明「是否纳入本中级篇取决于示例难度」。 +- **D-21:** (概念·`data-vs-render`)数据端负责「算」、渲染端负责「画」;比喻「棋局 / 账本」对「棋盘 / 摄像机」;两者分别在 `src/data`、`src/client`;只讲好处,重点**录像验证**(只跑数据端);附 mermaid 图;**不提**「渲染端不能反向推送」(留高级);不提「后续再讲」。 +- **D-22:** (概念·`src-tour`)详讲 `src/data`、`src/client`、`src/content`、`src/constant.ts`;**一句带过** `src/bootstrap.ts`、`src/data.ts`(数据端入口、需外部 bootstrap)、`src/package.json`、`src/styles.less`(一般不需要动);**不讲** `src/main.ts`、`src/App.vue`、`src/types/`(均已弃用);含一张「该不该改」表。 +- **D-23:** (概念·`code-basics`)顺序 ①简明语法 ②模块与引入 ③类型入门;类型把**泛型比喻成「类型的函数」**;讲 `interface` → `implements` 约定(**建议但非强制**,不遵循也不会出问题);三者都大概讲;结尾给 JS/TS 教程链接 `https://h5mota.com/bbs/thread/?tid=1018&p=3#p41`(该教程针对本引擎设计、更适合本引擎作者)。 +- **D-24:** (概念·`hooks`)钩子是什么;常用钩子族一览;**需求 → 钩子对照表**(核心);`addHook` / `removeHook`;`hook/initialize.ts` 一笔带过;async 钩子只提哪些是;「钩子可能被非本对象触发」简说一句(多主对象时才需要判断,一般不会串)。 +- **D-25:** (基础·`data-state-overview`)数据端主对象 `state` 是访问一切入口;一张表列常用对象与获取方式:`hero` / `maps` / `flags` / `enemyManager` / `enemyContext` / `combat` / `eventSystem` / `pathfinding` / `tileStore` / `itemStore` / `mapStore` / `eventStore` / `faceManager` / `roleFace` / `replaySystem` / `loader` / `loadManager` / `getDataPlugin(id)` / `emitTip(...)`;按 L0–L3 **轻量分组标注**(分层不影响接口使用,细讲留高级)。放在基础接口组最前。 +- **D-26:** (基础·`hero-attribute`)属性列表;基础 vs 最终属性(`getBaseAttribute` / `getFinalAttribute`);改基础属性 `set` / `add` / `mul` / `div`(经 `getModifiableAttribute()`);**修饰器**:内置 `ValueModifier`(数值加减,`@system/value`)与 `PercentageModifier`(**按基础值算百分比**,`@system/percentage`);`addModifier` / `deleteModifier`;提 `save` 参数——**不进存档时,应由外部其它系统负责分配该修饰器,避免读档后不一致**;`getAttribute()` 只读、`getIsolatedAttribute()` 试算;示例:攻击 +N 永久、防御 −X% 临时;指回 hooks 篇。 +- **D-27:** (基础·`hero-items`)`IHeroItems`(`addItem` 可传负数、`Pick` 立刻生效、`getItemState`、`iterateItems`、`useItem` 仅 `Constant` / `Consumable`、`itemCount`);**四种类别区分**:`Constant`(永久)/ `Consumable`(消耗)/ `Pick`(即捡即用)/ `Equipment`(装备,虽是道具但属单独系统);道具效果提一句属**内容层**,并说明道具效果可调用哪些内容(`state`、道具数据);示例:发黄钥匙 / 减道具 / 使用道具。 +- **D-28:** (基础·`hero-equipment`)`IHeroEquipment`(`slots` / `canEquipTo` / `equip` 含 `autoUnload` / `unequip` / `equipped` / `getEquipped` / `getEquips`);`IHeroEquipsStore`(`add` / `delete` / `get` / `instancesOf` / `instances` / `count`);`IEquipmentState` 的 `value` / `percentage`(本质是修饰器);`compareEquip` **简单讲**;**排序器不讲**(高级);以「会用常用接口」为界。 +- **D-29:** (基础·`hero-location`)只讲**获取位置**(`hero.location` 的 `x` / `y` / `direction`、`hero.getLocation()`)与**获取楼层**(`floorId` / `map`);移动位置、切换楼层留高级。 +- **D-30:** (基础·`map-structure`)`IMapState` 楼层集合(`maps` / `getMap` / `getActiveMap` / `iterateAllMaps`)与**楼层激活**(active);`IGameMap`(`layerList` / `eventLayer` / `tileStore` / 宽高);`IMapLayer`(`alias` 别名、宽高);**事件层本质就是个图层,归本篇**。 +- **D-31:** (基础·`map-tiles`)只讲 `getBlock` / `setBlock` / `removeBlock` / `putMapData` / `getMapData` / `iterateBlocks`;`getLocationData` 属低级需求,**留高级**。 +- **D-32:** (基础·`flags`)全局**可存档**变量系统;**所有接口都讲(含 `occupied`)**:`setField` / `getField` / `getOrInsert` / `getOrInsertComputed` / `deleteField` / `setFieldValue` / `addFieldValue` / `getFieldValue` / `getFieldValueDefaults`;会进存档;**建议**不滥用、不用的变量及时 `deleteField`;示例:记录开关 / 计数。 +- **D-33:** (战斗·`combat-overview`)伤害大致怎么算;流程(基础数据 → 光环 / 查询 / 最终 → 伤害计算 → 战前脚本 → 战斗 → 战后结算 + 状态钩子);mermaid 图;末尾「流程步骤 → 对应文档」对照表;临界值不在本篇提。 +- **D-34:** (战斗·`enemy-specials`)特殊属性是什么、在数据里的样子、两类定义(无参 `defineNonePropertySpecial` / 有参 `defineCommonSerializableSpecial`);`src/data/enemy/special.ts` 注册;**如何自定义新特殊属性**(步骤);**「属性实现位置」对照表**(内置属性各自实现在哪);**多个示例**(有参 / 无参、`name` / `desc` 用字符串与函数);编辑器识别约定(`// {code} - {name}`、`/** {code} - {name} */`、`ISpecial{code}Value`、`#region` 不能删);**怪物比较**简单说、一般不改、真要改参考高级教程。 +- **D-35:** (战斗·`enemy-aura`)**先讲优先级**(高优先级可影响低优先级、反之不行;与查询效果共用优先级);内置光环(普通 23 / 支援 24 / 特殊光环 26);运作方式(写「转换器 + 视图」并注册,**不深讲为何**);四种内置范围只列名(`FullRange` / `RectRange` / `ManhattanRange` / `RayRange`);**多个自定义示例**:基础光环(减少周围怪物金币与经验)+ 高级特殊光环(给周围怪物上光环、优先级高于特殊光环);借示例讲清基础光环 vs 特殊光环差距;`apply` 与 `applySpecial` 差别讲清。 +- **D-36:** (战斗·`enemy-query`)**新增内容(引擎未内置)**;解决「特殊属性查询外部状态」;常规查询 `CommonQuery`(`registerCommonQueryEffect(code, effect)`,`apply(handler, special, query)`)与特殊查询 `SpecialQuery`(`registerSpecialQueryEffect(effect)`,`for(ctx)` → 修饰器 `shouldQuery` / `add` / `delete` / `modify`);与光环共用优先级;**三个示例**:抱团(范围内每有一个带该属性的怪物就给自身加成)、同化(把范围内怪物特殊属性同化到自身,数值型加算叠加、否则取位置索引最小者)、号角(场上每有一只怪物就给自身加成);说明三者能干什么即可;用**抱团**对比「用光环实现复杂度更高」;设计原因留高级。 +- **D-37:** (战斗·`enemy-final`)最终属性效果:**只影响自身、不依赖也不影响外部环境**的一段收尾调整;内置坚固(3)、模仿(8);接口 `IEnemyFinalEffect`(`priority` + `apply`)、`registerFinalEffect`;**三方对比**(光环:不依赖外部但影响外部;查询:依赖外部仅影响自身;最终:两者皆无);示例:**仿攻**(怪物攻击 = 玩家攻击);`priority` 一句带过;回指 `combat-overview` 流程。 +- **D-38:** (战斗·`enemy-calculator`)`IDamageCalculator`(`calculate` → `{ damage, turn }`、`getCriticalLimit`);注册 `useCalculator`;**逐段导读**内置伤害公式并指明「加自己的属性加在哪一步」;**支援怪**较难 → 写「实在理解不了可暂时跳过」;示例:**等差数列属性**(怪物攻击每回合按固定值递增,等差公式)等;核心讲明调整的是 `f(enemy, hero)` 里的 **`f`**;**临界**:定义 + 现有约定(伤害值单调递减;不单调也可、但临界不准)+ 临界值唯一依赖「最后的伤害值」,**不讲二分过程**。 +- **D-39:** (战斗·`enemy-map-damage`)概念(领域 / 夹击 / 阻击 / 激光 / 伏击);三部分(伤害视图 / 转换器 / 合并器,浅讲);注册 `useConverter` / `useReducer`;内置五类表(`code` / 实现位置**写文档时核对源码**);自定义示例:**受地形遮挡的激光**;合并器简单讲(决定一个点受多个地图伤害时取什么伤害类型,深入留高级);**实际结算用户届时补**。 +- **D-40:** (战斗·`enemy-combat-script`)战斗脚本(战前 `before` 返回 `bool` 拦截、战后 `after` 结算,按 `priority`);内置 `MainCombatScript`;注册 `addCombatScript`;**「脚本 vs 钩子」选型表**;战前示例「怪物拥有某特殊属性时无法与其战斗」、战后示例「战后玩家回复固定量生命值」;`priority` 一句。 +- **D-41:** (战斗·`enemy-status-hooks`)状态类属性(中毒 / 衰弱 / 重生)必须挂钩子;三类内置(中毒:`onStepEnd` + `onAfterTeleportTo` **两个钩子**;衰弱:`onAfterCombat` + 修饰器;重生:`onAfterCombat` + `onSetFloor` **跨钩子协同**);注册在 `initialize.ts`;**强调一个状态可能要用多个钩子**;自定义示例「下次战斗伤害乘二」(并说明还要改伤害计算器,串起 `enemy-calculator`);修饰器概念见 `hero-attribute`。 +- **D-42:** (其他·`events`)只讲**如何自定义新事件**(内建函数),不讲原理、不列内建全量;`BuiltInFunction`(`name` + `func(param, env)`);`IBlockEventEnv` 常用字段(其余指向 API);参数接口在 `types.ts`;注册在 `createEventRegistrations()`;内置示例导读;**自定义示例:设置 flag、勇士回血**;编辑器识别方式未设计完 → 文档留余地。 +- **D-43:** (其他·`replay-commands`)录像系统用途 + **核心原则**「玩家直接产生、会影响最终成绩的行为必须计入录像」并解释其含义;**如何新增自定义录像需改 `packages-user` 下的引擎内容**;主动录像 vs 被动录像(`ReplayCommandType.Active` / `Passive`);`@shouldReplay` / `@ignoreReplay` 的功能与检测(`begin` / `end` 区间);**不给示例**,后续其他教程引入自定义录像时直接链接过去。 +- **D-44:** (其他·`data-plugins`)插件是强拓展系统,中级只讲基础、**高级教程详解其工作原理**;`IDataPluginDefine`(`id` / `dependencies` / `awake` / `initialize` / `afterLoad`);时机次序 `awake`(装配)→ `initialize`(**加载前**)→ `afterLoad`(加载后);注册在 `src/data/plugin/list.ts` 的 `dataPlugins`;`state.addSaveableContent`;示例**占位**(待用户提供**较简单**插件,**不用商店插件**);写明「是否纳入本中级篇取决于示例难度」。 + +### 文档写作流程(长期规范,适用于后续所有文档写作,不止本批) + +- **D-45(prettier 格式化):** 文档写完后**必须**跑 prettier 格式化——prettier 也管 markdown;不格式化虽不报 eslint 错,但用户日后手动修改会产生大量格式变动。推荐命令:`node node_modules/prettier/bin/prettier.cjs --write "docs/intermediate/*.md"`(或等价的 `pnpm exec prettier --write`)。若格式化会阻断某条命令执行,**先跳过并在对话中提出**,由用户自行格式化。 +- **D-46(两阶段修订循环):** 修订必须依次经过两阶段,缺一不可: + 1. **子代理自审循环**:如 plan-checker,以及**按文档实际情况选择合适视角**的子代理反向审阅(面向新手的文档可用「不懂代码的读者」人格;面向有基础作者或特定领域的文档,应选更贴近该受众/领域的视角)→ 问题清单 → 迭代修正。**审阅视角不固定为「不懂代码的读者」,须因文档而异。** + 2. **用户主导的修订循环**:用户以更贴近本领域的视角提出问题 → AI 派子代理逐条修改文档 → 回报 → 用户继续提,直到用户满意并确认。 + **必须经过用户循环且获用户确认后才可收尾**,不得由 AI 自行收尾。适用于后续所有文档写作。 +- **D-47(高级教程预留,修订 D-16):** 凡「留到高级教程细讲」的内容,必须**显式说明**并**预留链接标记**。由于 `docs/advanced/` 尚未建立,**不得**写指向它的真实 markdown 链接(会造成死链、构建失败);改用非断链占位(如代码 span `高级教程 · ` 或 ``),待高级教程存在后再替换为真链接。此条在「高级教程引用」范围内取代 D-16 的「禁元叙述」。 +- **D-48(实操写法,长期规范):** 系统/原理讲解保持现状(可以讲、且现在这样就好);需要重做的是**「实操/示例」部分**。每处实操统一写成可分步照抄的形式: + 1. **目标**:先说要做出什么效果; + 2. **改哪里**:哪个文件、文件内哪个位置; + 3. **复制什么**:只给要新增的那段,**不含 `import`**;写清「顶部通常已有这些 import,不要再粘;若报缺少 X,再在顶部补 `import ...`」; + 4. **改什么**:复制后只需改的数值/字段/名称; + 5. **怎么验证**:改完看什么现象; + 6. **注意**:常见错误、别碰什么。 + 另外:示例取**最短可行**(如「下次战斗伤害翻倍」直接用 `onAfterCombat`,不额外写战后脚本);**不写 `dev.md` 的引擎开发约定**(如 import 必须置顶、注释位置等);**不写发布前/数量类无关信息**(如「发布前会有约 100 个内置事件」)。适用于后续所有文档写作。 +- **D-49(示例与「改内置优先」):** + 1. 示例代码必须**贴合引擎现状**:定义 `class` 再 `new`,**不要**写 `const obj = { ... }` 这类 plain object(引擎代码即如此)。例外:引擎本身就以对象字面量声明的内容(如 `src/content/item.ts` 的 `ItemEffect`),可沿用其现有写法。 + 2. 文档主体应主要讲**如何改内置类**(如 `src/data/enemy/*`、`src/data/hook/*` 中已有的类);在**末尾**再简述「何时需要自己新写类、怎么写」。 + 3. 必须明确写明:**如果你不怎么懂代码,建议直接改内置,而不是自己写新的类。** +- **D-50(通用模板 vs 示例):** 对每类可扩展机制,文档要提供的是**机制本身的通用模板**,而不是某个具体效果的模板/示例: + 1. 单独一节(如「模板:复制什么、改什么」),给出一段可复制的**骨架**(类 / 转换器 / 视图 / 效果等),并用 `[需改]` 标出作者需按自己需求修改的部分; + 2. 模板与示例**分开**,不要把某个特定效果(如某个具体光环)当作模板; + 3. 示例仅用于辅助说明,尽量短。 + 示例:光环应给「光环转换器 + 光环视图」的通用骨架 + 需改点,而不是给「减少金币经验的光环」当模板。 +- **D-51(特殊属性实现的落点与讲解方式):** + 1. **不要建议新建文件**:已有实现文件(如光环的 `src/data/enemy/aura.ts`)就全部写在其中,不要建议「新增一个自己的文件」。样板集中放置是为了方便管理与后续编辑器自动识别;只有**查询属性**需要单独新建 `query.ts`。 + 2. 对 `aura.ts` / `query.ts` 这类独立的特殊属性实现,建议用 `#region` 包裹,结构更清晰、后续编辑器也可能据此识别。暂定识别约定:`//#region {code}-{name}` … `//#endregion`。 + 3. 必须用**通俗语言解释「副作用」是什么**(刚接触代码的作者不懂这个概念)。 + 4. 说明**地图伤害是按「计算后怪物」计算**的:因此若光环给怪物新增了「领域」这类属性,该怪物确实会有领域效果;旧样板无法动态给怪物加特殊属性,这一点值得单独说明。若某种属性确实既要依赖外部环境、又要修改外部环境,应拆成「查询效果 + 光环效果」两个,二者通过新增一个怪物属性桥接(指向高级教程)。 + 5. `getCriticalLimit` 一般用不到,提一句即可,并说明高级教程会详细讲。 + 6. **接口讲解方式**:不要上来就搬接口列表;先说「为了实现 X,我们需要修改 Y 接口」,再讲这个接口的参数,这样更自然。 +- **D-52(加粗边界,长期规范):** `**加粗**` 的内容**不要以标点开头或结尾**(CommonMark emphasis 边界规则会导致不渲染)。若确需加粗含标点的短语,应调整结构,使 `**` 相邻处为标点/空白,或改用 HTML ``。提交前用 flanking 规则自检所有 markdown。适用于后续所有文档写作。 +- **D-53(高级教程分层):** 高级教程本身按难度/依赖分为 **I / II / III** 三层(避免与顶层「初级/中级/高级教程」撞名): + - **I**:核心为**插件系统**,后续内容都是基于插件系统的拓展; + - **II**:系统级实现(如战斗上下文/光环/查询/优先级、伤害与地图伤害、渲染层与布局、加载与插件生命周期、自定义录像行为等); + - **III**:原理与架构(双端分离约束、数据端 L0–L3、编辑器识别协议、录像深层原理、性能与调试等)。 + 由于渲染端尚未完工,**当前只规划/编写数据端内容**。 +- **D-54(高级教程定位):** 文档面向**使用者**(不是引擎编写者):在适度解析的同时,必须告诉读者**怎么用、怎么改、怎么自定义**;仍沿用 D-48(实操六步)、D-49(改内置优先/类示例)、D-50(通用模板单独成节)、D-51(落点/副作用/接口讲解方式)、D-52(加粗边界)等规范。 +- **D-55(优先插件):** 明确建议:**新增系统优先通过插件系统实现**;只有不得不修改内置系统时才去改内部。理由:版本迁移时 `src` 基本不会改,但内置包(`packages` / `packages-user`)有可能会改,改内置会增加升级时的维护成本。 +- **D-56(高级 I 内容,7 篇):** + 1. **高级 I 导览**:I/II/III 分层、阅读路径与前置、D-55 原则; + 2. **声明式 vs 命令式接口**:旧样板命令式(写一行即生效)vs 新样板声明式(告诉系统要做什么,系统在合适时机执行);必须讲透,作为后续基础; + 3. **插件系统**:自定义新插件、插件间调用、`dependencies` 依赖与排序、生命周期进阶、插件 id 稳定性与版本迁移; + 4. **可存档内容(`ISaveableContent`)**:存档一致性、三级压缩、注册存档对象、存档顺序 / `afterLoad` / `bindSaveableExecuter`、跨对象引用、结构化约束(structured-cloneable:Object/Array/number/string/boolean/null/Map/Set/TypedArray/ArrayBuffer 等)、必须返回经 `structuredClone` 的对象; + 5. **录像一致性**:`@shouldReplay` / `@ignoreReplay` 该写在什么方法/函数上、能保证什么、不能保证什么;自定义录像行为 → II,深层原理 → III; + 6. **data-common 的 Store**:讲清各 Store 存了什么,明确是**内容层原始数据**、运行时一般**只读**,修改很可能绕过存档/录像/一致性而引发问题; + 7. **钩子深入**:多对象场景(地图与图层)、如何判断「触发者是不是我关心的 map/layer」(身份校验)、同一钩子被多个对象复用、注册/移除与异步钩子。 + 每篇仍保留「怎么验证」。 +- **D-57(高级 I 读者修订循环):** 高级 I 完成后,**必须**再走用户主导的修订循环(D-46 第 2 阶段);且这次的读者人格**不能复用中级教程的「不懂代码的游戏作者」**——应面向**有一定代码基础、已读完中级教程、想做深度定制/理解扩展机制**的作者(具体人格在计划中明确,并经用户确认)。 @@ -192,4 +243,4 @@ *Phase: 10-docs-restructure* *Context gathered: 2026-10-14* -*Context updated: 2026-10-17(旧内容删除并入本批 Wave 0 / 10-01;中级教程第一批 24 篇讨论结果沿用)* +*Context updated: 2026-10-18(旧内容删除并入本批 Wave 0 / 10-01;中级教程第一批 24 篇讨论结果沿用;新增文档写作流程规范 D-45/D-46:prettier 格式化 + 两阶段修订循环)* diff --git a/.planning/phases/10-docs-restructure/10-PATTERNS.md b/.planning/phases/10-docs-restructure/10-PATTERNS.md index 98a06df..30dbcc7 100644 --- a/.planning/phases/10-docs-restructure/10-PATTERNS.md +++ b/.planning/phases/10-docs-restructure/10-PATTERNS.md @@ -145,7 +145,7 @@ export const specials: SpecialDeclaration[] = [ **Analog:** 自身(edit-in-place),形状参照同文件既有 `nav`/`sidebar`。 -**现有 `nav`(待新增一条,不改旧条目)**(`docs/.vitepress/config.ts:53-58`): +**现有 `nav`(本批将新增 `中级教程` 一条,并删除 `主页`/`指南`/`API`/`错误代码` 旧条目——D-04 已并入本批 `10-01` Wave 0)**(`docs/.vitepress/config.ts:53-58`): ```ts nav: [ { text: '主页', link: '/' }, @@ -192,15 +192,13 @@ sidebar: { ] } ], - '/guide/': [ /* 原样保留,禁止改动 */ ], - '/logger/': [ /* 原样保留,禁止改动 */ ], - '/api/': [{ text: 'API 列表', items: api }] + // '/guide/'、'/logger/'、'/api/' 三个旧键已随 D-04 并入本批(10-01),随旧内容一并移除 } ``` **硬约束:** -- 只**新增** `nav` 一条 + `sidebar['/intermediate/']` 键;**禁止**改动/删除 `'/guide/'`、`'/logger/'`、`'/api/'` 及既有 `nav` 条目(D-14 / RESEARCH Pitfall 3)。 -- **不要**触碰 `docs/.vitepress/api.ts`、`init.ts`、`apiSidebar.ts`(后者 gitignored 且会被覆盖,`api.ts:71`)。 +- **新增** `nav` 一条 + `sidebar['/intermediate/']` 键;**同时**(D-04 已并入本批 / `10-01`)移除因旧内容被删而失效的旧 `nav` 条目(主页 / 指南 / API / 错误代码)与旧 `sidebar` 键 `'/guide/'`、`'/logger/'`、`'/api/'`。删除 / 改键的路径白名单以 `10-01-PLAN.md` 为准。 +- `docs/.vitepress/api.ts` 仅按 `10-01` 追加「`docs/api/` 缺失即跳过」守卫,`generateSidebar()` 的扫描逻辑不改(D-03);`init.ts` 不改。`apiSidebar.ts` 为 gitignored 生成物,`10-01` 会一并删除(已无消费者,`api.ts:71` 会被覆盖)。 - 24 条链接必须全部挂上,否则页面「写了但看不见」(RESEARCH Pitfall 1 / D-09 人工审阅)。 --- @@ -427,7 +425,7 @@ graph LR ### 越界红线 **Source:** D-14(CONTEXT)、RESEARCH Pitfall 3 **Apply to:** 本批**全部**改动。 -> 本批**只**做 `docs/intermediate/*` + `config.ts` 的 intermediate 注册。**禁止**删除/修改 `docs/guide/*`、`docs/dev/*`、`docs/logger/*`,也**禁止**改动 `nav`/`sidebar` 的既有 guide/logger/api 键与 `api.ts`/`init.ts`。越界项只记录、不实施。 +> 本批范围 = `docs/intermediate/*` + `config.ts` 的 intermediate 注册,**并**(D-04 已并入本批 / `10-01`)删除旧内容(`docs/guide/*`、`docs/dev/*`、`docs/logger/*`、`docs/api/*` 与首页 `docs/index.md`)、清理其失效的 `nav`/`sidebar` 键,及给 `docs/.vitepress/api.ts` 追加缺失目录守卫。**不**触碰 `src/`、`packages/`、`packages-user/` 生产代码;`init.ts` 不改。越界项只记录、不实施。 --- diff --git a/.planning/phases/10-docs-restructure/10-RESEARCH.md b/.planning/phases/10-docs-restructure/10-RESEARCH.md index c3a7486..a4fdb4f 100644 --- a/.planning/phases/10-docs-restructure/10-RESEARCH.md +++ b/.planning/phases/10-docs-restructure/10-RESEARCH.md @@ -244,9 +244,9 @@ nav: [ 链接写法:`{ text: '数据端与渲染端', link: '/intermediate/data-vs-render' }`(与既有 `'/guide/'` 写法一致,用绝对路径、不带 `.md`)。 -**不需要改的:** `docs/.vitepress/api.ts`、`init.ts`(它们的 `generateSidebar` **只**扫描 `docs/api/`,与本批无关,[VERIFIED: docs/.vitepress/api.ts:5,19])。 +**需要小改 / 不需改的:** `docs/.vitepress/init.ts` 不需改(其 `generateSidebar` **只**扫描 `docs/api/`,[VERIFIED: docs/.vitepress/api.ts:5,19]);`docs/.vitepress/api.ts` 需按 `10-01` 追加「`docs/api/` 缺失即跳过」守卫(扫描逻辑不变)。 -**注意(D-04 与本批的关系):** 计划**不要**在本批删除 `docs/guide/*`、`docs/dev/*`、`docs/logger/*` 或改动既有 `nav`/`sidebar` 的 guide/logger 键——旧内容删除是 D-04 的独立任务,本批只负责 intermediate(见 CONTEXT「第一批已派发内容」界定与 D-14)。若计划顺带改了旧键,属越界。 +**注意(D-04 与本批的关系,已更新):** D-04 的旧内容删除**已并入本批**:`10-01`(Wave 0)会删除 `docs/guide/*`、`docs/dev/*`、`docs/logger/*`、`docs/api/*` 与首页 `docs/index.md`,并清理 `config.ts` 中指向这些内容的旧 `nav` 条目(主页 / 指南 / API / 错误代码)与旧 `sidebar` 键(guide / logger / api 三个路径前缀)。删除后本批 dead-link 数应为 0(原 `docs/dev/store/tile-store.md` 的 3 处死链随删除消失)。本文件下方若仍见「本批不得删除旧文档 / 不得改旧键」之类的旧版表述,均已作废,以本段与 `10-01-PLAN.md` 为准。 ### API 参考生成机制(回答 research question 2) @@ -255,7 +255,7 @@ nav: [ - 触发方式:仅通过 `docs/.vitepress/init.ts`(init.ts:3-5)在 `docs:dev` 时执行(package.json:26);并有一个 vite dev 插件 `listenSidebar()` 在文件增删/重命名时重新生成(config.ts:7-36,192)。 - 没有 `typedoc` / `api-extractor` 的仓库内脚本或配置(`package.json` scripts 无相关命令;`.github/`、`script/` 下无相关配置)。`pnpm-lock.yaml` 里存在 `@microsoft/api-extractor@7.57.6`(作为某依赖的传递依赖),但**仓库没有任何脚本调用它**。 -**结论(供 D-03 计划):** 「API 参考按包生成」的现有机制 = 已检入的 `docs/api/**/*.md` 文件树 + `generateSidebar()` 扫描。本批(中级教程)**不涉及** `docs/api/` 的改动;且 `docs/api/` 当前**已含 legacy 包条目**(`docs/api/index.md:10-13,25,30-31` 列出 `@motajs/legacy-*`、`@user/legacy-plugin-*`、`@user/data-fallback`),与 D-05「排除 legacy」冲突——但 D-05 属第 5 部分(API 参考)任务,**不在本批范围**,仅记录备用。 +**结论(供 D-03 计划):** 「API 参考按包生成」的现有机制 = 已检入的 `docs/api/**/*.md` 文件树 + `generateSidebar()` 扫描。本批**不做** API 参考的**内容重建**(注:`docs/api/` 目录本体会随 D-04 并入本批的 `10-01` 一并删除,此处「不涉及改动」仅指不重建其正文);且 `docs/api/` 当前**已含 legacy 包条目**(`docs/api/index.md:10-13,25,30-31` 列出 `@motajs/legacy-*`、`@user/legacy-plugin-*`、`@user/data-fallback`),与 D-05「排除 legacy」冲突——但 D-05 属第 5 部分(API 参考)任务,**不在本批范围**,仅记录备用。 ### 写作 / 验收工作流(回答 research question 3) @@ -341,7 +341,7 @@ graph TD ### Anti-Patterns to Avoid - **手改 `docs/.vitepress/apiSidebar.ts`:** 它被 `.gitignore` 忽略(.gitignore:54)、被 `.prettierignore` 忽略(.prettierignore:17)、且每次 `generateSidebar()` 运行会被覆盖(api.ts:71)。改它无效。 -- **在本批删除旧文档:** D-04 的删除是独立任务;本批只做 intermediate(D-14 不擅自扩大范围)。 +- **(旧范围界定,已作废)把旧文档删除当作本批外任务:** D-04 已更新——旧内容删除(`docs/guide/*`、`docs/dev/*`、`docs/logger/*`、`docs/api/*` 与首页 `docs/index.md`)连同旧 `nav`/`sidebar` 键清理**已并入本批 `10-01`(Wave 0)**,与中级教程同期进行。 - **新增文档却忘了注册 sidebar:** 不会导致构建失败,但页面在导航中不可见——D-09 的人工审阅会因此不通过。 - **在文档里写"渲染端教程后续再讲"之类元叙述:** D-16 明令禁止。 @@ -449,11 +449,11 @@ graph TD **How to avoid:** 先 `pnpm docs:dev` 或 `tsx docs/.vitepress/init.ts` 生成后再构建;或把构建命令写成先 init 再 build。 **Warning signs:** 构建报 `Failed to resolve import "./apiSidebar"`。 -### Pitfall 3: 越界改旧文档 -**What goes wrong:** 顺手删/改了 `docs/guide/*`、`docs/dev/*`、`docs/logger/*` 或改动了 `nav`/`sidebar` 的 guide/logger 键。 -**Why it happens:** D-04 确有"旧内容全删重写"的总目标,但那是独立任务。 -**How to avoid:** 严守 D-14 与"第一批 = 中级教程"的界定;越界项只记录不实施。 -**Warning signs:** 计划 diff 触及 `docs/guide/**`、`docs/dev/**`、`docs/logger/**`。 +### Pitfall 3: 越界删除 / 改旧文档(范围已随 D-04 更新) +**What goes wrong:** 越过 `10-01` 的白名单去删除或修改内容——白名单仅为 `docs/guide/*`、`docs/dev/*`、`docs/logger/*`、`docs/api/*` 与首页 `docs/index.md`;此外误改 `src/`、`packages/`、`packages-user/` 或 `docs/intermediate/` 也属越界。 +**Why it happens:** D-04 已把「旧内容全删重写」并入本批 `10-01`(Wave 0),删除本身**现在是本批内**的合法动作;越界风险已从「不该删」转为「删多了 / 删错对象 / 误改生产代码」。 +**How to avoid:** 严守 `10-01` 的五个路径白名单并保持站点可构建;删除对象受 git 跟踪,误删可 `git restore` 回滚。 +**Warning signs:** 计划 diff 触及 `src/**`、`packages*/**` 或 `docs/intermediate/**`。 ### Pitfall 4: 写出被禁止的元叙述 **What goes wrong:** 出现"渲染端教程后续再讲""这块留到高级教程再说"之类。 @@ -475,7 +475,7 @@ graph TD ## Code Examples -### 在 config.ts 里注册 intermediate 侧边栏(本批唯一必改的配置) +### 在 config.ts 里注册 intermediate 侧边栏(本批配置改动之一;旧键清理见 `10-01`) ```ts // 依据现有写法:docs/.vitepress/config.ts:59-156 的 sidebar 为按路径前缀分键的手写对象 @@ -500,9 +500,7 @@ sidebar: { ] } ], - '/guide/': [ /* 原样保留 */ ], - '/logger/': [ /* 原样保留 */ ], - '/api/': [{ text: 'API 列表', items: api }] + // '/guide/'、'/logger/'、'/api/' 三个旧键已随 D-04 并入本批(10-01)随旧内容一并移除 } ``` (来源:结构参照仓库现行 `docs/.vitepress/config.ts:59-156`;`link` 写法与 config.ts:64-124 一致) @@ -532,7 +530,7 @@ sidebar: { | `docs/api/index.md` 含 legacy 包 | 计划排除 legacy(D-05) | 2026-10-14 | 非本批 | **Deprecated/outdated:** -- 详细介绍引擎接口设计的旧"开发文档"(`docs/dev/*`,含 `template.md` 的「需求综述 → 需求理解 → 接口设计分析 → 实现思路」方法论)——D-04 将删除重写;本批不涉及。 +- 详细介绍引擎接口设计的旧"开发文档"(`docs/dev/*`,含 `template.md` 的「需求综述 → 需求理解 → 接口设计分析 → 实现思路」方法论)——D-04 已并入本批 Wave 0(`10-01`)删除重写。 ## Assumptions Log @@ -546,7 +544,7 @@ sidebar: { **若上表非空:** A1、A3 建议由用户在计划前的汇报中被确认(涉及验收命令);A4 可在执行时直接观察。 -## Open Questions +## Open Questions (RESOLVED) 1. **验收命令的确切形态** - What we know: `docs:dev` 会生成 `apiSidebar.ts`;`docs:build` 不会;当前工作区无该文件。 @@ -678,7 +676,7 @@ sidebar: { **Confidence:** HIGH **要点回顾:** -1. 本批 = 24 篇中文中级教程(`docs/intermediate/`)+ 在 `docs/.vitepress/config.ts` 新增 `nav` 与 `sidebar['/intermediate/']`;API 参考与旧文档删除**不在本批**。 +1. 本批 = 24 篇中文中级教程(`docs/intermediate/`)+ 在 `docs/.vitepress/config.ts` 新增 `nav` 与 `sidebar['/intermediate/']`;旧文档删除与旧 `nav`/`sidebar` 键清理**已并入本批 Wave 0(`10-01`)**;API 参考的按包生成机制保留、正文重建另行安排。 2. 站点 `nav`/`sidebar` 为手写(config.ts:53-156);`docs/api/` 侧边栏由 `generateSidebar()` 扫描生成,正文为已检入静态文件,仓库无 typedoc/api-extractor 脚本。 3. 验收命令:先 `tsx docs/.vitepress/init.ts`(生成 gitignored 的 `apiSidebar.ts`,当前工作区缺失),再 `pnpm docs:build`;预览用 `pnpm docs:dev`。 4. 24 篇的源码入口已逐篇核实到 `file:line`(见「24 篇文档的源码入口」表)。 diff --git a/.planning/phases/10-docs-restructure/10-VALIDATION.md b/.planning/phases/10-docs-restructure/10-VALIDATION.md index ca324fb..eb15148 100644 --- a/.planning/phases/10-docs-restructure/10-VALIDATION.md +++ b/.planning/phases/10-docs-restructure/10-VALIDATION.md @@ -23,7 +23,7 @@ created: "2026-10-16" | **Full suite command** | `tsx docs/.vitepress/init.ts` then `pnpm docs:build` (build success == D-09 "构建通过") | | **Estimated runtime** | ~20–60 seconds (VitePress build; `init.ts` a few seconds) | -> NOTE: `docs/.vitepress/config.ts` statically imports `./apiSidebar`; that file is gitignored and currently absent, so `docs:build` fails until `tsx docs/.vitepress/init.ts` regenerates it. Wave 0 must run it first. +> NOTE: `10-01`(Wave 0)已删除旧内容并解除 `docs/.vitepress/config.ts` 对生成物 `./apiSidebar` 的静态依赖,给 `docs/.vitepress/api.ts` 的 `generateSidebar()` 加了缺失目录守卫。旧内容删除后 **`docs:build` 必须转绿、dead link = 0**(此前的死链失败源 `docs/dev/store/tile-store.md` 已被删除)。`tsx docs/.vitepress/init.ts` 仍在构建前运行,但在 `docs/api/` 缺失时会安全跳过、不再抛 ENOENT。 --- @@ -31,7 +31,7 @@ created: "2026-10-16" - **After every task commit:** `pnpm docs:dev` preview (or at minimum one `docs:build`) - **After every plan wave:** `tsx docs/.vitepress/init.ts && pnpm docs:build` -- **Before `/gsd-verify-work`:** Full build must pass and the user must have reviewed (D-09) +- **Before `/gsd-verify-work`:** Full build must pass with **dead link = 0**(旧内容已删除,构建必须转绿,不再有「既有死链不可避免」的豁免)and the user must have reviewed (D-09) - **Max feedback latency:** ~60 seconds --- @@ -40,20 +40,34 @@ created: "2026-10-16" | Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | |---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| -| 10-W0-CFG | W0 | 1 | D-02/D-20 | — | Sidebar/nav edits are same-dir docs config only; no raw HTML/script | smoke | `tsx docs/.vitepress/init.ts && pnpm docs:build` | ❌ W0 | ⬜ pending | -| 10-TBD | TBD | TBD | D-09 (links) | T-10-info | Doc pages contain no secrets/internal paths; only the single approved external link | integration (build link resolution) + manual | `pnpm docs:build` | ❌ | ⬜ pending | +| 10-01-T1 | 10-01 | 1 | D-04 / D-14 | T-10-01 / T-10-02 | config.ts / api.ts 同目录编辑;删除限于白名单;无原始 HTML/script | smoke(静态 + 构建) | `tsx docs/.vitepress/init.ts && pnpm docs:build`(+ 删除 / 配置 / 守卫断言) | ❌ W0 | ⬜ pending | +| 10-01-T2 | 10-01 | 1 | D-13 / D-20 | T-10-01 | 侧边栏注册仅改同目录 config;无原始 HTML/script | smoke(静态 + 构建) | 同一构建 + 24 链接 / 4 分组断言 | ❌ W0 | ⬜ pending | +| 10-02-T1 | 10-02 | 2 | D-21 / D-22 | T-10-04 | 2 篇 markdown:无原始 ` - - diff --git a/packages/legacy-ui/src/components/boxAnimate.vue b/packages/legacy-ui/src/components/boxAnimate.vue deleted file mode 100644 index be7a5f7..0000000 --- a/packages/legacy-ui/src/components/boxAnimate.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - - - diff --git a/packages/legacy-ui/src/components/colomn.vue b/packages/legacy-ui/src/components/colomn.vue deleted file mode 100644 index 7cc5464..0000000 --- a/packages/legacy-ui/src/components/colomn.vue +++ /dev/null @@ -1,145 +0,0 @@ - - - - - diff --git a/packages/legacy-ui/src/components/enemyOne.vue b/packages/legacy-ui/src/components/enemyOne.vue deleted file mode 100644 index d0cce02..0000000 --- a/packages/legacy-ui/src/components/enemyOne.vue +++ /dev/null @@ -1,232 +0,0 @@ - - - - - diff --git a/packages/legacy-ui/src/components/index.ts b/packages/legacy-ui/src/components/index.ts deleted file mode 100644 index 5c95dfa..0000000 --- a/packages/legacy-ui/src/components/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { default as Box } from './box.vue'; -export { default as BoxAnimate } from './boxAnimate.vue'; -export { default as Column } from './colomn.vue'; -export { default as EnemyOne } from './enemyOne.vue'; -export { default as Scroll } from './scroll.vue'; diff --git a/packages/legacy-ui/src/components/scroll.vue b/packages/legacy-ui/src/components/scroll.vue deleted file mode 100644 index dc9bf7b..0000000 --- a/packages/legacy-ui/src/components/scroll.vue +++ /dev/null @@ -1,267 +0,0 @@ - - - - - diff --git a/packages/legacy-ui/src/controller.ts b/packages/legacy-ui/src/controller.ts deleted file mode 100644 index 7f66dee..0000000 --- a/packages/legacy-ui/src/controller.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { Component, shallowReactive } from 'vue'; -import { EventEmitter } from '@motajs/legacy-common'; -import { IGameUi, IUiController } from './interface'; - -interface FocusEvent { - focus: (before: T | null, after: T) => void; - unfocus: (before: T | null) => void; - add: (item: T) => void; - pop: (item: T | null) => void; - splice: (spliced: T[]) => void; -} - -export class Focus extends EventEmitter> { - /** 显示列表 */ - stack: T[]; - focused: T | null = null; - - /** 聚焦目标是否平等,在平等时,关闭聚焦目标不再会将其之后的目标全部删除,而是保留 */ - readonly equal: boolean; - - constructor(react: boolean = false, equal: boolean = false) { - super(); - this.stack = react ? shallowReactive([]) : []; - this.equal = equal; - } - - /** - * 聚焦于一个目标 - * @param target 聚焦目标 - * @param add 如果聚焦目标不在显示列表里面,是否自动追加 - */ - focus(target: T, add: boolean = false) { - if (target === this.focused) return; - const before = this.focused; - if (!this.stack.includes(target)) { - if (add) { - this.add(target); - this.focused = target; - } else { - console.warn( - `聚焦于一个不存在的目标,同时没有传入自动追加的参数`, - `聚焦目标:${target}` - ); - return; - } - } else { - this.focused = target; - } - this.emit('focus', before, this.focused); - } - - /** - * 取消聚焦 - */ - unfocus() { - const before = this.focused; - this.focused = null; - this.emit('unfocus', before); - } - - /** - * 向显示列表中添加物品 - * @param item 添加的物品 - */ - add(item: T) { - this.stack.push(item); - this.emit('add', item); - } - - /** - * 弹出显示列表中的最后一个物品 - */ - pop() { - const item = this.stack.pop() ?? null; - const last = this.stack.at(-1) ?? null; - if (!last) this.unfocus(); - else this.focus(last); - this.emit('pop', item); - return item; - } - - /** - * 从一个位置开始删除显示列表,如果平等,则只会删除一个,否则会将其之后的所有的目标全部删除 - * @param item 从哪开始删除,包括此项 - */ - splice(item: T) { - const index = this.stack.indexOf(item); - this.spliceIndex(index); - } - - /** - * 根据索引在显示列表中删除一项 - * @param index 要删除的项的索引 - */ - spliceIndex(index: number) { - if (index === -1) { - this.emit('splice', []); - return; - } - if (!this.stack[index]) return; - const last = this.stack.at(-1) ?? null; - if (!last) this.unfocus(); - else this.focus(last); - this.emit( - 'splice', - this.stack.splice(index, this.equal ? 1 : Infinity) - ); - } -} - -interface GameUiEvent { - close: () => void; - open: () => void; -} - -interface ShowableGameUi { - ui: GameUi; - vOn?: UiVOn; - vBind?: UiVBind; -} - -type UiVOn = Record void>; -type UiVBind = Record; - -interface MountedVBind { - num: number; - ui: GameUi; - controller: UiController; - [x: string]: any; -} - -export class GameUi extends EventEmitter implements IGameUi { - static uiList: GameUi[] = []; - - component: Component; - id: string; - symbol: symbol = Symbol(); - - constructor(id: string, component: Component) { - super(); - this.component = component; - this.id = id; - GameUi.uiList.push(this); - } - - /** - * 根据 v-on 和 v-bind 创建可以显示的 ui 组件 - * @param vOn 监听的事件 - * @param vBind 绑定的数据 - */ - with(vBind?: UiVBind, vOn?: UiVOn): ShowableGameUi { - return { ui: this, vOn, vBind }; - } -} - -interface IndexedGameUi extends ShowableGameUi { - num: number; - vBind?: MountedVBind; -} - -interface HoldOnController { - end(noClosePanel?: boolean): void; -} - -export class UiController - extends Focus - implements IUiController -{ - static list: UiController[] = []; - list: Record = {}; - num: number = 0; - show: 'end' | 'all' = 'all'; - - private hold: boolean = false; - - constructor(equal?: boolean) { - super(true, equal); - UiController.list.push(this); - this.on('splice', spliced => { - spliced.forEach(v => { - v.ui.emit('close'); - }); - if (this.stack.length === 0) { - if (!this.hold) this.emit('end', false); - this.hold = false; - } - }); - this.on('add', item => { - if (this.stack.length === 1) { - this.emit('start'); - } - item.ui.emit('open'); - }); - } - - /** - * 设置为仅显示最后一个ui - */ - showEnd() { - this.show = 'end'; - } - - /** - * 设置为显示所有ui - */ - showAll() { - this.show = 'all'; - } - - /** - * 根据id获取到ui - * @param id ui的id - */ - get(id: string) { - return this.list[id]; - } - - /** - * 暂时保持下一次删除ui不会导致ui整体被关闭,引起ui背景闪烁。 - * 例如可以用于道具栏,打开道具时就应当 holdOn,然后通过道具使用钩子来判断接下来是否要隐藏 app: - * ```txt - * hold on -> close -> use item -> hook -> stack.length === 0 ? end() : no action - * ``` - */ - holdOn(): HoldOnController { - this.hold = true; - - return { - end: (noClosePanel: boolean = false) => { - this.emit('end', noClosePanel); - } - }; - } - - /** - * 关闭一个ui,注意如果不是平等模式,在其之后的ui都会同时关闭掉 - * @param num 要关闭的ui的唯一标识符 - */ - close(num: number) { - const ui = this.stack.findIndex(v => v.num === num); - this.spliceIndex(ui); - } - - /** - * 根据id关闭所有同id的ui,注意非平等模式下,会将第一个ui后的所有ui都关闭掉 - * @param id 要关闭的ui的id - */ - closeByName(id: string) { - if (!this.equal) { - const ui = this.stack.findIndex(v => v.ui.id === id); - this.spliceIndex(ui); - } else { - let ui; - while ((ui = this.stack.findIndex(v => v.ui.id === id)) !== -1) { - this.spliceIndex(ui); - } - } - } - - /** - * 打开一个新的ui - * @param id 要打开的ui的id - * @param vOn 监听的事件 - * @param vBind 绑定的数据 - * @returns ui的唯一标识符 - */ - open(id: string, vBind?: UiVBind, vOn?: UiVOn) { - if (core.isReplaying() && !this.equal) return -1; - const ui = this.get(id); - if (!ui) { - console.warn(`Unknown UI: '${id}'.`); - return -1; - } - const num = this.num++; - const bind = { - num, - ui, - controller: this, - ...(vBind ?? {}) - }; - const sui = ui.with(bind, vOn); - this.add({ - num, - ...sui - } as IndexedGameUi); - return num; - } - - /** - * 注册一个ui - * @param id ui的id - * @param ui 对应的GameUi实例 - */ - register(...ui: GameUi[]) { - ui.forEach(v => { - const id = v.id; - if (id in this.list) { - console.warn(`已存在id为'${id}'的ui,已将其覆盖`); - } - this.list[id] = v; - }); - } - - /** - * 取消注册一个ui - * @param id 要取消注册的ui的id - */ - unregister(...id: string[]) { - id.forEach(v => { - delete this.list[v]; - }); - return this; - } - - /** - * 根据ui的唯一标识符进行聚焦 - * @param num 要聚焦于的ui的唯一标识符 - */ - focusByNum(num: number) { - const ui = this.getByNum(num); - if (!ui) return; - this.focus(ui); - } - - /** - * 根据唯一标识符获取对应的ui - * @param num ui的唯一标识符 - */ - getByNum(num: number) { - return this.stack.find(v => v.num === num); - } - - /** - * 根据ui的唯一标识符来判断当前是否存在某个ui - * @param id ui的唯一标识符 - */ - hasName(id: string) { - return this.stack.some(v => v.ui.id === id); - } -} diff --git a/packages/legacy-ui/src/data/settings.json b/packages/legacy-ui/src/data/settings.json deleted file mode 100644 index 0320032..0000000 --- a/packages/legacy-ui/src/data/settings.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "screen": { - "fullscreen": [ - "是否全屏进行游戏,全屏后按ESC退出全屏,开启后将不能通过按ESC开启系统设置菜单,", - "请按下方的按钮打开。进入或退出全屏后请存读档一下,以解决一部分绘制问题。" - ], - "itemDetail": ["是否在地图上显示宝石血瓶装备等增加的属性值"], - "transition": [ - "是否展示当一个ui界面,如怪物手册等的打开与关闭时的动画。当此项开启时,", - "所有界面被打开或关闭时都会展示动画,否则会直接展示出来" - ], - "fontSize": [ - "在各种 ui 界面中显示的文字大小,范围为 2 - 48。注意,字体过大可能会引起 ui 布局发生错误" - ], - "criticalGem": ["临界是否显示为在当前地图要吃的宝石数"] - }, - "action": { - "hotkey": ["设置游戏中会用到的一些快捷键"] - }, - "utils": { - "autoScale": [ - "开启后,每次进入游戏时会自动缩放游戏画面至合适值。该项只对电脑端有效。" - ] - } -} diff --git a/packages/legacy-ui/src/index.ts b/packages/legacy-ui/src/index.ts index d5453e5..9b174c9 100644 --- a/packages/legacy-ui/src/index.ts +++ b/packages/legacy-ui/src/index.ts @@ -1,16 +1 @@ -import { createPreset } from './preset'; - -export function create() { - createPreset(); -} - -export * as UI from './ui'; -export * as Components from './components'; export * from './preset'; -export * from './tools'; -export * from './animateController'; -export * from './controller'; -export * from './setting'; -export * from './use'; -export * from './utils'; -export * from './uiUtils'; diff --git a/packages/legacy-ui/src/interface.ts b/packages/legacy-ui/src/interface.ts deleted file mode 100644 index fa5b6ec..0000000 --- a/packages/legacy-ui/src/interface.ts +++ /dev/null @@ -1,99 +0,0 @@ -export interface IGameUi { - id: string; - symbol: symbol; -} - -export interface IMountedVBind { - num: number; - ui: IGameUi; - controller: IUiController; - [x: string]: any; -} - -interface HoldOnController { - end(noClosePanel?: boolean): void; -} - -type UiVOn = Record void>; -type UiVBind = Record; - -export interface IUiController { - stack: any[]; - - /** - * 设置为仅显示最后一个ui - */ - showEnd(): void; - - /** - * 设置为显示所有ui - */ - showAll(): void; - - /** - * 根据id获取到ui - * @param id ui的id - */ - get(id: string): void; - - /** - * 暂时保持下一次删除ui不会导致ui整体被关闭,引起ui背景闪烁。 - * 例如可以用于道具栏,打开道具时就应当 holdOn,然后通过道具使用钩子来判断接下来是否要隐藏 app: - * ```txt - * hold on -> close -> use item -> hook -> stack.length === 0 ? end(): no action - * ``` - */ - holdOn(): HoldOnController; - - /** - * 关闭一个ui,注意如果不是平等模式,在其之后的ui都会同时关闭掉 - * @param num 要关闭的ui的唯一标识符 - */ - close(num: number): void; - - /** - * 根据id关闭所有同id的ui,注意非平等模式下,会将第一个ui后的所有ui都关闭掉 - * @param id 要关闭的ui的id - */ - closeByName(id: string): void; - - /** - * 打开一个新的ui - * @param id 要打开的ui的id - * @param vOn 监听的事件 - * @param vBind 绑定的数据 - * @returns ui的唯一标识符 - */ - open(id: string, vBind?: UiVBind, vOn?: UiVOn): void; - - /** - * 注册一个ui - * @param id ui的id - * @param ui 对应的GameUi实例 - */ - register(...ui: IGameUi[]): void; - - /** - * 取消注册一个ui - * @param id 要取消注册的ui的id - */ - unregister(...id: string[]): void; - - /** - * 根据ui的唯一标识符进行聚焦 - * @param num 要聚焦于的ui的唯一标识符 - */ - focusByNum(num: number): void; - - /** - * 根据唯一标识符获取对应的ui - * @param num ui的唯一标识符 - */ - getByNum(num: number): void; - - /** - * 根据ui的唯一标识符来判断当前是否存在某个ui - * @param id ui的唯一标识符 - */ - hasName(id: string): void; -} diff --git a/packages/legacy-ui/src/panel/enemyCritical.vue b/packages/legacy-ui/src/panel/enemyCritical.vue deleted file mode 100644 index 224cfb7..0000000 --- a/packages/legacy-ui/src/panel/enemyCritical.vue +++ /dev/null @@ -1,309 +0,0 @@ - - - - - diff --git a/packages/legacy-ui/src/panel/enemySpecial.vue b/packages/legacy-ui/src/panel/enemySpecial.vue deleted file mode 100644 index eb8ca9a..0000000 --- a/packages/legacy-ui/src/panel/enemySpecial.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - - - diff --git a/packages/legacy-ui/src/preset/index.ts b/packages/legacy-ui/src/preset/index.ts index 9d0f50b..a3dbd3d 100644 --- a/packages/legacy-ui/src/preset/index.ts +++ b/packages/legacy-ui/src/preset/index.ts @@ -1,12 +1 @@ -import { createSetting, createUI } from './ui'; - -export function createPreset() { - createUI(); - createSetting(); -} - -export * from './ui'; -export * from './settings'; export * from './keyboard'; -export * from './uiIns'; -export * from './settingIns'; diff --git a/packages/legacy-ui/src/preset/settingIns.ts b/packages/legacy-ui/src/preset/settingIns.ts deleted file mode 100644 index 6ef3be9..0000000 --- a/packages/legacy-ui/src/preset/settingIns.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { MotaSetting } from '../setting'; - -export const mainSetting = new MotaSetting(); diff --git a/packages/legacy-ui/src/preset/settings.tsx b/packages/legacy-ui/src/preset/settings.tsx deleted file mode 100644 index f10304c..0000000 --- a/packages/legacy-ui/src/preset/settings.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import type { SettingComponent, SettingComponentProps } from '../setting'; -import { Button, InputNumber, Radio } from 'ant-design-vue'; -import { mainUi } from './uiIns'; -import { gameKey } from '@motajs/system'; - -interface Components { - Default: SettingComponent; - Boolean: SettingComponent; - Number: SettingComponent; - HotkeySetting: SettingComponent; - ToolbarEditor: SettingComponent; - Radio: (items: string[]) => SettingComponent; - Performance: SettingComponent; -} - -export type { Components as SettingDisplayComponents }; - -export function createSettingComponents() { - const com: Components = { - Default: DefaultSetting, - Boolean: BooleanSetting, - Number: NumberSetting, - HotkeySetting, - ToolbarEditor, - Radio: RadioSetting, - Performance: PerformanceSetting - }; - return com; -} - -function DefaultSetting(_props: SettingComponentProps) { - return ( -
- 未知的设置类型 -
- ); -} - -function BooleanSetting(props: SettingComponentProps) { - const { setting, displayer, item } = props; - const changeValue = () => { - setting.setValue(displayer.selectStack.join('.'), !item.value); - displayer.update(); - }; - - return ( -
- 当前 {item.value ? '开启' : '关闭'} - -
- ); -} - -function NumberSetting(props: SettingComponentProps) { - const { setting, displayer, item } = props; - const changeValue = (value: number) => { - if (typeof value !== 'number') return; - if (value < (item.step?.[0] ?? 0) || value > (item.step?.[1] ?? 100)) { - return; - } - setting.setValue(displayer.selectStack.join('.'), Math.round(value)); - displayer.update(); - }; - - return ( -
-
- 修改设置: - changeValue(value as number)} - > -
-
- - -
-
- ); -} - -function RadioSetting(items: string[]) { - return (props: SettingComponentProps) => { - const { setting, displayer, item } = props; - - const changeValue = (value: number) => { - if (isNaN(value)) return; - setting.setValue(displayer.selectStack.join('.'), value); - displayer.update(); - }; - - return ( -
- {items.map((v, i) => { - return ( - changeValue(i)} - > - {v} - - ); - })} -
- ); - }; -} - -function showSpecialSetting(id: string, vBind?: any) { - const ui = mainUi.get(id); - mainUi.showEnd(); - ui.once('close', () => { - mainUi.showAll(); - }); - mainUi.open(id, vBind); -} - -function HotkeySetting(_props: SettingComponentProps) { - return ( -
- -
- ); -} - -function ToolbarEditor(_props: SettingComponentProps) { - return ( -
- -
- ); -} - -function PerformanceSetting(_props: SettingComponentProps) { - return ( -
- -
- ); -} diff --git a/packages/legacy-ui/src/preset/ui.ts b/packages/legacy-ui/src/preset/ui.ts deleted file mode 100644 index 83c2529..0000000 --- a/packages/legacy-ui/src/preset/ui.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { GameStorage } from '@motajs/legacy-system'; -import { createSettingComponents } from './settings'; -import { isMobile } from '../use'; -import { MotaSetting } from '../setting'; -import { triggerFullscreen } from '../utils'; -import settingsText from '../data/settings.json'; -import { mainUi } from './uiIns'; -import { mainSetting } from './settingIns'; - -//#region legacy-ui - -export function createUI() { - const { hook } = Mota.require('@user/data-base'); - hook.once('mounted', () => { - const ui = document.getElementById('ui-main')!; - const blur = mainSetting.getSetting('screen.blur'); - - mainUi.on('start', () => { - ui.style.display = 'flex'; - if (blur?.value) { - ui.style.backdropFilter = 'blur(5px)'; - ui.style.backgroundColor = 'rgba(0,0,0,0.7333)'; - } else { - ui.style.backdropFilter = 'none'; - ui.style.backgroundColor = 'rgba(0,0,0,0.85)'; - } - core.lockControl(); - }); - mainUi.on('end', noClosePanel => { - ui.style.display = 'none'; - if (!noClosePanel) { - core.closePanel(); - } - }); - }); -} - -//#endregion - -//#region legacy-setting - -const COM = createSettingComponents(); - -// 添加不参与全局存储的设置 -MotaSetting.noStorage.push('action.autoSkill', 'screen.fullscreen'); - -const storage = new GameStorage(GameStorage.fromAuthor('AncTe', 'setting')); - -export { storage as settingStorage }; - -// ----- 监听设置修改 -mainSetting.on('valueChange', (key, n, o) => { - if (!MotaSetting.noStorage.includes(key)) { - storage.setValue(key, n); - } - - const [root, setting] = key.split('.'); - - if (root === 'screen') { - handleScreenSetting(setting, n, o); - } else if (root === 'audio') { - handleAudioSetting(setting, n, o); - } else if (root === 'ui') { - handleUiSetting(setting, n, o); - } -}); - -const root = document.getElementById('root') as HTMLDivElement; - -function handleScreenSetting( - key: string, - n: T, - _o: T -) { - if (key === 'fullscreen') { - // 全屏 - triggerFullscreen(n as boolean); - } else if (key === 'fontSize') { - // 字体大小 - root.style.fontSize = `${n}px`; - const absoluteSize = (n as number) * devicePixelRatio; - storage.setValue('@@absoluteFontSize', absoluteSize); - storage.write(); - } else if (key === 'scale') { - const { MAIN_HEIGHT, MAIN_WIDTH } = Mota.require( - '@user/client-modules' - ); - const max = Math.min( - (window.innerHeight / MAIN_HEIGHT) * 100, - (window.innerWidth / MAIN_WIDTH) * 100, - n as number - ); - const scale = Number((Math.floor((max / 100) * 4) / 4).toFixed(2)); - // @ts-expect-error 遗留问题 - core.domStyle.scale = scale; - Mota.require('@user/client-modules').mainRenderer.setScale(scale); - } -} - -function handleAudioSetting( - key: string, - n: T, - _o: T -) { - const { client } = Mota.require('@user/client-modules'); - const { bgmPlayer, soundPlayer } = client; - if (key === 'bgmEnabled') { - bgmPlayer.setEnabled(n as boolean); - core.checkBgm(); - } else if (key === 'bgmVolume') { - bgmPlayer.setVolume((n as number) / 100); - } else if (key === 'soundEnabled') { - soundPlayer.setEnabled(n as boolean); - } else if (key === 'soundVolume') { - soundPlayer.setVolume((n as number) / 100); - } -} - -function handleUiSetting( - _key: string, - _n: T, - _o: T -) {} - -// ----- 游戏的所有设置项 -mainSetting - .register( - 'screen', - '显示设置', - new MotaSetting() - .register('fullscreen', '全屏游戏', false, COM.Boolean) - .register('scale', '画面缩放', 100, COM.Number, [50, 500, 25]) - .setDisplayFunc('scale', value => `${value}%`) - .register('itemDetail', '宝石血瓶显伤', true, COM.Boolean) - .register('transition', '界面动画', false, COM.Boolean) - .register('fontSize', '字体大小', 16, COM.Number, [2, 48, 1]) - .register('criticalGem', '临界显示方式', false, COM.Boolean) - .setDisplayFunc('criticalGem', value => (value ? '宝石数' : '攻击')) - .register('keyScale', '虚拟键盘缩放', 100, COM.Number, [25, 5, 500]) - .register('blur', '背景虚化', !isMobile, COM.Boolean) - ) - .register( - 'action', - '操作设置', - new MotaSetting() - .register('hotkey', '快捷键', false, COM.HotkeySetting) - .setDisplayFunc('hotkey', () => '') - ) - .register( - 'audio', - '音频设置', - new MotaSetting() - .register('bgmEnabled', '开启音乐', true, COM.Boolean) - .register('bgmVolume', '音乐音量', 80, COM.Number, [0, 100, 5]) - .register('soundEnabled', '开启音效', true, COM.Boolean) - .register('soundVolume', '音效音量', 80, COM.Number, [0, 100, 5]) - ) - .register( - 'utils', - '系统设置', - new MotaSetting().register('autoScale', '自动放缩', true, COM.Boolean) - ) - .register( - 'ui', - 'ui设置', - new MotaSetting() - .register( - 'bookScale', - '怪物手册缩放', - 100, - COM.Number, - [10, 500, 10] - ) - .setDisplayFunc('bookScale', value => `${value}%`) - ); - -interface SettingTextData { - [x: string]: string[] | SettingTextData; -} - -mainSetting - .setDescription('audio.bgmEnabled', `是否开启背景音乐`) - .setDescription('audio.bgmVolume', `背景音乐的音量`) - .setDescription('audio.soundEnabled', `是否开启音效`) - .setDescription('audio.soundVolume', `音效的音量`) - .setDescription( - 'ui.bookScale', - `怪物手册界面中每个怪物框体的高度缩放,最小值限定为 20% 屏幕高度` - ) - .setDescription( - 'screen.blur', - '打开任意ui界面时是否有背景虚化效果,移动端打开后可能会有掉帧或者发热现象。关闭ui后生效' - ); - -function setFontSize() { - const absoluteSize = storage.getValue( - '@@absoluteFontSize', - 16 * devicePixelRatio - ); - const size = Math.round(absoluteSize / devicePixelRatio); - mainSetting.setValue('screen.fontSize', size); -} -setFontSize(); - -function getSettingText(obj: SettingTextData, key?: string) { - for (const [k, value] of Object.entries(obj)) { - const setKey = key ? key + '.' + k : k; - if (value instanceof Array) { - mainSetting.setDescription(setKey, value.join('\n')); - } else { - getSettingText(value, setKey); - } - } -} -getSettingText(settingsText); - -window.addEventListener('resize', () => { - setFontSize(); -}); - -export function createSetting() { - const { loading } = Mota.require('@user/data-base'); - loading.once('coreInit', () => { - mainSetting.reset({ - 'screen.fullscreen': !!document.fullscreenElement, - 'screen.scale': storage.getValue('screen.scale', 100), - 'screen.itemDetail': !!storage.getValue('screen.itemDetail', true), - 'screen.transition': !!storage.getValue('screen.transition', false), - 'screen.fontSize': storage.getValue( - 'screen.fontSize', - isMobile ? 9 : 16 - ), - 'screen.criticalGem': !!storage.getValue( - 'screen.criticalGem', - false - ), - 'audio.bgmEnabled': !!storage.getValue('audio.bgmEnabled', true), - 'audio.bgmVolume': storage.getValue('audio.bgmVolume', 80), - 'audio.soundEnabled': !!storage.getValue( - 'audio.soundEnabled', - true - ), - 'audio.soundVolume': storage.getValue('audio.soundVolume', 80), - 'utils.autoScale': !!storage.getValue('utils.autoScale', true), - 'ui.bookScale': storage.getValue( - 'ui.bookScale', - isMobile ? 100 : 80 - ) - }); - }); -} diff --git a/packages/legacy-ui/src/preset/uiIns.ts b/packages/legacy-ui/src/preset/uiIns.ts deleted file mode 100644 index 79cf479..0000000 --- a/packages/legacy-ui/src/preset/uiIns.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { GameUi, UiController } from '../controller'; -import * as UI from '../ui'; -import { VirtualKey } from '@motajs/legacy-system'; - -export const mainUi = new UiController(); -mainUi.register( - new GameUi('toolbox', UI.Toolbox), - new GameUi('equipbox', UI.Equipbox), - new GameUi('settings', UI.Settings), - new GameUi('fly', UI.Fly), - new GameUi('shop', UI.Shop), - new GameUi('hotkey', UI.Hotkey), - new GameUi('virtualKey', VirtualKey) -); -mainUi.showAll(); diff --git a/packages/legacy-ui/src/setting.ts b/packages/legacy-ui/src/setting.ts deleted file mode 100644 index ddb2916..0000000 --- a/packages/legacy-ui/src/setting.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { FunctionalComponent, reactive } from 'vue'; -import { EventEmitter } from '@motajs/legacy-common'; -import { isNil } from 'lodash-es'; - -export interface SettingComponentProps { - item: MotaSettingItem; - setting: MotaSetting; - displayer: SettingDisplayer; -} - -export type SettingComponent = FunctionalComponent; -type MotaSettingType = boolean | number | MotaSetting; - -export interface MotaSettingItem { - name: string; - key: string; - value: T; - controller: SettingComponent; - description?: string; - defaults?: boolean | number; - step?: [number, number, number]; - display?: (value: T) => string; -} - -interface SettingEvent { - valueChange: ( - key: string, - newValue: T, - oldValue: T - ) => void; -} - -export type SettingText = { - [key: string]: string[] | SettingText; -}; - -export interface SettingDisplayInfo { - item: MotaSettingItem | null; - list: Record; - text: string[]; -} - -export class MotaSetting extends EventEmitter { - static noStorage: string[] = []; - - readonly list: Record = {}; - - /** - * 重设设置 - * @param setting 设置信息 - */ - reset(setting: Record) { - for (const [key, value] of Object.entries(setting)) { - this.setValue(key, value); - } - } - - /** - * 注册一个数字型设置 - * @param key 设置的键名 - * @param value 设置的值 - */ - register( - key: string, - name: string, - value: number, - com?: SettingComponent, - step?: [number, number, number] - ): this; - /** - * 注册一个非数字型设置 - * @param key 设置的键名 - * @param value 设置的值 - */ - register( - key: string, - name: string, - value: boolean | MotaSetting, - com?: SettingComponent - ): this; - register( - key: string, - name: string, - value: MotaSettingType, - com: SettingComponent, - step: [number, number, number] = [0, 100, 1] - ) { - const setting: MotaSettingItem = { - name, - value, - key, - controller: com - }; - if (!(value instanceof MotaSetting)) setting.defaults = value; - if (typeof value === 'number') setting.step = step; - this.list[key] = setting; - return this; - } - - /** - * 获取一个设置信息 - * @param key 要获取的设置的键 - */ - getSetting(key: string): Readonly { - const list = key.split('.'); - return this.getSettingBy(list); - } - - /** - * 设置一个设置的值 - * @param key 要设置的设置的键 - * @param value 要设置的值 - */ - setValue(key: string, value: boolean | number) { - const setting = this.getSettingBy(key.split('.')); - if (typeof setting.value !== typeof value) { - throw new Error( - `Setting type mismatch on setting '${key}'.` + - `Expected: ${typeof setting.value}. Recieve: ${typeof value}` - ); - } - const old = setting.value as boolean | number; - setting.value = value; - - this.emit('valueChange', key, value, old); - } - - /** - * 增加一个设置的值 - * @param key 要改变的设置的值 - * @param value 值的增量 - */ - addValue(key: string, value: number) { - const setting = this.getSettingBy(key.split('.')); - if (typeof setting.value !== 'number') { - throw new Error( - `Cannot execute addValue method on a non-number setting.` + - `Type expected: number. See: ${typeof setting.value}` - ); - } - const old = setting.value as boolean | number; - setting.value += value; - this.emit('valueChange', key, old, value); - } - - /** - * 获取一个设置的值,如果获取到的是一个MotaSetting实例,那么返回undefined - * @param key 要获取的设置 - */ - getValue(key: string): boolean | number | undefined; - /** - * 获取一个设置的值,如果获取到的是一个MotaSetting实例,那么返回defaultValue - * @param key 要获取的设置 - * @param defaultValue 设置的默认值 - */ - getValue(key: string, defaultValue: T): T; - getValue( - key: string, - defaultValue?: T - ): T | undefined { - const setting = this.getSetting(key); - if (isNil(setting) && isNil(defaultValue)) return void 0; - if (setting instanceof MotaSetting) { - if (!isNil(setting)) return defaultValue; - return void 0; - } else { - return !isNil(setting) ? (setting.value as T) : (defaultValue as T); - } - } - - /** - * 设置一个设置的值显示函数 - * @param key 要设置的设置的键 - * @param func 显示函数 - */ - setDisplayFunc(key: string, func: (value: MotaSettingType) => string) { - const setting = this.getSettingBy(key.split('.')); - setting.display = func; - return this; - } - - /** - * 设置一个设置的修改部分组件 - * @param key 要设置的设置的键 - * @param com 设置修改部分的组件 - */ - setValueController(key: string, com: SettingComponent) { - const setting = this.getSettingBy(key.split('.')); - setting.controller = com; - return this; - } - - /** - * 设置一个设置的说明 - * @param key 要设置的设置的id - * @param desc 设置的说明 - */ - setDescription(key: string, desc: string) { - const setting = this.getSettingBy(key.split('.')); - setting.description = desc; - return this; - } - - private getSettingBy(list: string[]) { - let now: MotaSetting = this; - - for (let i = 0; i < list.length - 1; i++) { - const item = now.list[list[i]].value; - if (!(item instanceof MotaSetting)) { - throw new Error( - `Cannot get setting. The parent isn't a MotaSetting instance.` + - `Key: '${list.join('.')}'. Reading: '${list[i]}'` - ); - } - now = item; - } - - return now.list[list.at(-1)!] ?? null; - } -} - -interface SettingDisplayerEvent { - update: (stack: string[], display: SettingDisplayInfo[]) => void; -} - -export class SettingDisplayer extends EventEmitter { - setting: MotaSetting; - /** 选项选中栈 */ - selectStack: string[] = []; - displayInfo: SettingDisplayInfo[] = reactive([]); - - constructor(setting: MotaSetting) { - super(); - this.setting = setting; - this.update(); - } - - /** - * 添加选择项 - * @param key 下一个选择项 - */ - add(key: string) { - this.selectStack.push(...key.split('.')); - this.update(); - } - - /** - * 剪切后面的选择项 - * @param index 从哪开始剪切 - */ - cut(index: number, noUpdate: boolean = false) { - this.selectStack.splice(index, Infinity); - if (!noUpdate) this.update(); - } - - update() { - const list = this.selectStack; - let now = this.setting; - this.displayInfo = []; - - for (let i = 0; i < list.length - 1; i++) { - const item = now.list[list[i]].value; - if (!(item instanceof MotaSetting)) { - throw new Error( - `Cannot get setting. The parent isn't a MotaSetting instance.` + - `Key: '${list.join('.')}'. Reading: '${list[i + 1]}'` - ); - } - - this.displayInfo.push({ - item: now.list[list[i]], - text: [], - list: now.list - }); - - now = item; - } - - const last = now.list[list.at(-1)!]; - if (last) { - const desc = last.description; - const text = desc ? desc.split('\n') : ['请选择设置']; - - this.displayInfo.push({ - item: last, - text, - list: now.list - }); - if (last.value instanceof MotaSetting) { - this.displayInfo.push({ - item: null, - text: ['请选择设置'], - list: (last.value as MotaSetting).list - }); - } - } else { - this.displayInfo.push({ - item: null, - text: ['请选择设置'], - list: this.setting.list - }); - } - this.emit('update', this.selectStack, this.displayInfo); - } -} diff --git a/packages/legacy-ui/src/tools/book.tsx b/packages/legacy-ui/src/tools/book.tsx deleted file mode 100644 index a3ed092..0000000 --- a/packages/legacy-ui/src/tools/book.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { isNil } from 'lodash-es'; -import { IDamageEnemy } from '@motajs/types'; - -export interface CurrentEnemy { - enemy: IDamageEnemy; - // 这个是干啥的? - onMapEnemy: IDamageEnemy[]; -} - -export interface ToShowEnemy extends CurrentEnemy { - critical: string; - criticalDam: string; - defDam: string; - /** [名称, 描述, 颜色] */ - special: [string, string, string][]; - damageColor: string; - showSpecial: [string, string, string][]; - damage: string; -} - -interface BookDetailInfo { - /** 怪物手册详细信息展示的怪物 */ - enemy?: ToShowEnemy; - /** 怪物手册的怪物详细信息的初始位置 */ - pos?: number; -} - -export const detailInfo: BookDetailInfo = {}; - -/** - * 获取怪物的特殊技能描述 - * @param enemy 怪物实例 - */ -export function getSpecialHint(enemy: ToShowEnemy) { - return ( -
- {enemy.special.map((v, i) => { - return ( -
- -     {v[0]}: - - -
- ); - })} -
- ); -} - -/** - * 获得怪物的最近100个加防减伤 - * @param enemy 怪物实例 - */ -export function getDefDamage( - enemy: ToShowEnemy, - addDef: number = 0, - addAtk: number = 0 -) { - const ratio = core.status.thisMap.ratio; - const res: [number, number][] = []; - - let origin: number | undefined; - let last = 0; - - const max = 100 - Math.floor(addDef / ratio); - - for (let i = 0; i <= max; i++) { - const dam = enemy.enemy.calDamage({ - atk: core.status.hero.atk + addAtk, - def: core.status.hero.def + addDef + i * ratio - }); - - if (res.length === 0) { - origin = dam.damage; - if (!isNil(origin)) { - res.push([addDef + i * ratio, origin]); - last = origin; - } - continue; - } - if (!isFinite(dam.damage)) continue; - if (dam.damage === res.at(-1)?.[1]) continue; - last = dam.damage; - res.push([ratio * i + addDef, dam.damage]); - } - - return res; -} - -/** - * 获取怪物的临界信息 - * @param enemy 怪物实例 - */ -export function getCriticalDamage( - enemy: ToShowEnemy, - addAtk: number = 0, - addDef: number = 0 -): [number, number][] { - const ratio = core.status.thisMap.ratio; - const res: [number, number][] = []; - - let origin: number | undefined; - let last = 0; - - const max = 100 - Math.floor(addAtk / ratio); - - for (let i = 0; i <= max; i++) { - const dam = enemy.enemy.calDamage({ - atk: core.status.hero.atk + addAtk + i * ratio, - def: core.status.hero.def + addDef - }); - - if (res.length === 0) { - origin = dam.damage; - if (!isNil(origin)) { - res.push([addAtk + i * ratio, origin]); - last = origin; - } - continue; - } - if (!isFinite(dam.damage)) continue; - if (dam.damage === res.at(-1)?.[1]) continue; - last = dam.damage; - res.push([ratio * i + addAtk, dam.damage]); - } - - return res; -} diff --git a/packages/legacy-ui/src/tools/common.ts b/packages/legacy-ui/src/tools/common.ts deleted file mode 100644 index 7ebfa8e..0000000 --- a/packages/legacy-ui/src/tools/common.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Ref, ref, watch } from 'vue'; -import { nextFrame } from '../utils'; - -interface ChangableValue { - change: Ref; - value: Ref; - stop: () => void; -} - -/** - * 创建一个监听响应式变量更改的,可以用于Changable的监听器 - * @param value 要监听的值 - */ -export function createChangable( - value: Ref, - key?: keyof T -): ChangableValue { - const change = ref(false); - const stop = watch(value, (n, o) => { - if (key) { - if (n[key] === o[key]) return; - } - if (change.value) { - change.value = false; - nextFrame(() => (change.value = true)); - } else { - change.value = true; - } - }); - - return { - change, - value, - stop - }; -} diff --git a/packages/legacy-ui/src/tools/equipbox.tsx b/packages/legacy-ui/src/tools/equipbox.tsx deleted file mode 100644 index 3ca9f76..0000000 --- a/packages/legacy-ui/src/tools/equipbox.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { isNil } from 'lodash-es'; -import { getStatusLabel } from '../utils'; - -/** - * 获取所有装备 - */ -export function getEquips(): [ItemIdOf<'equips'>, number][] { - return Object.entries(core.status.hero.items.equips) as [ - ItemIdOf<'equips'>, - number - ][]; -} - -/** - * 获取装备增加的属性 - * @param equip 装备 - */ -export function getAddStatus(equip: Equip) { - const toGet = Object.assign({}, equip.value, equip.percentage); - const keys = Object.keys(toGet) as (keyof typeof toGet)[]; - - return ( -
- {keys.map(v => { - const value = Math.floor( - (equip.value[v] ?? 0) * core.getBuff(v) + - (core.status.hero[v as keyof HeroStatus] * - (equip.percentage?.[v] ?? 0)) / - 100 - ); - - return ( - - {getStatusLabel(v)} -      - 0 ? 'lightgreen' : 'lightsalmon' - }} - > - {value > 0 ? `+${value}` : value} - - - ); - })} -
- ); -} - -/** - * 获取当前勇士属性,如果有选中的装备,会在后面追加显示增加量 - * @param nowEquip 当前选中的装备 - */ -export function getNowStatus(nowEquip?: Equip, onCol: boolean = false) { - const toShow = [ - 'hp', - 'lv', - 'atk', - 'def', - 'mdef', - 'mana', - 'hpmax', - 'money' - ] as (keyof SelectType)[]; - // @ts-expect-error 需要重构 - const { getHeroStatusOn } = Mota.require('@user/data-state'); - - return ( -
- {toShow.map(v => { - let status: string; - if (v === 'lv') status = core.getLvName() ?? ''; - else status = getHeroStatusOn(v)?.toString(); - - let add = 0; - if (!isNil(nowEquip)) { - add += Math.floor( - (nowEquip.value[v] ?? 0) * core.getBuff(v) - ); - const per = Math.floor( - ((nowEquip.percentage?.[v] ?? 0) * core.getStatus(v)) / - 100 - ); - add += isNaN(per) ? 0 : per; - } - if (onCol) add = -add; - - return ( -
- - {getStatusLabel(v)} - -
- {status} - {add !== 0 && ( - 0 - ? 'lightgreen' - : 'lightsalmon' - }} - > - {add > 0 ? '+' + add : '-' + -add} - - )} -
-
- ); - })} -
- ); -} diff --git a/packages/legacy-ui/src/tools/fixed.ts b/packages/legacy-ui/src/tools/fixed.ts deleted file mode 100644 index ba05235..0000000 --- a/packages/legacy-ui/src/tools/fixed.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { getDamageColor } from '../utils'; -import { ToShowEnemy } from './book'; -import { isMobile } from '../use'; -import { EnemyInfo, IDamageEnemy } from '@motajs/types'; - -export function getLocFromMouseLoc(x: number, y: number): LocArr { - const mx = Math.round(x + core.bigmap.offsetX / 32); - const my = Math.round(y + core.bigmap.offsetY / 32); - return [mx, my]; -} - -export function getDetailedEnemy( - enemy: IDamageEnemy, - floorId: FloorIds = core.status.floorId -): ToShowEnemy { - const ratio = core.status.maps[floorId].ratio; - - const dam = enemy.calDamage().damage; - const cri = enemy.calCritical(1)[0]; - const critical = core.formatBigNumber(cri?.atkDelta); - const criticalDam = core.formatBigNumber(cri?.delta); - const defDam = core.formatBigNumber(enemy.calDefDamage(ratio).delta); - const damage = core.formatBigNumber(dam); - - const fromFunc = ( - func: string | ((enemy: EnemyInfo) => string), - enemy: EnemyInfo - ) => { - return typeof func === 'string' ? func : func(enemy); - }; - const special: [string, string, string][] = [...enemy.info.special] - // @ts-expect-error 之后修 - .filter(v => !enemy.info.specialHalo?.includes(v)) - .map(vv => { - // @ts-expect-error 之后修 - const s = Mota.require('@user/data-state').specials[vv]; - return [ - fromFunc(s.name, enemy.info), - fromFunc(s.desc, enemy.info), - s.color as string - ]; - }); - const l = isMobile ? 1 : 2; - const showSpecial = - special.length > l - ? special.slice(0, l).concat([['...', '', '#fff']]) - : special.slice(); - - const damageColor = getDamageColor(dam) as string; - - const detail: ToShowEnemy = { - enemy, - onMapEnemy: [enemy], - critical, - criticalDam, - defDam, - damageColor, - special, - showSpecial, - damage - }; - return detail; -} diff --git a/packages/legacy-ui/src/tools/fly.ts b/packages/legacy-ui/src/tools/fly.ts deleted file mode 100644 index fc8fc1c..0000000 --- a/packages/legacy-ui/src/tools/fly.ts +++ /dev/null @@ -1,568 +0,0 @@ -import { isNil } from 'lodash-es'; -import { mainSetting } from '../preset/settingIns'; -import { tip } from '../use'; -import { downloadCanvasImage } from '../utils'; - -type BFSFromString = `${FloorIds},${number},${number},${Dir}`; -type BFSToString = `${FloorIds},${number},${number}`; - -interface MapBFSResult { - maps: FloorIds[]; - link: Record; -} - -interface MapDrawData { - locs: Partial>; - line: [number, number, number, number][]; - width: number; - height: number; -} - -let area: Record = {}; - -const bfsCache: Partial> = {}; -/** - * 键的格式:FloorIds,interval,border - */ -const drawCache: Record = {}; - -const arrow: Partial> = { - 92: 'left', - 94: 'right', - 91: 'up', - 93: 'down' -}; - -/** - * 切分地图区域 - */ -export function splitArea() { - area = {}; - const used: FloorIds[] = []; - for (const id of core.floorIds) { - if (used.includes(id) || core.status.maps[id].deleted) continue; - const data = getMapData(id, true); - used.push(...data.maps); - if (data.maps.length > 1) { - const title = core.status.maps[id].title; - area[title] = data.maps; - } - } -} - -export function getArea() { - return area; -} - -/** - * 获取地图绘制信息 - * @param floorId 中心楼层 - * @param interval 地图间距 - * @param border 边框宽度 - * @param noCache 是否不使用缓存 - */ -export function getMapDrawData( - floorId: FloorIds, - interval: number = 5, - border: number = 1, - noCache: boolean = false -): MapDrawData { - const id = `${floorId},${interval},${border}`; - if (!floorId) return { locs: {}, line: [], width: 1, height: 1 }; - if (drawCache[id] && !noCache) return drawCache[id]; - const { link, maps } = getMapData(floorId, noCache); - const locs: Partial> = {}; - const line: [number, number, number, number][] = []; - const center = core.status.maps[floorId]; - let left = -center.width / 2, - right = center.width / 2, - top = -center.height / 2, - bottom = center.height / 2; - for (const [from, to] of Object.entries(link)) { - const [fromId, fxs, fys, dir] = from.split(',') as [ - FloorIds, - string, - string, - Dir - ]; - const [toId, txs, tys] = to.split(',') as [FloorIds, string, string]; - const fromMap = core.status.maps[fromId]; - const toMap = core.status.maps[toId]; - const fx = parseInt(fxs), - fy = parseInt(fys), - tx = parseInt(txs), - ty = parseInt(tys); - const fw = fromMap.width, - fh = fromMap.height; - const tw = toMap.width, - th = toMap.height; - locs[fromId] ??= [0, 0]; - const [fromX, fromY] = locs[fromId]!; - if (!locs[toId]) { - const dx = core.utils.scan[dir].x, - dy = core.utils.scan[dir].y; - const toX = - fromX + - (fx - fw / 2) - - (tx - tw / 2) + - (border * 2 + interval) * dx, - toY = - fromY + - (fy - fh / 2) - - (ty - th / 2) + - (border * 2 + interval) * dy; - // 地图位置和连线位置 - locs[toId] = [toX, toY]; - } - const [toX, toY] = locs[toId]!; - line.push([ - fromX + (fx - fw / 2 + 0.5), - fromY + (fy - fh / 2 + 0.5), - toX + (tx - tw / 2 + 0.5), - toY + (ty - th / 2 + 0.5) - ]); - - // 计算地图总长宽 - const l = toX - tw / 2, - r = toX + tw / 2, - t = toY - th / 2, - b = toY + th / 2; - if (l < left) left = l; - if (r > right) right = r; - if (t < top) top = t; - if (b > bottom) bottom = b; - } - - // 移动位置,居中 - Object.values(locs).forEach(v => { - v[0] -= left; - v[1] -= top; - }); - line.forEach(v => { - v[0] -= left; - v[2] -= left; - v[1] -= top; - v[3] -= top; - }); - - left -= 5; - right += 5; - top -= 5; - bottom += 5; - - const res = { locs, line, width: right - left, height: bottom - top }; - - return (drawCache[id] = res); -} - -/** - * 广度优先搜索地图信息 - * @param floorId 中心楼层id - * @param noCache 是否不使用缓存 - */ -export function getMapData( - floorId: FloorIds, - noCache: boolean = false -): MapBFSResult { - if (!floorId) return { maps: [], link: {} }; - if (!isNil(bfsCache[floorId]) && !noCache) return bfsCache[floorId]!; - - const queue = [floorId]; - const used: Partial> = { - [floorId]: true - }; - const floors = [floorId]; - const link: Record = {}; - - while (queue.length > 0) { - const now = queue.shift()!; - const floor = core.floors[now]; - const change = floor.changeFloor; - for (const [loc, ev] of Object.entries(change)) { - if (!ev) continue; - const target = ev.floorId as FloorIds; - if (target.startsWith(':')) continue; - const [x, y] = loc.split(',').map(v => parseInt(v)); - const id = floor.map[y][x] as AllNumbers; - if (id in arrow) { - if (!used[target]) { - const from = `${now},${loc},${arrow[id]}` as BFSFromString; - const to = `${target},${ev.loc![0]},${ - ev.loc![1] - }` as BFSToString; - link[from] = to; - queue.push(target); - floors.push(target); - } - } - } - used[now] = true; - } - - const res = { - maps: floors, - link - }; - - return (bfsCache[floorId] = res); -} - -type Loc2 = [number, number, number, number]; - -export class MinimapDrawer { - ctx: CanvasRenderingContext2D; - canvas: HTMLCanvasElement; - scale: number = 1; - nowFloor: FloorIds = core.status.floorId; - nowArea: string = ''; - - // position & config - ox: number = 0; - oy: number = 0; - noBorder: boolean = false; - showInfo: boolean = false; - - // cache - drawedThumbnail: Partial> = {}; - thumbnailLoc: Partial> = {}; - - // temp - private tempCanvas: HTMLCanvasElement = document.createElement('canvas'); - private tempCtx: CanvasRenderingContext2D; - - private downloadMode: boolean = false; - private innerRatio: number = 1; - - constructor(canvas: HTMLCanvasElement) { - this.canvas = canvas; - this.ctx = canvas.getContext('2d')!; - this.tempCtx = this.tempCanvas.getContext('2d')!; - this.innerRatio = mainSetting.getValue('ui.mapScale', 100) / 100; - this.scale *= this.innerRatio; - } - - link(canvas: HTMLCanvasElement) { - this.canvas = canvas; - this.ctx = canvas.getContext('2d')!; - } - - clearCache() { - this.drawedThumbnail = {}; - this.thumbnailLoc = {}; - } - - /** - * 绘制小地图 - * @param noCache 是否不使用缓存 - */ - drawMap(noCache: boolean = false) { - const border = this.noBorder ? 0.5 : 1; - const data = getMapDrawData( - this.nowFloor, - this.noBorder ? 0 : 5, - border, - noCache - ); - const temp = this.tempCanvas; - const ctx = this.tempCtx; - const s = this.scale * devicePixelRatio; - temp.width = data.width * s; - temp.height = data.height * s; - ctx.lineWidth = (border * devicePixelRatio) / 2; - ctx.strokeStyle = '#fff'; - ctx.scale(s, s); - ctx.translate(5, 5); - - if (!this.noBorder) { - // 绘制连线 - data.line.forEach(([x1, y1, x2, y2]) => { - ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.stroke(); - }); - } - - // 绘制地图及缩略图 - for (const [id, [x, y]] of Object.entries(data.locs) as [ - FloorIds, - LocArr - ][]) { - if (!this.noBorder) this.drawBorder(id, x, y); - this.drawThumbnail(id, x, y); - } - this.drawToTarget(); - } - - /** - * 绘制一个楼层的边框 - */ - drawBorder(id: FloorIds, x: number, y: number) { - const border = this.noBorder ? 0.5 : 1; - const ctx = this.tempCtx; - ctx.lineWidth = border * devicePixelRatio; - const map = core.status.maps[id]; - - if (!core.hasVisitedFloor(id)) { - ctx.fillStyle = '#d0d'; - } else { - ctx.fillStyle = '#000'; - } - if (id === this.nowFloor) { - ctx.strokeStyle = 'gold'; - } else { - ctx.strokeStyle = '#fff'; - } - - ctx.strokeRect( - x - map.width / 2, - y - map.height / 2, - map.width, - map.height - ); - - ctx.fillRect( - x - map.width / 2, - y - map.height / 2, - map.width, - map.height - ); - if (id === this.nowFloor) { - ctx.fillStyle = '#ff04'; - ctx.fillRect( - x - map.width / 2, - y - map.height / 2, - map.width, - map.height - ); - } - if (!this.noBorder && this.scale * this.innerRatio <= 7) - this.drawEnemy(ctx, id, x, y); - } - - /** - * 将临时画布的内容绘制到目标画布上 - */ - drawToTarget() { - const mapCtx = this.ctx; - const map = this.canvas; - const temp = this.tempCanvas; - - mapCtx.clearRect(0, 0, map.width, map.height); - mapCtx.drawImage( - temp, - 0, - 0, - temp.width, - temp.height, - this.ox * devicePixelRatio + (map.width - temp.width) / 2, - this.oy * devicePixelRatio + (map.height - temp.height) / 2, - temp.width, - temp.height - ); - } - - /** - * 检查是否应该绘制缩略图 - */ - checkThumbnail(floorId: FloorIds, x: number, y: number) { - const scale = this.scale; - const ox = this.ox; - const oy = this.oy; - const map = this.canvas; - const temp = this.tempCanvas; - - const floor = core.status.maps[floorId]; - const s = scale * devicePixelRatio; - const px = ox * devicePixelRatio + (map.width - temp.width) / 2 + 5 * s; - const py = - oy * devicePixelRatio + (map.height - temp.height) / 2 + 5 * s; - const left = px + (x - floor.width / 2) * s; - const top = py + (y - floor.height / 2) * s; - const right = left + floor.width * s; - const bottom = top + floor.height * s; - - this.thumbnailLoc[floorId] = [left, top, right, bottom]; - - if ( - this.drawedThumbnail[floorId] || - (!this.noBorder && scale * this.innerRatio <= 7) || - right < 0 || - bottom < 0 || - left > map.width || - top > map.height - ) - return false; - - return true; - } - - /** - * 绘制缩略图 - */ - drawThumbnail( - floorId: FloorIds, - x: number, - y: number, - noCheck: boolean = false - ) { - if ( - !this.downloadMode && - !noCheck && - !this.checkThumbnail(floorId, x, y) - ) - return; - const floor = core.status.maps[floorId]; - this.drawedThumbnail[floorId] = true; - - // 绘制缩略图 - const ctx = this.tempCtx; - core.drawThumbnail(floorId, void 0, { - all: true, - inFlyMap: true, - x: x - floor.width / 2, - y: y - floor.height / 2, - w: floor.width, - h: floor.height, - ctx, - damage: this.scale * this.innerRatio > 15 - }); - if (!this.downloadMode) { - if (!core.hasVisitedFloor(floorId)) { - ctx.fillStyle = '#d0d6'; - ctx.fillRect( - x - floor.width / 2, - y - floor.height / 2, - floor.width, - floor.height - ); - ctx.fillStyle = '#000'; - } - if (this.nowFloor === floorId) { - ctx.fillStyle = '#ff04'; - ctx.fillRect( - x - floor.width / 2, - y - floor.height / 2, - floor.width, - floor.height - ); - ctx.fillStyle = '#000'; - } - } - this.drawEnemy(ctx, floorId, x, y); - } - - /** - * 当移动时检查是否应该绘制缩略图 - */ - checkMoveThumbnail() { - const border = this.noBorder ? 0.5 : 1; - const data = getMapDrawData( - this.nowFloor, - this.noBorder ? 0 : 5, - border - ); - for (const [id, [x, y]] of Object.entries(data.locs) as [ - FloorIds, - LocArr - ][]) { - if (this.checkThumbnail(id, x, y)) { - this.drawThumbnail(id, x, y, true); - } - } - } - - download() { - if (this.nowArea === '') { - tip('error', '当前地图不在任意一个区域内!'); - return; - } - this.downloadMode = true; - const before = this.scale; - this.scale = 32; - this.drawMap(); - downloadCanvasImage(this.tempCanvas, this.nowArea); - this.scale = before; - this.downloadMode = false; - tip('success', '图片下载成功!'); - } - - /** - * 居中地图 - * @param id 楼层id - */ - locateMap(id: FloorIds) { - const data = getMapDrawData( - id, - this.noBorder ? 0 : 5, // 可恶的0和5,写反了找一个多小时 - this.noBorder ? 0.5 : 1 - ); - if (!data.locs[id]) return; - - const [x, y] = data.locs[id]!; - this.ox = (-x + data.width / 2 - 5) * this.scale; - this.oy = (-y + data.height / 2 - 5) * this.scale; - } - - private drawEnemy( - ctx: CanvasRenderingContext2D, - floorId: FloorIds, - x: number, - y: number - ) { - if ( - this.scale * this.innerRatio > 2 && - this.scale * this.innerRatio < 40 && - !this.downloadMode && - this.showInfo - ) { - ctx.save(); - ctx.lineWidth = 0.5; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.font = `3px "normal"`; - ctx.strokeStyle = 'black'; - // Mota.require('@user/data-state').ensureFloorDamage(floorId); - ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; - ctx.fillRect(x - 6, y - 2, 12, 4); - ctx.fillStyle = 'white'; - // const enemy = core.status.maps[floorId].enemy.list; - // if (enemy.size === 0) { - // ctx.strokeStyle = 'lightgreen'; - // ctx.lineCap = 'round'; - // ctx.lineJoin = 'round'; - // ctx.beginPath(); - // ctx.moveTo(x - 1.5, y); - // ctx.lineTo(x - 0.5, y + 1); - // ctx.lineTo(x + 1.5, y - 1); - // ctx.stroke(); - // } else if (enemy.size <= 2) { - // const idSet = new Set(); - // enemy.forEach(v => { - // idSet.add(v.id); - // }); - // const ids: EnemyIds[] = [...idSet]; - // if (ids.length === 1) { - // core.drawIcon(ctx, ids[0], x - 2, y - 2, 4, 4); - // } else if (ids.length === 2) { - // core.drawIcon(ctx, ids[0], x - 4, y - 2, 4, 4); - // core.drawIcon(ctx, ids[1], x, y - 2, 4, 4); - // } else { - // core.drawIcon(ctx, ids[0], x - 5, y - 2, 4, 4); - // core.drawIcon(ctx, ids[1], x - 1, y - 2, 4, 4); - // ctx.fillStyle = 'white'; - // ctx.strokeStyle = 'black'; - // ctx.strokeText('…', x + 4, y); - // ctx.fillText('…', x + 4, y); - // } - // } else { - // ctx.fillStyle = 'white'; - // ctx.textAlign = 'center'; - // ctx.textBaseline = 'middle'; - // ctx.fillText(`+${enemy.size}`, x, y); - // } - - ctx.restore(); - } - } -} diff --git a/packages/legacy-ui/src/tools/index.ts b/packages/legacy-ui/src/tools/index.ts deleted file mode 100644 index 462345c..0000000 --- a/packages/legacy-ui/src/tools/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './book'; -export * from './common'; -export * from './equipbox'; -export * from './fixed'; -export * from './fly'; diff --git a/packages/legacy-ui/src/tools/toolbox.tsx b/packages/legacy-ui/src/tools/toolbox.tsx deleted file mode 100644 index af318b3..0000000 --- a/packages/legacy-ui/src/tools/toolbox.tsx +++ /dev/null @@ -1,46 +0,0 @@ -interface ClsMap { - tools: '消耗道具'; - constants: '永久道具'; - all: '全部道具'; -} - -interface AllItem { - tools: [ItemIdOf<'tools'>, number][]; - constants: [ItemIdOf<'constants'>, number][]; -} - -const clsMap: ClsMap = { - tools: '消耗道具', - constants: '永久道具', - all: '全部道具' -}; - -/** - * 根据道具的cls获取中文类型 - * @param cls 道具的cls - */ -export function getClsName(cls: T): ClsMap[T] { - return clsMap[cls]; -} - -/** - * 获取某个类型的所有道具 - * @param cls 道具类型 - */ -export function getItems(cls: 'all'): AllItem; -export function getItems(cls: keyof ClsMap): [string, number][]; -export function getItems(cls: keyof ClsMap | 'all') { - const i = core.status.hero.items; - const ignore = ['book']; - if (cls === 'all') { - return { - tools: Object.entries(i.tools).filter(v => !ignore.includes(v[0])), - constants: Object.entries(i.constants).filter( - v => !ignore.includes(v[0]) - ) - }; - } - const items = i[cls]; - const data = Object.entries(items); - return data; -} diff --git a/packages/legacy-ui/src/ui/bookDetail.vue b/packages/legacy-ui/src/ui/bookDetail.vue deleted file mode 100644 index 5c71eaf..0000000 --- a/packages/legacy-ui/src/ui/bookDetail.vue +++ /dev/null @@ -1,235 +0,0 @@ - - - - - - diff --git a/packages/legacy-ui/src/ui/equipbox.vue b/packages/legacy-ui/src/ui/equipbox.vue deleted file mode 100644 index 625afbc..0000000 --- a/packages/legacy-ui/src/ui/equipbox.vue +++ /dev/null @@ -1,717 +0,0 @@ - - - - - diff --git a/packages/legacy-ui/src/ui/fly.vue b/packages/legacy-ui/src/ui/fly.vue deleted file mode 100644 index a66008b..0000000 --- a/packages/legacy-ui/src/ui/fly.vue +++ /dev/null @@ -1,638 +0,0 @@ - - - - - diff --git a/packages/legacy-ui/src/ui/hotkey.vue b/packages/legacy-ui/src/ui/hotkey.vue deleted file mode 100644 index 9906150..0000000 --- a/packages/legacy-ui/src/ui/hotkey.vue +++ /dev/null @@ -1,255 +0,0 @@ - - - - - diff --git a/packages/legacy-ui/src/ui/index.ts b/packages/legacy-ui/src/ui/index.ts deleted file mode 100644 index a24dbd3..0000000 --- a/packages/legacy-ui/src/ui/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { default as BookDetail } from './bookDetail.vue'; -export { default as Equipbox } from './equipbox.vue'; -export { default as Fly } from './fly.vue'; -export { default as Settings } from './settings.vue'; -export { default as Shop } from './shop.vue'; -export { default as Toolbox } from './toolbox.vue'; -export { default as Hotkey } from './hotkey.vue'; diff --git a/packages/legacy-ui/src/ui/settings.vue b/packages/legacy-ui/src/ui/settings.vue deleted file mode 100644 index 2441ab8..0000000 --- a/packages/legacy-ui/src/ui/settings.vue +++ /dev/null @@ -1,366 +0,0 @@ - - - - - diff --git a/packages/legacy-ui/src/ui/shop.vue b/packages/legacy-ui/src/ui/shop.vue deleted file mode 100644 index 681fbec..0000000 --- a/packages/legacy-ui/src/ui/shop.vue +++ /dev/null @@ -1,484 +0,0 @@ - - - - - diff --git a/packages/legacy-ui/src/ui/toolbox.vue b/packages/legacy-ui/src/ui/toolbox.vue deleted file mode 100644 index 009cdd4..0000000 --- a/packages/legacy-ui/src/ui/toolbox.vue +++ /dev/null @@ -1,437 +0,0 @@ - - - - - diff --git a/packages/legacy-ui/src/uiUtils.ts b/packages/legacy-ui/src/uiUtils.ts deleted file mode 100644 index 34698da..0000000 --- a/packages/legacy-ui/src/uiUtils.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { KeyCode } from '@motajs/client-base'; -import { KeyboardEmits, Keyboard, isAssist } from '@motajs/system'; -import { mainUi } from './preset/uiIns'; - -/** - * 唤起虚拟键盘,并获取到一次按键操作 - * @param emitAssist 是否可以获取辅助按键,为true时,如果按下辅助按键,那么会立刻返回该按键, - * 否则会视为开关辅助按键 - * @param assist 初始化的辅助按键 - */ -export function getVitualKeyOnce( - emitAssist: boolean = false, - assist: number = 0, - emittable: KeyCode[] = [] -): Promise { - // todo: 正确触发后删除监听器 - return new Promise(res => { - const key = Keyboard.get('full')!; - key.withAssist(assist); - const id = mainUi.open('virtualKey', { keyboard: key }); - key.on('emit', (item, assist, _index, ev) => { - ev.preventDefault(); - if (emitAssist) { - if (emittable.length === 0 || emittable.includes(item.key)) { - res({ key: item.key, assist: 0 }); - key.disposeScope(); - mainUi.close(id); - } - } else { - if ( - !isAssist(item.key) && - (emittable.length === 0 || emittable.includes(item.key)) - ) { - res({ key: item.key, assist }); - key.disposeScope(); - mainUi.close(id); - } - } - }); - }); -} diff --git a/packages/legacy-ui/src/use.ts b/packages/legacy-ui/src/use.ts deleted file mode 100644 index 1b53f4c..0000000 --- a/packages/legacy-ui/src/use.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { sleep } from 'mutate-animate'; -import { message } from 'ant-design-vue'; -import { MessageApi } from 'ant-design-vue/lib/message'; - -message.config({ - maxCount: 3 -}); -export function tip( - type: Exclude, - text: string -) { - message[type]({ - content: text, - class: 'antdv-message' - }); -} - -let num = 0; -export function requireUniqueSymbol() { - return num++; -} - -type DragFn = (x: number, y: number, e: MouseEvent | TouchEvent) => void; - -type DragMap = [ - (e: MouseEvent) => void, - (e: TouchEvent) => void, - ((e: MouseEvent) => void)?, - ((e: TouchEvent) => void)? -]; - -const dragFnMap = new Map(); - -/** - * 是否是移动设备 - */ -export let isMobile = matchMedia('(max-width: 600px)').matches; - -let alerted = false; -window.addEventListener('resize', () => { - requestAnimationFrame(() => { - isMobile = matchMedia('(max-width: 600px)').matches; - checkMobile(); - }); -}); -checkMobile(); - -sleep(2000).then(() => { - tip('info', `2.B 暂不支持竖屏游玩,手机端请考虑横屏游玩`); -}); - -function checkMobile() { - if (isMobile && !alerted) { - tip( - 'info', - '手机端建议使用新版APP或者自带的浏览器进行游玩,并在进入游戏后开启游戏内的全屏设置游玩' - ); - alerted = true; - } -} - -/** - * 向一个元素添加拖拽事件 - * @param ele 目标元素,当为全局拖拽时,传入数组表示所有元素共用一个全局拖拽函数 - * @param fn 拖拽时触发的函数,传入x y和鼠标事件或点击事件 - * @param ondown 鼠标按下时执行的函数 - * @param global 是否全局拖拽,即拖拽后鼠标或手指离开元素后是否依然视为正在拖拽 - */ -export function useDrag( - ele: HTMLElement | HTMLElement[], - fn: DragFn, - ondown?: DragFn, - onup?: (e: MouseEvent | TouchEvent) => void, - global: boolean = false -) { - const touchFn = (e: TouchEvent) => { - fn(e.touches[0].clientX, e.touches[0].clientY, e); - }; - - const mouseFn = (e: MouseEvent) => { - fn(e.clientX, e.clientY, e); - }; - - const mouseUp = (e: MouseEvent) => { - const ele = global ? document : e.target; - if (ele) { - (ele as HTMLElement).removeEventListener('mousemove', mouseFn); - } - onup?.(e); - }; - - const touchUp = (e: TouchEvent) => { - const ele = global ? document : e.target; - if (ele) { - (ele as HTMLElement).removeEventListener('touchmove', touchFn); - } - onup?.(e); - }; - - const md = (e: MouseEvent) => { - const ele = global ? document : e.target; - if (ele) { - (ele as HTMLElement).addEventListener('mousemove', mouseFn); - } - if (ondown) ondown(e.clientX, e.clientY, e); - }; - const td = (e: TouchEvent) => { - const ele = global ? document : e.target; - if (ele) { - (ele as HTMLElement).addEventListener('touchmove', touchFn); - } - if (ondown) ondown(e.touches[0].clientX, e.touches[0].clientY, e); - }; - - if (ele instanceof Array) { - ele.forEach(v => { - v.addEventListener('mousedown', md); - v.addEventListener('touchstart', td); - }); - } else { - ele.addEventListener('mousedown', md); - ele.addEventListener('touchstart', td); - } - - const target = global ? document : ele; - - if (target instanceof Array) { - target.forEach(v => { - v.addEventListener('mouseup', mouseUp); - v.addEventListener('touchend', touchUp); - }); - } else { - target.addEventListener('mouseup', mouseUp as EventListener); - target.addEventListener('touchend', touchUp as EventListener); - } - dragFnMap.set(fn, [mouseUp, touchUp]); -} - -/** - * 去除一个全局拖拽函数 - * @param fn 要去除的函数 - */ -export function cancelGlobalDrag(fn: DragFn): void { - const fns = dragFnMap.get(fn); - dragFnMap.delete(fn); - if (!fns) return; - document.removeEventListener('mouseup', fns[0]); - document.removeEventListener('touchend', fns[1]); -} - -/** - * 当触发滚轮时执行函数 - * @param ele 目标元素 - * @param fn 当滚轮触发时执行的函数 - */ -export function useWheel( - ele: HTMLElement, - fn: (x: number, y: number, z: number, e: WheelEvent) => void -): void { - ele.addEventListener('wheel', e => { - fn(e.deltaX, e.deltaY, e.deltaZ, e); - }); -} - -/** - * 当鼠标或手指松开时执行函数 - * @param ele 目标元素 - * @param fn 当鼠标或手指松开时执行的函数 - */ -export function useUp(ele: HTMLElement, fn: DragFn): void { - ele.addEventListener('mouseup', e => { - fn(e.clientX, e.clientY, e); - }); - ele.addEventListener('touchend', e => { - fn(e.touches[0].clientX, e.touches[0].clientY, e); - }); -} - -/** - * 当鼠标或手指按下时执行函数 - * @param ele 目标元素 - * @param fn 当鼠标或手指按下时执行的函数 - */ -export function useDown(ele: HTMLElement, fn: DragFn): void { - ele.addEventListener('mousedown', e => { - fn(e.clientX, e.clientY, e); - }); - ele.addEventListener('touchstart', e => { - fn(e.touches[0].clientX, e.touches[0].clientY, e); - }); -} diff --git a/packages/legacy-ui/src/utils.ts b/packages/legacy-ui/src/utils.ts deleted file mode 100644 index ec12e21..0000000 --- a/packages/legacy-ui/src/utils.ts +++ /dev/null @@ -1,386 +0,0 @@ -import { isNil } from 'lodash-es'; -import { Animation, sleep, TimingFn } from 'mutate-animate'; -import { Ref, ref } from 'vue'; -import axios from 'axios'; -import { decompressFromBase64 } from 'lz-string'; -import { logger } from '@motajs/common'; - -type CanParseCss = keyof { - [P in keyof CSSStyleDeclaration as CSSStyleDeclaration[P] extends string - ? P extends string - ? P - : never - : never]: CSSStyleDeclaration[P]; -}; - -/** - * 根据伤害大小获取颜色 - * @param damage 伤害大小 - */ -export function getDamageColor(damage: number): string { - if (typeof damage !== 'number') return '#f00'; - if (damage === 0) return '#2f2'; - if (damage < 0) return '#7f7'; - if (damage < core.status.hero.hp / 3) return '#fff'; - if (damage < (core.status.hero.hp * 2) / 3) return '#ff4'; - if (damage < core.status.hero.hp) return '#f93'; - return '#f22'; -} - -/** - * 设置画布的长宽 - * @param canvas 画布 - * @param w 宽度 - * @param h 高度 - */ -export function setCanvasSize( - canvas: HTMLCanvasElement, - w: number, - h: number -): void { - canvas.width = w; - canvas.height = h; - canvas.style.width = `${w}px`; - canvas.style.height = `${h}px`; -} - -/** - * 解析css字符串为CSSStyleDeclaration对象 - * @param css 要解析的css字符串 - */ -export function parseCss(css: string): Partial> { - if (css.length === 0) return {}; - - let pointer = -1; - let inProp = true; - let prop = ''; - let value = ''; - let upper = false; - const res: Partial> = {}; - - while (++pointer < css.length) { - const char = css[pointer]; - - if ((char === ' ' || char === '\n' || char === '\r') && inProp) { - continue; - } - - if (char === '-' && inProp) { - if (prop.length !== 0) { - upper = true; - } - continue; - } - - if (char === ':') { - if (!inProp) { - logger.error(3, pointer.toString(), css); - return res; - } - inProp = false; - continue; - } - - if (char === ';') { - if (prop.length === 0) continue; - if (inProp) { - logger.error(4, pointer.toString(), css); - return res; - } - res[prop as CanParseCss] = value.trim(); - inProp = true; - prop = ''; - value = ''; - continue; - } - - if (upper) { - if (!inProp) { - logger.error(5, pointer.toString(), css); - } - prop += char.toUpperCase(); - upper = false; - } else { - if (inProp) prop += char; - else value += char; - } - } - if (inProp && prop.length > 0) { - logger.error(6, pointer.toString(), css); - return res; - } - if (!inProp && value.trim().length === 0) { - logger.error(7, pointer.toString(), css); - return res; - } - if (prop.length > 0) res[prop as CanParseCss] = value.trim(); - - return res; -} - -export function stringifyCSS(css: Partial>) { - let str = ''; - - for (const [key, value] of Object.entries(css)) { - let pointer = -1; - let prop = ''; - while (++pointer < key.length) { - const char = key[pointer]; - if (char.toLowerCase() === char) { - prop += char; - } else { - prop += `-${char.toLowerCase()}`; - } - } - str += `${prop}:${value};`; - } - - return str; -} - -/** - * 使用打字机效果显示一段文字 - * @param str 要打出的字符串 - * @param time 打出总用时,默认1秒,当第四个参数是true时,该项为每个字的平均时间 - * @param timing 打出时的速率曲线,默认为线性变化 - * @param avr 是否将第二个参数视为每个字的平均时间 - * @returns 文字的响应式变量 - */ -export function type( - str: string, - time: number = 1000, - timing: TimingFn = n => n, - avr: boolean = false -): Ref { - const toShow = eval('`' + str + '`') as string; - if (typeof toShow !== 'string') { - throw new TypeError('Error str type in typing!'); - } - if (toShow.startsWith('!!html')) return ref(toShow); - if (avr) time *= toShow.length; - const ani = new Animation(); - const content = ref(''); - const all = toShow.length; - - const fn = (time: number) => { - if (isNil(time)) return; - const now = ani.x; - content.value = toShow.slice(0, Math.floor(now)); - if (Math.floor(now) === all) { - ani.ticker.destroy(); - content.value = toShow; - } - }; - - ani.ticker.add(fn); - - ani.mode(timing).time(time).move(all, 0); - - setTimeout(() => ani.ticker.destroy(), time + 100); - - return content; -} - -/** - * 设置文字分段换行等 - * @param str 文字 - */ -export function splitText(str: string[]) { - return str - .map((v, i, a) => { - if (/^\d+\./.test(v)) return `${' '.repeat(12)}${v}`; - else if ( - (!isNil(a[i - 1]) && v !== '
' && a[i - 1] === '
') || - i === 0 - ) { - return `${' '.repeat(8)}${v}`; - } else return v; - }) - .join(''); -} - -/** - * 在下一帧执行某个函数 - * @param cb 执行的函数 - */ -export function nextFrame(cb: (time: number) => void) { - requestAnimationFrame(() => { - requestAnimationFrame(cb); - }); -} - -/** - * 下载一个画布对应的图片 - * @param canvas 画布 - * @param name 图片名称 - */ -export function downloadCanvasImage( - canvas: HTMLCanvasElement, - name: string -): void { - const data = canvas.toDataURL('image/png'); - download(data, name); -} - -/** - * 下载一个文件 - * @param content 下载的内容 - * @param name 文件名称 - */ -export function download(content: string, name: string) { - const a = document.createElement('a'); - a.download = `${name}.png`; - a.href = content; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); -} - -/** - * 间隔一段时间调用一个函数 - * @param funcs 函数列表 - * @param interval 调用间隔 - */ -export async function doByInterval( - funcs: (() => void)[], - interval: number, - awaitFirst: boolean = false -) { - if (awaitFirst) { - await sleep(interval); - } - for await (const fn of funcs) { - fn(); - await sleep(interval); - } -} - -/** - * 更改一个本地存储 - * @deprecated - * @param name 要更改的信息 - * @param fn 更改时执行的函数 - * @param defaultValue 如果不存在时获取的默认值 - */ -export function changeLocalStorage( - name: string, - fn: (data: T) => T, - defaultValue?: T -) { - const now = core.getLocalStorage(name, defaultValue); - const to = fn(now); - core.setLocalStorage(name, to); -} - -export async function swapChapter(chapter: number, hard: number) { - const h = hard === 2 ? 'hard' : 'easy'; - const save = await axios.get( - `${import.meta.env.BASE_URL}swap/${chapter}.${h}.h5save`, - { - responseType: 'text', - responseEncoding: 'utf-8' - } - ); - const data = JSON.parse(decompressFromBase64(save.data)); - - core.loadData(data.data, () => { - core.removeFlag('__fromLoad__'); - core.drawTip('读档成功'); - }); -} - -export function ensureArray(arr: T): T extends any[] ? T : T[] { - // @ts-expect-error 暂时无法推导 - return arr instanceof Array ? arr : [arr]; -} - -export async function triggerFullscreen(full: boolean) { - if (!!document.fullscreenElement && !full) { - if (window.jsinterface) { - window.jsinterface.requestPortrait(); - return; - } - await document.exitFullscreen(); - } - if (full && !document.fullscreenElement) { - if (window.jsinterface) { - window.jsinterface.requestLandscape(); - return; - } - await document.body.requestFullscreen(); - } -} - -/** - * 获得某个状态的中文名 - * @param name 要获取的属性名 - */ -export function getStatusLabel(name: string) { - return ( - { - name: '名称', - lv: '等级', - hpmax: '生命回复', - hp: '生命', - manamax: '魔力上限', - mana: '额外攻击', - atk: '攻击', - def: '防御', - mdef: '智慧', - money: '金币', - exp: '经验', - point: '加点', - steps: '步数', - up: '升级', - magicDef: '魔法防御', - none: '无' - }[name] || name - ); -} - -export function formatSize(size: number) { - return size < 1 << 10 - ? `${size.toFixed(2)}B` - : size < 1 << 20 - ? `${(size / (1 << 10)).toFixed(2)}KB` - : size < 1 << 30 - ? `${(size / (1 << 20)).toFixed(2)}MB` - : `${(size / (1 << 30)).toFixed(2)}GB`; -} - -export function getIconHeight(icon: AllIds | 'hero') { - if (icon === 'hero') { - if (core.isPlaying()) { - return ( - core.material.images.images[core.status.hero.image].height / 4 - ); - } else { - return 48; - } - } - return core.getBlockInfo(icon)?.height ?? 32; -} - -const ascii = 16 ** 2; -const other = 16 ** 4; -const emoji = 16 ** 5; -export function calStringSize(str: string) { - let size = 0; - - for (const char of str) { - const num = char.charCodeAt(0); - if (num < ascii) size += 1; - else if (num < other) size += 2; - else if (num < emoji) size += 2.5; - } - - return size; -} - -export function clamp(num: number, start: number, end: number) { - const s = Math.min(start, end); - const e = Math.max(start, end); - if (num < s) return s; - else if (num > e) return e; - return num; -}