# idle-wuxia **Repository Path**: aowind/idle-wuxia ## Basic Information - **Project Name**: idle-wuxia - **Description**: 江湖 · 山门 —— 挂机武侠:山门建设 / 装备系统(品质·词条·套装·传奇威能)/ 仓库 / 编队 / 技能书 / 挂机掉落 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-07 - **Last Updated**: 2026-10-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 江湖 · 山门(idle-wuxia) > 一款用浏览器打开的**武侠挂机游戏** —— 挂机推图、营建山门、招募门人、参悟功法。 > 后端是一个**权威世界**,前端只是一扇**渲染视窗**。 > > 🎮 **线上体验:**

Three.js Vite Node Playwright

--- ## 一、这是一款什么游戏 玩家是掌门,三条线同时推进: | 线 | 内容 | |---|---| | **挂机线** | 门人在「江湖游历」大地图的 **71 个关卡**(12 区域)自动战斗,打怪拿经验 / 金币 / 装备 / 材料 / 功法,一路往深处推 | | **经营线** | 回到山门(**2D 折纸山**),8 类建筑开局即就位,靠**升级建筑等级**提升产能与名额 | | **养成线** | 人物属性 + 装备 8 槽 + 功法 7 格,三层养成叠加出战力 | **核心原则:后端权威。** 所有状态存服务器(`server/worlds//`),前端只负责展示; 掉落、升级、结算全在后端算 —— 刷新页面不丢进度,多标签页不会打架。 --- ## 二、核心循环 ``` 挂机推图 ──► 掉落入仓 ──► 养成变强 ──► 招募扩编 ──► 升级建筑 ──┐ 71 关自动战斗 装备/材料/功法 穿装备·学功法 大殿刷候选 三材升级 1→5 级 打怪结算经验金币 后端 roll·入仓 人物升级 厢房招新门人 产能/名额提升 ▲ │ └────────────────────────────────────────────────────────────────────┘ ``` 推动循环的两大引擎:**关卡推进**(解锁更强的怪与掉落档位)与**声望积累** (决定建筑能升到几级 —— 关卡进度就是同一把尺子)。 --- ## 三、系统一览 | 系统 | 要点 | 文档 | |---|---|---| | 江湖游历 | 12 区域 × 71 关;每关刷小怪,杀满可打 Boss;区域末关必掉 4 件金以上装备 | `敌人图鉴与关卡生态设计.html` | | 山门建筑 | 8 类建筑(大殿 / 武器铺 / 仓库 / 四种资源田 / 厢房),各可升 **1→5 级** | `建筑图鉴.html` · `建造与资源.html` | | 门人 | **18 职业 × 男女**;资质 8 档(甲→辛)决定属性池;口粮 50/人/游戏天 | `人物养成.html` · `厢房系统设计.html` | | 装备 | 8 槽;品质白→绿→蓝→金→传奇;加工 +1~+10 / 鉴定 / 铭刻 / 萃取 | `装备图鉴.html` · `装备强化与加工.html` | | 功法 | 内功 2 + 外功 2 + 武器功法 3 = 7 格;总量 **240 条**;五品阶 | `功法收集与升级.html` | | 声望 | 历练声望 + 门派声望;只决定建筑等级上限(0 / 260 / 980 / 2350 / 4800) | `声望系统设计.html` | | 资源 | 顶栏铁石木粮(田产)+ 金币(战斗掉);仓库材料 纸 / 玄铁 / 玄晶碎片 / 灵玉 | `建造与资源.html` | | 管理后台 | `GET /admin`:注册账号 / 在线情况 / 每位玩家的进度·门人·装备·功法(**只读**) | `server/admin.mjs` | | 运营数据 | 在管理后台内:按天/按区间的 **DAU**、**在线时长**、**来源 IP 与地区** | 见下方「运营统计」 | **运营统计**(管理后台 →「运营数据」,2026-10-10 上线): | 指标 | 口径 | |---|---| | 在线时长 | 相邻两次请求间隔 **≤ 120 秒**的那部分之和。超时视为离开,中间空档**一分钟都不算** —— 「开着页面去睡觉」不会刷出 8 小时在线 | | DAU | 当天有过请求的账号数(按**本地日期**分桶,服务器在 CST 才有"今天"的直觉) | | 来源 IP | 优先读 `X-Forwarded-For`(生产是 nginx 全量反代,直接用 `socket.remoteAddress` 会**全是 127.0.0.1**);地区走 ip-api.com 批量接口,一个 IP 只查一次、永久缓存,可关 | | 保留 | 180 天自动裁剪;落在 `server/stats/analytics.json`(**活数据,发布时不同步**) | > ⚠️ **统计从上线那天开始攒**:账号表里只有注册时间、存档里只有"最后一次写入时间", > 历史 DAU / 在线时长**回溯不出来**(当时没存)。唯一能回溯的是「当日新增账号」 > (由 `createdAt` 推导)—— 所以上线首日图上会看到"前几天有新增折线、DAU 柱子却是空的", > 这是对的,不是 bug。 **时间模型**:倍率 ×60 —— 1 真实秒 = 1 游戏分钟,**24 真实分钟 = 1 游戏天**(开局清晨 07:30)。 资源田每游戏小时产出,囤满停产、满仓自动收;离线最多补 24 真实分钟。 --- ## 四、跑起来 ```bash npm install # 开发:后端(127.0.0.1:8000) + 前端(127.0.0.1:5180),前端走 vite 代理打 /api node tools/dev.mjs # 也可以分开起: npm run server | npm run dev # 发布:打包编译出 dist/(8000 端口伺服的发布形态) npm run build ``` | 端口 | 用途 | |---|---| | **5180** | vite dev,改动即时生效 —— **开发用这个** | | **8000** | node 后端;同时伺服 `dist/` 构建产物 —— **发布形态**,改源码不会自动更新,要重新 build | > 🔴 8000 端口伺服的是 **dist/**(构建产物),不是源码。改完源码必须 `npm run build` 才生效。 --- ## 五、技术架构 ### 5.1 后端权威 + 前端插值 - 每个账号一套世界:`server/worlds//{state,resources,heroes,equips,arts,buildings,idle,squads}.json` - 鉴权:邮箱 + 密码(scrypt 加盐),会话 token 由服务端持有(**故意不用 JWT** —— 要能立刻踢会话)。 除 `health / config / auth` 外全部接口要 `Authorization: Bearer `, 服务端从 token 反查 userId 再取世界 ⇒ **数据隔离是结构性的**,不靠每个接口自觉过滤。 - 存储:**JSON 文件 + tmp→rename 原子写**(重试 3 次防杀软 EPERM)。单机规模,不引数据库。 - 时间:**锚点式**,只存 `anchorReal + anchorHour + speed`,当前时刻由公式推导。 不跑定时器 ⇒ 重启后自动连续、跨天计数不归零。 ### 5.2 为什么后端是 Node 而不是 Python 本机安全策略**拦截 python 进程对外提供 HTTP 服务**:服务 LISTENING 正常, 但任何客户端连接都 ETIMEDOUT(换端口 / 路径 / 进程名 / 绑定地址全试过无效), 而 node 同方式起服务 curl 直接 200。⇒ 后端用 Node(零依赖 `node:http`), Python 只做**离线工具**(生图、生视频、Blender 脚本)。 ### 5.3 判据外置(前后端共用一份真源) 写死在代码里的断言要整理成对应的 JSON: | 判据归属 | json 位置 | 谁读 | |---|---|---| | 前端模块 `src/<模块>.js` | `src/specs/<模块>.json` | vite `import`,构建时内联进 bundle | | 页面 `docs/<页面>.html` | `docs/specs/<页面>.json` | 生成器读 + 构建时内联 | | 验收脚本 `tools/<脚本>.mjs` | `tools/specs/<脚本>.json` | `tools/spec.mjs` 的 `spec(name)` | | 前后端共用(如开号名字 2–8 字) | `src/specs/starter.json` | 前端 import **+** 后端 `fs.readFileSync`(同一份,杜绝"前端放行后端 400") | ### 5.4 发布时剥离 HTML 注释 源码里的 `` 是**开发笔记**(写清了每个区块的来历与踩过的坑), 是资产、但不该随页面发给外人。所以 **源码一个字不动**,只在"出门"的两个环节剥掉: - 构建:`tools/build.mjs` → `strip_html_comments.mjs` 处理 `dist/*.html` - 伺服:`server/index.mjs` 的 `/docs/*` 对 `.html` 剥(带 mtime 缓存) 两处**共用同一份实现**(`tools/strip_html_comments.mjs`,状态机,`script`/`style` 内容原样搬运), 杜绝"构建剥了、docs 没剥"这种不一致。 --- ## 六、目录结构 ``` index.html 游戏主页面(单页,HUD + 浮窗都在这) game-docs.html 游戏文档索引页(左目录右内容,独立入口) action_viewer.html 动作精灵表查看器(开发工具页) src/ 前端(Three.js + 原生 ESM,无框架) main.js 入口:场景装配、相机/灯光/雾、HUD、浮窗接线 api.js 统一注入 token、统一处理 401 login.js 进入山门浮窗(注册 / 登录已合并) hall.js 大殿:招募候选 + 门人装配浮窗 warehouse.js 仓库:装备/材料,加工·鉴定·铭刻·萃取 resources.js 资源系统前端表现层(田产圆圈、收取) journey.js 江湖游历大地图浮窗(71 节点) flatScene.js 2D 折纸美术层(山 + 云,取代 3D 六边形山门) flatBuildings.js 折纸山建筑感应区(点击进功能界面 + 等级标签) daylight.js 日夜光照:10 关键点采样 → 光照 / 雾色 / 染色插值 timeClient.js 时间客户端:后端权威 + 本地插值 celestial.js 日月行天 confirm.js 统一确认浮层(别用裸 window.confirm) idle/ 挂机这一条线 idleWorld.js 江湖世界数据层 idleBattle.js 战斗逻辑(纯逻辑,不做绘制) idleBand.js 底部条带(2D 图片滚动)+ 战斗表现 idleDrops.js 掉落物落地 + 自动拾取飞行 anim.js 动作状态机(纯逻辑) journeyTips.js 游历提示 server/ 后端(Node 零依赖,只用内置模块) index.mjs HTTP 服务与路由(59 个端点)+ dist/ · /docs/ · /admin 的静态伺服 accounts.mjs 账号与会话(scrypt) admin.mjs 管理后台:独立凭据 + **只读**玩家索引(不实例化任何 Store) admin.html 管理后台单页(GET /admin 伺服,原生 JS 零依赖) analytics.mjs 运营统计:在线时长 / DAU / 来源 IP(**只写自己的 server/stats/**) geoip.mjs IP 归属地解析(ip-api.com 批量接口,异步 + 永久缓存,可关) world.mjs 世界管理:每个账号惰性一套 Store clock.mjs 锚点式游戏时钟(权威时间) buildings.mjs 建筑实例与升级 buildingLevel.mjs 建筑等级数值**唯一真源** heroes.mjs 门人:属性、经验、口粮关联 jobs.mjs 十八职业定义 resources.mjs 资源与田产产出 / 收取 upkeep.mjs 口粮消耗 equips.mjs 装备实例:加工 / 鉴定 / 铭刻 / 萃取 equipDefs.mjs 装备模板、词条、套装、威能(静态表) arts.mjs 功法定义与掉落分层 artsStore.mjs 功法仓库与穿戴 skills.mjs 技能表 squads.mjs 编队(单小队最多 3 人) idle.mjs 挂机进度(第几关 / 杀了几只 / Boss / 自动进关) prestige.mjs 声望(纯函数推导,不落盘) ranks.mjs 资质阶梯与招募经济 journeyNodes.mjs 游历节点数值快照(纯函数,零存储) atomicFile.mjs 原子写(tmp + rename + 重试) docs/ 游戏文档(24 篇,游戏内可浏览) catalog.json 分类清单;game-docs.html 索引页读它分组,正文走 /docs/* 由后端伺服 tools/ 离线工具与验收脚本 build.mjs 发布构建(清 dist → vite build → 剥 HTML 注释 → 体积报告) dev.mjs 一键起后端 + 前端 strip_html_comments.mjs HTML 注释剥离(构建与伺服共用) spec.mjs 判据 json 加载器 _shot_*.mjs 无头截图(登录页 / 大殿 / 文档页 / 管理后台 …) verify_admin.mjs 管理后台验收(71 项:鉴权 / 分页 / 排序 / 汇总自洽 / 两套 token 隔离 / 运营统计) _ui_helpers.mjs UI 自测公共引导(注册 → 建主角) gen_*.py 生图 / 生视频 / Blender 出模脚本 deploy/prod/ 生产部署(阿里云) push.sh 一键发布:构建 → 打包 → 上传 → 重启 ecosystem.config.cjs PM2 进程配置 nginx-idle-wuxia.conf 站点配置 verify.mjs 线上验收(HTTP 全链路) verify_ui.mjs 线上验收(真实浏览器 + 出图) ``` --- ## 七、部署到服务器 **服务器**:`8.134.249.230`(阿里云 Alibaba Cloud Linux 4 / 2C1.6G) **代码根**:`/opt/idle-wuxia` **SSH 别名**:`menpai-aliyun` ### 7.1 服务架构 ``` 公网 :80 ──► Nginx ──► 127.0.0.1:8000 的 node 服务 (全量反代,静态不直出 —— 理由见 nginx 配置里的注释) 后端由 PM2 守护(进程名 idle-wuxia)→ pm2-root.service 开机自启 ``` > **为什么 Nginx 不做静态直出**:① 缓存策略(哈希资源 `immutable` / 其余 `no-cache`) > 只有 node 一处真相源,两边各写一份迟早漂;② `/docs/*` 的注释剥离在 node 里做, > Nginx 直出会绕过去,内部笔记直接送到浏览器;③ 未登录可见性由后端路由表判, > 直出会制造绕鉴权的旁路。代价是多一次用户态转发 —— 这台机器流量不大,值。 ### 7.2 发布 ```bash # 日常更新(本机构建 + 上传 + 平滑重载) bash deploy/prod/push.sh # 用现有 dist,只同步后端代码与文档 bash deploy/prod/push.sh --no-build # 首次部署 / 换机器:额外接管 8000 端口并安装 nginx 站点 bash deploy/prod/push.sh --init ``` 发布后跑一遍验收(**两条都要绿**): ```bash node deploy/prod/verify.mjs # HTTP 全链路 25 项 API=http://8.134.249.230 WEB_URL=http://8.134.249.230/ \ node deploy/prod/verify_ui.mjs # 真实浏览器 + 出图 ``` > 后端**零依赖**,服务器上不需要 `npm install` —— 前端在本机构建好再上传。 > 上传约 190M(dist 里是全部美术资产)。 ### 7.3 固化措施 | 措施 | 位置 | 作用 | |---|---|---| | `pm2 save` + `pm2-root` 开机自启 | 系统服务 | 机器重启后自动拉起 | | `autorestart` + `max_restarts: 20` + `min_uptime: 20s` | ecosystem | 崩溃自愈;启动即崩则停下,不再刷日志把 bug 伪装成"服务在跑" | | `kill_timeout: 15s` | ecosystem | 停止时留时间把存档落盘,不丢进度 | | `max_memory_restart: 600M` | ecosystem | 内存护栏(存档在盘上,重启不丢) | | `instances: 1`(**不许开 cluster**) | ecosystem | 世界状态是进程内缓存,多实例会互相覆盖存档 | | `--init` 里先探活再切 nginx | push.sh | 发布过程不出现 502 | **实测**:`kill -9` 掉后端进程,PM2 在 **4 秒内**自动拉起,探活恢复 200。 > 🔐 **管理后台(`/admin`)的凭据不在代码里**(2026-10-10 起) > > 页面是公开的(登录框得能打开),**看得到**全部账号、进度与在线情况,所以 > nginx 全量反代之下,密码就是唯一的门。 > > - 凭据只从环境变量读:`WUXIA_ADMIN_USER`(默认 `admin`)/ `WUXIA_ADMIN_PASS`(**必填**)。 > - 🔴 代码里**没有任何默认密码**:`WUXIA_ADMIN_PASS` 没配 ⇒ 管理登录整体 > **禁用**(fail-closed,`/api/admin/login` 一律 400),不会回落到某个"默认值"。 > 靠默认值上线 = 把一把全服钥匙挂在公网上,这个坑不留。 > - 放密码的地方(都不进仓库,`.gitignore` 已挡): > - 本机:项目根 **`.env`**(`server/index.mjs` 启动时自动读) > - 生产:服务器 **`/opt/idle-wuxia/.env.prod`**(一行 `KEY=VALUE`, > 由 `deploy/prod/ecosystem.config.cjs` 注入 PM2 env) > > ```bash > # 服务器上首次配置(然后必须 --update-env 才生效) > echo 'WUXIA_ADMIN_PASS=换成一个只有你知道的密码' > /opt/idle-wuxia/.env.prod > pm2 restart idle-wuxia --update-env > ``` > > ⚠️ 管理 token 只在进程内存里,重启即失效 —— 管理员重登即可,不是故障。 > 📊 **运营统计是"活数据",发布时不会被覆盖** > > `server/stats/analytics.json` 与 `server/worlds/` 同性质 —— 是服务器上跑出来的东西, > **不在 `push.sh` 的打包列表里**(那里只收 `server/*.mjs` 与 `server/admin.html`)。 > 覆盖它 = 历史 DAU / 在线时长 / IP 归属全没。 > > 想手工清空统计(比如上线前想把自测产生的数据抹掉): > ```bash > rm /opt/idle-wuxia/server/stats/analytics.json && pm2 restart idle-wuxia > # 下次心跳会从当天重新开始记;账号表与存档不受影响 > ``` > 关掉外网 IP 归属查询(隐私考虑):env 里给 `WUXIA_GEO=0` 再 `--update-env`。 ### 7.4 回滚 「门派经营」的代码与配置**原样保留**,没有被删除: ```bash # 1) 恢复 nginx 站点 mv /etc/nginx/conf.d/menpai.conf.disabled-<时间戳> /etc/nginx/conf.d/menpai.conf rm /etc/nginx/conf.d/idle-wuxia.conf systemctl reload nginx # 2) 停江湖、起门派经营(两者都占 8000,不能同时跑) pm2 stop idle-wuxia pm2 start menpai-game pm2 save ``` --- ## 八、游戏文档 游戏内点右上角「**游戏文档**」按钮,或直接开 (左目录右内容,新标签页复用登录态)。 文档分六类,清单在 `docs/catalog.json`: | 类目 | 文档 | |---|---| | 游戏总览 | 游戏总览 | | 玩法系统 | 建筑图鉴 / 建造与资源 / 声望系统设计 / 厢房系统设计 / 大殿设计 / 敌人图鉴与关卡生态设计 | | 升级与收集 | 功法图鉴 / 装备图鉴 / 功法收集与升级 / 装备强化与加工 / 装备合成机制 / 装备合成界面规范 / 人物养成 / 功法系统设计 / 装备系统设计 | | 美术资产 | 门人动作展示 / 门人形象设计 / 门人职业与形象设计 / 敌人动作检查 / 敌人设定图检查 / 人物动作提示词-H3.md | | 技术方案 | 技术架构总览 / 优化方案-代码与架构 / 本地生图方案 / 本地H3动作生成方案 | | 规格契约 | equip-impl-spec.md | **加一篇文档**:把 HTML 放进 `docs/`,再到 `catalog.json` 对应类目的 `files` 里补一行文件名即可 (没登记的会落进「未分类」,不会丢)。 --- ## 九、开发约定(踩过的坑,别再踩) 1. **发布必须走 `npm run build`**,不要直接 `npx vite build`: `vite.config.js` 里 `emptyOutDir: false`,清理权只归 `tools/build.mjs` (它用 .NET 静态调用删 dist,绕开本机 safe-delete 钩子;vite 自带的清理会被 按「批量删除」拦下 → `Build failed in prepare-out-dir`)。 2. **并发开发**:`git add <明确路径>`,**绝不 `git add -A`**(多条线同时在改)。 3. **验收断言别写死 CSS 类名 / DOM 结构 / 具体 px** —— 另一条线会改界面。 4. **计时 / 飘字 / 特效走真实时间**,不乘游戏时间倍率;动作触发判**上升沿**不判电平。 5. **"随时间累积"的东西一律挂 `clock.gameSeconds()`**;"已花掉的账"必须落账幂等。 6. **确认框用 `src/confirm.js`**,别用裸 `window.confirm`(无头环境下会被自动 dismiss)。 7. **自测脚本必须走账号引导**(`tools/_ui_helpers.mjs` 的 `registerViaUI` → `passStarterUI`): 新账号不再自动发主角,跳过这步会拿到 `undefined` 然后在某句 `hero.job` 上死掉, 报错看起来像"脚本写错了"。 8. **Playwright 开多页要自己建 context**:`browser.newPage()` 每次另起一个 context, localStorage 不共享 —— 用它开 game-docs.html 会看到"未登录"(那不是功能坏了)。 9. **PM2 配置文件必须是 `.cjs`**:项目 `"type": "module"`,`.js` 会被当 ESM 加载, 而 PM2 按 CommonJS 读 → `ReferenceError: module is not defined`。 10. **索引页必须叫 `game-docs.html` 而不是 `docs.html`**:vite 的对象式代理按路径前缀匹配, `/docs` 会把 `/docs.html` 也劫走转发给后端(详见 `vite.config.js` 的注释)。 11. **`_gen_*` / `_out` / `_shots` / `dist` 不进版本库**(`.gitignore` 里);`docs/` 的 HTML 进。 12. **改服务端存档文件前先停服务**:`AccountStore` 等 Store 在内存里持有数据并会全量回写, 直接改磁盘文件会被覆盖(改完 `pm2 restart` 让服务重新读盘)。 --- ## 十、后续 - **门人重做**:十八职业 × 男女 = 36 人,重新设计(设定图)+ 全跑动作。 技能先出透明背景 PNG,再拿它做动作;门人全套动作**朝右**(敌人朝左,方向相对才成对战)。 - 美术资产管线已全部本地化(ComfyUI + Z-Image 生图 / MiniMax H3 生视频), 不再依赖云接口 —— 详见 `docs/本地生图方案.html` 与 `docs/技术架构总览.html`。 --- ## 许可 代码以 **MIT License** 开源,见 [LICENSE](LICENSE)。 `public/assets/` 与 `docs/` 里的美术、音频、设计文档是这款游戏的作品资产, 版权归作者所有(**不自动跟随 MIT 授权**);如需复用素材或整包商用,请先联系作者。 --- ## 附录:早期 3D 山门路线的踩坑存档 现在的山门是 **2D 折纸层**(`src/flatScene.js`,`main.js` 里 `FLAT_ART = true`)。 3D 那套(六边形地块山峰 / 实体云海 / Blender 植被)代码**仍然保留**在 `FLAT_ART = false` 分支(`hexmap.js` / `clouds.js` / `plants.js` / `backdrop.js`), 将来若要回 3D 或做混合,这几条是当时踩出来的: - `InstancedMesh` 容量必须在构造时给对(事后改 `.count` 不扩容缓冲,`setMatrixAt` 静默越界) - 六棱柱半径用**外接圆** `HEX_SIZE`;`CylinderGeometry` 首顶点在 +z,不要再 `rotateY(π/6)` - 侧壁贴图要用 `onBeforeCompile` 按**世界高度**取样(吃几何 UV 会随柱高拉伸) - 每根柱子要给随机 UV 偏移,否则相邻纹理逐字重复,并排像栅栏 - 柱壁分两段画:上段不透明、下段 alpha 淡出(近侧柱壁 y=0~-10 段没有云能遮住) - 环幕:`CylinderGeometry(R,R,H,96,1,true)` + `BackSide` + `fog:false`, `H = πR / 3.2` 让横纵像素密度一致;顶边必须出画,否则抬头看到"天花板" - 生图边缘常带卷轴条带,镜像平铺后会变成竖分界线 → 平铺前先裁左右各 1.5% - Blender 出模:**颜色烘焙进顶点色**(多材质会被 GLTF 拆成多个 primitive,没法单个 InstancedMesh 批量画) - GLB 的 accessor min/max ≠ 真实尺寸(节点上还有缩放),要用 `Box3().setFromObject()` 量 - 光照配比决定"有没有阴影":太阳当主角、环境光只托底;`shadow.normalBias` 别贪大(0.028) 更完整的当时版本在 git 历史里(`git log README.md`)。 --- 美术资产全部由本地模型生成;游戏文档(`docs/`)随源码一起进版本库。