10 Commits

Author SHA1 Message Date
panFD
4be5252cc4 docs: 更新 API 文档并补充技能级局部更新说明
新增技能级 PATCH 接口说明,明确 PUT 浅合并规则与 PATCH 局部更新的适用场景
调整智能体操作流程,补充顶层字段与技能级的不同修改方式指引
2026-10-06 00:11:05 +08:00
panFD
04cea0fbb2 feat: 新增技能级局部更新的 PATCH 接口
新增 PATCH /api/heroes/:uuid/skills/:skillUuid 接口,支持按层级合并技能字段及 lv_entries,避免整表替换技能数据。同时更新 API 文档,说明该接口的功能与使用方式。
2026-10-05 17:18:01 +08:00
panFD
0db3404481 feat: 新增英雄单查、新增、更新、删除接口及API文档
新增了英雄相关的四个核心API接口:单英雄查询、新增/覆盖英雄、局部更新英雄、删除英雄,并补充了详细的API调用规范文档,方便智能体配置英雄。
2026-10-05 15:10:39 +08:00
panFD
8c7748436b feat: 更新复刻英雄配置与任务提示动画
```

本次提交包含以下变更:
1. 更新英雄配置规则,将触发技能解锁等级从 lv6 调整为 lv4,大招成长档从 lv9/lv16 调整为 lv8/lv16,品质统一设为 1,英雄编号范围扩展至 5001~5018
2. 重写技能配置说明文档,细化各字段含义与数值口径,新增增益/减益类型说明
3. 修改任务提示动画,从左右位移飞入改为原地缩放弹入,优化动画时长与效果
4. 新增全英雄数值评审文档,包含属性模板、成长节奏、阵容推荐等内容

(注:因要求使用中文撰写 commit message,故按指定语言生成)
2026-10-05 14:48:46 +08:00
panFD
311b8ebb54 chore: 配置开发服务器监听所有网卡并新增技能数据文件 2026-10-03 15:46:03 +08:00
panFD
5d398d0a29 chore(server): 添加 HOST 配置支持容器部署
在示例环境配置、README 和服务代码中添加 HOST 配置项,默认监听 0.0.0.0 以适配容器和局域网部署场景,同时更新服务启动日志显示正确的监听地址。
2026-10-03 15:37:24 +08:00
panFD
9c9cc99e64 feat: 重构服务支持双模式并接入 MongoDB 数据库
- 新增 MongoDB 存储层实现英雄、预设和版本数据的持久化
- 新增 push-meta 和 migrate-to-mongo 脚本支持数据同步迁移
- 支持开发版和线上设计版双模式运行
- 新增版本闸门机制,线上设计版落后时进入只读模式
- 优化前端 API 配置适配不同部署环境
- 新增线上设计版静态资源托管和路由回退
2026-10-03 12:02:47 +08:00
pan
1bb275607a refactor: 修改英雄图鉴和详情页的排序规则为按职业升序
调整英雄列表排序逻辑,从原有的按品质降序改为按职业升序,保持与养成进度解耦的稳定展示。
2026-09-24 15:45:46 +08:00
pan
95bf2a4e17 feat: 添加回合进度指示 UI 组件及刷新逻辑 2026-09-24 15:44:06 +08:00
pan
3f2989ab55 refactor: 移除性能监控面板相关代码
本次提交移除了 MissionComp 组件中所有性能监控相关的代码,包括:
- 性能监控面板的属性、初始化和更新逻辑
- 相关的导入依赖
- 未使用的变量和方法

这些代码主要用于显示内存、帧率等性能数据,移除后不会影响游戏核心功能,有助于简化代码结构和减少不必要的性能开销。
2026-09-24 14:04:46 +08:00
28 changed files with 5085 additions and 1053 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,7 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{1349,1095},{616,182}}</string>
<string>{{675,1669},{616,182}}</string>
<key>textureRotated</key>
<false/>
</dict>
@@ -51,7 +51,7 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{675,1775},{672,92}}</string>
<string>{{675,1853},{672,92}}</string>
<key>textureRotated</key>
<false/>
</dict>
@@ -68,9 +68,9 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{675,1869},{672,76}}</string>
<string>{{1931,675},{672,76}}</string>
<key>textureRotated</key>
<false/>
<true/>
</dict>
<key>0l5_mountains.png</key>
<dict>
@@ -102,7 +102,7 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{1,1589},{672,162}}</string>
<string>{{675,1193},{672,162}}</string>
<key>textureRotated</key>
<false/>
</dict>
@@ -119,9 +119,9 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{675,1507},{672,138}}</string>
<string>{{1663,675},{672,138}}</string>
<key>textureRotated</key>
<false/>
<true/>
</dict>
<key>0l8_ground.png</key>
<dict>
@@ -136,9 +136,9 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{1349,381},{672,216}}</string>
<string>{{1729,1},{672,216}}</string>
<key>textureRotated</key>
<false/>
<true/>
</dict>
<key>1l1_wall.png</key>
<dict>
@@ -221,9 +221,9 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{1349,1279},{662,90}}</string>
<string>{{1349,1349},{662,90}}</string>
<key>textureRotated</key>
<true/>
<false/>
</dict>
<key>1l6_ground.png</key>
<dict>
@@ -238,9 +238,9 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{1441,1279},{672,76}}</string>
<string>{{1293,1699},{672,76}}</string>
<key>textureRotated</key>
<true/>
<false/>
</dict>
<key>3l1_sky.png</key>
<dict>
@@ -272,9 +272,9 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{675,1353},{672,152}}</string>
<string>{{1509,675},{672,152}}</string>
<key>textureRotated</key>
<false/>
<true/>
</dict>
<key>3l3_pyramid.png</key>
<dict>
@@ -289,9 +289,9 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{1621,1279},{528,154}}</string>
<string>{{1349,1543},{528,154}}</string>
<key>textureRotated</key>
<true/>
<false/>
</dict>
<key>3l4_bg-ground01.png</key>
<dict>
@@ -306,7 +306,7 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{1,1753},{672,156}}</string>
<string>{{675,1357},{672,156}}</string>
<key>textureRotated</key>
<false/>
</dict>
@@ -323,7 +323,7 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{1349,599},{672,180}}</string>
<string>{{1,1589},{672,180}}</string>
<key>textureRotated</key>
<false/>
</dict>
@@ -340,9 +340,9 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{675,1647},{672,126}}</string>
<string>{{1803,675},{672,126}}</string>
<key>textureRotated</key>
<false/>
<true/>
</dict>
<key>3l7_ground.png</key>
<dict>
@@ -357,7 +357,24 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{1349,941},{672,152}}</string>
<string>{{675,1515},{672,152}}</string>
<key>textureRotated</key>
<false/>
</dict>
<key>Icon_Battle.png</key>
<dict>
<key>aliases</key>
<array/>
<key>anchor</key>
<string>{0.5,0.5}</string>
<key>spriteOffset</key>
<string>{0,0}</string>
<key>spriteSize</key>
<string>{64,57}</string>
<key>spriteSourceSize</key>
<string>{64,57}</string>
<key>textureRect</key>
<string>{{1349,1894},{64,57}}</string>
<key>textureRotated</key>
<false/>
</dict>
@@ -374,9 +391,9 @@
<key>spriteSourceSize</key>
<string>{68,51}</string>
<key>textureRect</key>
<string>{{1777,1626},{68,51}}</string>
<string>{{1879,1629},{68,51}}</string>
<key>textureRotated</key>
<false/>
<true/>
</dict>
<key>Stone_A_04.png</key>
<dict>
@@ -391,9 +408,9 @@
<key>spriteSourceSize</key>
<string>{60,51}</string>
<key>textureRect</key>
<string>{{1850,1562},{60,51}}</string>
<string>{{1293,1777},{60,51}}</string>
<key>textureRotated</key>
<false/>
<true/>
</dict>
<key>Stone_A_07.png</key>
<dict>
@@ -408,9 +425,9 @@
<key>spriteSourceSize</key>
<string>{107,51}</string>
<key>textureRect</key>
<string>{{1621,1809},{107,51}}</string>
<string>{{1947,1},{107,51}}</string>
<key>textureRotated</key>
<false/>
<true/>
</dict>
<key>Stone_B_01.png</key>
<dict>
@@ -425,7 +442,7 @@
<key>spriteSourceSize</key>
<string>{68,70}</string>
<key>textureRect</key>
<string>{{1519,1865},{68,70}}</string>
<string>{{1935,1441},{68,70}}</string>
<key>textureRotated</key>
<false/>
</dict>
@@ -442,7 +459,7 @@
<key>spriteSourceSize</key>
<string>{66,45}</string>
<key>textureRect</key>
<string>{{1777,1679},{66,45}}</string>
<string>{{1415,1894},{66,45}}</string>
<key>textureRotated</key>
<false/>
</dict>
@@ -459,7 +476,7 @@
<key>spriteSourceSize</key>
<string>{35,22}</string>
<key>textureRect</key>
<string>{{1730,1809},{35,22}}</string>
<string>{{1293,1669},{35,22}}</string>
<key>textureRotated</key>
<false/>
</dict>
@@ -476,9 +493,9 @@
<key>spriteSourceSize</key>
<string>{71,62}</string>
<key>textureRect</key>
<string>{{1777,1562},{71,62}}</string>
<string>{{1899,1777},{71,62}}</string>
<key>textureRotated</key>
<false/>
<true/>
</dict>
<key>Stone_B_05.png</key>
<dict>
@@ -493,7 +510,7 @@
<key>spriteSourceSize</key>
<string>{84,43}</string>
<key>textureRect</key>
<string>{{1967,1095},{84,43}}</string>
<string>{{1879,1543},{84,43}}</string>
<key>textureRotated</key>
<true/>
</dict>
@@ -510,7 +527,7 @@
<key>spriteSourceSize</key>
<string>{31,15}</string>
<key>textureRect</key>
<string>{{1519,1937},{31,15}}</string>
<string>{{1947,110},{31,15}}</string>
<key>textureRotated</key>
<false/>
</dict>
@@ -527,7 +544,41 @@
<key>spriteSourceSize</key>
<string>{44,16}</string>
<key>textureRect</key>
<string>{{1,1911},{44,16}}</string>
<string>{{1,1931},{44,16}}</string>
<key>textureRotated</key>
<false/>
</dict>
<key>Title_01_NoDeco_Orange.Png</key>
<dict>
<key>aliases</key>
<array/>
<key>anchor</key>
<string>{0.5,0.5}</string>
<key>spriteOffset</key>
<string>{0,0}</string>
<key>spriteSize</key>
<string>{273,115}</string>
<key>spriteSourceSize</key>
<string>{273,115}</string>
<key>textureRect</key>
<string>{{1349,1777},{273,115}}</string>
<key>textureRotated</key>
<false/>
</dict>
<key>Title_01_NoDeco_Tangerine.Png</key>
<dict>
<key>aliases</key>
<array/>
<key>anchor</key>
<string>{0.5,0.5}</string>
<key>spriteOffset</key>
<string>{0,0}</string>
<key>spriteSize</key>
<string>{273,115}</string>
<key>spriteSourceSize</key>
<string>{273,115}</string>
<key>textureRect</key>
<string>{{1624,1777},{273,115}}</string>
<key>textureRotated</key>
<false/>
</dict>
@@ -546,7 +597,7 @@
<key>textureRect</key>
<string>{{1349,1},{672,378}}</string>
<key>textureRotated</key>
<false/>
<true/>
</dict>
<key>l2_mountains.png</key>
<dict>
@@ -578,9 +629,9 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{1519,1279},{584,100}}</string>
<string>{{1349,1441},{584,100}}</string>
<key>textureRotated</key>
<true/>
<false/>
</dict>
<key>l4_bg-ground01.png</key>
<dict>
@@ -595,9 +646,9 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{675,1193},{672,158}}</string>
<string>{{1349,675},{672,158}}</string>
<key>textureRotated</key>
<false/>
<true/>
</dict>
<key>l5_bg-ground02.png</key>
<dict>
@@ -612,7 +663,7 @@
<key>spriteSourceSize</key>
<string>{672,378}</string>
<key>textureRect</key>
<string>{{1349,781},{672,158}}</string>
<string>{{1,1771},{672,158}}</string>
<key>textureRotated</key>
<false/>
</dict>
@@ -633,23 +684,6 @@
<key>textureRotated</key>
<false/>
</dict>
<key>logo2.png</key>
<dict>
<key>aliases</key>
<array/>
<key>anchor</key>
<string>{0.5,0.5}</string>
<key>spriteOffset</key>
<string>{1,2}</string>
<key>spriteSize</key>
<string>{281,231}</string>
<key>spriteSourceSize</key>
<string>{307,307}</string>
<key>textureRect</key>
<string>{{1777,1279},{281,231}}</string>
<key>textureRotated</key>
<true/>
</dict>
</dict>
<key>metadata</key>
<dict>
@@ -662,9 +696,9 @@
<key>realTextureFileName</key>
<string>mps.png</string>
<key>size</key>
<string>{2022,1953}</string>
<string>{2012,1952}</string>
<key>smartupdate</key>
<string>$TexturePacker:SmartUpdate:c995e79e2727f3d5b64d2e4345b903e7:ff13e6153e38a1ee043318ccbb0a835f:4c0e3901f0d5fcb2cf0cd4dcba1c7f8b$</string>
<string>$TexturePacker:SmartUpdate:afc599cf42d3db8475edd4bf42d848d7:62891583945cafa92fc112c0ecd848ee:4c0e3901f0d5fcb2cf0cd4dcba1c7f8b$</string>
<key>textureFileName</key>
<string>mps.png</string>
</dict>

View File

@@ -7,6 +7,52 @@
".json"
],
"subMetas": {
"57674": {
"importer": "sprite-frame",
"uuid": "81af97ed-7940-45e6-91a1-21bd0df1a343@57674",
"displayName": "",
"id": "57674",
"name": "Icon_Battle",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 1349,
"trimY": 1894,
"width": 64,
"height": 57,
"rawWidth": 64,
"rawHeight": 57,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [],
"indexes": [],
"uv": [],
"nuv": [],
"minPos": [],
"maxPos": []
},
"isUuid": true,
"imageUuidOrDatabaseUri": "ca2b26a5-8d54-49ec-bdaf-e9f777afc9cb@6c48a",
"atlasUuid": "81af97ed-7940-45e6-91a1-21bd0df1a343",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"58094": {
"importer": "sprite-frame",
"uuid": "81af97ed-7940-45e6-91a1-21bd0df1a343@58094",
@@ -53,6 +99,98 @@
],
"subMetas": {}
},
"63222": {
"importer": "sprite-frame",
"uuid": "81af97ed-7940-45e6-91a1-21bd0df1a343@63222",
"displayName": "",
"id": "63222",
"name": "Title_01_NoDeco_Tangerine",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 1624,
"trimY": 1777,
"width": 273,
"height": 115,
"rawWidth": 273,
"rawHeight": 115,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [],
"indexes": [],
"uv": [],
"nuv": [],
"minPos": [],
"maxPos": []
},
"isUuid": true,
"imageUuidOrDatabaseUri": "ca2b26a5-8d54-49ec-bdaf-e9f777afc9cb@6c48a",
"atlasUuid": "81af97ed-7940-45e6-91a1-21bd0df1a343",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"86850": {
"importer": "sprite-frame",
"uuid": "81af97ed-7940-45e6-91a1-21bd0df1a343@86850",
"displayName": "",
"id": "86850",
"name": "Title_01_NoDeco_Orange",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 1349,
"trimY": 1777,
"width": 273,
"height": 115,
"rawWidth": 273,
"rawHeight": 115,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 135,
"borderRight": 138,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [],
"indexes": [],
"uv": [],
"nuv": [],
"minPos": [],
"maxPos": []
},
"isUuid": true,
"imageUuidOrDatabaseUri": "ca2b26a5-8d54-49ec-bdaf-e9f777afc9cb@6c48a",
"atlasUuid": "81af97ed-7940-45e6-91a1-21bd0df1a343",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"4f746": {
"importer": "sprite-frame",
"uuid": "81af97ed-7940-45e6-91a1-21bd0df1a343@4f746",
@@ -110,8 +248,8 @@
"rotated": false,
"offsetX": -4,
"offsetY": 82,
"trimX": 1349,
"trimY": 1095,
"trimX": 675,
"trimY": 1669,
"width": 616,
"height": 182,
"rawWidth": 672,
@@ -157,7 +295,7 @@
"offsetX": 0,
"offsetY": -3,
"trimX": 675,
"trimY": 1775,
"trimY": 1853,
"width": 672,
"height": 92,
"rawWidth": 672,
@@ -199,11 +337,11 @@
"name": "0l4_clouds02",
"userData": {
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": 0,
"offsetY": 122,
"trimX": 675,
"trimY": 1869,
"trimX": 1931,
"trimY": 675,
"width": 672,
"height": 76,
"rawWidth": 672,
@@ -294,8 +432,8 @@
"rotated": false,
"offsetX": 0,
"offsetY": -108,
"trimX": 1,
"trimY": 1589,
"trimX": 675,
"trimY": 1193,
"width": 672,
"height": 162,
"rawWidth": 672,
@@ -337,11 +475,11 @@
"name": "0l7_ground02",
"userData": {
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": 0,
"offsetY": -120,
"trimX": 675,
"trimY": 1507,
"trimX": 1663,
"trimY": 675,
"width": 672,
"height": 138,
"rawWidth": 672,
@@ -383,11 +521,11 @@
"name": "0l8_ground",
"userData": {
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": 0,
"offsetY": -81,
"trimX": 1349,
"trimY": 381,
"trimX": 1729,
"trimY": 1,
"width": 672,
"height": 216,
"rawWidth": 672,
@@ -613,11 +751,11 @@
"name": "1l5_crystals",
"userData": {
"trimThreshold": 1,
"rotated": true,
"rotated": false,
"offsetX": -2,
"offsetY": -85,
"trimX": 1349,
"trimY": 1279,
"trimY": 1349,
"width": 662,
"height": 90,
"rawWidth": 672,
@@ -659,11 +797,11 @@
"name": "1l6_ground",
"userData": {
"trimThreshold": 1,
"rotated": true,
"rotated": false,
"offsetX": 0,
"offsetY": -151,
"trimX": 1441,
"trimY": 1279,
"trimX": 1293,
"trimY": 1699,
"width": 672,
"height": 76,
"rawWidth": 672,
@@ -705,11 +843,11 @@
"name": "3l2_clouds",
"userData": {
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": 0,
"offsetY": -48,
"trimX": 675,
"trimY": 1353,
"trimX": 1509,
"trimY": 675,
"width": 672,
"height": 152,
"rawWidth": 672,
@@ -751,11 +889,11 @@
"name": "3l3_pyramid",
"userData": {
"trimThreshold": 1,
"rotated": true,
"rotated": false,
"offsetX": 2,
"offsetY": -7,
"trimX": 1621,
"trimY": 1279,
"trimX": 1349,
"trimY": 1543,
"width": 528,
"height": 154,
"rawWidth": 672,
@@ -800,8 +938,8 @@
"rotated": false,
"offsetX": 0,
"offsetY": -111,
"trimX": 1,
"trimY": 1753,
"trimX": 675,
"trimY": 1357,
"width": 672,
"height": 156,
"rawWidth": 672,
@@ -846,8 +984,8 @@
"rotated": false,
"offsetX": 0,
"offsetY": -99,
"trimX": 1349,
"trimY": 599,
"trimX": 1,
"trimY": 1589,
"width": 672,
"height": 180,
"rawWidth": 672,
@@ -889,11 +1027,11 @@
"name": "3l6_bg-ground03",
"userData": {
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": 0,
"offsetY": -126,
"trimX": 675,
"trimY": 1647,
"trimX": 1803,
"trimY": 675,
"width": 672,
"height": 126,
"rawWidth": 672,
@@ -938,8 +1076,8 @@
"rotated": false,
"offsetX": 0,
"offsetY": -113,
"trimX": 1349,
"trimY": 941,
"trimX": 675,
"trimY": 1515,
"width": 672,
"height": 152,
"rawWidth": 672,
@@ -981,11 +1119,11 @@
"name": "Stone_A_01",
"userData": {
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": 0,
"offsetY": 0,
"trimX": 1777,
"trimY": 1626,
"trimX": 1879,
"trimY": 1629,
"width": 68,
"height": 51,
"rawWidth": 68,
@@ -1027,11 +1165,11 @@
"name": "Stone_A_04",
"userData": {
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": 0,
"offsetY": 0,
"trimX": 1850,
"trimY": 1562,
"trimX": 1293,
"trimY": 1777,
"width": 60,
"height": 51,
"rawWidth": 60,
@@ -1073,11 +1211,11 @@
"name": "Stone_A_07",
"userData": {
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": 0,
"offsetY": 0,
"trimX": 1621,
"trimY": 1809,
"trimX": 1947,
"trimY": 1,
"width": 107,
"height": 51,
"rawWidth": 107,
@@ -1122,8 +1260,8 @@
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 1519,
"trimY": 1865,
"trimX": 1935,
"trimY": 1441,
"width": 68,
"height": 70,
"rawWidth": 68,
@@ -1168,8 +1306,8 @@
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 1777,
"trimY": 1679,
"trimX": 1415,
"trimY": 1894,
"width": 66,
"height": 45,
"rawWidth": 66,
@@ -1214,8 +1352,8 @@
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 1730,
"trimY": 1809,
"trimX": 1293,
"trimY": 1669,
"width": 35,
"height": 22,
"rawWidth": 35,
@@ -1257,11 +1395,11 @@
"name": "Stone_B_04",
"userData": {
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": 0,
"offsetY": 0,
"trimX": 1777,
"trimY": 1562,
"trimX": 1899,
"trimY": 1777,
"width": 71,
"height": 62,
"rawWidth": 71,
@@ -1306,8 +1444,8 @@
"rotated": true,
"offsetX": 0,
"offsetY": 0,
"trimX": 1967,
"trimY": 1095,
"trimX": 1879,
"trimY": 1543,
"width": 84,
"height": 43,
"rawWidth": 84,
@@ -1352,8 +1490,8 @@
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 1519,
"trimY": 1937,
"trimX": 1947,
"trimY": 110,
"width": 31,
"height": 15,
"rawWidth": 31,
@@ -1399,7 +1537,7 @@
"offsetX": 0,
"offsetY": 0,
"trimX": 1,
"trimY": 1911,
"trimY": 1931,
"width": 44,
"height": 16,
"rawWidth": 44,
@@ -1441,7 +1579,7 @@
"name": "l1_sky",
"userData": {
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": 0,
"offsetY": 0,
"trimX": 1349,
@@ -1533,11 +1671,11 @@
"name": "l3_clouds",
"userData": {
"trimThreshold": 1,
"rotated": true,
"rotated": false,
"offsetX": -6,
"offsetY": 89,
"trimX": 1519,
"trimY": 1279,
"trimX": 1349,
"trimY": 1441,
"width": 584,
"height": 100,
"rawWidth": 672,
@@ -1579,11 +1717,11 @@
"name": "l4_bg-ground01",
"userData": {
"trimThreshold": 1,
"rotated": false,
"rotated": true,
"offsetX": 0,
"offsetY": -110,
"trimX": 675,
"trimY": 1193,
"trimX": 1349,
"trimY": 675,
"width": 672,
"height": 158,
"rawWidth": 672,
@@ -1628,8 +1766,8 @@
"rotated": false,
"offsetX": 0,
"offsetY": -110,
"trimX": 1349,
"trimY": 781,
"trimX": 1,
"trimY": 1771,
"width": 672,
"height": 158,
"rawWidth": 672,
@@ -1708,52 +1846,6 @@
".json"
],
"subMetas": {}
},
"14c74": {
"importer": "sprite-frame",
"uuid": "81af97ed-7940-45e6-91a1-21bd0df1a343@14c74",
"displayName": "",
"id": "14c74",
"name": "logo2",
"userData": {
"trimThreshold": 1,
"rotated": true,
"offsetX": 1,
"offsetY": 2,
"trimX": 1777,
"trimY": 1279,
"width": 281,
"height": 231,
"rawWidth": 307,
"rawHeight": 307,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [],
"indexes": [],
"uv": [],
"nuv": [],
"minPos": [],
"maxPos": []
},
"isUuid": true,
"imageUuidOrDatabaseUri": "ca2b26a5-8d54-49ec-bdaf-e9f777afc9cb@6c48a",
"atlasUuid": "81af97ed-7940-45e6-91a1-21bd0df1a343",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

After

Width:  |  Height:  |  Size: 396 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -82,31 +82,40 @@ export enum SummonTMode {
=== 技能配置系统使用说明 ===
1. 基础属性:
- uuid: 技能唯一ID
- name: 技能名称
- uuid: 技能唯一ID(6001~6013 单体普攻、6100~6204 群体大招、6301~6302 治疗护盾、
6401~6416 永久强化、6501~6519 计时强化/特殊、7001~7024 驻场光环)
- name: 技能名称(内部底座代号;英雄语境对外名由 heroSet 侧 name 字段覆写)
- sp_name: 特效名称
- AtkedName: 攻击类型
- path: 图片资源路径
- icon: 图标ID
2. 目标和效果:
- TGroup: 目标群体 (敌方、友方等)
- SType: 技能类型 (伤害、治疗、护盾等)
- TGroup: 目标群体(Self 自身 / Ally 全体友方含自己 / Team 全体友方 / Enemy 敌方 / All 全部)
- kind: 主效果类型(Damage 伤害 / Heal 治疗 / Shield 护盾 / Support 强化)
3. 执行参数:
- act: 角色执行的动画
- DTType: 伤害类型 (单体、范围)
- EType: 结束条件
- fname: 特效文件名
- DTType: 伤害类型(single 单体 / range 范围)
- EType: 结束条件(动画结束/时间结束/碰撞次数)
- RType: 弹道类型(linear 直线 / bezier 贝塞尔 / fixed 固定起点 / fixedEnd 固定终点)
- with: 暂时无用
4. 数值参数:
- ap: 攻击力百分比
- cd: 冷却时间
- hit_count: 可命中次数
- t_num: 目标数量
- hitcd: 持续伤害的伤害间隔
- speed: 移动速度
- cost: 消耗值
- ap: 伤害/治疗为攻击力百分比;护盾为生命最大值百分比;强化技能为修饰数值
- cd: 冷却时间(普攻 cd 即攻击间隔)
- hit_count: 可命中目标数(伤害技能=弹道穿透数;辅助技能=作用目标数)
- hitcd: 持续伤害的伤害间隔(秒)
- speed: 弹道移动速度
- summon_count: 单次施法召唤的弹道/实体个数(>1 视为召唤类技能)
5. 增益/减益:
- buff_type + ap: 永久强化(直接累加到属性,无时长)
- timed_buff_id + buff_value/buff_duration/buff_chance/buff_target: 计时强化(走 BuffList)
- timed_debuff_id + debuff_value/debuff_duration/debuff_chance: 命中敌方后挂的计时减益
- proc_skill: 施法后概率触发额外技能(技能级触发器,无视 CD)
6. 文案约定:
- info 仅作语义描述,禁止写死数值;数值由 heroSet/卡片 overrides 注入,
UI 文案由 SkillDescSet / HeroSkillText 按合并后实时配置动态生成
*/
export enum DType {
ATK = 0, // 物理
@@ -150,8 +159,8 @@ export interface SkillConfig {
TGroup: TGroup, // 目标群体(敌方/友方/自身等)
act: string, // 角色执行的动画
DTType: DTType, // 伤害类型(单体/范围)
ap: number, // 伤害/治疗技能为攻击百分比,护盾技能为免疫次数
hit_count: number, // 可命中次数
ap: number, // 伤害/治疗为攻击力百分比;护盾为生命最大值百分比;永久强化为修饰数值
hit_count: number, // 可命中目标数(伤害技能=弹道穿透数;辅助技能=作用目标数,>1 生效)
hitcd: number, // 持续伤害的伤害间隔(秒)
speed: number, // 移动速度
with: number, // 宽度(暂时无用)
@@ -331,17 +340,17 @@ export const SkillSet: Record<number, SkillConfig> = {
6013: {
uuid: 6013, name: "闪电", sp_name: "m3", icon: "109-Red-Potion", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
DTType: DTType.single, ap: 100, hit_count: 1, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.Melee, is_accel: true,
RType: RType.fixedEnd, EType: EType.collision, info: "闪电",
RType: RType.fixedEnd, EType: EType.collision, info: "单体雷击(固定终点落雷),可附加麻痹",
},
6014: {
uuid: 6014, name: "小火球", sp_name: "m4", icon: "109-Red-Potion", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
DTType: DTType.single, ap: 100, hit_count: 1, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.Melee, is_accel: true,
RType: RType.fixedEnd, EType: EType.collision, info: "小火球",
RType: RType.fixedEnd, EType: EType.collision, info: "小火球(固定终点),可附加灼烧/易伤",
},
6015: {
uuid: 6015, name: "小飓风", sp_name: "m5", icon: "109-Red-Potion", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
DTType: DTType.single, ap: 100, hit_count: 1, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.Melee, is_accel: true,
RType: RType.fixedEnd, EType: EType.collision, info: "小飓风抛",
RType: RType.fixedEnd, EType: EType.collision, info: "小飓风(固定终点),直线穿透",
},
// ==================== 群体大招(按职业分段:6100-6149 坦克/战士、6150-6199 刺客/射手、6200-6249 法师/辅助) ====================
// 坦克/战士群体攻击大招
@@ -405,30 +414,30 @@ export const SkillSet: Record<number, SkillConfig> = {
6204: {
uuid: 6204, name: "雷暴", sp_name: "maxm7", icon: "o_warlock_hyzy_skill", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "max1",
DTType: DTType.single, ap: 100, hit_count: 99, hitcd: 0.3, speed: 720, with: 90, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.linear, EType: EType.collision, bezier_start_y: 20, bezier_mid_y: 140, bezier_arc: 1.05, info: "一个大毒气弹",
RType: RType.linear, EType: EType.collision, bezier_start_y: 20, bezier_mid_y: 140, bezier_arc: 1.05, info: "雷暴多段打击(hit_count=99 直线穿透),可附加麻痹",
},
//============================= ====== 辅助技能 回合技能 ====== ==========================
6301: {
uuid: 6301, name: "护盾", sp_name: "buff_wind", icon: "28-Electric-Wave", TGroup: TGroup.Self, readyAnm: "up_blue", endAnm: "", act: "max1",
DTType: DTType.single, kind: SkillKind.Shield, ap: 30, hit_count: 1, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.support, shield_duration: 7,
RType: RType.fixed, EType: EType.animationEnd, info: "获得施法者最大生命30%的护盾, 持续7秒",
RType: RType.fixed, EType: EType.animationEnd, info: "获得施法者最大生命一定比例的护盾,持续7秒(ap=百分比)",
},
6302: {
uuid: 6302, name: "治疗", sp_name: "buff_wind", icon: "g_priest_zldy_skill", TGroup: TGroup.Team, readyAnm: "up_green", endAnm: "", act: "max1",
DTType: DTType.single, kind: SkillKind.Heal, ap: 300, hit_count: 1, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.support,
RType: RType.fixed, EType: EType.animationEnd, info: "治疗伙伴/自己",
RType: RType.fixed, EType: EType.animationEnd, info: "治疗友方目标,回复量=目标最大生命×ap%",
},
//==========================buff 技能=====================
6401: {
uuid: 6401, name: "攻击强化", sp_name: "buff_wind", icon: "36-Guardian-Stance", TGroup: TGroup.Team, readyAnm: "up_ap", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Support, ap: 5, hit_count: 1, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.support,
RType: RType.fixed, EType: EType.animationEnd, buff_type: Attrs.ap, info: "全体友方攻击力提升5%, 持续1次",
RType: RType.fixed, EType: EType.animationEnd, buff_type: Attrs.ap, info: "全体友方攻击力永久提升(ap=百分比,单次施加)",
},
6402: {
uuid: 6402, name: "生命强化", sp_name: "buff_wind", icon: "29-Endure", TGroup: TGroup.Team, readyAnm: "up_hp", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Support, ap: 20, hit_count: 1, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.support,
RType: RType.fixed, EType: EType.animationEnd, buff_type: Attrs.hp_max, info: "全体友方最大生命值提升20%, 持续1次",
RType: RType.fixed, EType: EType.animationEnd, buff_type: Attrs.hp_max, info: "全体友方最大生命值永久提升(ap=百分比,单次施加)",
},
6403: {
uuid: 6403, name: "暴击强化", sp_name: "buff_wind", icon: "5-Apply-Burn", TGroup: TGroup.Team, readyAnm: "up_ap", endAnm: "", act: "atk",
@@ -481,7 +490,7 @@ export const SkillSet: Record<number, SkillConfig> = {
6412: {
uuid: 6412, name: "防御强化", sp_name: "buff_wind", icon: "46-Iron-Clad", TGroup: TGroup.Team, readyAnm: "up_blue", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Support, ap: 50, hit_count: 1, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.support,
RType: RType.fixed, EType: EType.animationEnd, buff_type: Attrs.defense, info: "全体友方防御提升50点, 持续1次",
RType: RType.fixed, EType: EType.animationEnd, buff_type: Attrs.defense, info: "全体友方防御永久提升(ap=固定点数,单次施加)",
},
6413: {
uuid: 6413, name: "反伤强化", sp_name: "buff_wind", icon: "75-Shield-Stun", TGroup: TGroup.Team, readyAnm: "up_blue", endAnm: "", act: "atk",
@@ -539,7 +548,7 @@ export const SkillSet: Record<number, SkillConfig> = {
6507: {
uuid: 6507, name: "攻速爆发", sp_name: "buff_wind", icon: "147-Chronomancy", TGroup: TGroup.Team, readyAnm: "up_ap", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Support, ap: 0, hit_count: 3, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.support,
RType: RType.fixed, EType: EType.animationEnd, buff_type: Attrs.speed, timed_buff_id: 7015, info: "全体友方攻击速度提升40%, 持续5秒",
RType: RType.fixed, EType: EType.animationEnd, buff_type: Attrs.speed, timed_buff_id: 7015, info: "全体友方攻击速度提升(buff_value=百分比,默认5秒)",
},
// 风怒机制已停用: 6508 风怒爆发技能已移除
/*
@@ -593,7 +602,7 @@ export const SkillSet: Record<number, SkillConfig> = {
6517: {
uuid: 6517, name: "生命爆发", sp_name: "buff_wind", icon: "29-Endure", TGroup: TGroup.Team, readyAnm: "up_green", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Support, ap: 0, hit_count: 3, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.support,
RType: RType.fixed, EType: EType.animationEnd, buff_type: Attrs.hp_max, timed_buff_id: 7029, info: "全体友方最大生命值提升30%, 持续5秒",
RType: RType.fixed, EType: EType.animationEnd, buff_type: Attrs.hp_max, timed_buff_id: 7029, info: "全体友方最大生命值提升(buff_value=百分比,默认5秒)",
},
6518: {
uuid: 6518, name: "闪避爆发", sp_name: "buff_wind", icon: "98-Enchant-Wind", TGroup: TGroup.Team, readyAnm: "up_blue", endAnm: "", act: "atk",
@@ -601,9 +610,9 @@ export const SkillSet: Record<number, SkillConfig> = {
RType: RType.fixed, EType: EType.animationEnd, buff_type: Attrs.dodge, timed_buff_id: 7030, info: "全体友方闪避概率提升20%, 持续5秒",
},
6519: {
uuid: 6519, name: "命中强化", sp_name: "buff_wind", icon: "27-Hawkeye", TGroup: TGroup.Team, readyAnm: "up_blue", endAnm: "", act: "atk",
uuid: 6519, name: "命中爆发", sp_name: "buff_wind", icon: "27-Hawkeye", TGroup: TGroup.Team, readyAnm: "up_blue", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Support, ap: 0, hit_count: 3, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.support,
RType: RType.fixed, EType: EType.animationEnd, buff_type: Attrs.accuracy, timed_buff_id: 7031, info: "全体友方命中概率提升20%, 持续5秒",
RType: RType.fixed, EType: EType.animationEnd, buff_type: Attrs.accuracy, timed_buff_id: 7031, info: "全体友方命中概率提升(buff_value=百分比,默认5秒)",
}
};
//***************驻场技能配置***************

View File

@@ -1,4 +1,4 @@
import { v3 } from "cc"
import { v3 } from "cc"
import { BoxSet, FacSet, FightSet } from "./GameSet"
import { SkillOverrides, TGroup, SkillSet, FieldSkillSet, SkillConfig, SkillKind, FieldSkillConfig, FieldSkillType, mergeSkillParams, BuffTarget } from "./SkillSet"
import { BuffList, BuffConfig, ModOp } from "./BuffSet"
@@ -577,11 +577,12 @@ export interface LvBonusAttrsEntry {
*=============英雄配置列表================
* 几何王国复刻版 — 依据 英雄设计指引.md(A/B 级复刻英雄)配置
*
* 规则:lv1 激活 skills[0]/skills[1],lv6 解锁 atking/atked/fstart/dead/field 触发技能(触发类统一 lv6 激活,无品质门槛),
* 大招(skills[1])lv9/lv16 走 lv_entries 分档成长,lv12 按职业对单项基础属性 +50%(基数取 lv11 有效值),
* revive[0] 与 skills[1] 互斥二选一(配置 revive 时按 lv1/lv9/lv16 走 lv_entries 3 级成长),
* 品质(rarity)仅影响 lv1 初始基础属性(相邻差 5%,仅 lv1 初始 AP/HP/DEF),不影响 uuid 段位/光环/技能解锁/属性补偿
* 编号:本批复刻英雄集中于 5001~5009,rarity 按配置(不遵循段位规则)
* 规则:lv1 激活 skills[0]/skills[1],lv4 解锁 atking/atked/fstart/dead/field 触发技能(触发类统一 lv4 激活,无品质门槛),
* 大招(skills[1])lv8/lv16 走 lv_entries 分档成长,lv12 按职业对单项基础属性 +50%(基数取"基础×品质+成长"快照),
* revive[0] 与 skills[1] 互斥二选一(配置 revive 时按 lv1/lv8/lv16 走 lv_entries 3 级成长),
* 品质(rarity)当前全部英雄统一为 1,不再区分品质;getHeroQualityCoef 保留但恒为 1.00,
* 若未来重启品质体系,仅影响 lv1 初始基础属性(相邻差 5%),不影响 uuid 段位/光环/技能解锁/属性补偿
* 编号:本批复刻英雄集中于 5001~5018,rarity 统一 1
*/
// @hero-setup:region-begin

View File

@@ -247,11 +247,13 @@ export class HeroInfoComp extends CCComp {
* @param delta 1 下一个,-1 上一个
*/
private switchHero(delta: 1 | -1) {
// 与图鉴页面排序同源:仅按品质降序 → uuid 升序
// 与图鉴页面排序同源:按职业(HRole)升序 → uuid 升序
const sorted = [...HeroList].sort((a, b) => {
const ra = HeroInfo[a]?.rarity ?? 1;
const rb = HeroInfo[b]?.rarity ?? 1;
if (ra !== rb) return rb - ra;
const heroA = HeroInfo[a];
const heroB = HeroInfo[b];
const roleA = heroA?.role ?? 0;
const roleB = heroB?.role ?? 0;
if (roleA !== roleB) return roleA - roleB;
return a - b;
});
if (sorted.length === 0) return;

View File

@@ -113,11 +113,13 @@ export class HerosListComp extends CCComp {
this.cards_node.removeAllChildren()
// 图鉴排序:仅按品质降序 → uuid 升序(与养成进度解耦,保持展示稳定)
// 图鉴排序:按职业(HRole)升序 → uuid 升序(与养成进度解耦,保持展示稳定)
const sorted = [...HeroList].sort((a, b) => {
const ra = HeroInfo[a]?.rarity ?? 1;
const rb = HeroInfo[b]?.rarity ?? 1;
if (ra !== rb) return rb - ra;
const heroA = HeroInfo[a];
const heroB = HeroInfo[b];
const roleA = heroA?.role ?? 0;
const roleB = heroB?.role ?? 0;
if (roleA !== roleB) return roleA - roleB;
return a - b;
})

View File

@@ -17,7 +17,7 @@
* - 第 5 节胜利 → open_Victory(final) 弹结算,确认退出并存档进下一关。
* - cleanComponents() / clearBattlePools() 仅在整关开始/结束时执行,节间不调用。
*/
import { _decorator, Vec3, Animation, instantiate, Prefab, Node, NodeEventType, ProgressBar, Label, CCInteger, tween, v3, Tween, Widget, UIOpacity } from "cc";
import { _decorator, Node, NodeEventType, Label, tween, v3, Tween, Widget, Color, Sprite, ProgressBar } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { smc } from "../common/SingletonModuleComp";
@@ -31,7 +31,6 @@ import { SkillTriggerHelper } from "../hero/SkillTriggerHelper";
import { UIID } from "../common/config/GameUIConfig";
import { SkillView } from "../skill/SkillView";
import { FacSet, FightSet } from "../common/config/GameSet";
import { HeroInfo } from "../common/config/heroSet";
import { mLogger } from "../common/Logger";
import { Monster } from "../hero/Mon";
import { Skill } from "../skill/Skill";
@@ -80,8 +79,6 @@ type SettleType = "defeat" | "final";
export class MissionComp extends CCComp {
@property({ tooltip: "是否启用调试日志" })
private debugMode: boolean = true;
@property({ tooltip: "是否显示战斗内存观测面板" })
private showMemoryPanel: boolean = false;
// ======================== 配置参数 ========================
@@ -97,6 +94,21 @@ export class MissionComp extends CCComp {
@property(Node)
tooltip: Node = null!
/** 回合指示节点(5 个,索引 0~4 对应回合 1~5),按回合进度染色 */
@property({ type: [Node], tooltip: "回合指示节点(按回合 1~5 顺序绑定)" })
round_nodes: Node[] = [];
/** 回合进度条:第 1 回合 0%,每推进一回合 +25%(第 5 回合满格) */
@property({ type: ProgressBar, tooltip: "回合进度条(第 1 回合 0%,每回合 +25%)" })
round_progress: ProgressBar = null!;
/** 回合指示:未激活(白色) */
private static readonly ROUND_INACTIVE = new Color(255, 255, 255, 255);
/** 回合指示:已激活(橙黄 #FFA300) */
private static readonly ROUND_ACTIVE = new Color(255, 163, 0, 255);
/** 回合指示:Boss 当前回合(红色) */
private static readonly ROUND_BOSS = new Color(230, 60, 60, 255);
/** 阶段名称映射表(用于 UI 显示) */
private static readonly PhaseNameMap: Record<MissionPhase, string> = {
[MissionPhase.None]: "未开始",
@@ -133,26 +145,6 @@ export class MissionComp extends CCComp {
private lastTimeStr: string = "";
/** 上一次显示的秒数(避免重复计算) */
private lastTimeSecond: number = -1;
/** 性能监控面板 Label 引用 */
private memoryLabel: Label | null = null;
/** 性能监控刷新计时器 */
private memoryRefreshTimer: number = 0;
/** 上一次性能文本(避免重复渲染) */
private lastMemoryText: string = "";
/** 帧间隔累加(用于计算平均 FPS) */
private perfDtAcc: number = 0;
/** 帧数计数 */
private perfFrameCount: number = 0;
/** 初始堆内存基准值(MB) */
private heapBaseMB: number = -1;
/** 堆内存峰值(MB) */
private heapPeakMB: number = 0;
/** 堆内存增长趋势(MB/分钟) */
private heapTrendPerMinMB: number = 0;
/** 趋势计算计时器 */
private heapTrendTimer: number = 0;
/** 趋势计算基准(MB) */
private heapTrendBaseMB: number = -1;
/** 怪物数量同步计时器(降低同步频率) */
private monsterCountSyncTimer: number = 0;
/** 当前回合数(= 当前小节号,驱动 HUD 回合/进度显示) */
@@ -161,8 +153,6 @@ export class MissionComp extends CCComp {
private isBossWave: boolean = false;
/** 超时回合已在 onBattleTimeout 预扣留存分,BattleEnd 需跳过重复累加 */
private skipRemainScoreOnBattleEnd: boolean = false;
/** 上节清场时间快照(含清场加速还原,供评分统计) */
private lastWaveClearTime: number = 0;
/** 当前任务阶段 */
public currentPhase: MissionPhase = MissionPhase.None;
/** 是否处于回合间倒计时状态 */
@@ -200,7 +190,6 @@ export class MissionComp extends CCComp {
this.heroViewMatcher = ecs.allOf(HeroViewComp);
this.skillViewMatcher = ecs.allOf(SkillView);
this.heroAttrsMatcher = ecs.allOf(HeroAttrsComp);
this.showMemoryPanel = false
// 注册生命周期事件
this.on(GameEvent.MissionEnd, this.mission_end, this)
this.on(GameEvent.NewWave, this.onNewWave, this)
@@ -210,10 +199,9 @@ export class MissionComp extends CCComp {
this.start_btn?.on(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this)
// 开局免费三选一选完后自动进入战斗
oops.message.on("OpeningPicksDone", this.onOpeningPicksDone, this);
this.removeMemoryPanel()
}
onAdded(args: any) {
onAdded() {
// 使用 scheduleOnce 将事件推迟到下一帧执行,
// 确保所有关联组件(如 MissionCardComp 等)都已经完成其 onLoad 生命周期。
this.scheduleOnce(() => {
@@ -423,22 +411,20 @@ export class MissionComp extends CCComp {
Tween.stopAllByTarget(this.tooltip);
// 动感动画设计:右侧进入 -> 屏幕中央(带有轻微的弹跳和滑动) -> 左侧飞出
// 假设屏幕宽度适配下,1200是一个足够的屏幕外距离,适配横竖屏
const startPos = v3(1200, this.tooltip.position.y, this.tooltip.position.z);
const centerPos = v3(0, this.tooltip.position.y, this.tooltip.position.z);
const driftPos = v3(-50, this.tooltip.position.y, this.tooltip.position.z); // 在中央时的缓慢漂移
const endPos = v3(-1200, this.tooltip.position.y, this.tooltip.position.z);
this.tooltip.setPosition(startPos);
// 动感动画设计:原地缩放弹入(0 -> 1,带轻微回弹)-> 短暂停留 -> 收缩消失
// 相比位移飞入飞出,缩放动画更轻快,避免横幅长时间占用屏幕
this.tooltip.setPosition(0, this.tooltip.position.y, this.tooltip.position.z);
this.tooltip.setScale(0, 0, 1);
tween(this.tooltip)
// 1. 从右侧快速飞入并带回弹效果 (0.5秒)
.to(0.5, { position: centerPos }, { easing: "backOut" })
// 2. 在屏幕中央缓慢向左漂移,增强动感停留 (1.0秒)
.to(1.0, { position: driftPos }, { easing: "sineInOut" })
// 3. 快速向左飞出并消失 (0.4秒)
.to(0.4, { position: endPos }, { easing: "backIn" })
// 1. 缩放从 0 快速弹到 1.1,制造轻微"冲出来"的动感 (0.18秒)
.to(0.18, { scale: v3(1.1, 1.1, 1) }, { easing: "sineOut" })
// 2. 回弹收缩到标准大小 1.0 (0.1秒)
.to(0.1, { scale: v3(1, 1, 1) }, { easing: "sineIn" })
// 3. 停留展示 (0.5秒)
.delay(0.5)
// 4. 向上轻弹并收缩消失,带背入加速感 (0.2秒)
.to(0.2, { scale: v3(0, 0, 1) }, { easing: "backIn" })
.call(() => {
this.tooltip.active = false;
})
@@ -452,7 +438,6 @@ export class MissionComp extends CCComp {
*/
private changePhase(targetPhase: MissionPhase) {
if (this.currentPhase === targetPhase) return;
const oldPhase = this.currentPhase;
this.currentPhase = targetPhase;
const phaseName = MissionComp.PhaseNameMap[targetPhase] || "未知";
@@ -553,8 +538,6 @@ export class MissionComp extends CCComp {
// 英雄为真实死亡(实体已销毁),本节死亡数取死亡记录长度
const heroDeathCount = smc.mission.dead_heroes.length;
// 快照清场时间供评分统计(onNewWave 会清零 clearTime,必须提前快照)
this.lastWaveClearTime = this.clearTime;
// 【评分系统 - 战绩分】记录全员存活的胜利节数(额外加分)
if (hasHero && heroDeathCount === 0) {
smc.vmdata.scores.wave_all_alive_count++;
@@ -748,11 +731,11 @@ export class MissionComp extends CCComp {
* - 胜利即 final(通关),存档推进到下一关;失败即 defeat(进度不变,可复活/再来一次)
* - 切入 BattleEnd,2s 后由 autoNextPhase 自动弹出 VictoryComp
*
* @param e 事件对象(未使用)
* @param is_hero_dead 是否因英雄全灭触发
* @param _e 事件对象(未使用)
* @param _is_hero_dead 是否因英雄全灭触发(当前未使用,保留签名以统一事件调用口径)
* @param victory 是否胜利(默认 false)
*/
open_Victory(e: any, is_hero_dead: boolean = false, victory: boolean = false) {
open_Victory(_e: any, _is_hero_dead: boolean = false, victory: boolean = false) {
// 战斗结束(通关/失败),标记暂停状态以切断节间流转逻辑
smc.mission.pause = true;
this._victory = victory;
@@ -828,20 +811,11 @@ export class MissionComp extends CCComp {
this.currentWave = 1;
this.isBossWave = false;
this.skipRemainScoreOnBattleEnd = false;
this.lastWaveClearTime = 0;
this.updateRoundNodes();
this.rewards = []
this.revive_times = 1;
this.lastTimeStr = "";
this.lastTimeSecond = -1;
this.memoryRefreshTimer = 0;
this.lastMemoryText = "";
this.perfDtAcc = 0;
this.perfFrameCount = 0;
this.heapBaseMB = -1;
this.heapPeakMB = 0;
this.heapTrendPerMinMB = 0;
this.heapTrendTimer = 0;
this.heapTrendBaseMB = -1;
this.monsterCountSyncTimer = 0;
spawningEngine.reset();
@@ -941,7 +915,7 @@ export class MissionComp extends CCComp {
* @param event 事件名
* @param data { wave: number, bossWave?: boolean }
*/
private onNewWave(event: string, data: any) {
private onNewWave(_event: string, data: any) {
const wave = Number(data?.wave ?? 0);
if (wave <= 0) return;
@@ -950,12 +924,46 @@ export class MissionComp extends CCComp {
this.currentWave = wave;
smc.vmdata.mission_data.level = wave;
this.updateRoundNodes();
// 金币改为怪物死亡掉落(见 HeroAtkSystem),不再每回合固定发放
this.lastTimeSecond = -1;
this.clearTime = 0;
this.update_time();
}
/**
* 刷新回合指示节点颜色:
* - 已激活(已完成 + 当前回合)→ 黄色;当前回合且为 Boss 节 → 红色;未激活 → 白色。
* Why: 回合推进时(onNewWave)与开局重置(data_init)调用,驱动 HUD 回合指示实时反馈。
*/
private updateRoundNodes() {
const len = Math.min(this.round_nodes.length, SECTIONS_PER_STAGE);
for (let i = 0; i < len; i++) {
const node = this.round_nodes[i];
if (!node || !node.isValid) continue;
const round = i + 1;
const color = round > this.currentWave
? MissionComp.ROUND_INACTIVE
: (round === this.currentWave && this.isBossWave)
? MissionComp.ROUND_BOSS
: MissionComp.ROUND_ACTIVE;
// 优先染色 Sprite(图标配色),无 Sprite 时退化到 Label,均无则跳过
const sprite = node.getComponent(Sprite);
if (sprite) {
sprite.color = color;
} else {
const label = node.getComponent(Label);
if (label) label.color = color;
}
}
// 进度条:第 1 回合 0%,每推进一回合 +25%(第 5 回合满格)
if (this.round_progress && this.round_progress.isValid) {
this.round_progress.progress =
Math.max(0, Math.min(SECTIONS_PER_STAGE, this.currentWave) - 1) / (SECTIONS_PER_STAGE - 1);
}
}
// ======================== 怪物数量管理 ========================
/**
@@ -1164,115 +1172,6 @@ export class MissionComp extends CCComp {
}
}
/** 获取战斗层的英雄和技能节点数量(用于性能监控) */
private getBattleLayerNodeCount() {
const scene = smc.map?.MapView?.scene;
const layer = scene?.entityLayer?.node;
if (!layer) return { heroNodes: 0, skillNodes: 0 };
const heroRoot = layer.getChildByName("HERO");
const skillRoot = layer.getChildByName("SKILL");
return {
heroNodes: heroRoot?.children.length || 0,
skillNodes: skillRoot?.children.length || 0
};
}
// ======================== 性能监控面板 ========================
/** 性能监控相关代码 */
/** 初始化性能监控面板:在本组件节点下创建 Label */
private initMemoryPanel() {
if (!this.showMemoryPanel) return;
let panel = this.node.getChildByName("mem_panel");
if (!panel) {
panel = new Node("mem_panel");
panel.parent = this.node;
panel.setPosition(0, -32, 0);
}
let label = panel.getComponent(Label);
if (!label) {
label = panel.addComponent(Label);
}
label.fontSize = 16;
label.lineHeight = 20;
this.memoryLabel = label;
}
/** 移除性能监控面板 */
private removeMemoryPanel() {
const panel = this.node?.getChildByName("mem_panel");
if (panel) {
panel.destroy();
}
this.memoryLabel = null;
this.lastMemoryText = "";
}
/**
* 更新性能监控面板内容(每 0.5 秒一次):
* 显示 堆内存 / 增长趋势 / 帧率 / 实体数量 / 对象池状态 等信息。
*/
private updateMemoryPanel(dt: number) {
if (!this.showMemoryPanel || !this.memoryLabel) return;
this.perfDtAcc += dt;
this.perfFrameCount += 1;
this.memoryRefreshTimer += dt;
if (this.memoryRefreshTimer < 0.5) return;
this.memoryRefreshTimer = 0;
let heroCount = 0;
ecs.query(this.heroViewMatcher).forEach(() => {
heroCount++;
});
let skillCount = 0;
ecs.query(this.skillViewMatcher).forEach(() => {
skillCount++;
});
const monPool = Monster.getPoolStats();
const skillPool = Skill.getPoolStats();
const tooltipPool = Tooltip.getPoolStats();
const layerNodes = this.getBattleLayerNodeCount();
const perf = (globalThis as any).performance;
const heapBytes = perf && perf.memory ? perf.memory.usedJSHeapSize : 0;
let heapMB = heapBytes > 0 ? heapBytes / 1024 / 1024 : -1;
if (heapMB > 0 && this.heapBaseMB < 0) {
this.heapBaseMB = heapMB;
this.heapPeakMB = heapMB;
this.heapTrendBaseMB = heapMB;
this.heapTrendTimer = 0;
}
if (heapMB > this.heapPeakMB) {
this.heapPeakMB = heapMB;
}
this.heapTrendTimer += 0.5;
if (heapMB > 0 && this.heapTrendBaseMB > 0 && this.heapTrendTimer >= 10) {
const deltaMB = heapMB - this.heapTrendBaseMB;
this.heapTrendPerMinMB = (deltaMB / this.heapTrendTimer) * 60;
this.heapTrendBaseMB = heapMB;
this.heapTrendTimer = 0;
}
const heapText = heapMB > 0 ? heapMB.toFixed(1) : "N/A";
const heapDeltaText = this.heapBaseMB > 0 && heapMB > 0 ? (heapMB - this.heapBaseMB).toFixed(1) : "N/A";
const heapPeakText = this.heapPeakMB > 0 ? this.heapPeakMB.toFixed(1) : "N/A";
const avgDt = this.perfFrameCount > 0 ? this.perfDtAcc / this.perfFrameCount : 0;
const fps = avgDt > 0 ? 1 / avgDt : 0;
this.perfDtAcc = 0;
this.perfFrameCount = 0;
const text =
`Heap:${heapText}MB Δ:${heapDeltaText} Peak:${heapPeakText}\n` +
`Trend:${this.heapTrendPerMinMB.toFixed(2)}MB/min\n` +
`Perf dt:${(avgDt * 1000).toFixed(1)}ms fps:${fps.toFixed(1)}\n` +
`Ent H:${heroCount} S:${skillCount} N:${layerNodes.heroNodes}/${layerNodes.skillNodes}\n` +
`Pool M:${monPool.total}(${monPool.paths}) K:${skillPool.total}(${skillPool.paths}) T:${tooltipPool.total}`;
if (text === this.lastMemoryText) return;
this.lastMemoryText = text;
this.memoryLabel.string = text;
}
/** ECS 组件移除时销毁节点 */
reset() {
this.PhaseTime = null as any;

View File

@@ -0,0 +1,447 @@
# 全英雄基础属性与能力说明(数值评审稿)
> 数据来源:`assets/script/game/common/config/heroSet.ts`(英雄配置)、`SkillSet.ts`(技能底座)、`BuffSet.ts`(buff/debuff 底座)
> 用途:交予数值设计师讨论分析。lv1 基础属性均已按 `HERO_ROLE_BASE + 手动增量` 实算。
---
## 〇、全局数值框架
### 职业基础模板 `HERO_ROLE_BASE`
英雄实际属性 = 模板 + 手动增量;品质(rarity)仅乘 lv1 基础值,相邻品质差 5%。
| 职业 | HP | AP | DEF | 成长系数 HP/AP/DEF |
|---|---|---|---|---|
| 坦克 Tank | 600 | 20 | 50 | 1.5 / 0.6 / 1.5 |
| 战士 Warrior | 560 | 22 | 30 | 1.0 / 1.0 / 1.0 |
| 刺客 Assassin | 560 | 22 | 10 | 0.7 / 1.4 / 0.5 |
| 射手 Archer | 280 | 32 | 10 | 0.8 / 1.3 / 0.5 |
| 法师 Mage | 280 | 32 | 10 | 0.9 / 1.3 / 0.5 |
| 辅助 Support | 280 | 32 | 15 | 1.1 / 0.7 / 1.0 |
### 攻速档 `AtkSpeedSet`(攻击间隔,越小越快)
| 档位 | 名称 | 间隔 |
|---|---|---|
| VeryFast | 极速 | 0.75s |
| Fast | 快速 | 0.90s |
| Normal | 中速 | 1.05s |
| Slow | 慢速 | 1.20s |
| VerySlow | 极慢 | 1.50s |
### 大招 CD 档 `UltCdSet`
| 档位 | 名称 | CD | 适用类型 |
|---|---|---|---|
| VeryFast | 极频 | 6s | 单体爆发/高频压制 |
| Fast | 高频 | 7s | 频繁压制(中间档) |
| Normal | 标准 | 8s | 群体伤害/控制 |
| Slow | 低频 | 9s | 中度团队增益(中间档) |
| VerySlow | 稀有 | 10s | 全体增益/治疗/护盾 |
### 成长与解锁节奏
- lv1:激活普攻 + 大招
- lv2:普攻强化档(lv_entries 第 1 档)
- lv4:解锁触发技 / 光环(配置实际生效等级)
- lv8 / lv16:大招升档(lv_entries)
- lv12:专项 bonus(单项属性 +50%,基数取"基础×品质+成长"快照)
### 触发技概率模型
t_num 次行为后判定,t_chance% 概率触发;未触发则 pity_add 累加下次概率(封顶 100%,触发成功后清零)。
---
## 一、前排坦克 / 战士
### 5001 叠甲骑士(近战·坦克)
**lv1:HP 620 / AP 20 / DEF 60** | 攻速 1.20s(慢速)| 大招 CD 10s(稀有)
> 定位:自身强护盾 + 防御强化
- **普攻 6004 强化普攻**:100% 攻击伤害 → lv2 提升至 120%
- **大招 6301 魔法盾**:自身获得生命最大值 **30%** 护盾 → lv8 60% → lv16 120%
- **受击触发 6412 防御强化**(lv4):受击 3 次后 50% 概率触发(保底 +20),全队防御 +50(单次)
- **lv12 钢铁壁垒**:DEF +50%
### 5009 甲虫领主(近战·坦克)
**lv1:HP 600 / AP 20 / DEF 50** | 攻速 1.20s | 大招 CD 8s
> 定位:受击自疗 + 击退,自愈坦克
- **普攻 6004 普攻强化** → lv2 伤害提升至 120%
- **大招 6102 火焰斩**:250% 攻击伤害,并 100% 触发治疗(回复自身 10% 生命)→ lv8 300%/15% → lv16 350%/20%
- **受击触发 6302 治疗**(lv4):受击 3 次后 30% 概率(保底 +20),回复自身 5% 生命
- **lv12 钢铁壁垒**:DEF +50%
### 5011 控血大师(近战·坦克)
**lv1:HP 600 / AP 20 / DEF 50** | 攻速 1.05s(中速)| 大招 CD 10s(稀有)
> 定位:永久 + 计时生命上限,控血流
- **普攻 6004 普攻强化** → lv2:10% 概率对自己释放回春(5s 回复 5% 生命)
- **大招 6103 闪电战**:200% 攻击伤害,自身最大生命 +30% 持续 5s → lv8 250%/+40% → lv16 300%/+50%
- **受击触发 6402 生命强化**(lv4):受击 5 次后 30% 概率(保底 +15),**全队永久最大生命 +1%**
- **lv12 钢铁壁垒**:DEF +50%
### 5002 易伤剑仙(近战·战士)
**lv1:HP 565 / AP 42 / DEF 35** | 攻速 1.20s | 大招 CD 8s
> 定位:易伤放大,近战 dps
- **普攻 6004 强化普攻** → lv2 提升至 120%
- **大招 6101 气斩**:150% 伤害 + 易伤(每层附加 30 固定受伤,持续 5s,可叠 5 层)→ lv8 200%/70 → lv16 250%/150 持续 7s
- **攻击触发 6001 飞剑**(lv4):普攻 3 次后 50% 概率(保底 +20),追加飞剑并挂易伤 10/5s
- **lv12 不屈战意**:HP +50%
### 5003 眩晕暴击哥(近战·战士)
**lv1:HP 565 / AP 42 / DEF 35** | 攻速 1.20s | 大招 CD 8s
> 定位:必晕爆发 + 暴击,近战收割
- **普攻 6004 强化普攻** → lv2:普攻 10% 概率击晕
- **大招 6104 开山斧**:250% 伤害 **必定击晕** → lv8 300% → lv16 350%
- **攻击触发 6504 暴击爆发**(lv4):普攻 3 次后 50% 概率(保底 +20),自身暴击率 +30% 持续 5s
- **lv12 不屈战意**:HP +50%
### 5014 闪避哥(近战·战士)
**lv1:HP 560 / AP 22 / DEF 30** | 攻速 1.05s | 大招 CD 8s
> 定位:强闪避
- **普攻 6004 普攻强化** → lv2:暴击率 +10%
- **大招 6100 寒冰斩**:200% 伤害,自身闪避 +50%(7030 闪避爆发)→ lv8 250%/50% → lv16 350%/80%
- **光环 7023(lv4)**:词条值 10(语义存疑,见待确认点 ①)
- **lv12 不屈战意**:HP +50%
---
## 二、刺客 / 中程输出
### 5004 刀刀暴击(中程·刺客)
**lv1:HP 560 / AP 52 / DEF 10** | 攻速 0.90s(快速)| 大招 CD 6s(极频)
> 定位:暴击流刺客主C
- **普攻 6002 匕首** → lv2:暴击率 +10%
- **大招 6150 剑刃风暴**:150% 伤害,50% 暴击 → lv8 200%/60% → lv16 250%/70%
- **攻击触发 6504 暴击爆发**(lv4):普攻 3 次后 50% 概率(保底 +20),自身暴击率 +10% 持续 5s
- **lv12 致命本能**:AP +50%
### 5005 出血贼(中程·刺客)
**lv1:HP 560 / AP 52 / DEF 10** | 攻速 0.90s | 大招 CD 6s(极频)
> 定位:出血 DoT 持续消耗
- **普攻 6002 强化普攻** → lv2:50% 概率使目标出血(每 0.5s 微量掉血,每层使目标被暴击率 +1%,可叠 5 层)
- **大招 6150 血刃风暴**:200% 伤害 + 100% 出血(-20/5s)→ lv8 250%/-30/7s → lv16 300%/-50/9s(lv16 文案附带 50% 麻痹但配置未注入,见待确认点 ②)
- **攻击触发 6401 攻击强化**(lv4):普攻 3 次后 50% 概率(保底 +20),全队攻击 +2%
- **lv12 致命本能**:AP +50%
### 5010 火箭哥(中程·射手)
**lv1:HP 280 / AP 32 / DEF 10** | 攻速 0.90s | 大招 CD 6s(极频)
> 定位:弹幕减攻
- **普攻 6006 射击** → lv2:暴击率 +10%
- **大招 6151 万箭齐发·火**:5 支箭各 70% 伤害,必挂减攻 -10%/5s → lv8 80%/-20% → lv16 100%/-30%
- **攻击触发 6008 红箭**(lv4):普攻 3 次后 50% 概率(保底 +20),追加 120% 伤害射击
- **lv12 鹰眼锁定**:AP +50%
### 5016 连珠神射(中程·射手)
**lv1:HP 280 / AP 42 / DEF 10** | 攻速 **0.75s(极速,全英雄最快)** | 大招 CD 6s(极频)
> 定位:极速连射暴击爆发主C
- **普攻 6008 连珠箭** → lv2:20% 概率额外射击,造成 80% 伤害
- **大招 6151 裂空箭雨**:5 箭各 150% 伤害,30% 暴击 → lv8 200%/50% → lv16 250%/70%/6 箭
- **光环 7004 暴击光环(lv4)**:全队暴击率提升(词条值 0.1)
- **lv12 猎杀直觉**:AP +50%
---
## 三、远程法师 / 射手 / 辅助
### 5006 冻箭哥(远程·射手)
**lv1:HP 280 / AP 62 / DEF 10** | 攻速 1.05s | 大招 CD 8s
> 定位:冰冻控场 + 多重箭
- **普攻 6006 强化普攻** → lv2:10% 概率冰冻
- **大招 6150 寒冰箭**:6 支箭各 150% 伤害,每支 30% 概率冰冻 → lv8 200% → lv16 250%(冰冻率恒 30%)
- **攻击触发 6401 攻击强化**(lv4):普攻 3 次后 50% 概率(保底 +20),**全队永久攻击 +2%**
- **lv12 鹰眼锁定**:AP +50%
- 备注:手动增量 ap+30,lv1 AP 62 为全英雄最高普攻基数(见待确认点 ③)
### 5015 毒箭猎手(远程·射手)
**lv1:HP 290 / AP 32 / DEF 10** | 攻速 1.05s | 大招 CD 8s
> 定位:毒伤 DoT 持续消耗型远程C
- **普攻 6007 淬毒箭** → lv2:10% 概率击退
- **大招 6150 万箭齐发·毒**:150% 伤害,50% 中毒(每 0.5s 30 伤害/5s,可叠 5 层)→ lv8 200%/100%/60/5s → lv16 250%/100%/100/7s
- **攻击触发 6007 蚀骨毒箭**(lv4):普攻 3 次后 50% 概率(保底 +20),追加 80% 伤害且 **必定中毒**(40/跳)
- **lv12 剧毒淬炼**:AP +50%
### 5007 击退火女(远程·法师)
**lv1:HP 275 / AP 42 / DEF 10** | 攻速 1.20s | 大招 CD 8s
> 定位:烈焰击退 + 攻击光环,法系压场
- **普攻 6010 紫珠** → lv2:20% 概率击退
- **大招 6200 火焰喷射**:250% 伤害 + 50% 暴击 + 击退 → lv8 350% → lv16 450%/4 个火球(升档后暴击加成降为 0,见待确认点 ④)
- **光环 7002 攻击光环(lv4)**:全队攻击 +15%
- **lv12 奥术共鸣**:AP +50%
### 5012 疾风法师(远程·法师)
**lv1:HP 280 / AP 32 / DEF 10** | 攻速 1.05s | 大招 CD 8s
> 定位:攻速 buff + 攻速光环,加速大师
- **普攻 6009 蓝珠** → lv2:10% 概率自身攻速 +30%(6507 攻速爆发)
- **大招 6201 飓风**:2 道飓风各 100% 伤害,每道 20% 概率全队攻速 +20% → lv8 3 道/25%/+25% → lv16 4 道/30%/+30%
- **光环 7006 攻速光环(lv4)**:全队攻速 +10%
- **lv12 奥术共鸣**:AP +50%
### 5017 碎甲术士(远程·法师)
**lv1:HP 280 / AP 42 / DEF 10** | 攻速 1.05s | 大招 CD 8s
> 定位:破甲 + 穿透光环,物理队核心增伤位
- **普攻 6012 蚀甲酸瓶** → lv2:30% 概率破甲(防御 -50%/5s)
- **大招 6203 碎甲能量波**:150% 伤害 + 必破甲 -30%/5s → lv8 200%/-50% → lv16 250%/-70%/7s
- **光环 7009 穿透光环(lv4)**:全队穿透概率 +10%
- **lv12 奥术共鸣**:AP +50%
### 5018 雷暴领主(远程·法师)
**lv1:HP 270 / AP 52 / DEF 10** | 攻速 0.90s(快速)| 大招 CD 8s
> 定位:雷暴 AOE + 麻痹控场,法系主C
- **普攻 6013 召唤闪电** → lv2:15% 概率麻痹(技能 CD 增长减慢 50%,持续 5s)
- **大招 6204 雷霆裂空**:150% 伤害,50% 麻痹 → lv8 200%/70% → lv16 250%/100% 可多次命中
- **光环 7024 命中光环(lv4)**:全队命中 +10%
- **lv12 奥术共鸣**:AP +50%
### 5008 团疗奶爸(远程·辅助)
**lv1:HP 290 / AP 52 / DEF 15** | 攻速 1.05s | 大招 CD 10s(稀有)
> 定位:全体治疗 + 团队续航
- **普攻 6011 绿瓶** → lv2:120% 伤害
- **大招 6302 治疗**:全队回复攻击 30% 生命 → lv8 40% → lv16 50% + 全队防御 +20/5s(7024 防御爆发词条)
- **光环 7001 战后恢复(lv4)**:回合结束休整回血 +20%
- **lv12 守护之心**:HP +50%
### 5013 生命哥(远程·辅助)
**lv1:HP 280 / AP 32 / DEF 15** | 攻速 1.05s | 大招 CD 10s(稀有)
> 定位:全体生命上限 + 反伤护盾
- **普攻 6004** → lv2:120% 伤害
- **大招 6517 生命护盾**:全队最大生命 +20%/5s(3 人)→ lv8 +25%/10s → lv16 +30%/10s
- **攻击触发 6414 反伤比例**(lv4):普攻 3 次后 30% 概率(保底 +15),全队比例反伤 +10%
- **lv12 守护之心**:HP +50%
---
## 四、英雄速查总表(lv1 基础值)
| UUID | 名称 | 定位 | 职业 | HP | AP | DEF | 普攻间隔 | 大招CD | 核心机制 |
|---|---|---|---|---|---|---|---|---|---|
| 5001 | 叠甲骑士 | 近战 | 坦克 | 620 | 20 | 60 | 1.20s | 10s | 自身护盾/受击叠防 |
| 5002 | 易伤剑仙 | 近战 | 战士 | 565 | 42 | 35 | 1.20s | 8s | 易伤放大 |
| 5003 | 眩晕暴击哥 | 近战 | 战士 | 565 | 42 | 35 | 1.20s | 8s | 必晕+暴击 |
| 5004 | 刀刀暴击 | 中程 | 刺客 | 560 | 52 | 10 | 0.90s | 6s | 暴击流主C |
| 5005 | 出血贼 | 中程 | 刺客 | 560 | 52 | 10 | 0.90s | 6s | 出血DoT |
| 5006 | 冻箭哥 | 远程 | 射手 | 280 | 62 | 10 | 1.05s | 8s | 冰冻控场 |
| 5007 | 击退火女 | 远程 | 法师 | 275 | 42 | 10 | 1.20s | 8s | 击退+攻击光环 |
| 5008 | 团疗奶爸 | 远程 | 辅助 | 290 | 52 | 15 | 1.05s | 10s | 全体治疗 |
| 5009 | 甲虫领主 | 近战 | 坦克 | 600 | 20 | 50 | 1.20s | 8s | 受击自疗 |
| 5010 | 火箭哥 | 中程 | 射手 | 280 | 32 | 10 | 0.90s | 6s | 弹幕减攻 |
| 5011 | 控血大师 | 近战 | 坦克 | 600 | 20 | 50 | 1.05s | 10s | 生命上限成长 |
| 5012 | 疾风法师 | 远程 | 法师 | 280 | 32 | 10 | 1.05s | 8s | 攻速buff+光环 |
| 5013 | 生命哥 | 远程 | 辅助 | 280 | 32 | 15 | 1.05s | 10s | 生命上限+反伤 |
| 5014 | 闪避哥 | 近战 | 战士 | 560 | 22 | 30 | 1.05s | 8s | 强闪避 |
| 5015 | 毒箭猎手 | 远程 | 射手 | 290 | 32 | 10 | 1.05s | 8s | 毒伤DoT |
| 5016 | 连珠神射 | 中程 | 射手 | 280 | 42 | 10 | 0.75s | 6s | 极速暴击爆发 |
| 5017 | 碎甲术士 | 远程 | 法师 | 280 | 42 | 10 | 1.05s | 8s | 破甲+穿透光环 |
| 5018 | 雷暴领主 | 远程 | 法师 | 270 | 52 | 10 | 0.90s | 8s | 雷暴+麻痹 |
---
## 五、官方推荐阵容(HeroComboSet)
| ID | 阵容 | 英雄 | 配合思路 |
|---|---|---|---|
| 8001 | 暴击爆发流 | 5001 + 5016 + 5004 | 叠甲骑士前排抗伤,连珠神射暴击光环覆盖全队+极速箭雨压场,刀刀暴击近战收割 |
| 8002 | 冰雷控制流 | 5003 + 5006 + 5018 | 开山斧必晕 + 寒冰箭冰冻链 + 雷霆麻痹,三控交替 |
| 8003 | 铁桶续航流 | 5009 + 5011 + 5008 | 受击自疗前排 + 全队永久生命成长 + 全体治疗,持久战 |
| 8004 | 破甲压制流 | 5001 + 5017 + 5010 | 双通道破甲+穿透光环放大物理伤害,五箭弹幕挂减攻 |
| 8005 | 光环覆盖流 | 5014 + 5007 + 5012 | 闪避/攻击/攻速三光环全覆盖 |
| 8006 | 易伤消耗流 | 5002 + 5005 + 5008 | 易伤放大出血 DoT,治疗保前排磨血 |
---
## 六、待数值设计师确认的配置口径疑问
1. **5014 光环语义不一致**:词条 uuid=7023,FieldSkillSet 7023 名称为"闪避光环",但 BuffSet 7023 为"防御强化"(防御 +50/10s)。需确认实际生效语义。
2. **5005 大招 lv16 文案与配置不符**:desc 写"附带 50% 概率麻痹",但 overrides 未注入 `para` 字段。
3. **5006 冻箭哥 AP 基数偏高**:手动增量 ap+30(lv1 AP 62),显著高于其他远程英雄(32~52),需确认是否刻意。
4. **5007 大招升档暴击消失**:lv1 档 crt:50,lv8/lv16 档 crt:0,升档后暴击加成消失,需确认是否刻意。
5. **触发技解锁等级**:统一 **lv4** 激活(atking/atked/fstart/dead/field 触发类无品质门槛)。heroSet 头注释原为 lv6,已修正为 lv4 与实际配置对齐。
6. **品质体系已停用**:全部英雄 rarity 统一为 1,不再区分品质;`getHeroQualityCoef` 保留但恒为 1.00。卡池费用统一 5、抽卡权重统一 40。若未来重启品质体系,仅影响 lv1 初始基础属性(相邻差 5%),不影响技能解锁/光环/属性补偿。
---
## 附录 A、技能底座表(SkillSet)
> 数值说明:`ap` 对伤害/治疗技能为攻击力百分比,对护盾(6301)为生命最大值百分比,对永久强化(64xx)为修饰数值;`hit_count` 对伤害技能为弹道穿透数,对辅助技能为作用目标数。
### A1. 单体普攻底座(6001~6013)
| UUID | 名称 | 弹道 | ap% | 穿透 | 附加语义 | 使用英雄 |
|---|---|---|---|---|---|---|
| 6001 | 飞剑 | 近战贝塞尔 | 100 | 1 | 可附加易伤 | 5002(触发技) |
| 6002 | 匕首 | 近战贝塞尔 | 100 | 1 | 可附加出血/暴击 | 5004、5005 |
| 6004 | 攻击 | 近战碰撞 | 100 | 1 | 默认 100% 击退(bck_chance=100) | 5001/5002/5003/5009/5011/5013/5014 |
| 6006 | 射击 | 远程箭 | 100 | 1 | 可附加冰冻/暴击 | 5006、5010 |
| 6007 | 蓝箭 | 远程箭 | 100 | 1 | 可附加击退/中毒 | 5015 |
| 6008 | 红箭 | 远程箭 | 100 | 1 | 可附加追射/暴击 | 5010(触发技)、5016 |
| 6009 | 蓝珠 | 近战弹道 | 100 | 1 | 可附加攻速 proc | 5012 |
| 6010 | 紫珠 | 近战弹道 | 100 | 1 | 可附加击退 | 5007 |
| 6011 | 绿瓶 | 近战弹道 | 100 | 1 | — | 5008 |
| 6012 | 红瓶 | 近战弹道 | 100 | 1 | 可附加破甲 | 5017 |
| 6013 | 闪电 | 固定终点落雷 | 100 | 1 | 可附加麻痹 | 5018 |
### A2. 群体大招底座(6100~6204)
| UUID | 名称 | 类型 | ap% | 穿透/弹道 | 附加语义 | 使用英雄 |
|---|---|---|---|---|---|---|
| 6100 | 寒冰斩 | 近战 | 150 | 5 | 可叠出血、自增益 | 5014 |
| 6101 | 裂气斩 | 近战 | 150 | 5 | 可叠击退、易伤 | 5002 |
| 6102 | 火焰斩 | 近战 | 150 | 5 | 可叠暴击、proc 治疗 | 5009 |
| 6103 | 闪电斩 | 近战 | 150 | 5 | 可叠自身增益 | 5011 |
| 6104 | 开天斩 | 远程范围 | 300 | 6 | 可叠击晕增强 | 5003 |
| 6150 | 万箭齐发·冰 | 远程箭 | 100 | 1 | 6 支蓝箭底座 | 5004/5005/5006/5015 |
| 6151 | 万箭齐发·火 | 远程箭 | 100 | 1 | 6 支红箭底座 | 5010、5016 |
| 6200 | 火球术 | 远程 | 100 | 1 | 单发大火球 | 5007 |
| 6201 | 飓风 | 远程直线 | 100 | 99 | 穿透飓风,可挂攻速 proc | 5012 |
| 6202 | 毒气弹 | 远程直线 | 100 | 1 | 穿透毒气 | 未启用 |
| 6203 | 能量波 | 远程直线 | 100 | 1 | 穿透能量波 | 5017 |
| 6204 | 雷暴 | 远程直线 | 100 | 99 | 雷暴多段穿透,可附加麻痹 | 5018 |
### A3. 治疗/护盾底座(6301~6302)
| UUID | 名称 | kind | ap 语义 | 默认数值 | 使用英雄 |
|---|---|---|---|---|---|
| 6301 | 护盾 | Shield | 生命最大值 % | 30%、持续 7s | 5001 |
| 6302 | 治疗 | Heal | 目标最大生命 × ap% | 300%(底座值,实际由 overrides 覆写) | 5008(大招)、5009(proc/受击触发) |
### A4. 永久强化底座(6401~6416,buff_type + ap,单次施加)
| UUID | 名称 | buff_type | 底座 ap | 语义 | 使用英雄 |
|---|---|---|---|---|---|
| 6401 | 攻击强化 | ap | 5 | 全体攻击永久 +N% | 5005、5006(触发技) |
| 6402 | 生命强化 | hp_max | 20 | 全体最大生命永久 +N% | 5011(触发技) |
| 6403 | 暴击强化 | critical | 1 | 全体暴击率永久 +N% | 未启用 |
| 6404 | 暴伤强化 | critical_damage | 1 | 全体暴伤永久 +N% | 未启用 |
| 6405 | 击晕强化 | stun_chance | 1 | 全体击晕概率永久 +N% | 未启用 |
| 6406 | 击退强化 | knockback_chance | 10 | 全体击退距离永久 +N% | 未启用 |
| 6408 | 穿刺强化 | puncture_chance | 20 | 全体穿刺概率永久 +N% | 未启用 |
| 6410 | 冰冻强化 | freeze_chance | 5 | 全体冰冻概率永久 +N% | 未启用 |
| 6411 | 溅射强化 | puncture_dmg_bonus | 10 | 全体穿透伤害永久 +N% | 未启用 |
| 6412 | 防御强化 | defense | 50 | 全体防御永久 +N 点 | 5001(触发技) |
| 6413 | 反伤强化 | thorns_dmg | 10 | 全体固定反伤永久 +N 点 | 未启用 |
| 6414 | 反伤比例强化 | thorns_rate | 10 | 全体比例反伤永久 +N% | 5013(触发技) |
| 6415 | 闪避强化 | dodge | 5 | 全体闪避永久 +N% | 未启用 |
| 6416 | 命中强化 | accuracy | 5 | 全体命中永久 +N% | 未启用 |
### A5. 计时强化底座(6501~6519,走 timed_buff_id → BuffList,默认 5s)
| UUID | 名称 | 关联 Buff | buff_type | 底座语义 | 使用英雄 |
|---|---|---|---|---|---|
| 6501 | 复活 | — | — | ap=复活生命百分比(dead 触发用) | 未启用 |
| 6502 | 计时回血 | 7010 再生 | hp | 全体每秒回血 5s | 5011(普攻 proc) |
| 6503 | 攻击爆发 | 7001 攻击强化 | ap | 全体攻击 +N% 5s | 未启用 |
| 6504 | 暴击爆发 | 7012 暴击爆发 | critical | 全体暴击 +N% 5s | 5003、5004(触发技) |
| 6505 | 击退爆发 | 7013 击退爆发 | knockback_chance | 全体击退距离 +N% 5s | 未启用 |
| 6506 | 穿透爆发 | 7014 穿透爆发 | puncture_chance | 全体穿透 +N% 5s | 未启用 |
| 6507 | 攻速爆发 | 7015 攻速爆发 | speed | 全体攻速 +N% 5s | 5012(普攻/大招 proc) |
| 6509 | 击晕爆发 | 7017 击晕爆发 | stun_chance | 全体击晕概率 +N% 5s | 未启用 |
| 6510 | 击晕抗性 | 7018 击晕抗性 | stun_res | 全体击晕抗性 +N% 5s | 未启用 |
| 6511 | 冰冻抗性 | 7019 冰冻抗性 | freeze_res | 全体冰冻抗性 +N% 5s | 未启用 |
| 6512 | 溅射爆发 | 7022 穿透伤害爆发 | puncture_dmg_bonus | 全体穿透伤害 +N% 5s | 未启用 |
| 6513 | 防御爆发 | 7024 防御爆发 | defense | 全体防御 +N 点 5s | 未启用 |
| 6514 | 反伤护盾 | 7027 荆棘 | thorns_dmg | 全体固定反伤 +N 点 5s | 未启用 |
| 6515 | 反伤比例 | 7028 荆棘比例 | thorns_rate | 全体比例反伤 +N% 5s | 未启用 |
| 6516 | 次级反伤 | — | — | 空底座,效果由引用处 overrides 注入 | 未启用 |
| 6517 | 生命爆发 | 7029 生命爆发 | hp_max | 全体最大生命 +N% 5s | 5013(大招) |
| 6518 | 闪避爆发 | 7030 闪避爆发 | dodge | 全体闪避 +N% 5s | 5014(大招经 7030) |
| 6519 | 命中爆发 | 7031 命中强化 | accuracy | 全体命中 +N% 5s | 未启用 |
## 附录 B、Buff/Debuff 底座表(BuffList)
### B1. 敌方减益(Debuff/Control)
| ID | 名称 | 类别 | 默认效果 | 持续 | 叠层 | 使用英雄 |
|---|---|---|---|---|---|---|
| 7002 | 易伤 | Debuff | 受伤附加 10 点固定伤害 | 5s | 5 | 5002 |
| 7003 | 冰冻 | Control | 无法行动 | 2s | 1 | 5006 |
| 7004 | 击晕 | Control | 无法行动 1s,CD 削减 20% 并停滞 | 1s | 1 | 5003 |
| 7005 | 中毒 | Debuff | 每 0.5s 10 点 + AP 30% 伤害 | 5s | 5 | 5015 |
| 7006 | 减攻 | Debuff | 攻击 -10% | 5s | 1 | 5010 |
| 7007 | 出血 | Debuff | 每 0.5s 微量伤害,每层被暴击率 +1% | 5s | 5 | 5005 |
| 7008 | 破甲 | Debuff | 防御 -50% | 5s | 1 | 5017 |
| 7025 | 麻痹 | Control | 技能 CD 增长减慢 50% | 5s | 1 | 5018 |
### B2. 友方增益(Buff,计时类默认 5s)
| ID | 名称 | 效果 | 使用英雄 |
|---|---|---|---|
| 7001 | 攻击强化 | 攻击 +5%,10s | 6503 底座 |
| 7010 | 再生 | 每 0.5s 回 10 血,5s | 5011(6502) |
| 7012 | 暴击爆发 | 暴击率 +30% | 5003/5004(6504) |
| 7013 | 击退爆发 | 击退距离 +30% | 6505 底座 |
| 7014 | 穿透爆发 | 穿透概率 +30% | 6506 底座 |
| 7015 | 攻速爆发 | 攻速 +40% | 5012(6507) |
| 7017 | 击晕爆发 | 击晕概率 +30% | 6509 底座 |
| 7018 | 击晕抗性 | 击晕抗性 +50% | 6510 底座 |
| 7019 | 冰冻抗性 | 冰冻抗性 +50% | 6511 底座 |
| 7020 | 暴伤爆发 | 暴伤 +30% | 6404/未启用 |
| 7021 | 冰冻概率爆发 | 冰冻概率 +5% | 未启用 |
| 7022 | 穿透伤害爆发 | 穿透伤害 +30% | 6512 底座 |
| 7023 | 防御强化 | 防御 +50,10s | 5014 光环词条(语义存疑①) |
| 7024 | 防御爆发 | 防御 +100 | 5008 大招 lv16 |
| 7027 | 荆棘 | 固定反伤 10 点,可叠 5 层 | 6514 底座 |
| 7028 | 荆棘比例 | 比例反伤 10%,可叠 3 层 | 6515 底座 |
| 7029 | 生命爆发 | 最大生命 +30% 并等量回血 | 5013(6517)、5011(6103) |
| 7030 | 闪避爆发 | 闪避 +20% | 5014(6100) |
| 7031 | 命中强化 | 命中 +20% | 6519 底座 |
## 附录 C、驻场光环底座表(FieldSkillSet)
| UUID | 名称 | 类型 | 数值口径 | 使用英雄 |
|---|---|---|---|---|
| 7001 | 战后恢复 | WaveHeal | 比例(0.2=+20%) | 5008 |
| 7002 | 攻击光环 | HeroAtk | 比例(0.15=+15%) | 5007 |
| 7003 | 击晕光环 | HeroStun | 比例 | 未启用 |
| 7004 | 暴击光环 | HeroCrit | 比例(0.1=+10%) | 5016 |
| 7005 | 暴伤光环 | HeroCritDamage | 比例 | 未启用 |
| 7006 | 攻速光环 | HeroSpeed | 比例(0.1=+10%) | 5012 |
| 7007 | 生命光环 | HeroHp | 比例 | 未启用 |
| 7009 | 穿透光环 | HeroPuncture | 比例(0.1=+10%) | 5017 |
| 7014 | 召唤光环 | SummonCount | 整数次数 | 未启用 |
| 7015 | 死亡光环 | DeadCount | 整数次数 | 未启用 |
| 7016 | 鼓舞光环 | StartCount | 整数次数 | 未启用 |
| 7018 | 连击光环 | AtkCount | 整数次数 | 未启用 |
| 7019 | 受击光环 | BeAtkCount | 整数次数 | 未启用 |
| 7020 | 溅射光环 | HeroPunctureDmg | 百分点 | 未启用 |
| 7021 | 反伤光环 | HeroThornsDmg | 整数 | 未启用 |
| 7022 | 反伤比例光环 | HeroThornsRate | 百分点 | 未启用 |
| 7023 | 闪避光环 | HeroDodge | 百分点(10=+10%) | 5014(语义存疑①) |
| 7024 | 命中光环 | HeroHit | 百分点(10=+10%) | 5018 |
> 注:BuffList 与 FieldSkillSet 共用 7000 号段但语义独立——BuffList 是计时/战斗内修饰,FieldSkillSet 是驻场光环元数据;同一 uuid(如 7023/7024)在两表含义不同,配置时需按引用位置区分。

6
put/.gitignore vendored
View File

@@ -1,4 +1,8 @@
node_modules
dist
*.local
.DS_Store
# 环境变量含数据库连接串,禁止提交(参考 server/.env.example)
.env
# 线上设计版部署所需的产物需提交:web/dist、server/data/meta.json(勿忽略)
!web/dist
!web/dist/**

89
put/README.md Normal file
View File

@@ -0,0 +1,89 @@
# hero_set — 英雄配置编辑器
英雄数值/技能/触发/光环的可视化配置工具。数据存储在 **MongoDB**,编辑器产出可写回游戏工程的 `heroSet.ts` 与 `英雄设计表.csv`。
## 双版本架构
| | 项目开发版(local) | 线上设计版(remote) |
|---|---|---|
| 使用人 | 开发者 | 数值设计人员 |
| 数据 | 同一个线上 MongoDB | 同一个线上 MongoDB |
| meta 底座(技能/Buff) | 从游戏工程 TS 实时编译 | `server/data/meta.json` 快照(需随部署发布) |
| 英雄/预设 增删改 | ✅ | ✅(库结构版本更高时只读) |
| 写 heroSet.ts / 导出 CSV | ✅ | ❌(接口返回 403,按钮隐藏) |
**版本闸门**:库中 `meta` 集合存 `schemaVersion`(只升不降)。线上版启动版本低于库版本时,保存接口返回 409、前端禁用保存——防止旧结构覆盖新数据。结构有破坏性变更时:本地 `SCHEMA_VERSION` +1 → `npm run push-meta` → 重新部署线上版。
## 目录
```
put/
├── server/ # Express API(单服务托管 API + 前端静态页)
│ ├── index.js # 接口与模式/版本闸门
│ ├── db.js # MongoDB 存储层
│ ├── data/ # meta.json 快照(remote 模式必需)
│ ├── scripts/
│ │ ├── migrate-to-mongo.js # 一次性:JSON 文件 → Mongo
│ │ └── push-meta.js # 开发版:生成 meta 快照 + 抬升库版本
│ └── .env.example
└── web/ # Vue3 + Element Plus 前端
└── dist/ # 构建产物(remote 模式由 server 托管,需提交)
```
## 部署线上设计版(给 AI 部署智能体)
前置:Node.js ≥ 18;可访问的 MongoDB 连接串。
```bash
# 1. 拉代码
git clone git@git.eoxnet.com:pan/hero_set.git && cd hero_set
# 2. 配置环境变量
cp server/.env.example server/.env
# 编辑 server/.env:
# MODE=remote
# MONGO_URL=mongodb://user:pass@host:27017
# MONGO_DB=heroconfig
# PORT=3001
# 3. 构建前端
cd web && npm ci && npm run build && cd ..
# 注意:web/dist 与 server/data/meta.json 必须存在(均已随仓库提交,若仓库有更新请重新构建)
# 4. 启动(server 自动托管 web/dist,单服务即可)
cd server && npm ci && npm start
# 访问 http://<host>:3001
```
进程守护(可选):`pm2 start index.js --name hero-set --cwd server`
## 开发版工作流(本地)
```bash
# server/.env:MODE=local + 同一个 MONGO_URL
cd server && npm ci && npm run dev # API :3001
cd web && npm ci && npm run dev # 前端 :4004(已代理 /api → :3001)
```
- **保存到数据库** → 全量覆盖 MongoDB(按 uuid upsert + 删除多余)
- **写入 heroSet.ts** → 渲染进 `assets/script/game/common/config/heroSet.ts` 的 `@hero-setup:heroes-begin/end` 标记区域
- **技能/底座结构变更后**:`SCHEMA_VERSION` +1 → `npm run push-meta`(需 dev 服务已启动)→ 提交并重新部署线上版
## 环境变量
| 变量 | 默认 | 说明 |
|---|---|---|
| `MODE` | `local` | `local` 开发版 / `remote` 线上设计版 |
| `MONGO_URL` | — | 必填,MongoDB 连接串 |
| `MONGO_DB` | `heroconfig` | 库名 |
| `PORT` | `3001` | 服务端口 |
| `HOST` | `0.0.0.0` | 监听地址;容器部署必须保持 0.0.0.0 并映射端口(如 `docker run -p 3001:3001`) |
| `SCHEMA_VERSION` | `1` | 数据结构版本,破坏性变更时 +1 |
前端单独部署(不经 server 托管)时,`web/.env` 配 `VITE_API_BASE=http://<host>:3001`。
## 数据库结构(MongoDB · heroconfig)
- `heroes`:每个英雄一条文档,`uuid` 唯一索引
- `templates`:`_id='presets'` 单文档(triggerPresets / fieldPresets)
- `meta`:`_id='version'` 单文档(schemaVersion)

166
put/package-lock.json generated
View File

@@ -527,6 +527,15 @@
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
"license": "MIT"
},
"node_modules/@mongodb-js/saslprep": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.5.5.tgz",
"integrity": "sha512-0PV4mvjGd2Q2/viDvXuoSNzTjQngHvtuWUry1vqvcK4Q/CTeO1zEKMf9set01DMocJnogl/pyNAdvyk+Pe0kQA==",
"license": "MIT",
"dependencies": {
"sparse-bitfield": "^3.0.3"
}
},
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
@@ -975,6 +984,21 @@
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
"license": "MIT"
},
"node_modules/@types/whatwg-url": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
"license": "MIT",
"dependencies": {
"@types/webidl-conversions": "*"
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
@@ -1253,6 +1277,15 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bson": {
"version": "6.10.4",
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz",
"integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==",
"license": "Apache-2.0",
"engines": {
"node": ">=16.20.1"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -1495,6 +1528,21 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dotenv": {
"version": "18.0.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.5.tgz",
"integrity": "sha512-aBrGvt6KhjxbEnatqMWOMidftwsrCiCvqh1yNIGB9QyF0J+VcWkKDMsvYOEm7ZS82oiV+BT1sTOtIlAmUlTpwQ==",
"license": "BSD-2-Clause",
"bin": {
"dotenv": "dist/index.cjs"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2102,6 +2150,12 @@
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"license": "MIT"
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
@@ -2153,6 +2207,62 @@
"node": ">= 0.6"
}
},
"node_modules/mongodb": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz",
"integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==",
"license": "Apache-2.0",
"dependencies": {
"@mongodb-js/saslprep": "^1.3.0",
"bson": "^6.10.4",
"mongodb-connection-string-url": "^3.0.2"
},
"engines": {
"node": ">=16.20.1"
},
"peerDependencies": {
"@aws-sdk/credential-providers": "^3.188.0",
"@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
"gcp-metadata": "^5.2.0",
"kerberos": "^2.0.1",
"mongodb-client-encryption": ">=6.0.0 <7",
"snappy": "^7.3.2",
"socks": "^2.7.1"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-providers": {
"optional": true
},
"@mongodb-js/zstd": {
"optional": true
},
"gcp-metadata": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"snappy": {
"optional": true
},
"socks": {
"optional": true
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
"license": "Apache-2.0",
"dependencies": {
"@types/whatwg-url": "^11.0.2",
"whatwg-url": "^14.1.0 || ^13.0.0"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -2309,6 +2419,15 @@
"node": ">=10"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/qs": {
"version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
@@ -2590,6 +2709,15 @@
"node": ">=0.10.0"
}
},
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"license": "MIT",
"dependencies": {
"memory-pager": "^1.0.2"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -2669,6 +2797,18 @@
"node": ">=0.6"
}
},
"node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -3299,6 +3439,28 @@
"resolved": "web",
"link": true
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -3360,8 +3522,10 @@
"version": "1.0.0",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^18.0.5",
"esbuild": "^0.24.0",
"express": "^4.21.2"
"express": "^4.21.2",
"mongodb": "^6.12.0"
}
},
"web": {

24
put/server/.env.example Normal file
View File

@@ -0,0 +1,24 @@
# ---- 运行模式 ----
# local = 项目开发版(本地,全功能:写 heroSet.ts / 导出 CSV / push-meta)
# remote = 线上设计版(VPS 部署,仅英雄/预设编辑,版本落后时只读)
MODE=local
# ---- MongoDB(两边都要配置,连同一个库)----
# 本地 MongoDB 示例:mongodb://127.0.0.1:27017
# 带账号密码示例: mongodb://user:password@your-vps-ip:27017
# Atlas 示例: mongodb+srv://user:password@cluster0.xxxx.mongodb.net
MONGO_URL=mongodb://user:password@your-vps-ip:27017
MONGO_DB=heroconfig
# ---- 服务端口 ----
PORT=3001
# 监听地址:默认 0.0.0.0(容器/局域网部署必须);仅本机访问可改 127.0.0.1
HOST=0.0.0.0
# ---- 数据结构版本 ----
# 英雄/底座结构有破坏性变更时 +1,并执行 npm run push-meta 推送到库中
# 线上设计版低于库中版本时将进入只读,需更新部署
SCHEMA_VERSION=1
# ---- 前端(web/.env,仅静态部署时需要;server 托管 dist 时不用配)----
# VITE_API_BASE=http://your-vps-ip:3001

View File

@@ -0,0 +1,314 @@
# 英雄配置 API 智能体调用规范
> 本文档面向 **Hermes / OpenClaw** 等智能体 Skill,说明如何通过 HTTP API 配置与修改英雄。
> 数据源头为 `heroes.json` / MongoDB,技能/光环/buff 底座定义请阅读 `heroes.json编辑指引.md`。
---
## 1. 服务信息
| 项 | 值 |
|---|---|
| 开发版地址 | `http://localhost:3001`(`MODE=local`) |
| 线上设计版地址 | 部署地址(`MODE=remote`,静态托管 `web/dist`) |
| 内容类型 | `application/json` |
| 认证 | 无(内网/本地环境) |
启动服务(在 `put` 目录):
```bash
npm run dev # 开发版
npm run dev:remote # 线上设计版(本地模拟 remote)
```
---
## 2. 核心接口一览
| 方法 | 路径 | 说明 |
|---|---|---|
| `GET` | `/api/meta` | 获取底座快照(技能/光环/buff/职业模板/版本) |
| `GET` | `/api/heroes` | 获取全部英雄列表 |
| `GET` | `/api/heroes/:uuid` | 获取单个英雄完整配置 |
| `POST` | `/api/heroes/:uuid` | **新增或覆盖**单个英雄(body 为完整英雄对象) |
| `PUT` | `/api/heroes/:uuid` | **局部更新**单个英雄(body 为需修改的字段,顶层浅合并) |
| `PATCH` | `/api/heroes/:uuid/skills/:skillUuid` | **技能级局部更新**:支持按 `lv` 合并 `lv_entries`,避免整表替换 `skills` |
| `DELETE` | `/api/heroes/:uuid` | 删除单个英雄 |
| `POST` | `/api/refresh` | 刷新底座缓存(修改 SkillSet.ts 等后调用) |
> **版本闸门**:线上设计版(remote)在库中 `schemaVersion` 高于服务内置版本时,写操作返回 `409`,需更新部署。
---
## 3. 英雄对象结构(Hero Schema)
智能体在构造或修改英雄时,必须遵循以下字段规范。
### 3.1 顶层字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `uuid` | number | 是 | 英雄唯一 ID,5000 段,新增从 5011 起(先 `GET /api/heroes` 查重) |
| `name` | string | 是 | 显示名 |
| `comment` | string | 否 | 设计说明/机制主题 |
| `path` | string | 是 | 美术资源名,需存在 `assets/resources/game/heros/{path}.prefab` |
| `rarity` | 1~5 | 是 | 稀有度,影响 lv1 基础属性缩放与卡池费用 |
| `roleKey` | string | 是 | 职业:`Tank`/`Warrior`/`Assassin`/`Archer`/`Mage`/`Support` |
| `growKey` | string | 是 | 成长曲线,通常与 `roleKey` 一致 |
| `typeKey` | string | 是 | 攻击定位:`Melee`/`Mid`/`Long` |
| `statBase` | `{hp,ap,def}` | 是 | 相对职业模板的偏移量,惯例 ±5/10/20/30 |
| `dis` / `speed` / `dodge` / `accuracy` | number\|null | 否 | 英雄一般 `null`,走默认 |
| `skills` | array | 是 | **恰好 2 个**:`[普攻, 大招]`,见 §3.2 |
| `atking`/`atked`/`fstart`/`fend`/`dead` | object | 是 | 触发组,见 §3.3;无则写 `{}` |
| `field` | array | 是 | 驻场光环档位,见 §3.4;无则写 `[]` |
| `bonus` | array | 是 | 额外属性加成,见 §3.5;常规英雄写 `[]` |
| `info` | string | 否 | 描述文案 |
### 3.2 skills 数组(恰好 2 项)
每项结构:`{uuid, name, cd, cdExpr, overrides, desc, lv_entries}`
| 字段 | 说明 |
|---|---|
| `uuid` | 技能底座 uuid,普攻必须 ∈ `ROLE_ATK_SKILL[roleKey]`,大招 ∈ `ROLE_ULT_SKILL[roleKey]` 或 `SUPPORT_ULT` |
| `name` | 留空回退底座名;建议起特色名 |
| `cd` | 数值冷却(普攻 0.75/0.9/1.05/1.2/1.5;大招 6/7/8/9/10) |
| `cdExpr` | 表达式,普攻=`AtkSpeedSet[AtkSpeedLv.X].cd`,大招=`UltCdSet[UltCdLv.X].cd` |
| `overrides` | 覆写键值对,见 §3.6 |
| `desc` | 描述模板,支持 `{target}` `{ap}` `{dur}` 等占位符 |
| `lv_entries` | 升级档位:普攻 1 档 `{lv:2,s_lv:2,...}`;大招 2 档 `{lv:8,s_lv:2,...}`、`{lv:16,s_lv:3,...}` |
### 3.3 触发组(atking / atked / fstart / fend / dead)
结构:`{ "<技能uuid字符串>": [条目, ...] }`
条目:`{lv, s_lv, name, t_num, t_chance?, pity_add?, overrides, desc}`
| 字段 | 说明 |
|---|---|
| `lv` | 解锁等级,惯例 **4** |
| `s_lv` | 技能等级,惯例 **1** |
| `t_num` | 触发间隔:atking/atked 为每 n 次;fstart/fend 固定 **1** |
| `t_chance` | 触发概率(0~100),仅 atking/atked 生效,缺省=100 |
| `pity_add` | 保底叠加,仅 atking/atked 生效,触发后清零 |
| `overrides` | 同技能覆写 |
| `desc` | 描述模板,支持 `{t_chance}` `{pity_add}` 占位符 |
**复活**:在 `dead` 组条目 `overrides` 中配置 `revive_hp`(回血%)+ `revive_count`(次数上限)。
### 3.4 field 光环
结构:`[{lv:4, uuids:[{uuid, value, name}], desc?}]`
- `uuid` 必须 ∈ `FieldSkillSet`(7001~7022 段)
- `value` 按光环类型:比例类为小数(如 0.15),次数/固定值类为整数
- 解锁等级惯例 **lv4**
### 3.5 bonus 额外属性
结构:`[{lv, attrs:{...}, name?, icon?, info?}]`
- `attrs` 键:`hp`/`ap`/`def`(百分比,基数=基础×品质+成长)、`crt`/`frz`/`stun`/`para`(固定百分点)
- 运行时累加所有 `lv ≤ 英雄等级` 的档位
- 配 `icon` 的档位在图鉴技能列表最末展示
### 3.6 overrides 白名单
可用键:
`ap, hit_count, hitcd, crt, frz, stun, para, bck, bck_chance, TGroup, buff_type, timed_buff_id, buff_value, buff_duration, buff_chance, buff_target, timed_debuff_id, debuff_value, debuff_duration, debuff_chance, call_hero, summon_count, is_accel, num, revive_hp, revive_count, proc_skill, icon`
特殊格式:
- `TGroup`:字符串 `"Self"`/`"Ally"`/`"Team"`/`"Enemy"`,导出还原枚举
- `buff_target`:数字 0~3(Hit/Self/Ally/Enemy)
- `proc_skill`:对象 `{s_uuid, chance?, overrides?}`
- `icon`:字符串图标帧名
**buff/debuff 语义**:
- `timed_buff_id`:增益,缺省挂施法者自身(伤害技能)或友方目标(辅助技能)
- `timed_debuff_id`:减益,命中后挂被命中者,目标不可覆写
- 对敌挂减益必须用 `timed_debuff_id`,误用 `timed_buff_id` 会把减益挂到自己身上
---
## 4. 调用示例
### 4.1 获取底座数据(修改前必读)
```http
GET /api/meta
```
返回包含 `SkillSet`、`FieldSkillSet`、`BuffList`、`HERO_ROLE_BASE`、`HERO_GROW_COEF`、`AtkSpeedSet`、`HeroDisVal` 等。
### 4.2 查询全部英雄
```http
GET /api/heroes
```
### 4.3 查询单个英雄
```http
GET /api/heroes/5001
```
### 4.4 新增英雄(完整对象)
```http
POST /api/heroes/5011
Content-Type: application/json
{
"uuid": 5011,
"name": "烈焰法师",
"comment": "群体火伤+燃烧",
"path": "mage_fire",
"rarity": 4,
"roleKey": "Mage",
"growKey": "Mage",
"typeKey": "Long",
"statBase": { "hp": -5, "ap": 30, "def": 0 },
"dis": null,
"speed": null,
"dodge": null,
"accuracy": null,
"skills": [
{
"uuid": 6009,
"name": "火球术",
"cd": 1.2,
"cdExpr": "AtkSpeedSet[AtkSpeedLv.Slow].cd",
"overrides": { "ap": 120 },
"desc": "对{target}造成{ap}%伤害",
"lv_entries": [
{ "lv": 2, "s_lv": 2, "overrides": { "ap": 150 }, "desc": "造成{ap}%伤害" }
]
},
{
"uuid": 6200,
"name": "烈焰风暴",
"cd": 8,
"cdExpr": "UltCdSet[UltCdLv.Normal].cd",
"overrides": { "ap": 200, "timed_debuff_id": 7002, "debuff_value": 30, "debuff_chance": 50 },
"desc": "对全体敌人造成{ap}%伤害,{debuff_chance}%附加燃烧",
"lv_entries": [
{ "lv": 8, "s_lv": 2, "overrides": { "ap": 250 }, "desc": "造成{ap}%伤害" },
{ "lv": 16, "s_lv": 3, "overrides": { "ap": 350 }, "desc": "造成{ap}%伤害" }
]
}
],
"atking": {},
"atked": {},
"fstart": {},
"fend": {},
"dead": {},
"field": [],
"bonus": [],
"info": "掌控火焰的法师"
}
```
### 4.5 局部修改英雄(如只改大招数值)
```http
PUT /api/heroes/5011
Content-Type: application/json
{
"skills": [
{ "uuid": 6009, "name": "火球术", "cd": 1.2, "cdExpr": "AtkSpeedSet[AtkSpeedLv.Slow].cd", "overrides": { "ap": 120 }, "desc": "对{target}造成{ap}%伤害", "lv_entries": [{ "lv": 2, "s_lv": 2, "overrides": { "ap": 150 }, "desc": "造成{ap}%伤害" }] },
{ "uuid": 6200, "name": "烈焰风暴", "cd": 8, "cdExpr": "UltCdSet[UltCdLv.Normal].cd", "overrides": { "ap": 250, "timed_debuff_id": 7002, "debuff_value": 50, "debuff_chance": 80 }, "desc": "对全体敌人造成{ap}%伤害,{debuff_chance}%附加燃烧", "lv_entries": [{ "lv": 8, "s_lv": 2, "overrides": { "ap": 300 }, "desc": "造成{ap}%伤害" }, { "lv": 16, "s_lv": 3, "overrides": { "ap": 400 }, "desc": "造成{ap}%伤害" }] }
]
}
```
> **注意**:`PUT` 是**顶层字段浅合并**。若修改 `skills` 数组,必须传入**完整的新数组**(不能只传被修改的技能对象)。若只需改某个技能的局部字段,请用 `PATCH /api/heroes/:uuid/skills/:skillUuid`(见 §4.7)。
### 4.6 删除英雄
```http
DELETE /api/heroes/5011
```
### 4.7 技能级局部更新(推荐智能体使用)
当只需要修改某个技能的局部字段(如调整 lv8 档位的 ap 值)时,使用技能级 PATCH,避免整表替换 `skills` 数组。
**合并规则**:
- `name`/`cd`/`cdExpr`/`desc`:存在则直接覆盖
- `overrides`:与现有 `overrides` **浅合并**(不是替换)
- `lv_entries`:按 `lv` 合并——同 `lv` 更新该档(`overrides` 浅合并),不同 `lv` 新增
```http
PATCH /api/heroes/5011/skills/6200
Content-Type: application/json
{
"overrides": { "ap": 280 },
"lv_entries": [
{ "lv": 8, "overrides": { "ap": 320 } },
{ "lv": 20, "s_lv": 4, "overrides": { "ap": 500 }, "desc": "造成{ap}%伤害" }
]
}
```
**效果**:
- 基础 `overrides.ap` 从 250 变为 280
- `lv_entries` 中 `lv:8` 档的 `overrides.ap` 从 300 变为 320(其他字段保留)
- `lv_entries` 中新增 `lv:20` 档
**对比:三种修改方式的选择**
| 场景 | 推荐接口 | 原因 |
|---|---|---|
| 只改英雄名/稀有度/statBase 等顶层字段 | `PUT /api/heroes/:uuid` | 简单直接,不涉及 skills |
| 改某个技能的 name/cd/desc/overrides 或某一档 lv_entry | `PATCH /api/heroes/:uuid/skills/:skillUuid` | 避免整表替换 skills,最安全 |
| 新增/删除技能、改技能 uuid、大规模重构 | `POST /api/heroes/:uuid`(全量覆盖) | 结构变动大,整表替换更清晰 |
---
## 5. 智能体操作流程(推荐)
1. **读取底座**:`GET /api/meta`,确认技能/光环/buff 可用值与职业模板
2. **查询现有英雄**:`GET /api/heroes`,确定新 uuid 不冲突(当前已用 5001~5010)
3. **构造英雄对象**:严格遵循 §3 结构,空组写 `{}`,空数组写 `[]`
4. **提交**:
- 新增/覆盖:`POST /api/heroes/:uuid`
- 顶层字段局部修改:`PUT /api/heroes/:uuid`
- 技能级局部修改:`PATCH /api/heroes/:uuid/skills/:skillUuid`
5. **验证**:`GET /api/heroes/:uuid` 确认数据已写入
6. **(开发版)应用**:`POST /api/apply` 将 heroes.json 渲染进 `heroSet.ts`(线上设计版无此权限)
---
## 6. 错误码与处理
| 状态码 | 场景 | 处理 |
|---|---|---|
| 400 | 参数错误(uuid 非数字、body 非对象/数组、heroes 为空等) | 检查请求格式 |
| 403 | 线上设计版调用本地专属接口(如 `/api/apply`) | 切换至开发版或放弃该操作 |
| 404 | 英雄不存在 | 先 `GET /api/heroes` 确认 uuid |
| 409 | 线上设计版结构版本落后 | 更新部署后再试 |
| 500 | 服务端错误(MongoDB 连接失败、文件读写失败等) | 查看服务日志 |
---
## 7. 常见陷阱(智能体必读)
- **不要手改 `heroSet.ts` 英雄区**:`POST /api/apply` 会整体覆盖标记区内容
- **新增英雄后必须手动把 uuid 加入 `HeroList`**(`heroSet.ts` 标记区之外),否则英雄不进卡池
- **触发组解锁等级是 lv4,大招档位是 lv8+lv16**,不要写 lv6/lv9
- **普攻 cdExpr 用 `AtkSpeedSet`,大招 cdExpr 用 `UltCdSet`**,两套体系不可混用
- **档位 overrides 与基础 overrides 浅合并**,档位只写变化键,不要全量复制
- **desc 用占位符 `{ap}`**,不要硬编码数字,避免与 overrides 数值脱节
- **对敌 debuff 用 `timed_debuff_id`,吸血/自强化用 `timed_buff_id`**,两者目标语义相反
- **复活已废弃独立 `revive` 字段**,改在 `dead` 组 overrides 配 `revive_hp`/`revive_count`
---
## 8. 参考文档
- 详细字段规则与数据源:`put/server/data/heroes.json编辑指引.md`
- 编辑器枚举与默认值:`put/web/src/constants.js`
- 技能底座定义:`assets/script/game/common/config/SkillSet.ts`
- Buff 底座定义:`assets/script/game/common/config/BuffSet.ts`
- 职业模板与成长:`assets/script/game/common/config/heroSet.ts`

File diff suppressed because one or more lines are too long

75
put/server/db.js Normal file
View File

@@ -0,0 +1,75 @@
/**
* 英雄/预设存储层:开发版与线上设计版统一读写线上 MongoDB。
* - heroes 集合:每个英雄一条文档,uuid 唯一索引
* - templates 集合:预设库(_id='presets' 单文档)
* - meta 集合:版本闸门(_id='version' 单文档,schemaVersion 由开发版推送 meta 时写入)
* 技能/Buff 底座不进数据库,开发版走 TS 编译,线上版走 meta.json 快照。
*/
import { MongoClient } from 'mongodb'
const MONGO_URL = process.env.MONGO_URL || ''
const DB_NAME = process.env.MONGO_DB || 'heroconfig'
let client = null
let db = null
/** 惰性连接 MongoDB(单例复用) */
async function mongo() {
if (db) return db
if (!MONGO_URL) throw new Error('未配置 MONGO_URL 环境变量')
client = new MongoClient(MONGO_URL)
await client.connect()
db = client.db(DB_NAME)
// uuid 唯一索引:保证英雄按 uuid 幂等覆盖
await db.collection('heroes').createIndex({ uuid: 1 }, { unique: true })
return db
}
/** 读取全部英雄(按 uuid 升序,剥离 Mongo 内部 _id) */
export async function getHeroes() {
const docs = await (await mongo()).collection('heroes').find().sort({ uuid: 1 }).toArray()
return docs.map(({ _id, ...h }) => h)
}
/** 全量覆盖:upsert 传入英雄,并删除库中多余的 uuid */
export async function saveHeroes(heroes) {
const col = (await mongo()).collection('heroes')
for (const h of heroes) {
await col.replaceOne({ uuid: h.uuid }, h, { upsert: true })
}
await col.deleteMany({ uuid: { $nin: heroes.map((h) => h.uuid) } })
}
/** 读取预设库(无文档时返回空池) */
export async function getTemplates() {
const doc = await (await mongo()).collection('templates').findOne({ _id: 'presets' })
return {
triggerPresets: Array.isArray(doc?.triggerPresets) ? doc.triggerPresets : [],
fieldPresets: Array.isArray(doc?.fieldPresets) ? doc.fieldPresets : [],
}
}
/** 全量覆盖预设库 */
export async function saveTemplates(triggerPresets, fieldPresets) {
await (await mongo())
.collection('templates')
.replaceOne({ _id: 'presets' }, { _id: 'presets', triggerPresets, fieldPresets }, { upsert: true })
}
/** 读取库中数据结构版本(首次为 0) */
export async function getSchemaVersion() {
const doc = await (await mongo()).collection('meta').findOne({ _id: 'version' })
return doc?.schemaVersion ?? 0
}
/**
* 推送数据结构版本:只允许升不允许降,避免旧部署误把版本刷低导致线上版解锁写入。
* @returns {Promise<number>} 写入后的实际版本
*/
export async function bumpSchemaVersion(version) {
const col = (await mongo()).collection('meta')
const cur = await getSchemaVersion()
const next = Math.max(cur, version)
await col.replaceOne({ _id: 'version' }, { _id: 'version', schemaVersion: next, updatedAt: new Date() }, { upsert: true })
return next
}

View File

@@ -1,9 +1,17 @@
/**
* 英雄配置工具服务(双模式)
* - 项目开发版(MODE=local):全功能,meta 从游戏工程 TS 编译,可写 heroSet.ts / 导出 CSV / 推送快照
* - 线上设计版(MODE=remote):meta 从 data/meta.json 快照加载,仅英雄/预设的增删改查;
* 库中 schemaVersion 高于本服务内置版本时进入只读(保存接口返回 409),需更新部署后才能修改
*/
import 'dotenv/config'
import express from 'express'
import cors from 'cors'
import esbuild from 'esbuild'
import fs from 'node:fs'
import path from 'node:path'
import url from 'node:url'
import * as store from './db.js'
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
@@ -14,7 +22,11 @@ const CSV_PATH = path.join(CONFIG_DIR, '英雄设计表.csv')
const HEROES_BEGIN = '// @hero-setup:heroes-begin'
const HEROES_END = '// @hero-setup:heroes-end'
const DATA_DIR = path.join(__dirname, 'data')
const HEROES_PATH = path.join(DATA_DIR, 'heroes.json')
const META_SNAPSHOT_PATH = path.join(DATA_DIR, 'meta.json')
const MODE = process.env.MODE || 'local'
/** 数据结构版本:英雄/底座结构有破坏性变更时 +1,并随 push-meta 推送到库中 */
const SCHEMA_VERSION = Number(process.env.SCHEMA_VERSION || 1)
const app = express()
app.use(cors())
@@ -43,6 +55,14 @@ async function loadConfigModule(file) {
let cache = null
async function buildCache() {
// 线上设计版:从快照文件加载(快照由开发版 scripts/push-meta.js 生成并随部署发布)
if (MODE === 'remote') {
if (!fs.existsSync(META_SNAPSHOT_PATH)) {
throw new Error('缺少 data/meta.json 快照,请先在开发版执行 npm run push-meta 并重新部署')
}
cache = JSON.parse(fs.readFileSync(META_SNAPSHOT_PATH, 'utf8'))
return cache
}
const [skill, buff, hero] = await Promise.all([
loadConfigModule('SkillSet.ts'),
loadConfigModule('BuffSet.ts'),
@@ -63,7 +83,12 @@ async function buildCache() {
app.get('/api/meta', async (_req, res) => {
try {
if (!cache) await buildCache()
res.json(cache)
res.json({
...cache,
mode: MODE,
schemaVersion: SCHEMA_VERSION,
dbSchemaVersion: await store.getSchemaVersion(),
})
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
@@ -78,51 +103,209 @@ app.post('/api/refresh', async (_req, res) => {
}
})
/** 英雄数据持久化:GET 读取(无文件时返回空列表) */
app.get('/api/heroes', (_req, res) => {
/** 版本闸门:线上设计版落后于库中结构版本时,写操作返回 409 强制只读 */
async function guardVersion(res) {
if (MODE !== 'remote') return true
const dbVer = await store.getSchemaVersion()
if (dbVer > SCHEMA_VERSION) {
res.status(409).json({
error: `线上设计版结构版本(v${SCHEMA_VERSION})低于数据库(v${dbVer}),请更新部署后再修改`,
dbSchemaVersion: dbVer,
schemaVersion: SCHEMA_VERSION,
})
return false
}
return true
}
/** 英雄数据:GET 从线上库读取 */
app.get('/api/heroes', async (_req, res) => {
try {
if (!fs.existsSync(HEROES_PATH)) return res.json({ heroes: [] })
res.json(JSON.parse(fs.readFileSync(HEROES_PATH, 'utf8')))
res.json({ heroes: await store.getHeroes() })
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
})
/** 英雄数据持久化:POST 全量覆盖保存 */
app.post('/api/heroes', (req, res) => {
/** 英雄数据:POST 全量覆盖写入线上库 */
app.post('/api/heroes', async (req, res) => {
const heroes = req.body?.heroes
if (!Array.isArray(heroes)) {
return res.status(400).json({ error: 'heroes 必须为数组' })
}
try {
fs.mkdirSync(DATA_DIR, { recursive: true })
fs.writeFileSync(HEROES_PATH, JSON.stringify({ heroes }, null, 2), 'utf8')
res.json({ ok: true, path: HEROES_PATH, count: heroes.length })
if (!(await guardVersion(res))) return
await store.saveHeroes(heroes)
res.json({ ok: true, count: heroes.length })
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
})
const TEMPLATES_PATH = path.join(DATA_DIR, 'templates.json')
/** 预设库持久化:GET 读取(无文件时返回空池) */
app.get('/api/templates', (_req, res) => {
/** 单英雄读取:GET /api/heroes/:uuid */
app.get('/api/heroes/:uuid', async (req, res) => {
try {
if (!fs.existsSync(TEMPLATES_PATH)) return res.json({ triggerPresets: [], fieldPresets: [] })
const data = JSON.parse(fs.readFileSync(TEMPLATES_PATH, 'utf8'))
// 兼容旧格式:若根上是 templates 数组,返回空池让前端走默认
if (Array.isArray(data.templates)) return res.json({ triggerPresets: [], fieldPresets: [] })
res.json({
triggerPresets: Array.isArray(data.triggerPresets) ? data.triggerPresets : [],
fieldPresets: Array.isArray(data.fieldPresets) ? data.fieldPresets : [],
})
const uuid = Number(req.params.uuid)
if (Number.isNaN(uuid)) return res.status(400).json({ error: 'uuid 必须为数字' })
const heroes = await store.getHeroes()
const hero = heroes.find((h) => h.uuid === uuid)
if (!hero) return res.status(404).json({ error: `英雄 ${uuid} 不存在` })
res.json(hero)
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
})
/** 职业模板持久化:POST 全量覆盖保存(结构:{ triggerPresets: [], fieldPresets: [] }) */
app.post('/api/templates', (req, res) => {
/** 单英雄新增/覆盖:POST /api/heroes/:uuid(body 为完整英雄对象,uuid 以 URL 为准) */
app.post('/api/heroes/:uuid', async (req, res) => {
try {
if (!(await guardVersion(res))) return
const uuid = Number(req.params.uuid)
if (Number.isNaN(uuid)) return res.status(400).json({ error: 'uuid 必须为数字' })
const hero = req.body
if (!hero || typeof hero !== 'object') return res.status(400).json({ error: 'body 必须为英雄对象' })
hero.uuid = uuid
const heroes = await store.getHeroes()
const idx = heroes.findIndex((h) => h.uuid === uuid)
if (idx >= 0) heroes[idx] = hero
else heroes.push(hero)
await store.saveHeroes(heroes)
res.json({ ok: true, uuid, action: idx >= 0 ? 'updated' : 'created' })
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
})
/** 单英雄局部更新:PUT /api/heroes/:uuid(body 为部分字段,与现有英雄浅合并) */
app.put('/api/heroes/:uuid', async (req, res) => {
try {
if (!(await guardVersion(res))) return
const uuid = Number(req.params.uuid)
if (Number.isNaN(uuid)) return res.status(400).json({ error: 'uuid 必须为数字' })
const patch = req.body
if (!patch || typeof patch !== 'object') return res.status(400).json({ error: 'body 必须为部分字段对象' })
const heroes = await store.getHeroes()
const idx = heroes.findIndex((h) => h.uuid === uuid)
if (idx < 0) return res.status(404).json({ error: `英雄 ${uuid} 不存在` })
// uuid 不可变,防止 patch 覆盖
delete patch.uuid
heroes[idx] = { ...heroes[idx], ...patch }
await store.saveHeroes(heroes)
res.json({ ok: true, uuid, action: 'patched' })
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
})
/** 单英雄删除:DELETE /api/heroes/:uuid */
app.delete('/api/heroes/:uuid', async (req, res) => {
try {
if (!(await guardVersion(res))) return
const uuid = Number(req.params.uuid)
if (Number.isNaN(uuid)) return res.status(400).json({ error: 'uuid 必须为数字' })
const heroes = await store.getHeroes()
const idx = heroes.findIndex((h) => h.uuid === uuid)
if (idx < 0) return res.status(404).json({ error: `英雄 ${uuid} 不存在` })
heroes.splice(idx, 1)
await store.saveHeroes(heroes)
res.json({ ok: true, uuid, action: 'deleted' })
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
})
/**
* 技能级局部更新:PATCH /api/heroes/:uuid/skills/:skillUuid
* 支持对技能字段及 lv_entries 按 lv 合并,避免整表替换 skills 数组。
*
* body 格式:
* {
* name?, cd?, cdExpr?, desc?, // 技能基础字段,存在则覆盖
* overrides?, // 与现有 overrides 浅合并(不是替换)
* lv_entries?: [ // 按 lv 合并:存在则更新该档,不存在则新增
* { lv, s_lv?, overrides?, desc? }
* ]
* }
*/
app.patch('/api/heroes/:uuid/skills/:skillUuid', async (req, res) => {
try {
if (!(await guardVersion(res))) return
const uuid = Number(req.params.uuid)
const skillUuid = Number(req.params.skillUuid)
if (Number.isNaN(uuid) || Number.isNaN(skillUuid)) {
return res.status(400).json({ error: 'uuid 与 skillUuid 必须为数字' })
}
const patch = req.body
if (!patch || typeof patch !== 'object') {
return res.status(400).json({ error: 'body 必须为技能补丁对象' })
}
const heroes = await store.getHeroes()
const hIdx = heroes.findIndex((h) => h.uuid === uuid)
if (hIdx < 0) return res.status(404).json({ error: `英雄 ${uuid} 不存在` })
const hero = heroes[hIdx]
const sIdx = hero.skills.findIndex((s) => s.uuid === skillUuid)
if (sIdx < 0) return res.status(404).json({ error: `英雄 ${uuid} 不存在技能 ${skillUuid}` })
const skill = hero.skills[sIdx]
// 基础字段直接覆盖
if (patch.name !== undefined) skill.name = patch.name
if (patch.cd !== undefined) skill.cd = patch.cd
if (patch.cdExpr !== undefined) skill.cdExpr = patch.cdExpr
if (patch.desc !== undefined) skill.desc = patch.desc
// overrides 浅合并(不是替换)
if (patch.overrides && typeof patch.overrides === 'object') {
skill.overrides = { ...skill.overrides, ...patch.overrides }
}
// lv_entries 按 lv 合并:同 lv 更新,不同 lv 新增
if (Array.isArray(patch.lv_entries)) {
if (!Array.isArray(skill.lv_entries)) skill.lv_entries = []
for (const entry of patch.lv_entries) {
if (!entry || typeof entry !== 'object') continue
const lv = entry.lv
if (lv === undefined) continue
const eIdx = skill.lv_entries.findIndex((e) => e.lv === lv)
if (eIdx >= 0) {
// 同 lv:浅合并 overrides,覆盖其他字段
const existing = skill.lv_entries[eIdx]
skill.lv_entries[eIdx] = {
...existing,
...entry,
overrides: entry.overrides
? { ...existing.overrides, ...entry.overrides }
: existing.overrides,
}
} else {
// 新 lv:直接插入
skill.lv_entries.push(entry)
}
}
// 按 lv 升序排序,保持数据整洁
skill.lv_entries.sort((a, b) => a.lv - b.lv)
}
await store.saveHeroes(heroes)
res.json({ ok: true, uuid, skillUuid, action: 'skill_patched' })
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
})
/** 预设库:GET 从线上库读取 */
app.get('/api/templates', async (_req, res) => {
try {
res.json(await store.getTemplates())
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
})
/** 预设库:POST 全量覆盖写入线上库(结构:{ triggerPresets: [], fieldPresets: [] }) */
app.post('/api/templates', async (req, res) => {
const body = req.body || {}
const triggerPresets = body.triggerPresets
const fieldPresets = body.fieldPresets
@@ -136,9 +319,9 @@ app.post('/api/templates', (req, res) => {
return res.status(400).json({ error: `触发预设 ${bad.id || bad.label} 的 group 非法:${bad.group}` })
}
try {
fs.mkdirSync(DATA_DIR, { recursive: true })
fs.writeFileSync(TEMPLATES_PATH, JSON.stringify({ triggerPresets, fieldPresets }, null, 2), 'utf8')
res.json({ ok: true, path: TEMPLATES_PATH, triggerCount: triggerPresets.length, fieldCount: fieldPresets.length })
if (!(await guardVersion(res))) return
await store.saveTemplates(triggerPresets, fieldPresets)
res.json({ ok: true, triggerCount: triggerPresets.length, fieldCount: fieldPresets.length })
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
@@ -425,8 +608,16 @@ function renderHeroCsv(heroes) {
return rows.map((r) => r.map(csvCell).join(',')).join('\n') + '\n'
}
/** 仅开发版可用的本地文件写操作(heroSet.ts / CSV),线上设计版调用返回 403 */
function localOnly(res) {
if (MODE === 'local') return true
res.status(403).json({ error: '线上设计版不支持此操作(写 heroSet.ts / 导出 CSV 请在项目开发版执行)' })
return false
}
app.post('/api/export-csv', async (req, res) => {
try {
if (!localOnly(res)) return
const heroes = req.body?.heroes
if (!Array.isArray(heroes) || !heroes.length) {
return res.status(400).json({ error: 'heroes 不能为空' })
@@ -440,12 +631,25 @@ app.post('/api/export-csv', async (req, res) => {
}
})
app.get('/api/export-csv', (_req, res) => {
app.get('/api/export-csv', (req, res) => {
if (!localOnly(res)) return
if (!fs.existsSync(CSV_PATH)) return res.status(404).json({ error: '尚未生成' })
res.setHeader('Content-Type', 'text/csv; charset=utf-8')
res.send(fs.readFileSync(CSV_PATH, 'utf8'))
})
/** 导出 meta 快照(开发版专用):供 push-meta 脚本发布到线上设计版 */
app.get('/api/meta-snapshot', async (_req, res) => {
try {
if (!localOnly(res)) return
if (!cache) await buildCache()
const { mode: _m, schemaVersion: _v, dbSchemaVersion: _d, ...snapshot } = cache
res.json(snapshot)
} catch (e) {
res.status(500).json({ error: String(e?.message || e) })
}
})
/** HeroList 定义匹配(用于导出时同步图鉴/卡池数据源) */
const HERO_LIST_RE = /export const HeroList: number\[\] = \[[\s\S]*?\n\];/
@@ -470,6 +674,7 @@ function syncHeroList(src, heroes) {
app.post('/api/apply', (req, res) => {
try {
if (!localOnly(res)) return
const heroes = req.body?.heroes
if (!Array.isArray(heroes) || !heroes.length) {
return res.status(400).json({ error: 'heroes 不能为空' })
@@ -491,7 +696,19 @@ app.post('/api/apply', (req, res) => {
}
})
const PORT = 3001
app.listen(PORT, () => {
console.log(`[hero-config-tool] server listening http://localhost:${PORT}`)
const PORT = Number(process.env.PORT || 3001)
// 默认监听所有网卡,保证容器/局域网端口映射可用;可用 HOST=127.0.0.1 限制为本机
const HOST = process.env.HOST || '0.0.0.0'
// 线上设计版:托管前端构建产物(web/dist),单个服务即可对外提供完整站点
if (MODE === 'remote') {
const DIST_DIR = path.resolve(__dirname, '..', 'web', 'dist')
if (fs.existsSync(DIST_DIR)) {
app.use(express.static(DIST_DIR))
app.get(/^\/(?!api\/).*/, (_req, res) => res.sendFile(path.join(DIST_DIR, 'index.html')))
}
}
app.listen(PORT, HOST, () => {
console.log(`[hero-config-tool] mode=${MODE} schemaVersion=${SCHEMA_VERSION} listening http://${HOST}:${PORT}`)
})

View File

@@ -4,11 +4,15 @@
"private": true,
"type": "module",
"scripts": {
"dev": "node --watch index.js"
"dev": "node --watch index.js",
"start": "node index.js",
"push-meta": "node scripts/push-meta.js"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^18.0.5",
"esbuild": "^0.24.0",
"express": "^4.21.2"
"express": "^4.21.2",
"mongodb": "^6.12.0"
}
}

View File

@@ -0,0 +1,35 @@
/**
* 一次性迁移:把本地 data/heroes.json、data/templates.json 导入线上 MongoDB
* 用法:配置 MONGO_URL 后执行 node scripts/migrate-to-mongo.js
* 已在库中的数据会按 uuid 幂等覆盖,可重复执行。
*/
import fs from 'node:fs'
import path from 'node:path'
import url from 'node:url'
import { saveHeroes, saveTemplates, getHeroes } from '../db.js'
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
const DATA_DIR = path.join(__dirname, '..', 'data')
const readJson = (file, fallback) => {
const p = path.join(DATA_DIR, file)
return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : fallback
}
const heroes = readJson('heroes.json', { heroes: [] }).heroes || []
const tpl = readJson('templates.json', { triggerPresets: [], fieldPresets: [] })
if (heroes.length) {
await saveHeroes(heroes)
console.log(`[migrate] heroes 导入 ${heroes.length} 条`)
} else {
console.log('[migrate] 无本地 heroes.json 或为空,跳过英雄导入')
}
if ((tpl.triggerPresets?.length || 0) + (tpl.fieldPresets?.length || 0) > 0) {
await saveTemplates(tpl.triggerPresets || [], tpl.fieldPresets || [])
console.log(`[migrate] templates 导入 trigger=${tpl.triggerPresets?.length || 0} field=${tpl.fieldPresets?.length || 0}`)
} else {
console.log('[migrate] 无本地 templates.json 或为空,跳过预设导入')
}
console.log(`[migrate] 库中现有英雄 ${(await getHeroes()).length} 条`)
process.exit(0)

View File

@@ -0,0 +1,29 @@
/**
* 推送 meta 快照 + 结构版本到线上(项目开发版使用)
* 用法:先启动本地 dev 服务,再执行 npm run push-meta
* 产物:server/data/meta.json —— 需随线上设计版一起部署(git 提交或拷贝到 VPS)
*/
import fs from 'node:fs'
import path from 'node:path'
import url from 'node:url'
import { bumpSchemaVersion } from '../db.js'
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
const SNAPSHOT_PATH = path.join(__dirname, '..', 'data', 'meta.json')
const LOCAL_API = process.env.LOCAL_API || 'http://localhost:3001'
const SCHEMA_VERSION = Number(process.env.SCHEMA_VERSION || 1)
const res = await fetch(`${LOCAL_API}/api/meta-snapshot`)
if (!res.ok) {
const body = await res.text().catch(() => '')
throw new Error(`拉取 meta 快照失败(请确认本地 dev 服务已启动): ${res.status} ${body}`)
}
const snapshot = await res.json()
fs.mkdirSync(path.dirname(SNAPSHOT_PATH), { recursive: true })
fs.writeFileSync(SNAPSHOT_PATH, JSON.stringify(snapshot), 'utf8')
const dbVersion = await bumpSchemaVersion(SCHEMA_VERSION)
console.log(`[push-meta] meta.json 已生成(${Buffer.byteLength(JSON.stringify(snapshot))} bytes)`)
console.log(`[push-meta] 数据库结构版本: v${dbVersion}(本服务 v${SCHEMA_VERSION})`)
console.log('[push-meta] 请将 meta.json 与最新代码一起部署到线上设计版')
process.exit(0)

88
put/web/dist/assets/index-CZTxENXL.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13
put/web/dist/index.html vendored Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>英雄配置编辑器</title>
<script type="module" crossorigin src="/assets/index-CZTxENXL.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dzv_EDk3.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -1,18 +1,33 @@
<template>
<div class="hero-app">
<header class="hero-header">
<h1>英雄配置编辑器</h1>
<h1>
英雄配置编辑器
<el-tag size="small" :type="isDev ? 'success' : 'warning'" effect="plain">
{{ isDev ? '项目开发版' : '线上设计版' }} v{{ meta?.schemaVersion ?? '-' }}
</el-tag>
</h1>
<div class="actions">
<el-radio-group v-model="view" size="small">
<el-radio-button value="heroes">英雄列表</el-radio-button>
<el-radio-button value="templates">预设库</el-radio-button>
</el-radio-group>
<el-button size="small" @click="refreshMeta" :loading="loadingMeta">刷新配置数据</el-button>
<el-button size="small" type="success" @click="doSave" :disabled="!heroes.length">保存到服务器</el-button>
<el-button size="small" type="success" @click="doSave" :disabled="!heroes.length || outdated">保存到数据库</el-button>
<template v-if="isDev">
<el-button size="small" type="primary" @click="doApply" :disabled="!heroes.length">写入 heroSet.ts</el-button>
<el-button size="small" type="danger" @click="doExportCsv" :disabled="!heroes.length">导出英雄设计表.csv</el-button>
</template>
</div>
</header>
<el-alert
v-if="outdated"
type="error"
:closable="false"
show-icon
:title="`数据库结构已升级到 v${meta.dbSchemaVersion},当前线上设计版(v${meta.schemaVersion})已切换为只读,请更新部署后再修改`"
style="margin-bottom: 8px"
/>
<div class="hero-main">
<!-- 预设库视图 -->
<TemplateManager
@@ -252,6 +267,10 @@ const editVisible = ref(false)
const current = computed(() => heroes.value[activeIdx.value])
// 版本/模式:isDev 区分开发版全功能;outdated 时线上设计版只读
const isDev = computed(() => meta.value?.mode === 'local')
const outdated = computed(() => (meta.value?.dbSchemaVersion ?? 0) > (meta.value?.schemaVersion ?? 0))
// 首页列表排序:none 原始顺序 / role 按职业枚举序(坦克→辅助)/ rarity 按星级
const sortKey = ref('none')
const sortOrder = ref('desc')
@@ -510,7 +529,7 @@ async function loadTemplates() {
async function doSave() {
try {
const r = await api.saveHeroes(heroes.value)
ElMessage.success(`已保存 ${r.count} 个英雄 → ${r.path}`)
ElMessage.success(`已保存 ${r.count} 个英雄到数据库`)
} catch (e) {
ElMessage.error('保存失败: ' + (e?.response?.data?.error || e.message))
}

View File

@@ -1,12 +1,16 @@
import axios from 'axios'
// 开发环境走 vite 代理(留空即可);部署静态站点时用 VITE_API_BASE 指向后端服务
const baseURL = import.meta.env.VITE_API_BASE || ''
const http = axios.create({ baseURL })
export const api = {
meta: () => axios.get('/api/meta').then((r) => r.data),
refresh: () => axios.post('/api/refresh').then((r) => r.data),
fetchHeroes: () => axios.get('/api/heroes').then((r) => r.data),
saveHeroes: (heroes) => axios.post('/api/heroes', { heroes }).then((r) => r.data),
applyHeroes: (heroes) => axios.post('/api/apply', { heroes }).then((r) => r.data),
exportCsv: (heroes) => axios.post('/api/export-csv', { heroes }).then((r) => r.data),
fetchTemplates: () => axios.get('/api/templates').then((r) => r.data),
saveTemplates: (templates) => axios.post('/api/templates', { templates }).then((r) => r.data),
meta: () => http.get('/api/meta').then((r) => r.data),
refresh: () => http.post('/api/refresh').then((r) => r.data),
fetchHeroes: () => http.get('/api/heroes').then((r) => r.data),
saveHeroes: (heroes) => http.post('/api/heroes', { heroes }).then((r) => r.data),
applyHeroes: (heroes) => http.post('/api/apply', { heroes }).then((r) => r.data),
exportCsv: (heroes) => http.post('/api/export-csv', { heroes }).then((r) => r.data),
fetchTemplates: () => http.get('/api/templates').then((r) => r.data),
saveTemplates: (templates) => http.post('/api/templates', { templates }).then((r) => r.data),
}

View File

@@ -4,6 +4,8 @@ import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
// 监听所有网卡,容器/局域网内可直接访问开发服务器
host: '0.0.0.0',
port: 4004,
strictPort: true,
proxy: {