mirror of
https://github.com/motajs/template.git
synced 2026-10-20 05:40:18 +08:00
refactor: 删除 legacy 的大部分内容
This commit is contained in:
parent
aea30987f6
commit
f53f611db0
@ -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 失败」。</action>
|
||||
<verify>
|
||||
<automated>$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'</automated>
|
||||
<automated>$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'</automated>
|
||||
<fails_when>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"</fails_when>
|
||||
</verify>
|
||||
<done>旧文档树与首页已删除;`config.ts` 不再含旧 nav/sidebar 键与 apiSidebar/api 导入;`api.ts` 含缺失目录守卫;`tsx docs/.vitepress/init.ts` 不抛错且 `docs:build` 打印 build complete、无 dead link / import 错误;输出 W0-CLEAR-OK。</done>
|
||||
@ -110,7 +110,7 @@ Output: 已清空的 `docs/`(无旧内容)+ 已清理并注册好的 `docs/.
|
||||
<files>docs/.vitepress/config.ts, docs/intermediate/</files>
|
||||
<action>用**就地编辑(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/` 缺失时安全跳过)。</action>
|
||||
<verify>
|
||||
<automated>$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'</automated>
|
||||
<automated>$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'</automated>
|
||||
<fails_when>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"</fails_when>
|
||||
</verify>
|
||||
<done>`config.ts` 新增 nav 一条与 `'/intermediate/'` sidebar 键(去重后恰 24 条 link、4 个分组,顺序 4/8/9/3);`docs/intermediate/` 目录存在;`docs:build` 通过并打印 build complete;输出 W0-REGISTER-OK。</done>
|
||||
|
||||
@ -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/`;含一张「该不该改」表。
|
||||
</behavior>
|
||||
<action>按 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)。</action>
|
||||
<action>按 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)。</action>
|
||||
<verify>
|
||||
<automated>$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 "(?<!`r)`n"){ Write-Error "LF-ONLY $p" }; if(([regex]::Matches($raw,'(?m)^# ')).Count -ne 1){ Write-Error "H1-COUNT $p" } }; if(([IO.File]::ReadAllText((Resolve-Path 'docs/intermediate/data-vs-render.md')) -notmatch 'mermaid')){ Write-Error "NO-MERMAID data-vs-render" }; Write-Output 'STATIC-OK'</automated>
|
||||
<fails_when>non-zero exit, or output contains "MISSING", "LF-ONLY", "H1-COUNT", or "NO-MERMAID"</fails_when>
|
||||
@ -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 钩子;用一句话说明「钩子可能被非本对象触发」。
|
||||
</behavior>
|
||||
<action>按 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写页面名而不加链接。</action>
|
||||
<action>按 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写页面名而不加链接。</action>
|
||||
<verify>
|
||||
<automated>$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 "(?<!`r)`n"){ Write-Error "LF-ONLY $p" }; if(([regex]::Matches($raw,'(?m)^# ')).Count -ne 1){ Write-Error "H1-COUNT $p" } }; if(([IO.File]::ReadAllText((Resolve-Path 'docs/intermediate/code-basics.md')) -notmatch 'https://h5mota\.com/bbs/thread/\?tid=1018')){ Write-Error "NO-APPROVED-LINK code-basics" }; if(([IO.File]::ReadAllText((Resolve-Path 'docs/intermediate/hooks.md')) -notmatch 'addHook')){ Write-Error "NO-ADDHOOK hooks" }; Write-Output 'STATIC-OK'</automated>
|
||||
<fails_when>non-zero exit, or output contains "MISSING", "LF-ONLY", "H1-COUNT", "NO-APPROVED-LINK", or "NO-ADDHOOK"</fails_when>
|
||||
|
||||
@ -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)。
|
||||
|
||||
@ -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` 差别。
|
||||
</behavior>
|
||||
<action>按 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`。遵守写作约定。</action>
|
||||
<action>按 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`。遵守写作约定。</action>
|
||||
<verify>
|
||||
<automated>$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 "(?<!`r)`n"){ Write-Error "LF-ONLY $p" }; if(([regex]::Matches($raw,'(?m)^# ')).Count -ne 1){ Write-Error "H1-COUNT $p" } }; if(([IO.File]::ReadAllText((Resolve-Path 'docs/intermediate/combat-overview.md')) -notmatch 'mermaid')){ Write-Error "NO-MERMAID combat-overview" }; if(([IO.File]::ReadAllText((Resolve-Path 'docs/intermediate/enemy-aura.md')) -notmatch 'applySpecial')){ Write-Error "NO-APPLYSPECIAL enemy-aura" }; Write-Output 'STATIC-OK'</automated>
|
||||
<fails_when>non-zero exit, or output contains "MISSING", "LF-ONLY", "H1-COUNT", "NO-MERMAID", or "NO-APPLYSPECIAL"</fails_when>
|
||||
|
||||
@ -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 处/篇。
|
||||
|
||||
@ -68,10 +68,10 @@ Output: 验收摘要 `10-06-SUMMARY.md`;站点构建产物 `public/_docs`(gi
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: 全量构建 + 静态验收核对</name>
|
||||
<files>docs/.vitepress/config.ts</files>
|
||||
<files>docs/.vitepress/config.ts(只读核对;本计划不改动仓库文件,frontmatter files_modified 为空,产物仅 SUMMARY)</files>
|
||||
<action>先跑全量构建,再做静态核对。(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`,逐一报告供用户判断,不擅自删改)。发现任何缺页/错链只报告,不擅自补写。</action>
|
||||
<verify>
|
||||
<automated>$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 "(?<!`r)`n"){ Write-Error "LF-ONLY $p" }; if(([regex]::Matches($raw,'(?m)^# ')).Count -ne 1){ Write-Error "H1-COUNT $p" } }; if($missing.Count -gt 0){ Write-Error ("MISSING: " + ($missing -join ',')) }; foreach($o in @('docs/guide','docs/dev','docs/logger','docs/api','docs/index.md')){ if(Test-Path $o){ Write-Error "OLD-CONTENT-PRESENT $o" } }; $cfg=[IO.File]::ReadAllText((Resolve-Path 'docs/.vitepress/config.ts')); $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 ',')) }; $ext=@(); foreach($n in $names){ $p="docs/intermediate/$n.md"; if(-not(Test-Path $p)){continue}; $m=[regex]::Matches([IO.File]::ReadAllText((Resolve-Path $p)),'https?://[^\s\)]+'); foreach($x in $m){ if($x.Value -notmatch '^https://h5mota\.com/bbs/thread/\?tid=1018'){ $ext+=$x.Value } } }; if($ext.Count -gt 0){ Write-Error ("EXT-LINK-NOT-APPROVED: " + (($ext | Select-Object -Unique) -join ',')) }; $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 'ACCEPTANCE-OK'</automated>
|
||||
<automated>$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 "(?<!`r)`n"){ Write-Error "LF-ONLY $p" }; if(([regex]::Matches($raw,'(?m)^# ')).Count -ne 1){ Write-Error "H1-COUNT $p" } }; if($missing.Count -gt 0){ Write-Error ("MISSING: " + ($missing -join ',')) }; foreach($o in @('docs/guide','docs/dev','docs/logger','docs/api','docs/index.md')){ if(Test-Path $o){ Write-Error "OLD-CONTENT-PRESENT $o" } }; $cfg=[IO.File]::ReadAllText((Resolve-Path 'docs/.vitepress/config.ts')); $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 ',')) }; $ext=@(); foreach($n in $names){ $p="docs/intermediate/$n.md"; if(-not(Test-Path $p)){continue}; $m=[regex]::Matches([IO.File]::ReadAllText((Resolve-Path $p)),'https?://[^\s\)]+'); foreach($x in $m){ if($x.Value -notmatch '^https://h5mota\.com/bbs/thread/\?tid=1018'){ $ext+=$x.Value } } }; if($ext.Count -gt 0){ Write-Error ("EXT-LINK-NOT-APPROVED: " + (($ext | Select-Object -Unique) -join ',')) }; $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 'ACCEPTANCE-OK'</automated>
|
||||
<fails_when>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"</fails_when>
|
||||
</verify>
|
||||
<done>旧内容删除基线成立(guide/dev/logger/api 与首页均不存在);构建通过且产物生成、dead-link 为 0;24 个文件齐全、CRLF、单 H1;sidebar 去重后恰 24 个目标并与文件一一对应;无非白名单外链;输出 ACCEPTANCE-OK。</done>
|
||||
|
||||
@ -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 `高级教程 · <topic>` 或 `<!-- TODO(advanced): <topic> -->`),待高级教程存在后再替换为真链接。此条在「高级教程引用」范围内取代 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 `<strong>`。提交前用 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 阶段);且这次的读者人格**不能复用中级教程的「不懂代码的游戏作者」**——应面向**有一定代码基础、已读完中级教程、想做深度定制/理解扩展机制**的作者(具体人格在计划中明确,并经用户确认)。
|
||||
|
||||
</decisions>
|
||||
|
||||
@ -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 格式化 + 两阶段修订循环)*
|
||||
|
||||
@ -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` 不改。越界项只记录、不实施。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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 篇文档的源码入口」表)。
|
||||
|
||||
@ -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:无原始 `<script>` / 内联 HTML | static structure | CRLF / 单 H1 / mermaid 断言 | ❌ | ⬜ pending |
|
||||
| 10-02-T2 | 10-02 | 2 | D-23 / D-24 | T-10-05 | 唯一外链为 D-23 批准链接 | static structure | CRLF / 单 H1 / 外链 / addHook 断言 | ❌ | ⬜ pending |
|
||||
| 10-03-T1 | 10-03 | 3 | D-25 / D-26 / D-27 | T-10-07 | 3 篇 markdown:无原始 `<script>` | static structure | CRLF / 单 H1 / 修饰器 / 类别断言 | ❌ | ⬜ pending |
|
||||
| 10-03-T2 | 10-03 | 3 | D-28 / D-29 / D-30 | T-10-07 | 3 篇 markdown:无原始 `<script>` | static structure | CRLF / 单 H1 / autoUnload / eventLayer 断言 | ❌ | ⬜ pending |
|
||||
| 10-03-T3 | 10-03 | 3 | D-31 / D-32 | T-10-07 | 2 篇 markdown:无原始 `<script>` | static structure | CRLF / 单 H1 / iterateBlocks / getOrInsertComputed 断言 | ❌ | ⬜ pending |
|
||||
| 10-04-T1 | 10-04 | 4 | D-33 / D-34 / D-35 | T-10-10 | 3 篇 markdown:无原始 `<script>` | static structure | CRLF / 单 H1 / mermaid / applySpecial 断言 | ❌ | ⬜ pending |
|
||||
| 10-04-T2 | 10-04 | 4 | D-36 / D-37 / D-38 | T-10-10 | 3 篇 markdown:无原始 `<script>` | static structure | CRLF / 单 H1 / register / getCriticalLimit 断言 | ❌ | ⬜ pending |
|
||||
| 10-04-T3 | 10-04 | 4 | D-39 / D-40 / D-41 | T-10-10 | 3 篇 markdown:无原始 `<script>` | static structure | CRLF / 单 H1 / useReducer / onAfterTeleportTo 断言 | ❌ | ⬜ pending |
|
||||
| 10-05-T1 | 10-05 | 5 | D-42 / D-43 / D-44 | T-10-13 | 3 篇 markdown:无原始 `<script>` | static structure | CRLF / 单 H1 / createEventRegistrations / @shouldReplay / afterLoad 断言 | ❌ | ⬜ pending |
|
||||
| 10-06-T1 | 10-06 | 6 | D-09 | T-10-16 / T-10-17 | 全批验收:仅白名单外链;无原始 `<script>` | smoke(构建)+ 静态 | 全量构建 + 24 文件 / sidebar / 外链断言 | ❌ | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
*Rows are seeded from RESEARCH.md; the planner must replace the generic rows with the concrete task IDs it defines.*
|
||||
*Rows map to the six plans' concrete task IDs(`10-01`..`10-06`)。每计划的 Task 0 检查点,以及 `10-06` Task 2,为人工门禁,见下方 Manual-Only Verifications。*
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] Run `tsx docs/.vitepress/init.ts` — regenerate gitignored `docs/.vitepress/apiSidebar.ts` (build prerequisite)
|
||||
- [ ] Create directory `docs/intermediate/`
|
||||
- [ ] Edit `docs/.vitepress/config.ts` — add `nav` entry + `sidebar['/intermediate/']` for the 24 pages
|
||||
> `10-01`(Wave 0)已扩展为「旧内容清空 + 站点注册」,下列为其实际范围(与 `10-01-PLAN.md` 一致):
|
||||
|
||||
- [ ] 删除旧内容 — `docs/index.md` + `docs/guide/`、`docs/dev/`、`docs/logger/`、`docs/api/`(D-04 已并入本批 Wave 0 / 10-01)
|
||||
- [ ] 清理 `docs/.vitepress/config.ts` 的旧 `nav`/`sidebar` 键 — 移除指向已删内容的旧 `nav` 入口(主页 / 指南 / API / 错误代码)与旧 sidebar 键(guide / logger / api 三个路径前缀)
|
||||
- [ ] 解除 `config.ts` 对 `./apiSidebar` 的静态依赖,并给 `docs/.vitepress/api.ts` 的 `generateSidebar()` 加缺失目录守卫(`node:fs` 的 `existsSync`)— 使 `tsx docs/.vitepress/init.ts` 与 `docs:dev` 在 `docs/api/` 缺失时安全跳过、不再抛 ENOENT
|
||||
- [ ] 注册 `nav` 入口与 `sidebar['/intermediate/']`(4 组 / 24 条,顺序 4/8/9/3)并创建 `docs/intermediate/` 目录 — 供后续写作计划落点
|
||||
- [ ] 完成后运行 `tsx docs/.vitepress/init.ts && pnpm docs:build` — 必须打印 `build complete`、**dead link = 0、无 import 失败**;否则视为 Wave 0 未通过
|
||||
|
||||
*No unit-test framework install needed — this batch has no test requirements.*
|
||||
|
||||
|
||||
20
packages-user/client-base/src/utils.ts
Normal file
20
packages-user/client-base/src/utils.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 切换全屏状态
|
||||
* @param full 是否全屏
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,2 @@
|
||||
export * from './patch';
|
||||
export * from './eventEmitter';
|
||||
export * from './utils';
|
||||
|
||||
@ -1,131 +0,0 @@
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
export const enum PatchClass {
|
||||
Actions,
|
||||
Control,
|
||||
Core,
|
||||
Data,
|
||||
Enemys,
|
||||
Events,
|
||||
Icons,
|
||||
Items,
|
||||
Loader,
|
||||
Maps,
|
||||
UI,
|
||||
Utils
|
||||
}
|
||||
|
||||
interface PatchList {
|
||||
[PatchClass.Actions]: Actions;
|
||||
[PatchClass.Control]: Control;
|
||||
[PatchClass.Core]: Core;
|
||||
[PatchClass.Data]: Omit<DataCore, 'main'>;
|
||||
[PatchClass.Enemys]: Enemys;
|
||||
[PatchClass.Events]: Events;
|
||||
[PatchClass.Icons]: Icons;
|
||||
[PatchClass.Items]: Items;
|
||||
[PatchClass.Loader]: Loader;
|
||||
[PatchClass.Maps]: Maps;
|
||||
[PatchClass.UI]: Ui;
|
||||
[PatchClass.Utils]: Utils;
|
||||
}
|
||||
|
||||
const patchName = {
|
||||
[PatchClass.Actions]: 'actions',
|
||||
[PatchClass.Control]: 'control',
|
||||
[PatchClass.Core]: 'core',
|
||||
[PatchClass.Data]: 'data',
|
||||
[PatchClass.Enemys]: 'enemys',
|
||||
[PatchClass.Events]: 'events',
|
||||
[PatchClass.Icons]: 'icons',
|
||||
[PatchClass.Items]: 'items',
|
||||
[PatchClass.Loader]: 'loader',
|
||||
[PatchClass.Maps]: 'maps',
|
||||
[PatchClass.UI]: 'ui',
|
||||
[PatchClass.Utils]: 'utils'
|
||||
};
|
||||
|
||||
export class Patch<T extends PatchClass> {
|
||||
private static patchList: Set<Patch<PatchClass>> = new Set();
|
||||
private static patched: Partial<Record<PatchClass, Set<string>>> = {};
|
||||
|
||||
private patches: Map<string, (...params: any[]) => any> = new Map();
|
||||
|
||||
constructor(public readonly patchClass: T) {
|
||||
Patch.patchList.add(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加函数修改
|
||||
* @param key 要修改的函数名
|
||||
* @param patch 修改为的函数内容
|
||||
*/
|
||||
add<
|
||||
K extends Exclude<
|
||||
SelectKey<PatchList[T], (...params: any[]) => any>,
|
||||
symbol | number
|
||||
>
|
||||
>(key: K, patch: PatchList[T][K]) {
|
||||
if (this.patches.has(key)) {
|
||||
logger.warn(49, patchName[this.patchClass], key);
|
||||
}
|
||||
this.patches.set(key, patch);
|
||||
}
|
||||
|
||||
private static getPatchClass(patch: PatchClass): any {
|
||||
switch (patch) {
|
||||
case PatchClass.Actions:
|
||||
return actions.prototype;
|
||||
case PatchClass.Control:
|
||||
return control.prototype;
|
||||
case PatchClass.Core:
|
||||
return core;
|
||||
case PatchClass.Data:
|
||||
return data.prototype;
|
||||
case PatchClass.Enemys:
|
||||
return enemys.prototype;
|
||||
case PatchClass.Events:
|
||||
return events.prototype;
|
||||
case PatchClass.Icons:
|
||||
return icons.prototype;
|
||||
case PatchClass.Items:
|
||||
return items.prototype;
|
||||
case PatchClass.Loader:
|
||||
return loader.prototype;
|
||||
case PatchClass.Maps:
|
||||
return maps.prototype;
|
||||
case PatchClass.UI:
|
||||
return ui.prototype;
|
||||
case PatchClass.Utils:
|
||||
return utils.prototype;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改添加的所有函数
|
||||
*/
|
||||
static patchAll() {
|
||||
this.patchList.forEach(v => this.patch(v));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改某个实例添加的所有函数
|
||||
* @param patch 要修改的函数实例
|
||||
*/
|
||||
static patch(patch: Patch<PatchClass>) {
|
||||
const patchClass = patch.patchClass;
|
||||
this.patched[patchClass] ??= new Set();
|
||||
const set = this.patched[patchClass];
|
||||
const obj = this.getPatchClass(patchClass);
|
||||
for (const [key, func] of patch.patches) {
|
||||
// console.log(key);
|
||||
|
||||
if (set.has(key)) {
|
||||
logger.warn(49, patchName[patchClass], key);
|
||||
}
|
||||
set.add(key);
|
||||
obj[key] = func;
|
||||
}
|
||||
this.patchList.delete(patch);
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
const animation: ((time: number) => void)[] = [];
|
||||
|
||||
let animateTime = 0;
|
||||
|
||||
export default function init() {
|
||||
core.registerAnimationFrame('animateController', true, time => {
|
||||
if (time - animateTime <= core.values.animateSpeed) return;
|
||||
for (const fn of animation) {
|
||||
fn(time);
|
||||
}
|
||||
animateTime = core.animateFrame.animateTime;
|
||||
});
|
||||
return { addAnimate, removeAnimate };
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个动画
|
||||
* @param fn 要添加的函数
|
||||
*/
|
||||
export function addAnimate(fn: (time: number) => void) {
|
||||
animation.push(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除一个动画
|
||||
* @param fn 要移除的函数
|
||||
*/
|
||||
export function removeAnimate(fn: (time: number) => void) {
|
||||
const index = animation.findIndex(v => v === fn);
|
||||
if (index === -1) return;
|
||||
animation.splice(index, 1);
|
||||
}
|
||||
@ -1,418 +0,0 @@
|
||||
<template>
|
||||
<div :id="`box-${id}`" class="box">
|
||||
<div :id="`box-main-${id}`" class="box-main" @click="click">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div
|
||||
v-if="dragable"
|
||||
:id="`box-move-${id}`"
|
||||
class="box-move"
|
||||
:selected="moveSelected"
|
||||
>
|
||||
<drag-outlined :id="`box-drag-${id}`" class="box-drag" />
|
||||
</div>
|
||||
<div
|
||||
v-if="resizable"
|
||||
:id="`box-size-${id}`"
|
||||
class="box-size"
|
||||
:selected="moveSelected"
|
||||
>
|
||||
<ArrowsAltOutlined :id="`box-resize-${id}`" class="box-resize" />
|
||||
</div>
|
||||
<div
|
||||
class="border border-vertical border-left"
|
||||
:id="`border-left-${id}`"
|
||||
:selected="moveSelected && resizable"
|
||||
:selectable="resizable"
|
||||
></div>
|
||||
<div
|
||||
class="border border-horizontal border-top"
|
||||
:id="`border-top-${id}`"
|
||||
:selected="moveSelected && resizable"
|
||||
:selectable="resizable"
|
||||
></div>
|
||||
<div
|
||||
class="border border-vertical border-right"
|
||||
:id="`border-right-${id}`"
|
||||
:selected="moveSelected && resizable"
|
||||
:selectable="resizable"
|
||||
></div>
|
||||
<div
|
||||
class="border border-horizontal border-bottom"
|
||||
:id="`border-bottom-${id}`"
|
||||
:selected="moveSelected && resizable"
|
||||
:selectable="resizable"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, onUpdated, ref, watch } from 'vue';
|
||||
import { ArrowsAltOutlined, DragOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
isMobile,
|
||||
useDrag,
|
||||
cancelGlobalDrag,
|
||||
requireUniqueSymbol
|
||||
} from '../use';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
// todo: 重写
|
||||
|
||||
const props = defineProps<{
|
||||
dragable?: boolean;
|
||||
resizable?: boolean;
|
||||
left?: number;
|
||||
top?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:left', data: number): void;
|
||||
(e: 'update:top', data: number): void;
|
||||
(e: 'update:width', data: number): void;
|
||||
(e: 'update:height', data: number): void;
|
||||
}>();
|
||||
|
||||
const id = requireUniqueSymbol().toFixed(0);
|
||||
|
||||
const moveSelected = ref(false);
|
||||
let moveTimeout = 0;
|
||||
let main: HTMLDivElement;
|
||||
let leftB: HTMLDivElement;
|
||||
let rightB: HTMLDivElement;
|
||||
let topB: HTMLDivElement;
|
||||
let bottomB: HTMLDivElement;
|
||||
let drag: HTMLElement;
|
||||
let size: HTMLElement;
|
||||
|
||||
const width = ref(
|
||||
isMobile ? window.innerWidth - 100 : window.innerHeight * 0.175
|
||||
);
|
||||
const height = ref(isMobile ? 250 : window.innerHeight - 100);
|
||||
const left = ref(isMobile ? 30 : 50);
|
||||
const top = ref(isMobile ? 30 : 50);
|
||||
|
||||
watch(left, n => emits('update:left', n));
|
||||
watch(top, n => emits('update:top', n));
|
||||
watch(width, n => emits('update:width', n));
|
||||
watch(height, n => emits('update:height', n));
|
||||
|
||||
async function click() {
|
||||
moveSelected.value = true;
|
||||
moveTimeout = window.setTimeout(() => {
|
||||
moveSelected.value = false;
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
|
||||
function clampX(x: number) {
|
||||
if (x < 16) x = 16;
|
||||
const mx = window.innerWidth - 16 - width.value;
|
||||
if (x > mx) x = mx;
|
||||
return x;
|
||||
}
|
||||
|
||||
function clampY(y: number) {
|
||||
if (y < 16) y = 16;
|
||||
const my = window.innerHeight - 16 - height.value;
|
||||
if (y > my) y = my;
|
||||
return y;
|
||||
}
|
||||
|
||||
function clampPos(x: number, y: number) {
|
||||
return { x: clampX(x), y: clampY(y) };
|
||||
}
|
||||
|
||||
function clampWidth(w: number) {
|
||||
if (w < 16) w = 16;
|
||||
const mw = window.innerWidth - 16 - left.value;
|
||||
if (w > mw) w = mw;
|
||||
return w;
|
||||
}
|
||||
|
||||
function clampHeight(h: number) {
|
||||
if (h < 16) h = 16;
|
||||
const mh = window.innerHeight - 16 - top.value;
|
||||
if (h > mh) h = mh;
|
||||
return h;
|
||||
}
|
||||
|
||||
function clampSize(w: number, h: number) {
|
||||
return { w: clampWidth(w), h: clampHeight(h) };
|
||||
}
|
||||
|
||||
function dragFn(x: number, y: number) {
|
||||
const { x: tx, y: ty } = clampPos(x + 8, y + 8);
|
||||
left.value = tx;
|
||||
top.value = ty;
|
||||
main.style.left = `${tx}px`;
|
||||
main.style.top = `${ty}px`;
|
||||
clearTimeout(moveTimeout);
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
}
|
||||
|
||||
let right = left.value + width.value;
|
||||
function leftDrag(x: number, y: number) {
|
||||
const tx = clampX(x);
|
||||
main.style.left = `${tx}px`;
|
||||
width.value = right - tx;
|
||||
left.value = tx;
|
||||
main.style.width = `${width.value}px`;
|
||||
}
|
||||
|
||||
let bottom = top.value + height.value;
|
||||
function topDrag(x: number, y: number) {
|
||||
const ty = clampY(y);
|
||||
main.style.top = `${ty}px`;
|
||||
height.value = bottom - ty;
|
||||
top.value = ty;
|
||||
main.style.height = `${height.value}px`;
|
||||
}
|
||||
|
||||
function rightDrag(x: number, y: number) {
|
||||
const w = clampWidth(x - main.offsetLeft);
|
||||
width.value = w;
|
||||
main.style.width = `${w}px`;
|
||||
}
|
||||
|
||||
function bottomDrag(x: number, y: number) {
|
||||
const h = clampHeight(y - main.offsetTop);
|
||||
height.value = h;
|
||||
main.style.height = `${h}px`;
|
||||
}
|
||||
|
||||
function resizeBox(x: number, y: number) {
|
||||
const { w, h } = clampSize(x - main.offsetLeft - 8, y - main.offsetTop - 8);
|
||||
width.value = w;
|
||||
height.value = h;
|
||||
main.style.width = `${w}px`;
|
||||
main.style.height = `${h}px`;
|
||||
}
|
||||
|
||||
function resize() {
|
||||
main = document.getElementById(`box-${id}`) as HTMLDivElement;
|
||||
leftB = document.getElementById(`border-left-${id}`) as HTMLDivElement;
|
||||
topB = document.getElementById(`border-top-${id}`) as HTMLDivElement;
|
||||
rightB = document.getElementById(`border-right-${id}`) as HTMLDivElement;
|
||||
bottomB = document.getElementById(`border-bottom-${id}`) as HTMLDivElement;
|
||||
drag = document.getElementById(`box-drag-${id}`) as HTMLElement;
|
||||
size = document.getElementById(`box-resize-${id}`) as HTMLElement;
|
||||
|
||||
if (!main) return;
|
||||
|
||||
if (!isNil(props.width)) width.value = props.width;
|
||||
if (!isNil(props.height)) height.value = props.height;
|
||||
if (!isNil(props.left)) left.value = props.left;
|
||||
if (!isNil(props.top)) top.value = props.top;
|
||||
|
||||
const beforeWidth = width.value;
|
||||
const beforeHeight = height.value;
|
||||
width.value = 16;
|
||||
height.value = 16;
|
||||
|
||||
left.value = clampX(left.value);
|
||||
top.value = clampY(top.value);
|
||||
main.style.left = `${left.value}px`;
|
||||
main.style.top = `${top.value}px`;
|
||||
|
||||
width.value = clampWidth(beforeWidth);
|
||||
height.value = clampHeight(beforeHeight);
|
||||
main.style.width = `${width.value}px`;
|
||||
main.style.height = `${height.value}px`;
|
||||
}
|
||||
|
||||
onUpdated(resize);
|
||||
|
||||
onMounted(async () => {
|
||||
resize();
|
||||
|
||||
if (!main) return;
|
||||
|
||||
if (props.dragable) {
|
||||
useDrag(
|
||||
drag,
|
||||
dragFn,
|
||||
(x, y) => {
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
},
|
||||
() => {
|
||||
moveSelected.value = false;
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if (props.resizable) {
|
||||
useDrag(
|
||||
leftB,
|
||||
leftDrag,
|
||||
(x, y) => {
|
||||
right = left.value + width.value;
|
||||
},
|
||||
void 0,
|
||||
true
|
||||
);
|
||||
|
||||
useDrag(
|
||||
topB,
|
||||
topDrag,
|
||||
(x, y) => {
|
||||
bottom = top.value + height.value;
|
||||
},
|
||||
void 0,
|
||||
true
|
||||
);
|
||||
|
||||
useDrag(rightB, rightDrag, void 0, void 0, true);
|
||||
useDrag(bottomB, bottomDrag, void 0, void 0, true);
|
||||
useDrag(size, resizeBox, void 0, void 0, true);
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resize);
|
||||
if (props.dragable) cancelGlobalDrag(dragFn);
|
||||
if (props.resizable) {
|
||||
cancelGlobalDrag(leftDrag);
|
||||
cancelGlobalDrag(topDrag);
|
||||
cancelGlobalDrag(rightDrag);
|
||||
cancelGlobalDrag(bottomDrag);
|
||||
cancelGlobalDrag(resizeBox);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.box {
|
||||
width: 300px;
|
||||
height: calc(100vh - 100px);
|
||||
position: fixed;
|
||||
left: 50px;
|
||||
top: 50px;
|
||||
display: flex;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.box-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.box-move {
|
||||
transition: font-size 0.3s ease-out;
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
top: -32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.box-size {
|
||||
transition: font-size 0.3s ease-out;
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.box-drag {
|
||||
cursor: all-scroll;
|
||||
user-select: none;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.box-resize {
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
transform: rotateX(180deg);
|
||||
user-select: none;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.box-move[selected='false'],
|
||||
.box-size[selected='false'] {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.box-move[selected='true'],
|
||||
.box-size[selected='true'] {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.border {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
.border-horizontal {
|
||||
width: 100%;
|
||||
height: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.border-horizontal[selected='true'][selectable='true'] {
|
||||
transform: scaleY(300%);
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.border-horizontal:hover[selectable='true'],
|
||||
.border-horizontal:active[selectable='true'] {
|
||||
transform: scaleY(500%);
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.border-vertical {
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.border-vertical[selected='true'][selectable='true'] {
|
||||
transform: scaleX(300%);
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.border-vertical:hover[selectable='true'],
|
||||
.border-vertical:active[selectable='true'] {
|
||||
transform: scaleX(500%);
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.border-left {
|
||||
left: 0;
|
||||
border-left: 2px solid #ddd9;
|
||||
}
|
||||
|
||||
.border-right {
|
||||
right: 0;
|
||||
border-right: 2px solid #ddd9;
|
||||
}
|
||||
|
||||
.border-top {
|
||||
top: 0;
|
||||
border-top: 2px solid #ddd9;
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
bottom: 0;
|
||||
border-bottom: 2px solid #ddd9;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.box {
|
||||
width: calc(100vw - 100px);
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,91 +0,0 @@
|
||||
<template>
|
||||
<canvas
|
||||
:width="width ?? 32"
|
||||
:height="height ?? 32"
|
||||
:id="`box-animate-${id}`"
|
||||
></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="tsx" setup>
|
||||
import { onMounted, onUnmounted, onUpdated } from 'vue';
|
||||
import { addAnimate, removeAnimate } from '../animateController';
|
||||
import { requireUniqueSymbol } from '../use';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
const id = requireUniqueSymbol().toFixed(0);
|
||||
|
||||
const props = defineProps<{
|
||||
id: AllIds | 'hero' | 'none';
|
||||
noborder?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
noAnimate?: boolean;
|
||||
}>();
|
||||
|
||||
let c: HTMLCanvasElement;
|
||||
let ctx: CanvasRenderingContext2D;
|
||||
|
||||
let drawFn: () => void;
|
||||
|
||||
function draw() {
|
||||
if (!isNil(drawFn)) removeAnimate(drawFn);
|
||||
|
||||
const cls = core.getClsFromId(props.id as AllIds);
|
||||
const frames = core.getAnimateFrames(cls);
|
||||
const w = props.width ?? 32;
|
||||
const h = props.height ?? 32;
|
||||
|
||||
if (!props.noborder) {
|
||||
c.style.border = '1.5px solid #ddd';
|
||||
c.style.backgroundColor = '#222';
|
||||
}
|
||||
|
||||
const scale = window.devicePixelRatio;
|
||||
|
||||
c.style.width = `${w}px`;
|
||||
c.style.height = `${h}px`;
|
||||
c.width = scale * w;
|
||||
c.height = scale * h;
|
||||
ctx.scale(scale, scale);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
|
||||
if (props.id === 'none') return;
|
||||
|
||||
if (props.id === 'hero') {
|
||||
const img = core.material.images.hero;
|
||||
ctx.drawImage(img, 0, 0, img.width / 4, img.height / 4, 0, 0, w, h);
|
||||
} else if (frames === 1) {
|
||||
core.drawIcon(ctx, props.id, 0, 0, props.width, props.height);
|
||||
} else {
|
||||
drawFn = () => {
|
||||
core.clearMap(ctx);
|
||||
const frame = props.noAnimate
|
||||
? 0
|
||||
: core.status.globalAnimateStatus % frames;
|
||||
core.drawIcon(ctx, props.id as AllIds, 0, 0, w, h, frame);
|
||||
};
|
||||
|
||||
drawFn();
|
||||
if (!props.noAnimate) {
|
||||
addAnimate(drawFn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
removeAnimate(drawFn);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
c = document.getElementById(`box-animate-${id}`) as HTMLCanvasElement;
|
||||
ctx = c.getContext('2d')!;
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
draw();
|
||||
});
|
||||
|
||||
onUpdated(() => {
|
||||
draw();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@ -1,145 +0,0 @@
|
||||
<template>
|
||||
<div class="colomn">
|
||||
<div class="tools">
|
||||
<span class="button-text" @click="emits('close')"
|
||||
><left-outlined /> 返回</span
|
||||
>
|
||||
</div>
|
||||
<div class="column-main" :id="`column-${id}`">
|
||||
<Scroll class="column-left" :id="`column-left-${id}`">
|
||||
<slot name="left"></slot>
|
||||
</Scroll>
|
||||
<Divider
|
||||
class="divider"
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
||||
></Divider>
|
||||
<Scroll class="column-right" :id="`column-right-${id}`"
|
||||
><slot name="right"></slot
|
||||
></Scroll>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUpdated } from 'vue';
|
||||
import { LeftOutlined } from '@ant-design/icons-vue';
|
||||
import { Divider } from 'ant-design-vue';
|
||||
import Scroll from './scroll.vue';
|
||||
import { isMobile, requireUniqueSymbol } from '../use';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
width?: number;
|
||||
height?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
}>();
|
||||
|
||||
const id = requireUniqueSymbol().toFixed(0);
|
||||
|
||||
let main: HTMLDivElement;
|
||||
let left: HTMLDivElement;
|
||||
let right: HTMLDivElement;
|
||||
|
||||
function resize() {
|
||||
main = document.getElementById(`column-${id}`) as HTMLDivElement;
|
||||
left = document.getElementById(`column-left-${id}`) as HTMLDivElement;
|
||||
right = document.getElementById(`column-right-${id}`) as HTMLDivElement;
|
||||
|
||||
if (!isNil(props.width) && !isMobile) main.style.width = `${props.width}%`;
|
||||
if (!isNil(props.height)) main.style.height = `${props.height}%`;
|
||||
if (!isNil(props.left)) left.style.flexBasis = `${props.left}%`;
|
||||
if (!isNil(props.right)) right.style.flexBasis = `${props.right}%`;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
resize();
|
||||
});
|
||||
onUpdated(resize);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.colomn {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'normal';
|
||||
font-size: 150%;
|
||||
user-select: none;
|
||||
}
|
||||
.column-main {
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.column-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.column-item {
|
||||
width: 100%;
|
||||
padding: 1% 3% 1% 3%;
|
||||
}
|
||||
|
||||
.column-item[selectable='false'] {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.column-left {
|
||||
flex-basis: 40%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.column-right {
|
||||
flex-basis: 60%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tools {
|
||||
width: 100%;
|
||||
font-family: 'normal';
|
||||
font-size: 3.2vh;
|
||||
height: 5vh;
|
||||
position: fixed;
|
||||
left: 10vw;
|
||||
top: 5vh;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.column-main {
|
||||
flex-direction: column;
|
||||
width: 90%;
|
||||
height: 75%;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin: 5% 0 5% 0;
|
||||
}
|
||||
|
||||
.column-left {
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
.column-right {
|
||||
height: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,232 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="enemy-container"
|
||||
@click="select"
|
||||
@mousemove="enter"
|
||||
:selected="selected"
|
||||
>
|
||||
<div class="info">
|
||||
<div class="leftbar">
|
||||
<span class="name">{{ enemy.enemy.enemy.name }}</span>
|
||||
<BoxAnimate
|
||||
:id="enemy.enemy.id"
|
||||
:width="isMobile ? 32 : w"
|
||||
:height="isMobile ? 32 : w"
|
||||
style="margin: 5%"
|
||||
></BoxAnimate>
|
||||
<div
|
||||
class="special-text"
|
||||
v-if="!isNil(enemy.special) && enemy.special.length > 0"
|
||||
>
|
||||
<template v-for="(text, i) in enemy.showSpecial">
|
||||
<span v-if="i < (isMobile ? 1 : 2)"
|
||||
> {{ text[0] }} </span
|
||||
>
|
||||
<span v-if="i === (isMobile ? 1 : 2)">...</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="special-text" v-else>无属性</div>
|
||||
</div>
|
||||
<Divider
|
||||
type="vertical"
|
||||
dashed
|
||||
style="height: 100%; margin: 0 3% 0 1%; border-color: #ddd4"
|
||||
></Divider>
|
||||
<div class="rightbar">
|
||||
<div class="detail">
|
||||
<div class="detail-info">
|
||||
<span style="color: lightgreen"
|
||||
>生命 {{
|
||||
core.formatBigNumber(enemy.enemy.info.hp)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span style="color: lightcoral"
|
||||
>攻击 {{
|
||||
core.formatBigNumber(enemy.enemy.info.atk)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span style="color: lightblue"
|
||||
>防御 {{
|
||||
core.formatBigNumber(enemy.enemy.info.def)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span style="color: lightyellow"
|
||||
>金币 {{
|
||||
core.formatBigNumber(enemy.enemy.enemy.money)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span style="color: lawngreen"
|
||||
>经验 {{
|
||||
core.formatBigNumber(enemy.enemy.enemy.exp)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span :style="{ color: enemy.damageColor }"
|
||||
>伤害 {{
|
||||
enemy.damage
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span style="color: lightsalmon"
|
||||
>临界 {{
|
||||
enemy.critical
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span style="color: lightpink"
|
||||
>减伤 <span
|
||||
:style="{
|
||||
color: 'lightpink'
|
||||
}"
|
||||
>{{ enemy.criticalDam }}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span style="color: cyan"
|
||||
>{{
|
||||
core.formatBigNumber(core.status.thisMap.ratio)
|
||||
}}防 {{
|
||||
core.formatBigNumber(enemy.defDam)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Divider } from 'ant-design-vue';
|
||||
import BoxAnimate from '../components/boxAnimate.vue';
|
||||
import { isMobile } from '../use';
|
||||
import { ToShowEnemy } from '../tools/book';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
const props = defineProps<{
|
||||
enemy: ToShowEnemy;
|
||||
selected?: boolean;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'select'): void;
|
||||
(e: 'hover'): void;
|
||||
}>();
|
||||
|
||||
const core = window.core;
|
||||
|
||||
const w = window.innerWidth * 0.032;
|
||||
|
||||
/**
|
||||
* 选择这个怪物时
|
||||
*/
|
||||
function select(e: MouseEvent) {
|
||||
emits('select');
|
||||
}
|
||||
|
||||
function enter() {
|
||||
emits('hover');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.enemy-container {
|
||||
border: 1.5px solid transparent;
|
||||
border-radius: 15px;
|
||||
transition: all 0.2s linear;
|
||||
height: 100%;
|
||||
|
||||
.info {
|
||||
flex-basis: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.enemy-container[selected='true'] {
|
||||
border: 1.5px solid gold;
|
||||
}
|
||||
|
||||
.leftbar {
|
||||
width: 15%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 120%;
|
||||
padding-left: 1%;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.special-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-items: space-between;
|
||||
}
|
||||
|
||||
.rightbar {
|
||||
font-size: 140%;
|
||||
width: 85%;
|
||||
height: 100%;
|
||||
padding: 1.5vh 0 1.5vh 0;
|
||||
|
||||
.detail {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
|
||||
.detail-info {
|
||||
flex-basis: 33.3%;
|
||||
line-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.rightbar {
|
||||
width: 80%;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.leftbar {
|
||||
width: 20%;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.enemy-container {
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -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';
|
||||
@ -1,267 +0,0 @@
|
||||
<template>
|
||||
<div :id="`scroll-div-${id}`" class="scroll-main">
|
||||
<div class="main-div">
|
||||
<div :id="`content-${id}`" class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<canvas :id="`scroll-${id}`" class="scroll"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { onMounted, onUnmounted, onUpdated } from 'vue';
|
||||
import {
|
||||
cancelGlobalDrag,
|
||||
useDrag,
|
||||
useWheel,
|
||||
requireUniqueSymbol
|
||||
} from '../use';
|
||||
|
||||
let main: HTMLDivElement;
|
||||
|
||||
const props = defineProps<{
|
||||
now?: number;
|
||||
type?: 'vertical' | 'horizontal';
|
||||
drag?: boolean;
|
||||
width?: number;
|
||||
update?: boolean;
|
||||
noScroll?: boolean;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:now', value: number): void;
|
||||
(e: 'update:drag', value: boolean): void;
|
||||
(e: 'update:update'): void;
|
||||
}>();
|
||||
|
||||
let now = 0;
|
||||
let total = 0;
|
||||
|
||||
const id = requireUniqueSymbol().toFixed(0);
|
||||
const scale = window.devicePixelRatio;
|
||||
const width = props.width ?? 20;
|
||||
|
||||
const cssTarget = props.type === 'horizontal' ? 'left' : 'top';
|
||||
const canvasAttr = props.type === 'horizontal' ? 'width' : 'height';
|
||||
|
||||
let ctx: CanvasRenderingContext2D;
|
||||
let content: HTMLDivElement;
|
||||
let fromSelf = false;
|
||||
|
||||
const resize = async () => {
|
||||
await calHeight(false);
|
||||
draw();
|
||||
};
|
||||
|
||||
/** 绘制 */
|
||||
function draw() {
|
||||
if (total === 0) return;
|
||||
if (total < ctx.canvas[canvasAttr] / scale) {
|
||||
now = 0;
|
||||
} else if (now > total - ctx.canvas[canvasAttr] / scale) {
|
||||
now = total - ctx.canvas[canvasAttr] / scale;
|
||||
} else if (now < 0) {
|
||||
now = 0;
|
||||
}
|
||||
if (props.noScroll) return;
|
||||
const w = ctx.canvas.width;
|
||||
const h = ctx.canvas.height;
|
||||
emits('update:now', now);
|
||||
const length =
|
||||
Math.min(ctx.canvas[canvasAttr] / total / scale, 1) *
|
||||
ctx.canvas[canvasAttr];
|
||||
const py = (now / total) * ctx.canvas[canvasAttr];
|
||||
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
ctx.beginPath();
|
||||
if (props.type === 'horizontal') {
|
||||
ctx.moveTo(Math.max(py + 5, 5), h / 2);
|
||||
ctx.lineTo(Math.min(py + length - 5, ctx.canvas.width - 5), h / 2);
|
||||
} else {
|
||||
ctx.moveTo(w / 2, Math.max(py + 5, 5));
|
||||
ctx.lineTo(w / 2, Math.min(py + length - 5, ctx.canvas.height - 5));
|
||||
}
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineWidth = 6;
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算元素总长度
|
||||
*/
|
||||
async function calHeight(first: boolean = false) {
|
||||
if (!first) {
|
||||
await sleep(20);
|
||||
}
|
||||
|
||||
const canvas = ctx.canvas;
|
||||
const style2 = getComputedStyle(canvas);
|
||||
if (!props.type || props.type === 'vertical') {
|
||||
if (props.noScroll) {
|
||||
canvas.style.width = `0px`;
|
||||
} else {
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.width = width * scale;
|
||||
canvas.height = parseFloat(style2.height) * scale;
|
||||
}
|
||||
} else {
|
||||
main.style.flexDirection = 'column';
|
||||
if (props.noScroll) {
|
||||
canvas.style.height = `0px`;
|
||||
} else {
|
||||
canvas.style.height = `${width}px`;
|
||||
canvas.style.width = '98%';
|
||||
canvas.style.margin = '0 1% 0 1%';
|
||||
canvas.width = parseFloat(style2.width) * scale;
|
||||
canvas.height = width * scale;
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise<void>(res => {
|
||||
requestAnimationFrame(() => {
|
||||
total =
|
||||
props.type === 'horizontal'
|
||||
? content.scrollWidth
|
||||
: content.scrollHeight;
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function scroll() {
|
||||
draw();
|
||||
content.style[cssTarget] = `${-now}px`;
|
||||
}
|
||||
|
||||
onUpdated(async () => {
|
||||
if (fromSelf) return;
|
||||
now = props.now ?? now;
|
||||
content.style.transition = `${cssTarget} 0.2s ease-out`;
|
||||
await calHeight(false);
|
||||
scroll();
|
||||
});
|
||||
|
||||
let last: number;
|
||||
let contentLast: number;
|
||||
|
||||
function canvasDrag(x: number, y: number) {
|
||||
emits('update:drag', true);
|
||||
const d = props.type === 'horizontal' ? x : y;
|
||||
const dy = d - last;
|
||||
last = d;
|
||||
if (ctx.canvas[canvasAttr] < total * scale)
|
||||
now += ((dy * total) / ctx.canvas[canvasAttr]) * scale;
|
||||
scroll();
|
||||
}
|
||||
|
||||
function contentDrag(x: number, y: number) {
|
||||
emits('update:drag', true);
|
||||
const d = props.type === 'horizontal' ? x : y;
|
||||
const dy = d - contentLast;
|
||||
contentLast = d;
|
||||
if (ctx.canvas[canvasAttr] < total * scale) now -= dy;
|
||||
scroll();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
main = document.getElementById(`scroll-div-${id}`) as HTMLDivElement;
|
||||
const d = document.getElementById(`content-${id}`) as HTMLDivElement;
|
||||
content = d;
|
||||
const canvas = document.getElementById(`scroll-${id}`) as HTMLCanvasElement;
|
||||
ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
|
||||
if (!props.noScroll) {
|
||||
// 绑定滚动条拖拽事件
|
||||
useDrag(
|
||||
canvas,
|
||||
canvasDrag,
|
||||
(x, y) => {
|
||||
fromSelf = true;
|
||||
last = props.type === 'horizontal' ? x : y;
|
||||
content.style.transition = '';
|
||||
},
|
||||
() => {
|
||||
setTimeout(() => emits('update:drag', false));
|
||||
fromSelf = false;
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// 绑定文本拖拽事件
|
||||
useDrag(
|
||||
content,
|
||||
contentDrag,
|
||||
(x, y) => {
|
||||
fromSelf = true;
|
||||
contentLast = props.type === 'horizontal' ? x : y;
|
||||
content.style.transition = '';
|
||||
},
|
||||
() => {
|
||||
setTimeout(() => emits('update:drag', false));
|
||||
fromSelf = false;
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
useWheel(content, (x, y) => {
|
||||
fromSelf = true;
|
||||
const d = x !== 0 ? x : y;
|
||||
if (Math.abs(d) > 30) {
|
||||
content.style.transition = `${cssTarget} 0.2s ease-out`;
|
||||
} else {
|
||||
content.style.transition = '';
|
||||
}
|
||||
now += d;
|
||||
scroll();
|
||||
fromSelf = false;
|
||||
});
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
await calHeight(true);
|
||||
await calHeight();
|
||||
draw();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resize);
|
||||
cancelGlobalDrag(canvasDrag);
|
||||
cancelGlobalDrag(contentDrag);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.scroll {
|
||||
opacity: 0.2;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
|
||||
.scroll:hover {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.scroll:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.scroll-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-div {
|
||||
flex-basis: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@ -1,340 +0,0 @@
|
||||
import { Component, shallowReactive } from 'vue';
|
||||
import { EventEmitter } from '@motajs/legacy-common';
|
||||
import { IGameUi, IUiController } from './interface';
|
||||
|
||||
interface FocusEvent<T> {
|
||||
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<T = any> extends EventEmitter<FocusEvent<T>> {
|
||||
/** 显示列表 */
|
||||
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<string, (param?: any) => void>;
|
||||
type UiVBind = Record<string, any>;
|
||||
|
||||
interface MountedVBind {
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
controller: UiController;
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
export class GameUi extends EventEmitter<GameUiEvent> 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<IndexedGameUi>
|
||||
implements IUiController
|
||||
{
|
||||
static list: UiController[] = [];
|
||||
list: Record<string, GameUi> = {};
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
{
|
||||
"screen": {
|
||||
"fullscreen": [
|
||||
"是否全屏进行游戏,全屏后按ESC退出全屏,开启后将不能通过按ESC开启系统设置菜单,",
|
||||
"请按下方的按钮打开。进入或退出全屏后请存读档一下,以解决一部分绘制问题。"
|
||||
],
|
||||
"itemDetail": ["是否在地图上显示宝石血瓶装备等增加的属性值"],
|
||||
"transition": [
|
||||
"是否展示当一个ui界面,如怪物手册等的打开与关闭时的动画。当此项开启时,",
|
||||
"所有界面被打开或关闭时都会展示动画,否则会直接展示出来"
|
||||
],
|
||||
"fontSize": [
|
||||
"在各种 ui 界面中显示的文字大小,范围为 2 - 48。注意,字体过大可能会引起 ui 布局发生错误"
|
||||
],
|
||||
"criticalGem": ["临界是否显示为在当前地图要吃的宝石数"]
|
||||
},
|
||||
"action": {
|
||||
"hotkey": ["设置游戏中会用到的一些快捷键"]
|
||||
},
|
||||
"utils": {
|
||||
"autoScale": [
|
||||
"开启后,每次进入游戏时会自动缩放游戏画面至合适值。该项只对电脑端有效。"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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<string, (param?: any) => void>;
|
||||
type UiVBind = Record<string, any>;
|
||||
|
||||
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;
|
||||
}
|
||||
@ -1,309 +0,0 @@
|
||||
<template>
|
||||
<div id="critical-main">
|
||||
<div id="critical">
|
||||
<div class="des">加攻伤害</div>
|
||||
<canvas ref="critical" class="chart"></canvas>
|
||||
<div class="slider-div">
|
||||
<span>加攻次数 {{ addAtk }}</span>
|
||||
<Slider
|
||||
class="slider"
|
||||
v-model:value="addAtk"
|
||||
:max="ceil((originCri.at(-1)?.[0] ?? 2) / ratio) - 1"
|
||||
></Slider>
|
||||
<span
|
||||
>最大值 {{
|
||||
ceil((originCri.at(-1)?.[0] ?? 2) / ratio) - 1
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<Divider
|
||||
dashed
|
||||
style="width: 100%; border-color: #ddd4; margin: 1vh 0 1vh 0"
|
||||
></Divider>
|
||||
<div id="def">
|
||||
<div class="des">加防伤害</div>
|
||||
<canvas ref="def" class="chart"></canvas>
|
||||
<div class="slider-div">
|
||||
<span>加防次数 {{ addDef }}</span>
|
||||
<Slider
|
||||
class="slider"
|
||||
v-model:value="addDef"
|
||||
:max="ceil((originDef.at(-1)?.[0] ?? 2) / ratio) - 1"
|
||||
></Slider>
|
||||
<span
|
||||
>最大值 {{
|
||||
ceil((originDef.at(-1)?.[0] ?? 2) / ratio) - 1
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div id="now-damage">
|
||||
<div>
|
||||
<span
|
||||
>当前加攻{{
|
||||
isMobile ? '' : ' '
|
||||
}}</span
|
||||
>
|
||||
<span class="changable" :change="addAtkChangable">{{
|
||||
format(addAtk * ratio)
|
||||
}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
>当前加防{{
|
||||
isMobile ? '' : ' '
|
||||
}}</span
|
||||
>
|
||||
<span class="changable" :change="addDefChangable">{{
|
||||
format(addDef * ratio)
|
||||
}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
>当前减伤{{
|
||||
isMobile ? '' : ' '
|
||||
}}</span
|
||||
>
|
||||
<span class="changable" :change="nowDamageChangable"
|
||||
><span style="font-family: 'FiraCode'">{{
|
||||
(nowDamage[0] as number) < 0 && isNil(enemy.damage)
|
||||
? '=>'
|
||||
: ''
|
||||
}}</span
|
||||
>{{
|
||||
(nowDamage[0] as number) < 0 && isNil(enemy.damage)
|
||||
? format(-nowDamage[0])
|
||||
: format(nowDamage[0])
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
>当前伤害{{
|
||||
isMobile ? '' : ' '
|
||||
}}</span
|
||||
>
|
||||
<span class="changable" :change="nowDamageChangable">{{
|
||||
format(nowDamage[1])
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Slider, Divider } from 'ant-design-vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { detailInfo, getCriticalDamage, getDefDamage } from '../tools/book';
|
||||
import Chart, { ChartConfiguration } from 'chart.js/auto';
|
||||
import { setCanvasSize } from '../utils';
|
||||
import { debounce, isNil } from 'lodash-es';
|
||||
import { isMobile } from '../use';
|
||||
import { createChangable } from '../tools/common';
|
||||
|
||||
const props = defineProps<{
|
||||
fromBook?: boolean;
|
||||
}>();
|
||||
|
||||
const critical = ref<HTMLCanvasElement>();
|
||||
const def = ref<HTMLCanvasElement>();
|
||||
|
||||
const enemy = detailInfo.enemy!;
|
||||
const ceil = Math.ceil;
|
||||
|
||||
const x = ref<number>();
|
||||
const y = ref<number>();
|
||||
x.value = !isNil(x.value)
|
||||
? Math.round(x.value + core.bigmap.offsetX / 32)
|
||||
: void 0;
|
||||
y.value = !isNil(y.value)
|
||||
? Math.round(y.value + core.bigmap.offsetY / 32)
|
||||
: void 0;
|
||||
|
||||
let originCri = getCriticalDamage(enemy, 0, 0);
|
||||
let originDef = getDefDamage(enemy, 0, 0);
|
||||
|
||||
// 当前数据
|
||||
const allCri = ref(originCri);
|
||||
const allDef = ref(originDef);
|
||||
|
||||
// 加攻加防数量
|
||||
const addAtk = ref(0);
|
||||
const addDef = ref(0);
|
||||
const addAtkChangable = createChangable(addAtk).change;
|
||||
const addDefChangable = createChangable(addDef).change;
|
||||
|
||||
const originDamage = enemy.enemy.calDamage(core.status.hero).damage;
|
||||
|
||||
// 转发core上的内容至当前作用域
|
||||
const format = core.formatBigNumber;
|
||||
const ratio = core.status.thisMap.ratio;
|
||||
|
||||
const nowDamage = computed(() => {
|
||||
const dam = enemy.enemy.calDamage({
|
||||
atk: core.status.hero.atk + addAtk.value * ratio,
|
||||
def: core.status.hero.def + addDef.value * ratio
|
||||
}).damage;
|
||||
if (!isFinite(dam)) return ['???', '???'];
|
||||
if (!isFinite(originDamage)) return [-dam, dam];
|
||||
return [originDamage - dam, dam];
|
||||
});
|
||||
const nowDamageChangable = createChangable(nowDamage, 1).change;
|
||||
|
||||
function generateChart(ele: HTMLCanvasElement, data: [number, number][]) {
|
||||
Chart.defaults.color = '#aaa';
|
||||
const config: ChartConfiguration = {
|
||||
type: 'line',
|
||||
data: generateData(data),
|
||||
options: {
|
||||
elements: {
|
||||
point: {
|
||||
radius: 5,
|
||||
hoverRadius: 7
|
||||
},
|
||||
line: {
|
||||
borderJoinStyle: 'round'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
grid: {
|
||||
color: '#ddd3'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return new Chart(ele, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图表数据
|
||||
* @param data 数据
|
||||
*/
|
||||
function generateData(data: [number, number][]): ChartConfiguration['data'] {
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
data: data.map(v => v[1]),
|
||||
label: '怪物伤害'
|
||||
}
|
||||
],
|
||||
labels: data.map(v => Math.round(v[0] / ratio))
|
||||
};
|
||||
}
|
||||
|
||||
const update = debounce((atk: Chart, def: Chart) => {
|
||||
allCri.value = getCriticalDamage(
|
||||
enemy,
|
||||
addAtk.value * ratio,
|
||||
addDef.value * ratio
|
||||
);
|
||||
allDef.value = getDefDamage(
|
||||
enemy,
|
||||
addDef.value * ratio,
|
||||
addAtk.value * ratio
|
||||
);
|
||||
if (allCri.value.length > originCri.length) originCri = allCri.value;
|
||||
if (allDef.value.length > originDef.length) originDef = allDef.value;
|
||||
|
||||
atk.data = generateData(allCri.value);
|
||||
def.data = generateData(allDef.value);
|
||||
atk.update('resize');
|
||||
def.update('resize');
|
||||
}, 200);
|
||||
|
||||
onMounted(() => {
|
||||
const div = document.getElementById('critical-main') as HTMLDivElement;
|
||||
const style = getComputedStyle(div);
|
||||
|
||||
const width = parseFloat(style.width);
|
||||
const height = window.innerHeight / 5;
|
||||
const c = critical.value!;
|
||||
const d = def.value!;
|
||||
|
||||
setCanvasSize(c, width, height);
|
||||
setCanvasSize(d, width, height);
|
||||
|
||||
const criChart = generateChart(c, allCri.value);
|
||||
const defChart = generateChart(d, allDef.value);
|
||||
|
||||
watch(addAtk, n => {
|
||||
update(criChart, defChart);
|
||||
});
|
||||
|
||||
watch(addDef, n => {
|
||||
update(criChart, defChart);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#critical-main {
|
||||
width: 100%;
|
||||
height: 50vh;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
top: 20vh;
|
||||
}
|
||||
|
||||
.des {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 140%;
|
||||
}
|
||||
|
||||
.slider-div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 120%;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
#now-damage {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
font-size: 160%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#now-damage {
|
||||
font-size: 100%;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
#critical-main {
|
||||
position: absolute;
|
||||
top: 25vh;
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
.slider-div span {
|
||||
font-size: 70%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<div id="special-main">
|
||||
<Scroll id="special-scroll">
|
||||
<div id="special">
|
||||
<component :is="info"></component>
|
||||
</div>
|
||||
</Scroll>
|
||||
<Divider
|
||||
dashed
|
||||
style="margin: 2vh 0 2vh 0; border-color: #ddd4"
|
||||
></Divider>
|
||||
<div id="critical">
|
||||
<div style="font-size: 2.5vh; width: 100%; text-align: center">
|
||||
临界表
|
||||
</div>
|
||||
<div id="critical-main">
|
||||
<div id="critical-des">
|
||||
<span>加攻</span>
|
||||
<span>减伤</span>
|
||||
</div>
|
||||
<div v-for="cri of criticals" class="critical">
|
||||
<span class="critical-atk">{{ format(cri.atkDelta) }}</span>
|
||||
<span>{{ format(cri.delta) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { isMobile } from '../use';
|
||||
import { detailInfo, getSpecialHint } from '../tools/book';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import { Divider } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps<{
|
||||
fromBook?: boolean;
|
||||
}>();
|
||||
|
||||
const enemy = detailInfo.enemy!;
|
||||
|
||||
const info = getSpecialHint(enemy);
|
||||
|
||||
const criticals = enemy.enemy.calCritical(isMobile ? 4 : 8);
|
||||
|
||||
const format = core.formatBigNumber;
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#special-main {
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
font-size: 160%;
|
||||
position: absolute;
|
||||
top: 20vh;
|
||||
}
|
||||
|
||||
#critical-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#critical-des,
|
||||
.critical {
|
||||
font-size: 110%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.critical-atk {
|
||||
border-bottom: 1px solid #ddd4;
|
||||
}
|
||||
|
||||
.critical {
|
||||
border-left: 1px solid #ddd4;
|
||||
padding-left: 1%;
|
||||
}
|
||||
|
||||
#special-scroll {
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#critical-des,
|
||||
.critical {
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
#special {
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
#special-main {
|
||||
top: 25vh;
|
||||
width: 90vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -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';
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
import { MotaSetting } from '../setting';
|
||||
|
||||
export const mainSetting = new MotaSetting();
|
||||
@ -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 (
|
||||
<div>
|
||||
<span> 未知的设置类型 </span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BooleanSetting(props: SettingComponentProps) {
|
||||
const { setting, displayer, item } = props;
|
||||
const changeValue = () => {
|
||||
setting.setValue(displayer.selectStack.join('.'), !item.value);
|
||||
displayer.update();
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="editor-boolean">
|
||||
<span>当前 {item.value ? '开启' : '关闭'}</span>
|
||||
<Button
|
||||
class="boolean-button"
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={changeValue}
|
||||
>
|
||||
{item.value ? '关闭' : '开启'}设置
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div class="editor-number">
|
||||
<div class="editor-number-input">
|
||||
<span> 修改设置: </span>
|
||||
<InputNumber
|
||||
class="number-input"
|
||||
size="large"
|
||||
// keyboard={true}
|
||||
min={item.step?.[0] ?? 0}
|
||||
max={item.step?.[1] ?? 100}
|
||||
step={item.step?.[2] ?? 1}
|
||||
value={item.value as number}
|
||||
onChange={value => changeValue(value as number)}
|
||||
></InputNumber>
|
||||
</div>
|
||||
<div class="editor-number-button">
|
||||
<Button
|
||||
class="number-button"
|
||||
type="primary"
|
||||
onClick={() =>
|
||||
changeValue(
|
||||
(item.value as number) - (item.step?.[2] ?? 1)
|
||||
)
|
||||
}
|
||||
>
|
||||
减少
|
||||
</Button>
|
||||
<Button
|
||||
class="number-button"
|
||||
type="primary"
|
||||
onClick={() =>
|
||||
changeValue(
|
||||
(item.value as number) + (item.step?.[2] ?? 1)
|
||||
)
|
||||
}
|
||||
>
|
||||
增加
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div>
|
||||
{items.map((v, i) => {
|
||||
return (
|
||||
<Radio
|
||||
value={i}
|
||||
checked={i === item.value}
|
||||
onClick={() => changeValue(i)}
|
||||
>
|
||||
{v}
|
||||
</Radio>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
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 (
|
||||
<div style="display: flex; justify-content: center">
|
||||
<Button
|
||||
style="font-size: 75%"
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={() =>
|
||||
showSpecialSetting('hotkey', {
|
||||
hotkey: gameKey
|
||||
})
|
||||
}
|
||||
>
|
||||
快捷键设置
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarEditor(_props: SettingComponentProps) {
|
||||
return (
|
||||
<div style="display: flex; justify-content: center">
|
||||
<Button
|
||||
style="font-size: 75%"
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={() => showSpecialSetting('toolEditor')}
|
||||
>
|
||||
自定义工具栏
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PerformanceSetting(_props: SettingComponentProps) {
|
||||
return (
|
||||
<div style="display: flex; justify-content: center">
|
||||
<Button
|
||||
style="font-size: 75%"
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={() => showSpecialSetting('performance')}
|
||||
>
|
||||
打开性能界面
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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<T extends number | boolean>(
|
||||
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<T extends number | boolean>(
|
||||
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<T extends number | boolean>(
|
||||
_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
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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();
|
||||
@ -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<SettingComponentProps>;
|
||||
type MotaSettingType = boolean | number | MotaSetting;
|
||||
|
||||
export interface MotaSettingItem<T extends MotaSettingType = MotaSettingType> {
|
||||
name: string;
|
||||
key: string;
|
||||
value: T;
|
||||
controller: SettingComponent;
|
||||
description?: string;
|
||||
defaults?: boolean | number;
|
||||
step?: [number, number, number];
|
||||
display?: (value: T) => string;
|
||||
}
|
||||
|
||||
interface SettingEvent {
|
||||
valueChange: <T extends boolean | number>(
|
||||
key: string,
|
||||
newValue: T,
|
||||
oldValue: T
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type SettingText = {
|
||||
[key: string]: string[] | SettingText;
|
||||
};
|
||||
|
||||
export interface SettingDisplayInfo {
|
||||
item: MotaSettingItem | null;
|
||||
list: Record<string, MotaSettingItem>;
|
||||
text: string[];
|
||||
}
|
||||
|
||||
export class MotaSetting extends EventEmitter<SettingEvent> {
|
||||
static noStorage: string[] = [];
|
||||
|
||||
readonly list: Record<string, MotaSettingItem> = {};
|
||||
|
||||
/**
|
||||
* 重设设置
|
||||
* @param setting 设置信息
|
||||
*/
|
||||
reset(setting: Record<string, boolean | number>) {
|
||||
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<MotaSettingItem | null> {
|
||||
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<T extends boolean | number>(key: string, defaultValue: T): T;
|
||||
getValue<T extends boolean | number>(
|
||||
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<SettingDisplayerEvent> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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 (
|
||||
<div>
|
||||
{enemy.special.map((v, i) => {
|
||||
return (
|
||||
<div class="special">
|
||||
<span style={{ color: v[2] }}>
|
||||
{v[0]}:
|
||||
</span>
|
||||
<span innerHTML={v[1]}></span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得怪物的最近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;
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
import { Ref, ref, watch } from 'vue';
|
||||
import { nextFrame } from '../utils';
|
||||
|
||||
interface ChangableValue<T> {
|
||||
change: Ref<boolean>;
|
||||
value: Ref<T>;
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个监听响应式变量更改的,可以用于Changable的监听器
|
||||
* @param value 要监听的值
|
||||
*/
|
||||
export function createChangable<T>(
|
||||
value: Ref<T>,
|
||||
key?: keyof T
|
||||
): ChangableValue<T> {
|
||||
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
|
||||
};
|
||||
}
|
||||
@ -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 (
|
||||
<div class="equip-add-detail">
|
||||
{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 (
|
||||
<span style="display: flex">
|
||||
<span style="flex-basis: 50%">{getStatusLabel(v)}</span>
|
||||
|
||||
<span
|
||||
style={{
|
||||
color: value > 0 ? 'lightgreen' : 'lightsalmon'
|
||||
}}
|
||||
>
|
||||
{value > 0 ? `+${value}` : value}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前勇士属性,如果有选中的装备,会在后面追加显示增加量
|
||||
* @param nowEquip 当前选中的装备
|
||||
*/
|
||||
export function getNowStatus(nowEquip?: Equip, onCol: boolean = false) {
|
||||
const toShow = [
|
||||
'hp',
|
||||
'lv',
|
||||
'atk',
|
||||
'def',
|
||||
'mdef',
|
||||
'mana',
|
||||
'hpmax',
|
||||
'money'
|
||||
] as (keyof SelectType<HeroStatus, number>)[];
|
||||
// @ts-expect-error 需要重构
|
||||
const { getHeroStatusOn } = Mota.require('@user/data-state');
|
||||
|
||||
return (
|
||||
<div id="hero-status">
|
||||
{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 (
|
||||
<div class="hero-status-one">
|
||||
<span class="hero-status-label">
|
||||
{getStatusLabel(v)}
|
||||
</span>
|
||||
<div class="hero-status-value">
|
||||
<span style="margin-right: 20%">{status}</span>
|
||||
{add !== 0 && (
|
||||
<span
|
||||
style={{
|
||||
color:
|
||||
add > 0
|
||||
? 'lightgreen'
|
||||
: 'lightsalmon'
|
||||
}}
|
||||
>
|
||||
{add > 0 ? '+' + add : '-' + -add}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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<BFSFromString, BFSToString>;
|
||||
}
|
||||
|
||||
interface MapDrawData {
|
||||
locs: Partial<Record<FloorIds, LocArr>>;
|
||||
line: [number, number, number, number][];
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
let area: Record<string, FloorIds[]> = {};
|
||||
|
||||
const bfsCache: Partial<Record<FloorIds, MapBFSResult>> = {};
|
||||
/**
|
||||
* 键的格式:FloorIds,interval,border
|
||||
*/
|
||||
const drawCache: Record<string, MapDrawData> = {};
|
||||
|
||||
const arrow: Partial<Record<AllNumbers, Dir>> = {
|
||||
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<Record<FloorIds, LocArr>> = {};
|
||||
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<Record<FloorIds, boolean>> = {
|
||||
[floorId]: true
|
||||
};
|
||||
const floors = [floorId];
|
||||
const link: Record<BFSFromString, BFSToString> = {};
|
||||
|
||||
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<Record<FloorIds, boolean>> = {};
|
||||
thumbnailLoc: Partial<Record<FloorIds, Loc2>> = {};
|
||||
|
||||
// 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<EnemyIds>();
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
export * from './book';
|
||||
export * from './common';
|
||||
export * from './equipbox';
|
||||
export * from './fixed';
|
||||
export * from './fly';
|
||||
@ -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<T extends keyof ClsMap | 'all'>(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;
|
||||
}
|
||||
@ -1,235 +0,0 @@
|
||||
<!-- 怪物详细信息 -->
|
||||
<template>
|
||||
<div id="detail">
|
||||
<div id="tools">
|
||||
<span id="back" class="button-text tools" @click="close"
|
||||
><left-outlined />返回</span
|
||||
>
|
||||
</div>
|
||||
<div id="info" :style="{ top: `${top}px` }">
|
||||
<EnemyOne :enemy="enemy!"></EnemyOne>
|
||||
<Divider
|
||||
dashed
|
||||
style="margin: 2vh 0 2vh 0; border-color: #ddd4; width: 100%"
|
||||
></Divider>
|
||||
</div>
|
||||
<Transition name="detail">
|
||||
<EnemySpecial
|
||||
:from-book="fromBook"
|
||||
v-if="panel === 'special'"
|
||||
></EnemySpecial>
|
||||
<EnemyCritical
|
||||
:from-book="fromBook"
|
||||
v-else-if="panel === 'critical'"
|
||||
></EnemyCritical>
|
||||
<!-- <EnemyTarget v-else-if="panel === 'target'"></EnemyTarget> -->
|
||||
</Transition>
|
||||
<div id="detail-more">
|
||||
<Transition name="detail">
|
||||
<div
|
||||
id="special-more"
|
||||
class="detial-more"
|
||||
v-if="panel === 'special'"
|
||||
>
|
||||
<span id="enemy-target" class="button-text more"> </span>
|
||||
<span
|
||||
id="critical-more"
|
||||
class="button-text more"
|
||||
@click="changePanel($event, 'critical')"
|
||||
>详细临界信息 <right-outlined
|
||||
/></span>
|
||||
</div>
|
||||
<div
|
||||
id="special-more"
|
||||
class="detial-more"
|
||||
v-else-if="panel === 'critical'"
|
||||
>
|
||||
<span
|
||||
id="enemy-pos"
|
||||
class="button-text more"
|
||||
@click="changePanel($event, 'special')"
|
||||
><left-outlined /> 怪物特殊属性</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
id="special-more"
|
||||
class="detial-more"
|
||||
v-else-if="panel === 'target'"
|
||||
>
|
||||
<span></span>
|
||||
<span
|
||||
id="enemy-pos"
|
||||
class="button-text more"
|
||||
@click="changePanel($event, 'special')"
|
||||
>怪物特殊属性 <RightOutlined
|
||||
/></span>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import EnemyOne from '../components/enemyOne.vue';
|
||||
import { useDrag } from '../use';
|
||||
import EnemySpecial from '../panel/enemySpecial.vue';
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
|
||||
import EnemyCritical from '../panel/enemyCritical.vue';
|
||||
import { detailInfo } from '../tools/book';
|
||||
import { gameKey } from '@motajs/system';
|
||||
import { Divider } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps<{
|
||||
fromBook?: boolean;
|
||||
defaultPanel?: 'special' | 'critical' | 'target';
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const enemy = detailInfo.enemy;
|
||||
const top = ref(detailInfo.pos);
|
||||
const panel = ref<string>(props.defaultPanel ?? 'special');
|
||||
const symbol = Symbol();
|
||||
|
||||
let detail: HTMLDivElement;
|
||||
|
||||
function changePanel(e: MouseEvent, to: string) {
|
||||
e.stopPropagation();
|
||||
panel.value = to;
|
||||
}
|
||||
|
||||
function close() {
|
||||
top.value = detailInfo.pos;
|
||||
detail.style.opacity = '0';
|
||||
emits('close');
|
||||
}
|
||||
|
||||
gameKey.use(symbol);
|
||||
gameKey
|
||||
.realize('exit', () => {
|
||||
close();
|
||||
})
|
||||
.realize('confirm', () => {
|
||||
close();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
top.value = 0;
|
||||
detail = document.getElementById('detail') as HTMLDivElement;
|
||||
detail.style.opacity = '1';
|
||||
|
||||
const style = getComputedStyle(detail);
|
||||
|
||||
let moved = false;
|
||||
let pos = [0, 0];
|
||||
|
||||
useDrag(
|
||||
detail,
|
||||
(x, y) => {
|
||||
if ((x - pos[0]) ** 2 + (y - pos[1]) ** 2 >= 100) moved = true;
|
||||
},
|
||||
(x, y) => {
|
||||
pos = [x, y];
|
||||
if (y > (parseFloat(style.height) * 4) / 5) moved = true;
|
||||
},
|
||||
() => {
|
||||
if (moved === false && panel.value === 'special') {
|
||||
close();
|
||||
}
|
||||
moved = false;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
gameKey.dispose(symbol);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#info {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
transition: all 0.6s ease;
|
||||
height: 20vh;
|
||||
padding: 0 1% 0 1%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#detail {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: 14%;
|
||||
font-family: 'normal';
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 72%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#detail-more {
|
||||
position: absolute;
|
||||
margin-top: 3%;
|
||||
width: 100%;
|
||||
font-size: 3vh;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.detial-more {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.more {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.detail-enter-active,
|
||||
.detail-leave-active {
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
|
||||
.detail-enter-from,
|
||||
.detail-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#tools {
|
||||
position: fixed;
|
||||
height: 6%;
|
||||
font-size: 3.2vh;
|
||||
width: 100%;
|
||||
left: 5%;
|
||||
top: 5%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#detail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 5%;
|
||||
left: 0%;
|
||||
}
|
||||
|
||||
#detail-more {
|
||||
font-size: 4vw;
|
||||
bottom: 5%;
|
||||
left: 5vw;
|
||||
width: 80vw;
|
||||
}
|
||||
|
||||
#info {
|
||||
transform: translateY(10%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,717 +0,0 @@
|
||||
<template>
|
||||
<div id="equipbox">
|
||||
<div id="tools">
|
||||
<span class="button-text tools" @click="exit"
|
||||
><left-outlined /> 返回游戏</span
|
||||
>
|
||||
<span class="button-text tools" @click="toTool"
|
||||
>道具栏 <right-outlined
|
||||
/></span>
|
||||
</div>
|
||||
<div id="equipbox-main">
|
||||
<div id="equip-list">
|
||||
<div id="filter">
|
||||
<Select v-model:value="norm" class="select">
|
||||
<SelectOption v-for="t of normList" :value="t">{{
|
||||
t === 'none' ? '所有' : getStatusLabel(t)
|
||||
}}</SelectOption>
|
||||
</Select>
|
||||
<Divider type="vertical" class="divider"></Divider>
|
||||
<Select v-model:value="sType" class="select">
|
||||
<SelectOption value="value">数值</SelectOption>
|
||||
<SelectOption value="percentage">百分比</SelectOption>
|
||||
</Select>
|
||||
<Divider type="vertical" class="divider"></Divider>
|
||||
<span
|
||||
@click="changeSort()"
|
||||
class="button-text"
|
||||
id="sort-type"
|
||||
>
|
||||
<span v-if="sort === 'down'">
|
||||
<sort-ascending-outlined />
|
||||
</span>
|
||||
<span v-else><sort-descending-outlined /></span>
|
||||
</span>
|
||||
</div>
|
||||
<Divider
|
||||
dashed
|
||||
style="border-color: #ddd4; margin: 1vh 0 1vh 0"
|
||||
></Divider>
|
||||
<Scroll id="equip-scroll"
|
||||
><div
|
||||
class="equip selectable"
|
||||
v-for="([id, num], i) of toShow"
|
||||
:selected="selected === i && !isCol"
|
||||
@mousedown="select(i)"
|
||||
@touchstart="select(i)"
|
||||
@click="clickList(i)"
|
||||
>
|
||||
<div class="equip-icon">
|
||||
<BoxAnimate
|
||||
:id="id ?? 'none'"
|
||||
:width="32"
|
||||
:height="32"
|
||||
:noborder="true"
|
||||
></BoxAnimate>
|
||||
<span class="equip-name">{{ all[id].name }}</span>
|
||||
</div>
|
||||
<span>× {{ num }}</span>
|
||||
</div></Scroll
|
||||
>
|
||||
</div>
|
||||
<div id="equip-status">
|
||||
<Divider
|
||||
class="divider"
|
||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
></Divider>
|
||||
<div id="equip-status-main">
|
||||
<div id="equip-now">
|
||||
<Scroll
|
||||
:style="{
|
||||
height: isMobile ? '10vh' : '30vh'
|
||||
}"
|
||||
>
|
||||
<div id="equip-now-div">
|
||||
<div
|
||||
v-for="(name, i) of equipCol"
|
||||
class="equip-now-one draginable selectable"
|
||||
:draged="draged"
|
||||
:access="canDragin(i)"
|
||||
:selected="isCol && selected === i"
|
||||
@mouseenter="dragin($event, i)"
|
||||
@mouseleave="dragout"
|
||||
@click="select(i, true)"
|
||||
>
|
||||
<BoxAnimate
|
||||
:id="equiped[i] ?? 'none'"
|
||||
></BoxAnimate>
|
||||
<span>{{ name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Scroll>
|
||||
</div>
|
||||
<Divider
|
||||
dashed
|
||||
style="border-color: #ddd4; margin: 1vh 0 1vh 0"
|
||||
></Divider>
|
||||
<div id="equip-hero" v-if="!isMobile">
|
||||
<div id="hero-icon">
|
||||
<BoxAnimate
|
||||
id="hero"
|
||||
:width="48"
|
||||
:height="64"
|
||||
noborder
|
||||
></BoxAnimate>
|
||||
</div>
|
||||
<div id="hero-status">
|
||||
<component :is="nowStatus"></component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider
|
||||
v-if="!isMobile"
|
||||
class="divider"
|
||||
type="vertical"
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
></Divider>
|
||||
</div>
|
||||
<div id="equip-desc">
|
||||
<div id="equip-icon">
|
||||
<BoxAnimate
|
||||
:id="
|
||||
isCol
|
||||
? (equiped[selected] ?? 'none')
|
||||
: (toShow[selected]?.[0] ?? 'none')
|
||||
"
|
||||
></BoxAnimate>
|
||||
<span>{{ equip.name }}</span>
|
||||
</div>
|
||||
<div id="equip-type">
|
||||
<span>装备孔:{{ equip.equip?.type }}</span>
|
||||
</div>
|
||||
<Divider
|
||||
dashed
|
||||
style="border-color: #ddd4; margin: 1vh 0 1vh 0"
|
||||
></Divider>
|
||||
<div id="equip-add">
|
||||
<span id="title">增减属性</span>
|
||||
<Scroll style="width: 100%; height: 100%">
|
||||
<component :is="addStatus"></component>
|
||||
</Scroll>
|
||||
</div>
|
||||
<Divider
|
||||
dashed
|
||||
style="border-color: #ddd4; margin: 1vh 0 1vh 0"
|
||||
></Divider>
|
||||
<div id="equip-desc-text">
|
||||
<span id="title">装备介绍</span>
|
||||
<Scroll id="desc-text" style="height: 100%; width: 100%">
|
||||
<div v-if="!descText.value!.startsWith('!!html')">
|
||||
{{ descText.value }}
|
||||
</div>
|
||||
<div v-else v-html="descText.value!.slice(6)"></div>
|
||||
</Scroll>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="icon-drag">
|
||||
<BoxAnimate
|
||||
class="drag-icon"
|
||||
v-if="draged"
|
||||
:id="toShow[selected]?.[0] ?? 'none'"
|
||||
:width="48"
|
||||
:height="48"
|
||||
noborder
|
||||
></BoxAnimate>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
SortAscendingOutlined,
|
||||
SortDescendingOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import { getAddStatus, getEquips, getNowStatus } from '../tools/equipbox';
|
||||
import BoxAnimate from '../components/boxAnimate.vue';
|
||||
import { type, getStatusLabel } from '../utils';
|
||||
import { cancelGlobalDrag, isMobile, tip, useDrag } from '../use';
|
||||
import { hyper } from 'mutate-animate';
|
||||
import { gameKey } from '@motajs/system';
|
||||
import { IMountedVBind } from '../interface';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { Divider, Select, SelectOption } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps<IMountedVBind>();
|
||||
|
||||
const equips = ref(getEquips());
|
||||
const col = ref('all');
|
||||
const all = core.material.items;
|
||||
const selected = ref(0);
|
||||
const isCol = ref(false); // 是否是选中的已装备的装备
|
||||
const equipCol = core.status.globalAttribute.equipName;
|
||||
const equiped = ref(core.status.hero.equipment);
|
||||
const listClicked = ref(false);
|
||||
|
||||
const draged = ref(false);
|
||||
const toEquipType = ref(-1); // 要穿至的装备孔,-1表示不穿
|
||||
|
||||
/** 排序方式,down表示下面大上面小 */
|
||||
const sort = ref<'up' | 'down'>('down');
|
||||
const norm = ref<keyof SelectType<HeroStatus, number> | 'none'>('none');
|
||||
const sType = ref<'value' | 'percentage'>('value');
|
||||
|
||||
// 攻击 防御 回血 额外攻击
|
||||
const normList = ['none', 'atk', 'def', 'hpmax', 'mana'];
|
||||
|
||||
watch(sort, n => {
|
||||
selected.value = toShow.value.length - selected.value - 1;
|
||||
});
|
||||
|
||||
watch(norm, n => {
|
||||
selected.value = 0;
|
||||
});
|
||||
|
||||
watch(sType, n => {
|
||||
selected.value = 0;
|
||||
});
|
||||
|
||||
/**
|
||||
* 当前装备
|
||||
*/
|
||||
const equip = computed(() => {
|
||||
const index = toShow.value[selected.value];
|
||||
const none = {
|
||||
name: '没有选择装备',
|
||||
cls: 'equip',
|
||||
text: '没有选择装备',
|
||||
equip: { type: '无', value: {}, percentage: {}, animate: '' }
|
||||
};
|
||||
if (isCol.value) {
|
||||
const id = equiped.value[selected.value];
|
||||
const e = core.material.items[id];
|
||||
if (isNil(e)) return none;
|
||||
return e;
|
||||
}
|
||||
if (isNil(index)) return none;
|
||||
return all[index[0]];
|
||||
});
|
||||
|
||||
const addStatus = computed(() => {
|
||||
// @ts-ignore
|
||||
return getAddStatus(equip.value.equip!, isCol.value);
|
||||
});
|
||||
|
||||
const descText = computed(() => {
|
||||
if (equip.value.text!.startsWith('!!html')) return ref(equip.value.text);
|
||||
return type(equip.value.text!, 25, hyper('sin', 'out'), true);
|
||||
});
|
||||
|
||||
const nowStatus = computed(() => {
|
||||
// @ts-ignore
|
||||
return getNowStatus(equip.value.equip, isCol.value);
|
||||
});
|
||||
|
||||
/**
|
||||
* 需要展示的装备,已进行排序等操作
|
||||
*/
|
||||
const toShow = computed(() => {
|
||||
const sortType = sort.value;
|
||||
const sortNorm = norm.value;
|
||||
const sortBy = sType.value;
|
||||
|
||||
const res = equips.value.filter(v => {
|
||||
const e = all[v[0]].equip!;
|
||||
const t = e.type;
|
||||
if (sortNorm !== 'none') {
|
||||
if (isNil(e[sortBy][sortNorm])) return false;
|
||||
}
|
||||
if (col.value === 'all') return true;
|
||||
if (typeof t === 'string') return t === col.value;
|
||||
else return core.status.globalAttribute.equipName[t] === col.value;
|
||||
});
|
||||
if (sortNorm === 'none') return res;
|
||||
else {
|
||||
if (sortType === 'down') {
|
||||
return res.sort((a, b) => {
|
||||
const ea = all[a[0]].equip!;
|
||||
const eb = all[b[0]].equip!;
|
||||
return ea[sortBy][sortNorm] - eb[sortBy][sortNorm];
|
||||
});
|
||||
} else {
|
||||
return res.sort((a, b) => {
|
||||
const ea = all[a[0]].equip!;
|
||||
const eb = all[b[0]].equip!;
|
||||
return eb[sortBy][sortNorm] - ea[sortBy][sortNorm];
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function changeSort() {
|
||||
if (sort.value === 'down') {
|
||||
sort.value = 'up';
|
||||
} else {
|
||||
sort.value = 'down';
|
||||
}
|
||||
}
|
||||
|
||||
function exit() {
|
||||
props.controller.close(props.num);
|
||||
}
|
||||
|
||||
function clickList(i: number) {
|
||||
if (i === selected.value && listClicked.value) {
|
||||
const id = toShow.value[selected.value]?.[0];
|
||||
if (!core.canEquip(id)) {
|
||||
tip('warn', '无法装备!');
|
||||
return;
|
||||
}
|
||||
core.loadEquip(id);
|
||||
update();
|
||||
listClicked.value = false;
|
||||
}
|
||||
listClicked.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择某个装备
|
||||
*/
|
||||
function select(i: number, col: boolean = false) {
|
||||
if (i !== selected.value && !col) {
|
||||
listClicked.value = false;
|
||||
}
|
||||
if (col) listClicked.value = false;
|
||||
if (col && isCol.value === col && selected.value === i) {
|
||||
core.unloadEquip(i);
|
||||
update();
|
||||
}
|
||||
isCol.value = col;
|
||||
selected.value = i;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以将当前装备拖入该栏
|
||||
* @param type 装备栏
|
||||
*/
|
||||
function canDragin(type: number) {
|
||||
if (type < 0) return false;
|
||||
const et = equip.value.equip?.type;
|
||||
if (!core.canEquip(toShow.value[selected.value]?.[0])) return false;
|
||||
if (isNil(et)) return false;
|
||||
if (typeof et === 'number') return type === et;
|
||||
return equipCol[type] === et;
|
||||
}
|
||||
|
||||
/**
|
||||
* 穿上某个装备
|
||||
*/
|
||||
function loadEquip() {
|
||||
const num = toEquipType.value;
|
||||
if (num < 0) return;
|
||||
if (!canDragin(num)) {
|
||||
tip('warn', '无法装备!');
|
||||
return;
|
||||
}
|
||||
const now = equiped.value[num];
|
||||
const to = toShow.value[selected.value]?.[0];
|
||||
core.items._realLoadEquip(num, to, now);
|
||||
update();
|
||||
}
|
||||
|
||||
function update() {
|
||||
equiped.value = core.status.hero.equipment;
|
||||
equips.value = getEquips();
|
||||
requestAnimationFrame(() => {
|
||||
bind();
|
||||
});
|
||||
}
|
||||
|
||||
// ----- 绑定函数
|
||||
function bind() {
|
||||
const equips = Array.from(
|
||||
document.querySelectorAll('.equip')
|
||||
) as HTMLDivElement[];
|
||||
|
||||
// 绑定武器拖拽事件,使武器可以拖拽穿脱
|
||||
useDrag(
|
||||
equips,
|
||||
dragEquip,
|
||||
(x, y) => {
|
||||
fx = x;
|
||||
fy = y;
|
||||
},
|
||||
() => {
|
||||
if (draged.value) {
|
||||
draged.value = false;
|
||||
loadEquip();
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// ----- 交互函数
|
||||
let [fx, fy] = [0, 0];
|
||||
function dragEquip(x: number, y: number, e: TouchEvent | MouseEvent) {
|
||||
if ((x - fx) ** 2 + (y - fy) ** 2 > 10 ** 2 && !draged.value) {
|
||||
draged.value = true;
|
||||
}
|
||||
if (draged.value) {
|
||||
const target = document.getElementById('icon-drag') as HTMLDivElement;
|
||||
target.style.left = `${x - 24}px`;
|
||||
target.style.top = `${y - 24}px`;
|
||||
}
|
||||
}
|
||||
|
||||
function dragin(e: Event, type: number) {
|
||||
e.stopPropagation();
|
||||
toEquipType.value = type;
|
||||
}
|
||||
|
||||
function dragout(e: Event) {
|
||||
e.stopPropagation();
|
||||
toEquipType.value = -1;
|
||||
}
|
||||
|
||||
function toTool() {
|
||||
props.controller.holdOn();
|
||||
exit();
|
||||
nextTick(() => {
|
||||
props.controller.open('toolbox');
|
||||
});
|
||||
}
|
||||
|
||||
gameKey.use(props.ui.symbol);
|
||||
gameKey
|
||||
.realize('exit', () => {
|
||||
exit();
|
||||
})
|
||||
.realize('equipbox', () => {
|
||||
exit();
|
||||
})
|
||||
.realize('quickEquip', id => {
|
||||
const num = parseInt(id.split('_').at(-1)!);
|
||||
core.quickSaveEquip(num);
|
||||
tip('success', `已保存至${num}号套装`);
|
||||
});
|
||||
|
||||
watch(toShow, n => {
|
||||
bind();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
bind();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelGlobalDrag(dragEquip);
|
||||
gameKey.dispose(props.ui.symbol);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#equipbox {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#tools {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 3.2vh;
|
||||
height: 5vh;
|
||||
justify-content: space-between;
|
||||
font-family: 'normal';
|
||||
|
||||
.tools {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
#equipbox-main {
|
||||
height: 85vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-family: 'normal';
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#equip-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-basis: 25%;
|
||||
|
||||
#filter {
|
||||
margin-top: 2vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
|
||||
.select {
|
||||
width: 100%;
|
||||
font-family: 'normal';
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
#sort-type {
|
||||
font-size: 75%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
#equip-scroll {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#equip-status {
|
||||
display: flex;
|
||||
flex-basis: 50%;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
#equip-status-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#equip-desc {
|
||||
display: flex;
|
||||
flex-basis: 25%;
|
||||
flex-direction: column;
|
||||
padding-top: 2vh;
|
||||
align-items: center;
|
||||
|
||||
#equip-icon {
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ddd4;
|
||||
padding-bottom: 1%;
|
||||
|
||||
span {
|
||||
margin-left: 5%;
|
||||
}
|
||||
}
|
||||
|
||||
#equip-add {
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
#title {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.equip-add-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#equip-desc-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
#title {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#equip-now {
|
||||
width: 100%;
|
||||
|
||||
#equip-now-div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding-left: 5%;
|
||||
|
||||
.equip-now-one {
|
||||
flex-basis: 30%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 3% 3.3% 3% 0;
|
||||
padding-left: 0.5%;
|
||||
|
||||
span {
|
||||
margin-left: 10%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#equip-hero {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#hero-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 10% 0 10% 0;
|
||||
}
|
||||
|
||||
#hero-status {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
.hero-status-one {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-basis: 50%;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
font-size: 110%;
|
||||
white-space: nowrap;
|
||||
|
||||
.hero-status-label {
|
||||
width: 100%;
|
||||
margin-right: 5%;
|
||||
}
|
||||
|
||||
.hero-status-value {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.equip {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5vh 0.5vw;
|
||||
border: #0000 0.5px solid;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
.equip-icon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.equip-name {
|
||||
margin-left: 5%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#icon-drag {
|
||||
position: fixed;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#equipbox-main {
|
||||
flex-direction: column-reverse;
|
||||
font-size: 225%;
|
||||
}
|
||||
|
||||
#equip-now-div {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
#equip-status {
|
||||
flex-direction: column;
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
#equip-list {
|
||||
flex-basis: 45%;
|
||||
|
||||
#filter #sort-type {
|
||||
font-size: 150%;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 1% 0 1% 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,638 +0,0 @@
|
||||
<template>
|
||||
<div id="fly">
|
||||
<div id="tools">
|
||||
<span class="button-text" @click="exit"
|
||||
><left-outlined /> 返回游戏</span
|
||||
>
|
||||
</div>
|
||||
<div id="fly-settings">
|
||||
<div id="fly-border">
|
||||
<span>无边框模式</span>
|
||||
<Switch
|
||||
class="fly-settings"
|
||||
v-model:checked="noBorder"
|
||||
checked-children="ON"
|
||||
un-checked-children="OFF"
|
||||
></Switch>
|
||||
</div>
|
||||
<div v-if="!isMobile" id="fly-tradition">
|
||||
<span>传统按键模式</span>
|
||||
<Switch
|
||||
class="fly-settings"
|
||||
v-model:checked="tradition"
|
||||
checked-children="ON"
|
||||
un-checked-children="OFF"
|
||||
></Switch>
|
||||
</div>
|
||||
<span
|
||||
v-if="!isMobile"
|
||||
class="button-text"
|
||||
id="fly-download"
|
||||
@click="download"
|
||||
>下载地图图片</span
|
||||
>
|
||||
</div>
|
||||
<div id="fly-main">
|
||||
<div id="fly-left">
|
||||
<Scroll id="fly-area"
|
||||
><div id="area-list">
|
||||
<span
|
||||
v-for="(v, k) in area"
|
||||
:selected="nowArea === k"
|
||||
class="selectable"
|
||||
@click="nowArea = k"
|
||||
>{{ k }}</span
|
||||
>
|
||||
</div></Scroll
|
||||
>
|
||||
<Divider type="vertical" dashed id="divider-left"></Divider>
|
||||
<div id="fly-map-div">
|
||||
<canvas id="fly-map" @click="click"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<Divider
|
||||
id="divider-right"
|
||||
dashed
|
||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
||||
></Divider>
|
||||
<div id="fly-right">
|
||||
<canvas id="fly-thumbnail" @click="fly" @wheel="wheel"></canvas>
|
||||
<div id="fly-tools">
|
||||
<double-left-outlined
|
||||
@click="changeFloorByDelta(-10)"
|
||||
class="button-text fly-button"
|
||||
/>
|
||||
<left-outlined
|
||||
@click="changeFloorByDelta(-1)"
|
||||
class="button-text fly-button"
|
||||
/>
|
||||
<span
|
||||
class="changable"
|
||||
id="fly-now"
|
||||
:change="titleChange"
|
||||
>{{ title }}</span
|
||||
>
|
||||
<right-outlined
|
||||
@click="changeFloorByDelta(1)"
|
||||
class="button-text fly-button"
|
||||
/>
|
||||
<double-right-outlined
|
||||
@click="changeFloorByDelta(10)"
|
||||
class="button-text fly-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import { getArea, getMapData, MinimapDrawer } from '../tools/fly';
|
||||
import { cancelGlobalDrag, isMobile, useDrag, useWheel } from '../use';
|
||||
import {
|
||||
LeftOutlined,
|
||||
DoubleLeftOutlined,
|
||||
RightOutlined,
|
||||
DoubleRightOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { tip } from '../use';
|
||||
import { gameKey } from '@motajs/system';
|
||||
import { createChangable } from '../tools/common';
|
||||
import { mainSetting } from '../preset/settingIns';
|
||||
import { GameStorage } from '@motajs/legacy-system';
|
||||
import { IMountedVBind } from '../interface';
|
||||
import { Divider, Switch } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps<IMountedVBind>();
|
||||
|
||||
type Loc2 = [number, number, number, number];
|
||||
|
||||
const area = getArea();
|
||||
const nowArea = ref(
|
||||
Object.keys(area).find(v => area[v].includes(core.status.floorId)) ?? ''
|
||||
);
|
||||
const nowFloor = ref(core.status.floorId);
|
||||
const noBorder = ref(true);
|
||||
const tradition = ref(false);
|
||||
const settingScale = mainSetting.getValue('ui.mapScale', 100) / 100;
|
||||
let scale = (isMobile ? 1.5 : 3) * settingScale;
|
||||
|
||||
const storage = GameStorage.for(GameStorage.fromAuthor('AncTe', 'flyConfig'));
|
||||
|
||||
noBorder.value = storage.getValue('noBorder', true);
|
||||
tradition.value = storage.getValue('flyTradition', false);
|
||||
|
||||
const floor = computed(() => {
|
||||
return core.status.maps[nowFloor.value];
|
||||
});
|
||||
|
||||
watch(nowFloor, n => {
|
||||
drawer.nowFloor = n;
|
||||
draw();
|
||||
});
|
||||
watch(nowArea, n => {
|
||||
scale = 3 * settingScale;
|
||||
lastScale = 3 * settingScale;
|
||||
drawer.nowArea = n;
|
||||
drawer.scale = 3 * settingScale;
|
||||
drawer.ox = 0;
|
||||
drawer.oy = 0;
|
||||
if (area[n] && !area[n].includes(nowFloor.value))
|
||||
nowFloor.value =
|
||||
area[n].find(v => v === core.status.floorId) ?? area[n][0];
|
||||
});
|
||||
watch(noBorder, n => {
|
||||
storage.setValue('noBorder', n);
|
||||
drawer.noBorder = n;
|
||||
drawer.drawedThumbnail = {};
|
||||
drawer.drawMap();
|
||||
});
|
||||
watch(tradition, n => {
|
||||
storage.setValue('flyTradition', n);
|
||||
});
|
||||
|
||||
let map: HTMLCanvasElement;
|
||||
let thumb: HTMLCanvasElement;
|
||||
let thumbCtx: CanvasRenderingContext2D;
|
||||
|
||||
function exit() {
|
||||
props.controller.close(props.num);
|
||||
}
|
||||
|
||||
const title = computed(() => {
|
||||
return core.status.maps[nowFloor.value].title;
|
||||
});
|
||||
const titleChange = createChangable(title).change;
|
||||
|
||||
let drawer: MinimapDrawer;
|
||||
|
||||
function drawRight() {
|
||||
let w = thumb.width;
|
||||
let h = thumb.height;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
const ratio = floor.value.width / floor.value.height;
|
||||
if (ratio > 1) {
|
||||
h = w / ratio;
|
||||
y = thumb.height / 2 - h / 2;
|
||||
}
|
||||
if (ratio < 1) {
|
||||
w = h * ratio;
|
||||
x = thumb.width / 2 - w / 2;
|
||||
}
|
||||
thumbCtx.fillStyle = '#000';
|
||||
thumbCtx.fillRect(0, 0, thumb.width, thumb.height);
|
||||
core.drawThumbnail(nowFloor.value, void 0, {
|
||||
ctx: thumbCtx,
|
||||
all: true,
|
||||
damage: true,
|
||||
inFlyMap: true,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制所有内容
|
||||
*/
|
||||
function draw() {
|
||||
drawer.clearCache();
|
||||
drawer.drawMap();
|
||||
drawRight();
|
||||
}
|
||||
|
||||
function download() {
|
||||
drawer.download();
|
||||
draw();
|
||||
}
|
||||
|
||||
function fly() {
|
||||
if (core.flyTo(nowFloor.value)) exit();
|
||||
else tip('error', `无法飞往${floor.value.title}`);
|
||||
}
|
||||
|
||||
let lastScale = scale;
|
||||
const changeScale = debounce((s: number) => {
|
||||
map.style.transform = '';
|
||||
drawer.drawedThumbnail = {};
|
||||
drawer.scale = s;
|
||||
drawer.drawMap();
|
||||
lastScale = s;
|
||||
}, 200);
|
||||
|
||||
function resize(delta: number) {
|
||||
drawer.ox *= delta;
|
||||
drawer.oy *= delta;
|
||||
scale = delta * scale;
|
||||
changeScale(scale);
|
||||
map.style.transform = `scale(${scale / lastScale})`;
|
||||
drawer.thumbnailLoc = {};
|
||||
}
|
||||
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
|
||||
let moved = false;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
|
||||
// -------------------- 点击事件
|
||||
|
||||
function drag(x: number, y: number) {
|
||||
if (touchScale) return;
|
||||
const dx = x - lastX;
|
||||
const dy = y - lastY;
|
||||
drawer.ox += dx;
|
||||
drawer.oy += dy;
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
drawer.checkMoveThumbnail();
|
||||
drawer.drawToTarget();
|
||||
if (Math.abs(x - startX) > 10 || Math.abs(y - startY) > 10) moved = true;
|
||||
}
|
||||
|
||||
function click(e: MouseEvent) {
|
||||
if (moved) return;
|
||||
|
||||
const x = e.offsetX * devicePixelRatio;
|
||||
const y = e.offsetY * devicePixelRatio;
|
||||
for (const [id, [left, top, right, bottom]] of Object.entries(
|
||||
drawer.thumbnailLoc
|
||||
) as [FloorIds, Loc2][]) {
|
||||
if (x >= left && x <= right && y >= top && y <= bottom) {
|
||||
if (id === nowFloor.value) {
|
||||
fly();
|
||||
} else {
|
||||
nowFloor.value = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wheel(ev: WheelEvent) {
|
||||
changeFloorByDelta(-Math.sign(ev.deltaY));
|
||||
}
|
||||
|
||||
function changeAreaByFloor(id: FloorIds) {
|
||||
nowArea.value = Object.keys(area).find(v => area[v].includes(id))!;
|
||||
}
|
||||
|
||||
function changeFloorByDelta(delta: number) {
|
||||
const now = core.floorIds.indexOf(nowFloor.value);
|
||||
let to = now + delta;
|
||||
if (to < 0) to = 0;
|
||||
if (to >= core.floorIds.length) to = core.floorIds.length - 1;
|
||||
const floor = core.status.maps[core.floorIds[to]];
|
||||
if (floor.deleted || floor.forceDelete) {
|
||||
while (to !== now) {
|
||||
to += Math.sign(delta);
|
||||
const floor = core.status.maps[core.floorIds[to]];
|
||||
if (floor.cannotViewMap) continue;
|
||||
if (!floor.deleted && !floor.forceDelete) break;
|
||||
if (to < 0 || to >= core.floorIds.length) break;
|
||||
}
|
||||
}
|
||||
nowFloor.value = core.floorIds[to];
|
||||
changeAreaByFloor(nowFloor.value);
|
||||
drawer.locateMap(nowFloor.value);
|
||||
}
|
||||
|
||||
function changeFloorByDir(dir: Dir) {
|
||||
const data = getMapData(nowFloor.value);
|
||||
|
||||
for (const [from, to] of Object.entries(data.link)) {
|
||||
if (!from.startsWith(nowFloor.value)) continue;
|
||||
const d = from.split(',')[3] as Dir;
|
||||
|
||||
if (d === dir) {
|
||||
const target = to.split(',')[0] as FloorIds;
|
||||
drawer.locateMap(target);
|
||||
nowFloor.value = target;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- 键盘事件
|
||||
|
||||
gameKey.use(props.ui.symbol);
|
||||
gameKey
|
||||
.realize('@fly_left', () => {
|
||||
if (!tradition.value) changeFloorByDir('left');
|
||||
})
|
||||
.realize('@fly_right', () => {
|
||||
if (!tradition.value) changeFloorByDir('right');
|
||||
})
|
||||
.realize('@fly_up', () => {
|
||||
if (!tradition.value) changeFloorByDir('up');
|
||||
})
|
||||
.realize('@fly_down', () => {
|
||||
if (!tradition.value) changeFloorByDir('down');
|
||||
})
|
||||
.realize('@fly_last', () => {
|
||||
if (!tradition.value) changeFloorByDelta(-1);
|
||||
})
|
||||
.realize('@fly_next', () => {
|
||||
if (!tradition.value) changeFloorByDelta(1);
|
||||
})
|
||||
.realize('@fly_down_t', () => {
|
||||
if (tradition.value) changeFloorByDelta(-1);
|
||||
})
|
||||
.realize('@fly_up_t', () => {
|
||||
if (tradition.value) changeFloorByDelta(1);
|
||||
})
|
||||
.realize('@fly_left_t', () => {
|
||||
if (tradition.value) changeFloorByDelta(-10);
|
||||
})
|
||||
.realize('@fly_right_t', () => {
|
||||
if (tradition.value) changeFloorByDelta(10);
|
||||
})
|
||||
.realize('exit', () => {
|
||||
exit();
|
||||
})
|
||||
.realize('confirm', () => {
|
||||
fly();
|
||||
})
|
||||
.realize('fly', () => {
|
||||
exit();
|
||||
});
|
||||
|
||||
// -------------------- 触摸事件
|
||||
|
||||
let touchScale = false;
|
||||
let lastDis = 0;
|
||||
function touchdown(e: TouchEvent) {
|
||||
if (e.touches.length >= 2) {
|
||||
touchScale = true;
|
||||
lastDis = Math.sqrt(
|
||||
(e.touches[0].clientX - e.touches[1].clientX) ** 2 +
|
||||
(e.touches[0].clientY - e.touches[1].clientY) ** 2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function touchup(e: TouchEvent) {
|
||||
if (e.touches.length < 2) touchScale = false;
|
||||
}
|
||||
|
||||
function touchmove(e: TouchEvent) {
|
||||
if (!touchScale) return;
|
||||
const dis = Math.sqrt(
|
||||
(e.touches[0].clientX - e.touches[1].clientX) ** 2 +
|
||||
(e.touches[0].clientY - e.touches[1].clientY) ** 2
|
||||
);
|
||||
resize(dis / lastDis);
|
||||
lastDis = dis;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
map = document.getElementById('fly-map') as HTMLCanvasElement;
|
||||
thumb = document.getElementById('fly-thumbnail') as HTMLCanvasElement;
|
||||
thumbCtx = thumb.getContext('2d')!;
|
||||
|
||||
drawer = new MinimapDrawer(map);
|
||||
drawer.scale = scale;
|
||||
drawer.noBorder = noBorder.value;
|
||||
drawer.showInfo = true;
|
||||
drawer.nowArea = nowArea.value;
|
||||
|
||||
const mapStyle = getComputedStyle(map);
|
||||
const thumbStyle = getComputedStyle(thumb);
|
||||
map.width = parseFloat(mapStyle.width) * devicePixelRatio;
|
||||
map.height = parseFloat(mapStyle.height) * devicePixelRatio;
|
||||
thumb.width = parseFloat(thumbStyle.width) * devicePixelRatio;
|
||||
thumb.height = parseFloat(thumbStyle.width) * devicePixelRatio;
|
||||
|
||||
Array.from(document.getElementsByClassName('fly-settings')).forEach(v => {
|
||||
v.addEventListener('click', e => (v as HTMLElement).blur());
|
||||
});
|
||||
|
||||
drawer.locateMap(nowFloor.value);
|
||||
draw();
|
||||
|
||||
useDrag(
|
||||
map,
|
||||
drag,
|
||||
(x, y) => {
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
startX = x;
|
||||
startY = y;
|
||||
},
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
moved = false;
|
||||
}, 50);
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
useWheel(map, (x, y) => {
|
||||
const delta = -Math.sign(y) * 0.1 + 1;
|
||||
resize(delta);
|
||||
});
|
||||
|
||||
map.addEventListener('touchstart', touchdown);
|
||||
map.addEventListener('touchend', touchup);
|
||||
map.addEventListener('touchmove', touchmove);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelGlobalDrag(drag);
|
||||
gameKey.dispose(props.ui.symbol);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#fly {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 150%;
|
||||
font-family: 'normal';
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#tools {
|
||||
width: 100%;
|
||||
font-family: 'normal';
|
||||
font-size: 3.2vh;
|
||||
height: 5vh;
|
||||
position: fixed;
|
||||
left: 5vw;
|
||||
top: 5vh;
|
||||
}
|
||||
|
||||
#fly-main {
|
||||
display: flex;
|
||||
height: 80%;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#fly-left {
|
||||
width: 50vw;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#fly-area {
|
||||
height: 100%;
|
||||
width: 15vw;
|
||||
}
|
||||
|
||||
#area-list {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#divider-left {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
border-color: #ddd4;
|
||||
}
|
||||
|
||||
#fly-map-div,
|
||||
#fly-map {
|
||||
width: 35vw;
|
||||
height: 72vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#divider-right {
|
||||
height: 100%;
|
||||
border-color: #ddd4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#fly-right {
|
||||
width: 40vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#fly-tools {
|
||||
margin: 0;
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background-color: #0004;
|
||||
}
|
||||
|
||||
#fly-thumbnail {
|
||||
width: 35vw;
|
||||
height: 35vw;
|
||||
max-height: 75vh;
|
||||
max-width: 75vh;
|
||||
border: 0.1vw solid #ddd4;
|
||||
}
|
||||
|
||||
#fly-settings {
|
||||
position: fixed;
|
||||
bottom: 5vh;
|
||||
left: 10vw;
|
||||
width: 80vw;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
margin-right: 5vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fly-settings[aria-checked='false'] {
|
||||
background-color: #ddd4;
|
||||
}
|
||||
|
||||
.fly-button {
|
||||
padding: 3%;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.301);
|
||||
border: 1px dashed #ddda;
|
||||
filter: drop-shadow(0px 0px 16px black);
|
||||
}
|
||||
|
||||
#fly-now {
|
||||
text-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
max-width: 50%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-shadow:
|
||||
1px 1px 1px black,
|
||||
1px -1px 1px black,
|
||||
-1px 1px 1px black,
|
||||
-1px -1px 1px black;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#fly {
|
||||
font-size: 225%;
|
||||
}
|
||||
|
||||
#fly-main {
|
||||
flex-direction: column;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
#fly-map-div,
|
||||
#fly-map {
|
||||
width: 60vw;
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
#fly-area {
|
||||
width: 30vw;
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
#fly-left {
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
#divider-right {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#fly-right {
|
||||
width: 90vw;
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
#fly-thumbnail {
|
||||
width: 80vw;
|
||||
height: 80vw;
|
||||
}
|
||||
|
||||
#tools {
|
||||
top: 2vh;
|
||||
}
|
||||
|
||||
#fly-settings {
|
||||
bottom: 2%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,255 +0,0 @@
|
||||
<template>
|
||||
<Column @close="close" :left="30">
|
||||
<template #left>
|
||||
<div id="hotkey-group">
|
||||
<span
|
||||
class="hotkey-group-one selectable"
|
||||
v-for="(name, group) of groupName"
|
||||
:selected="selectedGroup === group"
|
||||
@click="selectedGroup = group"
|
||||
>
|
||||
{{ name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #right>
|
||||
<div id="hotkey-data">
|
||||
<div class="hotkey-one" v-for="(data, id) of show">
|
||||
<span class="hotkey-one-name"> {{ data.name }} </span>
|
||||
<div class="hotkey-one-set">
|
||||
<span
|
||||
v-for="key of data.keys"
|
||||
class="hotkey-one-set-item"
|
||||
:selected="
|
||||
data.id === selectedKey.id &&
|
||||
key.index === selectedKey.index
|
||||
"
|
||||
@click="select(data.id, key.index)"
|
||||
>
|
||||
{{ getKeyShow(key.key, key.assist) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Hotkey, gameKey } from '@motajs/system';
|
||||
import Column from '../components/colomn.vue';
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { KeyCode, KeyCodeUtils } from '@motajs/client-base';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { generateBinary, keycode } from '@motajs/legacy-common';
|
||||
import { IMountedVBind } from '../interface';
|
||||
|
||||
interface HotkeyKeys {
|
||||
index: number;
|
||||
key: KeyCode;
|
||||
/** 从低到高依次为 `ctrl` `shift` `alt` */
|
||||
assist: number;
|
||||
}
|
||||
|
||||
interface HotkeySetting {
|
||||
id: string;
|
||||
name: string;
|
||||
keys: HotkeyKeys[];
|
||||
}
|
||||
|
||||
interface SelectedKey {
|
||||
id: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const props = defineProps<IMountedVBind & { hotkey: Hotkey }>();
|
||||
|
||||
const hotkey = props.hotkey;
|
||||
|
||||
function close() {
|
||||
props.controller.close(props.num);
|
||||
}
|
||||
|
||||
const selectedGroup = ref('ui');
|
||||
const keyData = generateData();
|
||||
const show = computed(() => {
|
||||
return keyData[selectedGroup.value];
|
||||
});
|
||||
const groupName = cloneDeep(hotkey.groupName);
|
||||
delete groupName.none;
|
||||
|
||||
const selectedKey: SelectedKey = reactive({
|
||||
id: 'none',
|
||||
index: -1
|
||||
});
|
||||
|
||||
function generateData() {
|
||||
const res: Record<string, Record<string, HotkeySetting>> = {};
|
||||
|
||||
for (const [group, data] of Object.entries(hotkey.groups)) {
|
||||
if (group === 'none') continue;
|
||||
res[group] = {};
|
||||
const d = res[group];
|
||||
data.forEach(v => {
|
||||
const split = v.split('_');
|
||||
const isMulti = /^\d+$/.test(split.at(-1)!);
|
||||
const id = isMulti ? split.slice(0, -1).join('_') : v;
|
||||
const index = isMulti ? parseInt(split.at(-1)!) : -1;
|
||||
|
||||
const key = hotkey.data[v];
|
||||
d[id] ??= {
|
||||
id,
|
||||
name: isMulti
|
||||
? key.name.split('_').slice(0, -1).join('_')
|
||||
: key.name,
|
||||
keys: reactive([])
|
||||
};
|
||||
d[id].keys.push({
|
||||
index,
|
||||
key: key.key,
|
||||
assist: generateBinary([key.ctrl, key.shift, key.alt])
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function unwarpAssist(assist: number) {
|
||||
let res = '';
|
||||
if (assist & (1 << 0)) {
|
||||
res += 'Ctrl + ';
|
||||
}
|
||||
if (assist & (1 << 1)) {
|
||||
res += 'Shift + ';
|
||||
}
|
||||
if (assist & (1 << 2)) {
|
||||
res += 'Alt + ';
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function getKeyShow(key: KeyCode, assist: number) {
|
||||
return (
|
||||
unwarpAssist(assist) +
|
||||
(key === KeyCode.Unknown ? '' : KeyCodeUtils.toString(key))
|
||||
);
|
||||
}
|
||||
|
||||
function select(id: string, index: number) {
|
||||
selectedKey.id = id;
|
||||
selectedKey.index = index;
|
||||
}
|
||||
|
||||
function keyup(e: KeyboardEvent) {
|
||||
if (selectedKey.id === 'none') return;
|
||||
e.preventDefault();
|
||||
const code = keycode(e.keyCode);
|
||||
|
||||
const assist = generateBinary([e.ctrlKey, e.shiftKey, e.altKey]);
|
||||
const id =
|
||||
selectedKey.index === -1
|
||||
? selectedKey.id
|
||||
: `${selectedKey.id}_${selectedKey.index}`;
|
||||
|
||||
const key = keyData[selectedGroup.value][selectedKey.id].keys.find(
|
||||
v => v.index === selectedKey.index
|
||||
);
|
||||
|
||||
if (key) {
|
||||
if (
|
||||
code === KeyCode.Ctrl ||
|
||||
code === KeyCode.Shift ||
|
||||
code === KeyCode.Alt ||
|
||||
code === KeyCode.Unknown
|
||||
) {
|
||||
const data = hotkey.data[id];
|
||||
key.assist = generateBinary([data.ctrl, data.shift, data.alt]);
|
||||
key.key = data.key;
|
||||
} else {
|
||||
key.key = code;
|
||||
key.assist = assist;
|
||||
hotkey.set(id, code, assist);
|
||||
}
|
||||
}
|
||||
|
||||
selectedKey.id = 'none';
|
||||
selectedKey.index = -1;
|
||||
}
|
||||
|
||||
function keydown(e: KeyboardEvent) {
|
||||
if (selectedKey.id === 'none') return;
|
||||
e.preventDefault();
|
||||
const code = keycode(e.keyCode);
|
||||
const assist = generateBinary([e.ctrlKey, e.shiftKey, e.altKey]);
|
||||
const key = keyData[selectedGroup.value][selectedKey.id].keys.find(
|
||||
v => v.index === selectedKey.index
|
||||
);
|
||||
if (!key) return;
|
||||
key.assist = assist;
|
||||
key.key = KeyCode.Unknown;
|
||||
|
||||
if (
|
||||
code !== KeyCode.Ctrl &&
|
||||
code !== KeyCode.Shift &&
|
||||
code !== KeyCode.Alt
|
||||
) {
|
||||
key.key = code;
|
||||
}
|
||||
}
|
||||
|
||||
// ban other keys
|
||||
gameKey.disable();
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keyup', keyup);
|
||||
document.addEventListener('keydown', keydown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
gameKey.enable();
|
||||
document.removeEventListener('keyup', keyup);
|
||||
document.removeEventListener('keydown', keydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#hotkey-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hotkey-group-one {
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.hotkey-one {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin: 0 3% 2% 3%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hotkey-one-set {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 50%;
|
||||
text-overflow: clip;
|
||||
align-items: flex-end;
|
||||
text-align: end;
|
||||
|
||||
.hotkey-one-set-item {
|
||||
background-color: #4446;
|
||||
padding: 0 5%;
|
||||
margin: 1% 0;
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.hotkey-one-set-item[selected='true'] {
|
||||
color: gold;
|
||||
border: 1px solid gold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -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';
|
||||
@ -1,366 +0,0 @@
|
||||
<template>
|
||||
<div class="setting-main">
|
||||
<div id="tools">
|
||||
<span class="button-text" @click="exit"
|
||||
><left-outlined /> 返回游戏</span
|
||||
>
|
||||
</div>
|
||||
<div class="setting-container">
|
||||
<div class="setting-select">
|
||||
<TransitionGroup name="list">
|
||||
<div
|
||||
v-for="(info, i) of display"
|
||||
:key="i"
|
||||
class="setting-display"
|
||||
>
|
||||
<Scroll class="setting-scroll">
|
||||
<div class="setting-list">
|
||||
<div
|
||||
v-for="item of info.list"
|
||||
class="setting-item selectable"
|
||||
:selected="item === info.item"
|
||||
@click="click(item.key, i, item)"
|
||||
>
|
||||
<span class="setting-name">{{
|
||||
item.name
|
||||
}}</span>
|
||||
<span
|
||||
:selected="item === info.item"
|
||||
class="setting-cascade"
|
||||
v-if="isCascade(item)"
|
||||
>
|
||||
<RightOutlined />
|
||||
</span>
|
||||
<span v-else class="setting-value">
|
||||
{{ getItemValue(item) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Scroll>
|
||||
<Divider
|
||||
class="display-divider"
|
||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
||||
dashed
|
||||
></Divider>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<div class="setting-info">
|
||||
<Scroll class="info-text-scroll">
|
||||
<div
|
||||
class="info-text"
|
||||
v-html="
|
||||
splitText(display.at(-1)?.text ?? ['请选择设置'])
|
||||
"
|
||||
></div>
|
||||
</Scroll>
|
||||
<Divider class="info-divider" dashed></Divider>
|
||||
<div class="info-editor" v-if="!!selectedItem">
|
||||
<div class="editor-custom">
|
||||
<component
|
||||
:is="selectedItem.controller as any"
|
||||
:item="selectedItem"
|
||||
:displayer="displayer"
|
||||
:setting="setting"
|
||||
:update="update"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onUnmounted, ref, shallowRef } from 'vue';
|
||||
import { mainSetting } from '../preset/settingIns';
|
||||
import {
|
||||
MotaSetting,
|
||||
MotaSettingItem,
|
||||
SettingDisplayer,
|
||||
SettingDisplayInfo
|
||||
} from '../setting';
|
||||
import { RightOutlined, LeftOutlined } from '@ant-design/icons-vue';
|
||||
import { splitText } from '../utils';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import { isMobile } from '../use';
|
||||
import { gameKey } from '@motajs/system';
|
||||
import { IMountedVBind } from '../interface';
|
||||
import { Divider } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps<IMountedVBind>();
|
||||
|
||||
const setting = props.info ?? mainSetting;
|
||||
const display = shallowRef<SettingDisplayInfo[]>([]);
|
||||
const selectedItem = computed(() => display.value.at(-1)?.item);
|
||||
const update = ref(false);
|
||||
|
||||
const displayer = new SettingDisplayer(setting);
|
||||
displayer.on('update', (stack, dis) => {
|
||||
display.value = dis;
|
||||
update.value = !update.value;
|
||||
});
|
||||
display.value = displayer.displayInfo;
|
||||
|
||||
function getItemValue(item: MotaSettingItem) {
|
||||
if (item.value instanceof MotaSetting) {
|
||||
return '';
|
||||
} else {
|
||||
if (item.display) {
|
||||
return item.display(item.value);
|
||||
} else {
|
||||
if (typeof item.value === 'number') {
|
||||
return item.value.toString();
|
||||
} else {
|
||||
return item.value ? 'ON' : 'OFF';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isCascade(item: MotaSettingItem) {
|
||||
return item.value instanceof MotaSetting;
|
||||
}
|
||||
|
||||
function click(key: string, index: number, item: MotaSettingItem) {
|
||||
if (item.value instanceof MotaSetting) {
|
||||
if (index === display.value.length - 1) {
|
||||
displayer.add(key);
|
||||
} else {
|
||||
if (displayer.selectStack.includes(key)) {
|
||||
displayer.cut(index);
|
||||
} else {
|
||||
displayer.cut(index, true);
|
||||
displayer.add(key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
displayer.cut(index, true);
|
||||
displayer.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
function exit() {
|
||||
props.controller.close(props.num);
|
||||
}
|
||||
|
||||
gameKey.use(props.ui.symbol);
|
||||
gameKey.realize('exit', () => {
|
||||
exit();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
gameKey.dispose(props.ui.symbol);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#tools {
|
||||
width: 100%;
|
||||
font-family: 'normal';
|
||||
font-size: 3.2vh;
|
||||
height: 5vh;
|
||||
position: fixed;
|
||||
left: 10vw;
|
||||
top: 10vh;
|
||||
}
|
||||
|
||||
.setting-main {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
font-family: normal;
|
||||
font-size: 150%;
|
||||
|
||||
.setting-container {
|
||||
height: 60%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-select {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.setting-display {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
|
||||
.setting-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.display-divider {
|
||||
border-color: #fff4;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
padding: 2%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.setting-cascade {
|
||||
font-size: 75%;
|
||||
margin-right: 40px;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.setting-cascade[selected='true'] {
|
||||
font-size: 90%;
|
||||
margin-right: 10px;
|
||||
color: cyan;
|
||||
}
|
||||
|
||||
.setting-name {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 70%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.setting-value {
|
||||
margin-right: 10px;
|
||||
color: rgb(242, 255, 101);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-scroll {
|
||||
width: 20vw;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.setting-info::v-deep(.editor-boolean) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 0 10% 0 5%;
|
||||
|
||||
.boolean-button {
|
||||
font-size: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-info::v-deep(.editor-number) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 0 10% 0 5%;
|
||||
|
||||
.number-input,
|
||||
.number-button {
|
||||
font-size: 80%;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.number-button {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.editor-number-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
width: 25vw;
|
||||
|
||||
.info-divider {
|
||||
border-color: #fff4;
|
||||
margin: 2% 0;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 85%;
|
||||
min-height: 30%;
|
||||
max-height: 50%;
|
||||
}
|
||||
|
||||
.info-text-scroll {
|
||||
max-height: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#tools {
|
||||
top: 2vh;
|
||||
}
|
||||
|
||||
.setting-main {
|
||||
font-size: 225%;
|
||||
|
||||
.setting-container {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.setting-select {
|
||||
flex-direction: column;
|
||||
width: 90vw;
|
||||
}
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
|
||||
.setting-scroll {
|
||||
width: 90vw;
|
||||
height: 20vh;
|
||||
}
|
||||
|
||||
.setting-display {
|
||||
width: 90vw;
|
||||
flex-direction: column;
|
||||
height: 22vh;
|
||||
|
||||
.display-divider {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin: 1vh 0;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
width: 90vw;
|
||||
height: 30vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,484 +0,0 @@
|
||||
<template>
|
||||
<div id="shop">
|
||||
<div id="tools">
|
||||
<span class="button-text" @click="exit"
|
||||
><left-outlined /> 返回游戏</span
|
||||
>
|
||||
</div>
|
||||
<span id="item-name">{{ info.name }}</span>
|
||||
<Divider dashed style="border-color: #ddd4" id="divider"></Divider>
|
||||
<div id="item-info">
|
||||
<Scroll id="item-desc" :no-scroll="true">
|
||||
<span v-html="desc"></span>
|
||||
</Scroll>
|
||||
</div>
|
||||
<Divider
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
id="divider-split"
|
||||
></Divider>
|
||||
<div id="shop-bottom">
|
||||
<div id="item-list">
|
||||
<Scroll style="width: 100%; height: 100%">
|
||||
<div
|
||||
class="selectable item-one"
|
||||
v-for="(i, index) of choices"
|
||||
:selected="index === selected"
|
||||
@click="selected = index"
|
||||
>
|
||||
<div class="item-icon">
|
||||
<BoxAnimate
|
||||
:id="i.id"
|
||||
:noborder="true"
|
||||
></BoxAnimate>
|
||||
<span>{{ all[i.id].name }}</span>
|
||||
</div>
|
||||
<span
|
||||
>× {{
|
||||
(mode === 'buy'
|
||||
? i.number - (f.itemShop[id]?.[i.id] ?? 0)
|
||||
: cnt(i.id)) + (update ? 0 : 0)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</Scroll>
|
||||
</div>
|
||||
<Divider
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
type="vertical"
|
||||
id="divider-vertical"
|
||||
></Divider>
|
||||
<div id="item-sell-info">
|
||||
<div id="shop-mode">
|
||||
<span
|
||||
class="button-text mode-button"
|
||||
:active="mode === 'buy'"
|
||||
@click="mode = 'buy'"
|
||||
>购买</span
|
||||
>
|
||||
<Divider
|
||||
dashed
|
||||
type="vertical"
|
||||
id="divider-vertical"
|
||||
style="border-color: #ddd4"
|
||||
></Divider>
|
||||
<span
|
||||
class="button-text mode-button"
|
||||
:active="mode === 'sell'"
|
||||
danger="true"
|
||||
@click="mode = 'sell'"
|
||||
>售出</span
|
||||
>
|
||||
</div>
|
||||
<Divider
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
id="divider-mode"
|
||||
></Divider>
|
||||
<div
|
||||
class="item-sell-info"
|
||||
:style="{
|
||||
color:
|
||||
mode === 'buy'
|
||||
? nowMoney >= parseInt(item.money)
|
||||
? 'lightgreen'
|
||||
: 'lightcoral'
|
||||
: 'white'
|
||||
}"
|
||||
>
|
||||
<span>买价</span>
|
||||
<span>{{ item.money }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="item-sell-info"
|
||||
:style="{ color: mode === 'sell' ? 'lightcoral' : 'white' }"
|
||||
>
|
||||
<span>卖价</span>
|
||||
<span>{{ item.sell }}</span>
|
||||
</div>
|
||||
<div class="item-sell-info">
|
||||
<span>存货</span>
|
||||
<span>{{ remain }}</span>
|
||||
</div>
|
||||
<div class="item-sell-info">
|
||||
<span>拥有</span>
|
||||
<span>{{ cnt(item.id) }}</span>
|
||||
</div>
|
||||
<Divider
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
id="divider-mode"
|
||||
></Divider>
|
||||
<div id="sell-count">
|
||||
<span>{{ mode === 'buy' ? '购买' : '售出' }}数量</span>
|
||||
<div id="sell-count-select">
|
||||
<double-left-outlined
|
||||
class="button-text"
|
||||
@click="count -= 10"
|
||||
/>
|
||||
<left-outlined class="button-text" @click="count--" />
|
||||
<span id="fly-now">{{ count }}</span>
|
||||
<right-outlined class="button-text" @click="count++" />
|
||||
<double-right-outlined
|
||||
class="button-text"
|
||||
@click="count += 10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Divider
|
||||
dashed
|
||||
style="border-color: #ddd4; margin: 2vh 0 2vh 0"
|
||||
></Divider>
|
||||
<div id="sell-confirm">
|
||||
<span
|
||||
id="sell-total"
|
||||
:style="{
|
||||
color:
|
||||
mode === 'buy'
|
||||
? nowMoney > cost
|
||||
? 'lightgreen'
|
||||
: 'lightcoral'
|
||||
: 'lightcoral'
|
||||
}"
|
||||
>总价:{{
|
||||
count *
|
||||
parseInt(mode === 'buy' ? item.money : item.sell)
|
||||
}}</span
|
||||
>
|
||||
<span
|
||||
id="sell-button"
|
||||
class="button-text"
|
||||
:danger="mode === 'sell'"
|
||||
danger-display="true"
|
||||
active="true"
|
||||
@click="confirm"
|
||||
>确认{{ mode === 'buy' ? '购买' : '售出' }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import {
|
||||
LeftOutlined,
|
||||
DoubleLeftOutlined,
|
||||
RightOutlined,
|
||||
DoubleRightOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { splitText } from '../utils';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import BoxAnimate from '../components/boxAnimate.vue';
|
||||
import { gameKey } from '@motajs/system';
|
||||
import { IMountedVBind } from '../interface';
|
||||
import { tip } from '../use';
|
||||
import { Divider } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps<IMountedVBind>();
|
||||
|
||||
const id = props.shopId;
|
||||
const shop = core.status.shops[id] as ItemShopEvent;
|
||||
if (!shop.item) {
|
||||
throw new TypeError(
|
||||
`Wrong global shop type delivered in opening item shop.`
|
||||
);
|
||||
}
|
||||
|
||||
flags.itemShop ??= {};
|
||||
|
||||
const f = flags;
|
||||
|
||||
const choices = shop.choices;
|
||||
const selected = ref(0);
|
||||
const mode = ref<'sell' | 'buy'>('buy');
|
||||
const all = core.material.items;
|
||||
const cnt = core.itemCount;
|
||||
const count = ref(0);
|
||||
const nowMoney = ref(core.status.hero.money);
|
||||
const update = ref(false);
|
||||
|
||||
let bought = false;
|
||||
|
||||
watch(count, n => {
|
||||
if (n < 0) n = 0;
|
||||
if (mode.value === 'buy') {
|
||||
if (n > remain.value) n = remain.value;
|
||||
} else {
|
||||
const c = cnt(item.value.id);
|
||||
if (n > c) n = c;
|
||||
}
|
||||
count.value = n;
|
||||
});
|
||||
|
||||
const item = computed(() => {
|
||||
return choices[selected.value];
|
||||
});
|
||||
const remain = computed(() => {
|
||||
update.value;
|
||||
return item.value.number - (flags.itemShop[id]?.[item.value.id] ?? 0);
|
||||
});
|
||||
const info = computed(() => {
|
||||
return core.material.items[item.value.id];
|
||||
});
|
||||
const desc = computed(() => {
|
||||
const text = info.value.text!;
|
||||
const res = splitText([text.startsWith('!!html') ? text.slice(6) : text]);
|
||||
return res;
|
||||
});
|
||||
const cost = computed(() => {
|
||||
return (
|
||||
count.value *
|
||||
parseInt(mode.value === 'buy' ? item.value.money : item.value.sell)
|
||||
);
|
||||
});
|
||||
|
||||
watch(remain, n => {
|
||||
if (n < count.value) {
|
||||
count.value = n;
|
||||
}
|
||||
});
|
||||
|
||||
function confirm() {
|
||||
if (count.value === 0) {
|
||||
return;
|
||||
}
|
||||
const money = core.status.hero.money;
|
||||
bought = true;
|
||||
if (mode.value === 'buy') {
|
||||
if (cost.value <= money) {
|
||||
core.getItem(item.value.id, count.value);
|
||||
core.status.hero.money -= cost.value;
|
||||
nowMoney.value -= cost.value;
|
||||
flags.itemShop[id] ??= {};
|
||||
flags.itemShop[id][item.value.id] ??= 0;
|
||||
flags.itemShop[id][item.value.id] += count.value;
|
||||
tip('success', `成功购买${count.value}个${info.value.name}!`);
|
||||
core.status.route.push(`buy:${item.value.id}:${count.value}`);
|
||||
} else {
|
||||
tip('error', '你的金币不够!');
|
||||
}
|
||||
} else {
|
||||
core.addItem(item.value.id, -count.value);
|
||||
core.status.hero.money += cost.value;
|
||||
nowMoney.value += cost.value;
|
||||
flags.itemShop[id] ??= {};
|
||||
flags.itemShop[id][item.value.id] ??= 0;
|
||||
flags.itemShop[id][item.value.id] -= count.value;
|
||||
tip('success', `成功卖出${count.value}个${info.value.name}!`);
|
||||
core.status.route.push(`sell:${item.value.id}:${count.value}`);
|
||||
}
|
||||
count.value = 0;
|
||||
update.value = !update.value;
|
||||
}
|
||||
|
||||
gameKey.use(props.ui.symbol);
|
||||
gameKey
|
||||
.realize('@shop_up', () => {
|
||||
if (selected.value >= 1) {
|
||||
selected.value--;
|
||||
}
|
||||
})
|
||||
.realize('@shop_down', () => {
|
||||
if (selected.value <= choices.length - 2) {
|
||||
selected.value++;
|
||||
}
|
||||
})
|
||||
.realize(
|
||||
'@shop_add',
|
||||
() => {
|
||||
count.value++;
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@shop_min',
|
||||
() => {
|
||||
count.value--;
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize('exit', () => {
|
||||
exit();
|
||||
})
|
||||
.realize('confirm', () => {
|
||||
confirm();
|
||||
})
|
||||
.realize('shop', () => {
|
||||
exit();
|
||||
});
|
||||
|
||||
function exit() {
|
||||
if (bought) core.status.route.push('closeShop');
|
||||
props.controller.close(props.num);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
core.lockControl();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
core.unlockControl();
|
||||
gameKey.dispose(props.ui.symbol);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#shop {
|
||||
width: 90vh;
|
||||
height: 90vh;
|
||||
font-family: 'normal';
|
||||
font-size: 150%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#tools {
|
||||
height: 5vh;
|
||||
font-size: 3.2vh;
|
||||
}
|
||||
|
||||
#item-name {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 140%;
|
||||
height: 5vh;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#item-info {
|
||||
height: 24vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#divider {
|
||||
width: 100%;
|
||||
margin: 1vh 0 1vh 0;
|
||||
}
|
||||
|
||||
#divider-split {
|
||||
margin: 1vh 0 0 0;
|
||||
}
|
||||
|
||||
#divider-vertical {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#shop-bottom {
|
||||
height: 53vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#item-list {
|
||||
height: 53vh;
|
||||
width: 53vh;
|
||||
padding: 1vh 0 1vh 0;
|
||||
}
|
||||
|
||||
#item-sell-info {
|
||||
width: 47vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
margin-left: 5%;
|
||||
}
|
||||
}
|
||||
|
||||
.item-one {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
|
||||
#shop-mode {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
height: 5vh;
|
||||
}
|
||||
|
||||
.mode-button {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#divider-mode {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item-sell-info {
|
||||
width: 60%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin: 1vh 0;
|
||||
transition: color 0.2s linear;
|
||||
}
|
||||
|
||||
#sell-count {
|
||||
width: 100%;
|
||||
padding-top: 1vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#sell-count-select {
|
||||
margin-top: 1vh;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
#sell-confirm {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
#sell-total {
|
||||
transition: color 0.2s linear;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#shop {
|
||||
width: 90vw;
|
||||
padding-top: 5vh;
|
||||
font-size: 225%;
|
||||
}
|
||||
|
||||
#item-list {
|
||||
width: 40vw;
|
||||
}
|
||||
|
||||
#shop-bottom {
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
#item-sell-info {
|
||||
width: 50vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,437 +0,0 @@
|
||||
<template>
|
||||
<div id="toolbox">
|
||||
<div id="tools">
|
||||
<span class="button-text tools" @click="exit"
|
||||
><left-outlined /> 返回游戏</span
|
||||
>
|
||||
<span class="button-text tools" @click="toEquip"
|
||||
>装备栏 <right-outlined
|
||||
/></span>
|
||||
</div>
|
||||
<div id="toolbox-main">
|
||||
<div v-for="cls of toShow" class="item-main">
|
||||
<div class="item-info">
|
||||
<div class="item-type" v-if="!isMobile">
|
||||
{{ getClsName(cls) }}
|
||||
</div>
|
||||
<div v-else id="item-type-mobile">
|
||||
<span
|
||||
class="button-text"
|
||||
@click="mode = 'tools'"
|
||||
:selected="mode === 'tools'"
|
||||
>消耗道具</span
|
||||
>
|
||||
<Divider
|
||||
dashed
|
||||
style="border-color: #ddd4; height: 100%"
|
||||
type="vertical"
|
||||
></Divider>
|
||||
<span
|
||||
class="button-text"
|
||||
@click="mode = 'constants'"
|
||||
:selected="mode === 'constants'"
|
||||
>永久道具</span
|
||||
>
|
||||
</div>
|
||||
<Divider
|
||||
dashed
|
||||
style="margin: 1vh 0 1vh 0; border-color: #ddd4"
|
||||
></Divider>
|
||||
<Scroll class="item-list">
|
||||
<div
|
||||
class="item selectable"
|
||||
v-for="[id, num] of items[cls]"
|
||||
:selected="selected === id"
|
||||
@click="
|
||||
mode = cls;
|
||||
select(id);
|
||||
"
|
||||
>
|
||||
<div class="item-icon">
|
||||
<BoxAnimate
|
||||
:id="id"
|
||||
:width="32"
|
||||
:height="32"
|
||||
:noborder="true"
|
||||
></BoxAnimate>
|
||||
<span class="item-name">{{
|
||||
all[id].name
|
||||
}}</span>
|
||||
</div>
|
||||
<span>× {{ num }}</span>
|
||||
</div>
|
||||
</Scroll>
|
||||
</div>
|
||||
<Divider
|
||||
dashed
|
||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
||||
class="divider"
|
||||
style="border-color: #ddd4; margin: 1%"
|
||||
></Divider>
|
||||
</div>
|
||||
<div id="detail">
|
||||
<div id="info">
|
||||
<BoxAnimate
|
||||
:id="selected"
|
||||
:width="32"
|
||||
:height="32"
|
||||
></BoxAnimate>
|
||||
<div id="basic-info">
|
||||
<span style="border-bottom: 1px solid #ddd4">{{
|
||||
selected === 'none'
|
||||
? '没有道具'
|
||||
: all[selected].name
|
||||
}}</span>
|
||||
<span>{{
|
||||
selected === 'none'
|
||||
? '永久道具'
|
||||
: (getClsName(all[selected].cls as ItemMode) ??
|
||||
'永久道具')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span style="margin-top: 2vh">点击该物品以使用</span>
|
||||
<Divider dashed style="border-color: #ddd4"></Divider>
|
||||
<div id="desc">
|
||||
<span>道具描述</span>
|
||||
<Scroll id="desc-text">
|
||||
<div v-if="!descText.value!.startsWith('!!html')">
|
||||
{{ descText.value }}
|
||||
</div>
|
||||
<div v-else v-html="descText.value!.slice(6)"></div>
|
||||
</Scroll>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import BoxAnimate from '../components/boxAnimate.vue';
|
||||
import { getClsName, getItems } from '../tools/toolbox';
|
||||
import { isMobile } from '../use';
|
||||
import { type } from '../utils';
|
||||
import { hyper } from 'mutate-animate';
|
||||
import { Divider, message } from 'ant-design-vue';
|
||||
import { gameKey } from '@motajs/system';
|
||||
import { IMountedVBind } from '../interface';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
const props = defineProps<IMountedVBind>();
|
||||
|
||||
type ItemMode = 'tools' | 'constants';
|
||||
type ShowItemIds = ItemIdOf<'constants' | 'tools'> | 'none';
|
||||
|
||||
const mode = ref<ItemMode>('tools');
|
||||
|
||||
const items = getItems('all');
|
||||
|
||||
const toShow = computed<ItemMode[]>(() => {
|
||||
return isMobile ? [mode.value] : ['tools', 'constants'];
|
||||
});
|
||||
|
||||
const all = core.material.items;
|
||||
|
||||
const selected = ref<ShowItemIds>(items[toShow.value[0]][0]?.[0] ?? 'none');
|
||||
const index = ref(0);
|
||||
|
||||
watch(index, n => {
|
||||
select(items[mode.value][n][0], true);
|
||||
});
|
||||
|
||||
watch(mode, n => {
|
||||
if (isNil(items[n][index.value])) {
|
||||
selected.value = 'none';
|
||||
return;
|
||||
}
|
||||
select(items[n][index.value][0], true);
|
||||
});
|
||||
|
||||
const descText = computed(() => {
|
||||
const s = selected.value;
|
||||
if (s === 'none') return ref('没有选择道具');
|
||||
return type(all[s].text!, 25, hyper('sin', 'out'), true);
|
||||
});
|
||||
|
||||
/**
|
||||
* 选择一个道具时
|
||||
* @param id 道具id
|
||||
*/
|
||||
async function select(id: ShowItemIds, nouse: boolean = false) {
|
||||
if (selected.value === id && !nouse) {
|
||||
use(id);
|
||||
}
|
||||
index.value = items[mode.value].findIndex(v => v[0] === id);
|
||||
selected.value = id;
|
||||
}
|
||||
|
||||
function exit() {
|
||||
props.controller.close(props.num);
|
||||
}
|
||||
|
||||
function use(id: ShowItemIds) {
|
||||
if (id === 'none') return;
|
||||
if (core.canUseItem(id)) {
|
||||
const hold = props.controller.holdOn();
|
||||
exit();
|
||||
nextTick(() => {
|
||||
core.tryUseItem(id, false, () => {
|
||||
if (props.controller.stack.length === 0) {
|
||||
hold.end(core.status.event.id !== 'toolbox');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
message.warn({
|
||||
content: '当前无法使用该道具!',
|
||||
class: 'antdv-message'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function toEquip() {
|
||||
props.controller.holdOn();
|
||||
exit();
|
||||
nextTick(() => {
|
||||
props.controller.open('equipbox');
|
||||
});
|
||||
}
|
||||
|
||||
gameKey.use(props.ui.symbol);
|
||||
gameKey
|
||||
.realize(
|
||||
'@toolbox_right',
|
||||
() => {
|
||||
const constants = items.constants.length;
|
||||
if (mode.value === 'tools') {
|
||||
if (index.value >= constants) {
|
||||
index.value = constants - 1;
|
||||
}
|
||||
mode.value = 'constants';
|
||||
}
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@toolbox_left',
|
||||
() => {
|
||||
const constants = items.tools.length;
|
||||
if (mode.value === 'constants') {
|
||||
if (index.value >= constants) {
|
||||
index.value = constants - 1;
|
||||
}
|
||||
mode.value = 'tools';
|
||||
}
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@toolbox_up',
|
||||
() => {
|
||||
if (index.value > 0) {
|
||||
index.value--;
|
||||
}
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@toolbox_down',
|
||||
() => {
|
||||
const total = items[mode.value].length;
|
||||
if (index.value < total - 1) {
|
||||
index.value++;
|
||||
}
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize('exit', () => {
|
||||
exit();
|
||||
})
|
||||
.realize('confirm', () => {
|
||||
use(selected.value);
|
||||
})
|
||||
.realize('toolbox', () => {
|
||||
exit();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
gameKey.dispose(props.ui.symbol);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#toolbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#toolbox-main {
|
||||
width: 100%;
|
||||
height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
font-family: 'normal';
|
||||
font-size: 150%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#tools {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-family: 'normal';
|
||||
font-size: 3.2vh;
|
||||
height: 5vh;
|
||||
justify-content: space-between;
|
||||
|
||||
.tools {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.item-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.item-info {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.item-list {
|
||||
width: 100%;
|
||||
height: 88vh;
|
||||
}
|
||||
|
||||
.item-type {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#item-type-mobile {
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-text[selected='true'] {
|
||||
color: aqua;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 1vh 0 0 0;
|
||||
padding: 0.5vh 0.5vw;
|
||||
border: #0000 0.5px solid;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
.item-icon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.item-name {
|
||||
margin-left: 5%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
#info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
#basic-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid #ddd4;
|
||||
padding-left: 5%;
|
||||
margin-left: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
#desc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-basis: 50%;
|
||||
|
||||
#desc-text {
|
||||
margin-top: 2vh;
|
||||
margin-left: 0.5vw;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#toolbox-main {
|
||||
flex-direction: column-reverse;
|
||||
height: 90%;
|
||||
font-size: 225%;
|
||||
margin-top: 10%;
|
||||
}
|
||||
|
||||
.item-list {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item-main {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
#detail {
|
||||
flex-basis: 30%;
|
||||
|
||||
#desc {
|
||||
#desc-text {
|
||||
max-height: 10vh;
|
||||
height: 10vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -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<KeyboardEmits> {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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<keyof MessageApi, 'open' | 'config' | 'destroy'>,
|
||||
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<DragFn, DragMap>();
|
||||
|
||||
/**
|
||||
* 是否是移动设备
|
||||
*/
|
||||
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);
|
||||
});
|
||||
}
|
||||
@ -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<Record<CanParseCss, string>> {
|
||||
if (css.length === 0) return {};
|
||||
|
||||
let pointer = -1;
|
||||
let inProp = true;
|
||||
let prop = '';
|
||||
let value = '';
|
||||
let upper = false;
|
||||
const res: Partial<Record<CanParseCss, string>> = {};
|
||||
|
||||
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<Record<CanParseCss, string>>) {
|
||||
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<string> {
|
||||
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 !== '<br>' && a[i - 1] === '<br>') ||
|
||||
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<T>(
|
||||
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<T>(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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user