52 Commits

Author SHA1 Message Date
pan
6c2f1defa9 refactor(monster): 重构波次怪物管理逻辑,优化配置与流程
1.  更新怪物配置注释,修正近战/远程怪物的描述与分类
2.  移除MissionComp中过时的自适应刷怪逻辑
3.  重构MissionMonComp:删除插队刷怪队列、简化波次流程、统一怪物生成逻辑
4.  移除冗余日志与注释,优化代码可读性
5.  调整波次准备阶段的怪物生成时机与数据处理
2026-06-23 17:05:07 +08:00
pan
46fa481607 feat(skill): add movement acceleration support for skills
1. 新增SMoveComp的isAccelerate字段控制加速逻辑
2. 添加技能配置is_accel字段启用加速效果
3. 实现二次方缓入加速曲线,兼顾起步速度与最终加速度
4. 为6001号技能默认开启加速效果并调整其移动类型为直线运动
2026-06-23 11:07:57 +08:00
pan
72cdf32a75 refactor(TalentItemComp): 优化天赋卡牌UI显示逻辑
1.  复用CardConfig中的pool_lv统一卡牌背景色,与技能卡保持一致
2.  新增对驻场天赋卡的图标显示支持,使用FieldSkillSet获取图标
3.  重构图标获取逻辑,与SCardComp保持对齐避免显示异常
4.  移除冗余的wave映射背景色代码
2026-06-22 16:24:23 +08:00
pan
a8642cb788 chore: 批量更新UI预制体资源配置
1.  更新引导文案文本内容
2.  调整卡牌控件的位置、尺寸、字体样式与布局参数
3.  更新轻量卡牌控件的激活状态与精灵帧引用
4.  调整方块控件的尺寸、缩放比例与填充参数
5.  移除废弃的精灵帧元数据配置
2026-06-22 15:08:01 +08:00
panFD
d60b66350a chore(.claude): update claude settings permissions and adjust config order
1. 调整statusLine配置项的位置到文件末尾
2. 新增WebSearch、git add/commit、npm install的权限许可
2026-06-21 17:33:17 +08:00
panFD
bfa434634c fix(config-editor): main entry exports methods object + load/unload (Cocos 3.8)
Root cause of 'Method does not exist / The methods of the module is undefined':
Cocos 3.8 expects handlers inside a 'methods' export (plus load/unload hooks),
not flat on module.exports. Handlers receive args directly (no event); return
value is the request reply. Verified vs official 3.8 first/messages docs.
Also: menu path + panel title switched to literal strings (i18n showed 'undefined').
2026-06-21 16:30:39 +08:00
panFD
fb65fa79c8 test(config-editor): record Plan A verification evidence
Task 13 of plan 2026-06-20-config-editor-foundation. Captures:
- Automated BLOCKING gate: 36/36 unit tests pass
- Automated BLOCKING gate: build produces dist/main.js (9.5mb) + dist/panels/default.js (628kb)
- Necessary esbuild.config.mjs fix documented (node:fs/node:path external for panel)
- ADVISORY in-editor checklist left for human completion
- DoD mapping
2026-06-21 09:57:19 +08:00
panFD
24b5c49891 feat(config-editor): extension entry + minimal Vue panel proving end-to-end IPC
Task 12 of plan 2026-06-20-config-editor-foundation. Adds:
- src/main/index.ts: onLoad + message handlers (return value = request resolve,
  per Cocos 3.x verified IPC mechanism; fallback note left in plan)
- src/panels/default/{index,app}.ts: Editor.Panel.define host + Vue 3 minimal
  app (table switcher, key list, record JSON dump)
- static/template/default/index.html + static/style/default/index.css

Deviation from plan (necessary, flagged): esbuild.config.mjs now marks
node:fs/node:path as external for the panel entry (platform:'browser').
The plan's panel reads static template/style at runtime via Node fs, which
requires these builtins; Cocos panel runs in an Electron renderer that
provides them. Without this, esbuild errors with 'Could not resolve node:fs'.

Build verified: dist/main.js (9.5mb, typescript compiler API bundled) and
dist/panels/default.js (628kb, vue.esm-bundler bundled) both generate.
2026-06-21 09:56:02 +08:00
panFD
e3102c63ff feat(config-editor): main-process store (in-memory truth + message impls + asset-db refresh)
Task 11 of plan 2026-06-20-config-editor-foundation. Holds three TsConfigFile
instances (hero/skill/field), implements query*/validate/saveRecord/revertRecord
message handlers. saveRecord validates before persisting, rolls back on error,
and refreshes asset-db on success. HeroList read via regex.
2026-06-21 09:26:03 +08:00
panFD
6a81630f6f feat(config-editor): port buildSkillDesc to JS for panel preview 2026-06-21 09:17:50 +08:00
panFD
4a5659b7ec feat(config-editor): add validation rules (dup/required/enum/ref/overrides/herolist) with tests 2026-06-21 09:11:47 +08:00
panFD
4df88c1c90 feat(config-editor): TsConfigFile load/read/patch/add/delete/save (entry-level AST patch, .bak + syntax check)
26 IO tests green: serializer, parser (speed/enumRef/raw), TsConfigFile
round-trip (load/patch/add/delete/save preserving symbolic expressions).
2026-06-21 00:13:42 +08:00
panFD
0d28ad7a5e test(config-editor): add hero/skill fixtures mirroring real config shape 2026-06-21 00:07:00 +08:00
panFD
0a960b737c feat(config-editor): add AST parser (speed/enumRef/raw) with tests 2026-06-21 00:06:28 +08:00
panFD
7bb5f8bacc feat(config-editor): add RecordValue serializer with tests 2026-06-21 00:05:53 +08:00
panFD
c0755b3b8d feat(config-editor): add schema types and hero/skill/field table schemas 2026-06-20 23:52:27 +08:00
panFD
88c1a28c80 feat(config-editor): add RecordValue type and enum mirror 2026-06-20 23:46:54 +08:00
panFD
acb038a70a feat(config-editor): scaffold extension manifest, build, i18n 2026-06-20 23:46:15 +08:00
panFD
315a1a6af9 docs(config-editor): add Plan A implementation plan (foundation + IO + tests) 2026-06-20 23:20:59 +08:00
panFD
5170b2d0dc fix(hero config): 调整全英雄基础属性与数值
对所有星级英雄的生命值、攻击力进行了统一平衡性调整,修正部分英雄的面板数值与技能加成匹配度,优化游戏前期和后期的战斗体验平衡。
2026-06-20 23:00:55 +08:00
panFD
d8f02b568b docs(config-editor): add design spec for hero/skill visual config editor extension
Schema-driven Cocos Creator 3.8.6 extension that round-trips the existing
Record<number,X> .ts configs via the TypeScript compiler API (preserves
symbolic AtkSpeedSet expressions and hand-written comments). Non-invasive:
zero changes to game runtime code.
2026-06-20 22:59:20 +08:00
panFD
b7388615ed feat(map): 为英雄信息弹窗和卡牌组件添加点击外部关闭交互
1.  全局添加触摸结束监听,实现点击弹窗/卡牌外区域自动关闭/隐藏控件
2.  通过包围盒检测避免误触内部元素,无需额外遮罩节点
3.  统一管理事件的绑定与解绑,防止内存泄漏
2026-06-20 21:59:29 +08:00
panFD
3056b61ced fix: 修复战斗相关UI显示与技能配置问题
1. 调整RPG地图预制件与UI元素的激活状态,修复战斗面板显示异常
2. 移除技能卡片多余的t_times配置项,简化技能触发逻辑
3. 优化战斗结束后战斗框的显隐控制
2026-06-20 17:53:04 +08:00
panFD
b634cf5383 fix(ui prefab): 调整引导UI位置、文案与样式
1. 修正guide1、guide3、guide4的控件垂直位置
2. 更新guide3的引导提示文案
3. 调整sbox的多处控件颜色为深灰色
4. 移除部分prefab的冗余targetOverrides字段
2026-06-20 17:24:27 +08:00
panFD
57d2805761 fix(map): 调整信息组件等级节点显示逻辑
根据卡片类型区分显示等级节点:技能卡隐藏等级,英雄卡正常显示等级
2026-06-20 16:20:21 +08:00
panFD
62af155ce8 refactor(skill-card): 统一技能卡牌波次配置为单一数据源
1. 新增SKILL_CARD_WAVES常量管理技能卡牌出现波次档位
2. 替换MissionCardComp中硬编码的波次判断逻辑
3. 重构CardSet中的技能卡牌波次映射和配置项,实现自动关联档位
4. 确保所有技能卡牌配置与波次配置严格对齐,避免抽卡池为空的问题
2026-06-20 16:13:13 +08:00
panFD
735bf205fd feat(skillBox): 优化技能框UI表现与图标逻辑
1. 调整ui3.plist.meta的边框内边距为25
2. 新增技能框背景颜色节点,根据等级切换对应配色
3. 增加自定义图标支持,优化多类型技能图标加载逻辑
2026-06-20 16:00:52 +08:00
panFD
4d6403e362 feat(card&ui): add custom card icon support and optimize icon display
1. 新增CardConfig的icon字段用于配置自定义卡牌图标,优先级最高
2. 为HInfoComp新增技能图标节点,区分英雄卡和技能卡的图标展示
3. 重构updateSkillAnimation方法,支持按配置优先级加载图标
4. 优化两种卡牌的图标显示互斥逻辑
2026-06-20 15:11:37 +08:00
panFD
0a281a95d1 refactor(config): 统一初始金币配置并调整刷新时机
1. 将分散的初始金币常量迁移到FightSet枚举中,删除冗余的CardInitCoins
2. 调整刷新费用UI更新时机,确保驻场技能效果正确生效
2026-06-20 14:42:19 +08:00
panFD
107e7fde96 fix(map): 修复卡牌描述逻辑的优先级顺序
调整卡牌描述的获取逻辑,先处理驻场技能卡的描述获取,再 fallback 到其他配置来源,修正原有的优先级混乱问题
2026-06-20 12:42:45 +08:00
panFD
d456b2d61f fix(config): 修正技能卡牌的uuid编号错误
对wave1、wave5、wave8三个难度档位的驻场卡、范围攻击卡的uuid进行了统一修正,确保卡牌编号逻辑一致,避免出现编号冲突或混乱的问题。
2026-06-20 12:32:49 +08:00
panFD
f61c4a506f refactor(config): 整理驻场技能ID与配置映射
本次提交统一调整了所有驻场相关技能的ID编号,将原有分散的技能配置按功能类型重新规整排序,同时同步更新了英雄配置、语言文件、卡牌配置中的技能ID引用,确保所有配置项的ID保持一致且逻辑清晰,修复了亡语法师技能ID不匹配的问题,优化了后续配置扩展的可读性和维护性。
2026-06-20 12:28:23 +08:00
panFD
4247299a86 fix(config): 调整战场技能配置的顺序和位置
将wave5档的基础增益技能和强化增益技能分别整理到正确的代码区块中,修复技能配置乱序的问题
2026-06-20 10:43:30 +08:00
panFD
48a174902d refactor(config): 重新排序野外技能配置条目
将原分散的触发类技能配置统一整理到配置块开头,优化配置可读性与维护性
2026-06-20 10:42:39 +08:00
panFD
b9a3c704c7 fix(config): 调整场地技能配置的位置和冗余注释
清理了旧的注释内容,将购买优惠、刷新优惠类技能调整到正确的分组位置,移除重复冗余的配置代码
2026-06-20 10:40:01 +08:00
panFD
ea34367d7b feat: 新增攻击/受击触发技能次数加成机制
1.  新增FieldSkillType枚举的AtkCount和BeAtkCount类型,添加对应强化技能配置
2.  调整战斗内波次金币、刷新/购买成本参数
3.  重构技能触发逻辑,支持根据字段技能调整触发次数
4.  新增三档强度的技能卡牌配置,优化卡牌池等级映射规则
2026-06-20 10:38:08 +08:00
panFD
1eaaf4ccc5 fix(config): 修改召唤类卡牌的触发波次为1
将原wave值为5的雷墙、火墙等6张技能卡牌的触发波次调整为1,让它们可以在战斗开局就生效。
2026-06-20 00:03:36 +08:00
panFD
e422844717 refactor(skillConfig&prefab): 调整技能配置分组与UI控制器尺寸
1.  重构技能卡牌配置表,调整技能波次分组逻辑
2.  更新角色控制器预制体的尺寸与目标覆盖配置
2026-06-20 00:01:05 +08:00
panFD
5d244e8091 refactor(ui): 移除废弃的技能UI预制件并调整布局
1. 删除了mskills.prefab及其元数据文件
2. 调整了mission.prefab中三处文本的实际字体大小从31改为25
3. 重新排布了MissSkillsComp的技能槽位置
2026-06-19 23:23:56 +08:00
panFD
dc8391847b refactor(cardSkill): 完成卡牌技能触发机制类型化改造
本次提交为全量的卡牌技能触发系统重构,主要变更包括:
1.  新增CardTriggerType枚举,统一卡牌触发类型定义
2.  补全依赖事件派发:每波战斗结束FightEnd、英雄死亡HeroDead(带阵营过滤)、复活成功ReviveSuccess
3.  重构SkillBoxComp,按触发类型动态注册事件监听,拆分即时/定时/驻场/事件型逻辑
4.  批量迁移所有卡牌配置,为旧技能补充显式触发类型
5.  新增全局触发次数上限机制,区分每波/全局触发计数规则
6.  新增配套设计文档,记录改造背景与方案细节

本次重构彻底解决了原有隐式配置难以维护、无法支持事件型触发的痛点,实现了技能触发逻辑的标准化与可扩展性。
2026-06-19 23:01:24 +08:00
panFD
a866cba8d1 fix(config): 调整技能卡牌的配置逻辑与描述
将原有一次性触发的技能改为周期性持续生效,更新技能描述文本,注释掉未完成的复活技能配置
2026-06-19 21:30:02 +08:00
panFD
25346c44a2 fix(card): 修复卡牌信息显示逻辑并调整默认状态
1.  将卡牌预制件默认激活状态改为false
2.  移除冗余的info_node和lvl_node配置,新增lvl_node为空引用
3.  重构卡牌信息文本的获取逻辑,增加多源优先级判断
4.  优化信息节点的Label组件查找方式,适配更灵活的节点结构
2026-06-19 18:05:54 +08:00
panFD
17452167c3 refactor(card): 重构卡牌触摸交互逻辑,替换长按为点击触发信息面板
1. 移除长按相关逻辑,改用点击触发英雄卡信息面板
2. 新增卡牌点击选中联动机制,统一管理召唤按钮显示
3. 调整触摸位移阈值,优化点击和拖拽的判定逻辑
4. 新增卡牌选中事件,实现多卡牌间的UI联动
5. 修复预制体默认激活状态,统一初始UI状态
2026-06-19 15:51:17 +08:00
panFD
c30900e508 feat(CardComp): add call_btn node reference
新增卡片组件的呼叫按钮节点绑定,同步更新预制件配置添加对应的节点id引用
2026-06-19 15:40:39 +08:00
panFD
3d7c9bfe54 feat: 新增技能触发类型标识与列表预制体,优化技能提示UI
1.  新增技能触发类型背景标识,支持追击/反击/复活等状态显示
2.  扩展技能提示接口,新增触发类型参数传递
3.  新增list-me列表预制体及其元数据
4.  调整部分UI精灵帧与布局参数
5.  修复技能名称显示调用参数不匹配问题
2026-06-19 15:40:28 +08:00
panFD
9220254c56 refactor(skill tooltip): 优化技能触发类型命名与 tooltip 背景配置
1. 修正 SkillTriggerName 中的技能触发类型文本翻译,使其更贴合游戏内实际表述
2. 为 tooltip 预制体和组件新增6种状态的背景节点引用
2026-06-19 15:10:19 +08:00
panFD
2d4bc1fd05 fix(ui/skillBox): 修复技能选择框布局与文本问题
调整了技能框预制体的节点位置、尺寸、字体大小,新增了标题文本"选择战斗技能",修正了资源引用id和布局偏移量,优化界面显示效果
2026-06-19 15:02:59 +08:00
panFD
18c873999b fix(missionCard): 修复卡牌升级提示逻辑并优化显示
1.  新增全局配置常量CARD_POOL_UPGRADE_WAVES,优先使用任务运行时配置
2.  重构升级波次计算逻辑,新增多种状态的提示文本:本回合升级、即将升级
3.  修复prefab布局,更新升级提示UI的样式和引用
4.  补充边界情况处理,避免配置耗尽后显示异常
2026-06-19 08:58:56 +08:00
panFD
40c27e04f2 refactor(config): 统一卡池等级与升级波次的数据源
1. 将卡池等级上限从硬编码改为引用FightSet.MAX_CARD_POOL_LEVEL
2. 提取卡池升级波次配置到GameSet中作为单一数据源
3. 移除MissionComp中的硬编码波次配置,改为引用全局配置
2026-06-19 08:38:39 +08:00
panFD
875d2d68b5 refactor: 调整测试模式、英雄等级和卡池配置
1. 开启单挑测试模式
2. 将英雄最大等级从3下调至2
3. 更新卡池升级波次配置为[4,7,10,13]
4. 修复任务预制体的精灵帧和节点缩放配置
2026-06-18 23:26:17 +08:00
panFD
9f738ab881 fix(map,card): 优化卡牌抽取逻辑,新增去重机制
1. 为drawCardsByRule新增unique参数,实现抽取卡牌不重复
2. 修复 fallback 抽取时的重复问题,优先选择未抽到过的卡牌
3. 修复驻场技能卡的图标显示逻辑,使用FieldSkillSet配置
2026-06-18 22:18:05 +08:00
panFD
e0c6622bec refactor(skill config): 统一技能图标资源路径并清理废弃技能
1.  修正预加载的图集资源路径
2.  批量更新所有技能的图标为统一命名的资源
3.  删除冗余的废弃技能配置条目
4.  为场地技能配置新增图标字段并补全对应资源路径
2026-06-18 21:46:40 +08:00
92 changed files with 24550 additions and 12301 deletions

View File

@@ -1,9 +1,5 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"statusLine": {
"type": "command",
"command": "bash .claude/statusline.sh"
},
"permissions": {
"allow": [
"Bash(git status*)",
@@ -15,7 +11,11 @@
"Bash(dir *)",
"Bash(python -m json.tool*)",
"Bash(python -m pytest*)",
"Bash(py -m pytest*)"
"Bash(py -m pytest*)",
"WebSearch",
"Bash(git add *)",
"Bash(git commit *)",
"Bash(npm install *)"
],
"deny": [
"Bash(rm -rf *)",
@@ -155,5 +155,9 @@
]
}
]
},
"statusLine": {
"type": "command",
"command": "bash .claude/statusline.sh"
}
}

3
.gitignore vendored
View File

@@ -20,7 +20,8 @@ native
# WebStorm
#//////////////////////////
.idea/
extensions/
extensions/*
!extensions/pixelhero-config-editor/
extensions/oops-plugin-framework
# === IDE and Editor ===
.vs/

File diff suppressed because it is too large Load Diff

View File

@@ -164,12 +164,12 @@
"a": 206
},
"_spriteFrame": {
"__uuid__": "2423272e-e63b-4736-b15b-30b40cf98a23@b0413",
"__uuid__": "2423272e-e63b-4736-b15b-30b40cf98a23@586a7",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 1,
"_fillType": 1,
"_sizeMode": 1,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -50,19 +50,22 @@
},
{
"__id__": 295
},
{
"__id__": 307
}
],
"_active": true,
"_components": [
{
"__id__": 307
"__id__": 323
},
{
"__id__": 309
"__id__": 325
}
],
"_prefab": {
"__id__": 311
"__id__": 327
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -2599,8 +2602,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -1.378,
"y": 1.378,
"x": 2.438,
"y": 0,
"z": 0
},
"_lrot": {
@@ -2640,8 +2643,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 17.90380859375,
"height": 35.5
"width": 34.748356206795606,
"height": 67
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -2679,16 +2682,16 @@
"_string": "3",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 25,
"_fontSize": 25,
"_actualFontSize": 40,
"_fontSize": 40,
"_fontFamily": "Arial",
"_lineHeight": 25,
"_lineHeight": 50,
"_overflow": 0,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isItalic": true,
"_isBold": true,
"_isUnderline": false,
"_underlineHeight": 2,
@@ -5390,7 +5393,7 @@
"__id__": 296
}
],
"_active": true,
"_active": false,
"_components": [
{
"__id__": 302
@@ -5667,6 +5670,396 @@
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "call",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 308
},
{
"__id__": 314
}
],
"_active": false,
"_components": [
{
"__id__": 320
}
],
"_prefab": {
"__id__": 322
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": -23.213,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "btn_yellow",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 307
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 309
},
{
"__id__": 311
}
],
"_prefab": {
"__id__": 313
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 0.8,
"y": 0.8,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 308
},
"_enabled": true,
"__prefab": {
"__id__": 310
},
"_contentSize": {
"__type__": "cc.Size",
"width": 185,
"height": 81
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "f5+WQKeepDpI7jXqgvmtiO"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 308
},
"_enabled": true,
"__prefab": {
"__id__": 312
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "6165ffc9-a838-4a33-b569-bdbaaab0e6b4@b2501",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 1,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "08MkVcRXlJh64uV97FC5zt"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "979t7Rjp5IGpxx6E++HMV4",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Label",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 307
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 315
},
{
"__id__": 317
}
],
"_prefab": {
"__id__": 319
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 314
},
"_enabled": true,
"__prefab": {
"__id__": 316
},
"_contentSize": {
"__type__": "cc.Size",
"width": 63.5999755859375,
"height": 54.4
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "feOuVKuYxJK6z+M3tV8Ey3"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 314
},
"_enabled": true,
"__prefab": {
"__id__": 318
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_string": "召唤",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 29.8,
"_fontSize": 29.8,
"_fontFamily": "Arial",
"_lineHeight": 40,
"_overflow": 0,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": true,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
"_enableOutline": true,
"_outlineColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_outlineWidth": 2,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_shadowOffset": {
"__type__": "cc.Vec2",
"x": 2,
"y": 2
},
"_shadowBlur": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "00BfCWkAJL/p90pcsuwUDH"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "83KbPaQKpJ3p+hfLZ1PBTs",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 307
},
"_enabled": true,
"__prefab": {
"__id__": 321
},
"_contentSize": {
"__type__": "cc.Size",
"width": 148,
"height": 64.8
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "94GKNoSM1MwIqd0zy183DD"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "57KHqoGQRBmIdHCIXwuEoC",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
@@ -5677,7 +6070,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 308
"__id__": 324
},
"_contentSize": {
"__type__": "cc.Size",
@@ -5705,10 +6098,13 @@
},
"_enabled": true,
"__prefab": {
"__id__": 310
"__id__": 326
},
"Lock": null,
"unLock": null,
"call_btn": {
"__id__": 307
},
"name_node": {
"__id__": 257
},
@@ -5725,11 +6121,9 @@
"__id__": 10
},
"info_node": {
"__id__": 295
},
"lvl_node": {
"__id__": 254
"__id__": 296
},
"lvl_node": null,
"ap_node": {
"__id__": 195
},

View File

@@ -1928,7 +1928,7 @@
"__id__": 129
}
],
"_active": false,
"_active": true,
"_components": [
{
"__id__": 135
@@ -1945,7 +1945,7 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 69.648,
"x": 64.648,
"y": 75.307,
"z": 0
},
@@ -2073,7 +2073,7 @@
"a": 255
},
"_spriteFrame": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@4b5bf",
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@23e64",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
@@ -2134,8 +2134,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -1.378,
"y": 1.378,
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
@@ -2175,8 +2175,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 17.90380859375,
"height": 35.5
"width": 34.748356206795606,
"height": 67
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -2214,16 +2214,16 @@
"_string": "3",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 25,
"_fontSize": 25,
"_actualFontSize": 40,
"_fontSize": 40,
"_fontFamily": "Arial",
"_lineHeight": 25,
"_lineHeight": 50,
"_overflow": 0,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isItalic": true,
"_isBold": true,
"_isUnderline": false,
"_underlineHeight": 2,

View File

@@ -406,7 +406,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"y": -71.244,
"z": 0
},
"_lrot": {

View File

@@ -747,7 +747,7 @@
"b": 255,
"a": 255
},
"_string": "选择一个战斗技能",
"_string": "选择一个战斗「天赋」",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 46,

View File

@@ -402,7 +402,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 248.35000000000002,
"y": -216.83000000000004,
"z": 0
},
"_lrot": {
@@ -743,7 +743,7 @@
"b": 255,
"a": 255
},
"_string": "选中卡牌向上滑动,召唤英雄",
"_string": "上划或点击召唤英雄",
"_horizontalAlign": 1,
"_verticalAlign": 1,
"_actualFontSize": 41,
@@ -1219,7 +1219,7 @@
"_left": 0,
"_right": 0,
"_top": 590,
"_bottom": 788.35,
"_bottom": 323.16999999999996,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
@@ -1344,7 +1344,6 @@
},
"fileId": "6dh4o/8p1Cy5An1p6o4Bc3",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": [
{
"__id__": 17

View File

@@ -406,7 +406,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"y": -307.501,
"z": 0
},
"_lrot": {
@@ -1348,7 +1348,6 @@
},
"fileId": "6dh4o/8p1Cy5An1p6o4Bc3",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": [
{
"__id__": 17

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,12 @@
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "0f2aeee0-d590-4c36-9f20-a93b058d5b91",
"uuid": "b2a3067a-af88-4ff7-acdd-7474b8a163d8",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "mskills"
"syncNodeName": "list-me"
}
}

View File

@@ -185,10 +185,10 @@
"a": 255
},
"_spriteFrame": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@d1468",
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@d5229",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_type": 1,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
@@ -200,7 +200,10 @@
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_atlas": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73",
"__expectedType__": "cc.SpriteAtlas"
},
"_id": ""
},
{
@@ -279,8 +282,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -297.949,
"y": 0,
"x": -276.372,
"y": 8.188,
"z": 0
},
"_lrot": {
@@ -418,8 +421,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -299.563,
"y": 0,
"x": -277.986,
"y": 8.188,
"z": 0
},
"_lrot": {
@@ -577,8 +580,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -217.727,
"y": 0,
"x": -214.902,
"y": 8.188,
"z": 0
},
"_lrot": {
@@ -590,8 +593,8 @@
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 0.35,
"y": 0.35,
"x": 0.3,
"y": 0.3,
"z": 1
},
"_mobility": 0,
@@ -713,8 +716,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -139.467,
"y": 0,
"x": -136.642,
"y": 8.188,
"z": 0
},
"_lrot": {
@@ -872,8 +875,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 225.016,
"y": 0,
"x": 167.228,
"y": 8.1,
"z": 0
},
"_lrot": {
@@ -952,8 +955,8 @@
"_string": "999999",
"_horizontalAlign": 0,
"_verticalAlign": 1,
"_actualFontSize": 26,
"_fontSize": 25,
"_actualFontSize": 31,
"_fontSize": 30,
"_fontFamily": "Arial",
"_lineHeight": 40,
"_overflow": 2,
@@ -1031,8 +1034,8 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 181.966,
"y": 0,
"x": 140.032,
"y": 8.1,
"z": 0
},
"_lrot": {
@@ -1044,8 +1047,8 @@
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 0.4,
"y": 0.4,
"x": 0.3,
"y": 0.3,
"z": 1
},
"_mobility": 0,

File diff suppressed because it is too large Load Diff

View File

@@ -1,203 +0,0 @@
[
{
"__type__": "cc.Prefab",
"_name": "mskills",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "mskills",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [],
"_active": true,
"_components": [
{
"__id__": 2
},
{
"__id__": 4
},
{
"__id__": 6
},
{
"__id__": 8
}
],
"_prefab": {
"__id__": 10
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -180,
"y": 990,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 3
},
"_contentSize": {
"__type__": "cc.Size",
"width": 360,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "4dNsDtN3ZGiZ+om9xELLFQ"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 5
},
"_alignFlags": 10,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 350,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 0,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "baKsIzXktAyqqNBZKVQXO+"
},
{
"__type__": "68387wnH45AVo6Nco+pXtG5",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 7
},
"skill_box": {
"__uuid__": "d19cde30-f5d0-47de-a0d5-3a272b696343",
"__expectedType__": "cc.Prefab"
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "d4AyZdLXNB07iSaFvEU6BB"
},
{
"__type__": "cc.Layout",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 9
},
"_resizeMode": 0,
"_layoutType": 1,
"_cellSize": {
"__type__": "cc.Size",
"width": 40,
"height": 40
},
"_startAxis": 0,
"_paddingLeft": 10,
"_paddingRight": 0,
"_paddingTop": 0,
"_paddingBottom": 0,
"_spacingX": 10,
"_spacingY": 0,
"_verticalDirection": 1,
"_horizontalDirection": 0,
"_constraint": 0,
"_constraintNum": 2,
"_affectedByScale": false,
"_isAlign": false,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "786tQWlIxKHLEpV0oUK9dj"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "5622mxbS1PNqMFP0FH5Mir",
"targetOverrides": null
}
]

View File

@@ -22,32 +22,32 @@
"__id__": 2
},
{
"__id__": 35
"__id__": 63
},
{
"__id__": 41
"__id__": 69
},
{
"__id__": 47
"__id__": 75
},
{
"__id__": 53
"__id__": 81
}
],
"_active": true,
"_components": [
{
"__id__": 59
"__id__": 87
},
{
"__id__": 61
"__id__": 89
},
{
"__id__": 63
"__id__": 91
}
],
"_prefab": {
"__id__": 65
"__id__": 93
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -169,6 +169,48 @@
},
{
"__id__": 34
},
{
"__id__": 35
},
{
"__id__": 37
},
{
"__id__": 39
},
{
"__id__": 41
},
{
"__id__": 43
},
{
"__id__": 45
},
{
"__id__": 47
},
{
"__id__": 49
},
{
"__id__": 51
},
{
"__id__": 53
},
{
"__id__": 55
},
{
"__id__": 57
},
{
"__id__": 59
},
{
"__id__": 61
}
],
"removedComponents": []
@@ -474,6 +516,280 @@
],
"value": 0
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 36
},
"propertyPath": [
"_active"
],
"value": true
},
{
"__type__": "cc.TargetInfo",
"localID": [
"4aTybcwdBL1IFgubnxRuUh"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 38
},
"propertyPath": [
"_contentSize"
],
"value": {
"__type__": "cc.Size",
"width": 220,
"height": 270
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"eedc9qvxRPQpepecxrSorR"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 40
},
"propertyPath": [
"_contentSize"
],
"value": {
"__type__": "cc.Size",
"width": 440,
"height": 540
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"deY6hTveBKBIzSn0k9oD+7"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 42
},
"propertyPath": [
"_contentSize"
],
"value": {
"__type__": "cc.Size",
"width": 432,
"height": 528
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"b7HYchf9tMK7WtemLDPjlE"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 44
},
"propertyPath": [
"_contentSize"
],
"value": {
"__type__": "cc.Size",
"width": 190,
"height": 140
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"25j0n7apFBratTNBweGTa7"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 46
},
"propertyPath": [
"_lpos"
],
"value": {
"__type__": "cc.Vec3",
"x": 0,
"y": 50,
"z": 0
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"93O2t0s3dBQYpkATdTKBPZ"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 48
},
"propertyPath": [
"_contentSize"
],
"value": {
"__type__": "cc.Size",
"width": 380,
"height": 280
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"1fWMnHXs1IPZv24wLXcOc2"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 50
},
"propertyPath": [
"_active"
],
"value": true
},
{
"__type__": "cc.TargetInfo",
"localID": [
"34+q4uHrVMer4aH0jqs5JM"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 52
},
"propertyPath": [
"_contentSize"
],
"value": {
"__type__": "cc.Size",
"width": 220,
"height": 270
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"2bJbEaLWxKYIZXGRHTBM1m"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 54
},
"propertyPath": [
"_contentSize"
],
"value": {
"__type__": "cc.Size",
"width": 440,
"height": 540
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"8b8xEuZsBB+KDupKZRRki9"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 56
},
"propertyPath": [
"_contentSize"
],
"value": {
"__type__": "cc.Size",
"width": 432,
"height": 528
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"30uXpT+kpFI5rT0dm/gN9k"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 58
},
"propertyPath": [
"_contentSize"
],
"value": {
"__type__": "cc.Size",
"width": 190,
"height": 140
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"c6lDSuSgFMJaDP3gZWwz54"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 60
},
"propertyPath": [
"_lpos"
],
"value": {
"__type__": "cc.Vec3",
"x": 0,
"y": 50,
"z": 0
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"24Xd/896JIz4KMqBZHi94n"
]
},
{
"__type__": "CCPropertyOverrideInfo",
"targetInfo": {
"__id__": 62
},
"propertyPath": [
"_contentSize"
],
"value": {
"__type__": "cc.Size",
"width": 380,
"height": 280
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"1dy2U4eNdI6KgBqcoGyOOb"
]
},
{
"__type__": "cc.Node",
"_name": "icon",
@@ -486,14 +802,14 @@
"_active": true,
"_components": [
{
"__id__": 36
"__id__": 64
},
{
"__id__": 38
"__id__": 66
}
],
"_prefab": {
"__id__": 40
"__id__": 68
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -530,11 +846,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 35
"__id__": 63
},
"_enabled": true,
"__prefab": {
"__id__": 37
"__id__": 65
},
"_contentSize": {
"__type__": "cc.Size",
@@ -558,11 +874,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 35
"__id__": 63
},
"_enabled": true,
"__prefab": {
"__id__": 39
"__id__": 67
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@@ -625,14 +941,14 @@
"_active": true,
"_components": [
{
"__id__": 42
"__id__": 70
},
{
"__id__": 44
"__id__": 72
}
],
"_prefab": {
"__id__": 46
"__id__": 74
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -669,11 +985,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 41
"__id__": 69
},
"_enabled": true,
"__prefab": {
"__id__": 43
"__id__": 71
},
"_contentSize": {
"__type__": "cc.Size",
@@ -697,11 +1013,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 41
"__id__": 69
},
"_enabled": true,
"__prefab": {
"__id__": 45
"__id__": 73
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@@ -784,14 +1100,14 @@
"_active": true,
"_components": [
{
"__id__": 48
"__id__": 76
},
{
"__id__": 50
"__id__": 78
}
],
"_prefab": {
"__id__": 52
"__id__": 80
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -828,11 +1144,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 47
"__id__": 75
},
"_enabled": true,
"__prefab": {
"__id__": 49
"__id__": 77
},
"_contentSize": {
"__type__": "cc.Size",
@@ -856,11 +1172,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 47
"__id__": 75
},
"_enabled": true,
"__prefab": {
"__id__": 51
"__id__": 79
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@@ -943,14 +1259,14 @@
"_active": false,
"_components": [
{
"__id__": 54
"__id__": 82
},
{
"__id__": 56
"__id__": 84
}
],
"_prefab": {
"__id__": 58
"__id__": 86
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -987,11 +1303,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 53
"__id__": 81
},
"_enabled": true,
"__prefab": {
"__id__": 55
"__id__": 83
},
"_contentSize": {
"__type__": "cc.Size",
@@ -1015,11 +1331,11 @@
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 53
"__id__": 81
},
"_enabled": true,
"__prefab": {
"__id__": 57
"__id__": 85
},
"_customMaterial": null,
"_srcBlendFactor": 2,
@@ -1100,7 +1416,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 60
"__id__": 88
},
"_contentSize": {
"__type__": "cc.Size",
@@ -1128,7 +1444,7 @@
},
"_enabled": false,
"__prefab": {
"__id__": 62
"__id__": 90
},
"_alignFlags": 40,
"_target": null,
@@ -1164,16 +1480,16 @@
},
"_enabled": true,
"__prefab": {
"__id__": 64
"__id__": 92
},
"lbl_name": {
"__id__": 44
"__id__": 72
},
"lbl_info": {
"__id__": 50
"__id__": 78
},
"icon": {
"__id__": 38
"__id__": 66
},
"bg": null,
"_id": ""
@@ -1194,7 +1510,7 @@
"instance": null,
"targetOverrides": [
{
"__id__": 66
"__id__": 94
}
],
"nestedPrefabInstanceRoots": [
@@ -1206,7 +1522,7 @@
{
"__type__": "cc.TargetOverrideInfo",
"source": {
"__id__": 63
"__id__": 91
},
"sourceInfo": null,
"propertyPath": [
@@ -1216,7 +1532,7 @@
"__id__": 2
},
"targetInfo": {
"__id__": 67
"__id__": 95
}
},
{

View File

@@ -3292,7 +3292,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 617.5,
"y": 657.577,
"z": 0
},
"_lrot": {
@@ -3304,8 +3304,8 @@
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"x": 1.5,
"y": 1.5,
"z": 1
},
"_mobility": 0,
@@ -3332,8 +3332,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 740,
"height": 55
"width": 540,
"height": 117
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -3369,12 +3369,12 @@
"a": 255
},
"_spriteFrame": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@98637",
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73@4dd1c",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_sizeMode": 1,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
@@ -3384,7 +3384,10 @@
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_atlas": {
"__uuid__": "cb93c900-b440-4571-91d1-7da1636e3d73",
"__expectedType__": "cc.SpriteAtlas"
},
"_id": ""
},
{
@@ -3399,15 +3402,15 @@
"node": {
"__id__": 134
},
"_enabled": true,
"_enabled": false,
"__prefab": {
"__id__": 140
},
"_alignFlags": 41,
"_target": null,
"_left": -10,
"_right": -10,
"_top": -5,
"_left": 90,
"_right": 0,
"_top": -89.59100000000001,
"_bottom": 0,
"_horizontalCenter": 0,
"_verticalCenter": 0,
@@ -3629,10 +3632,10 @@
"__id__": 151
},
"asset": {
"__uuid__": "0c80c911-642f-423a-8ebb-98aebbbe2ef0",
"__uuid__": "b2a3067a-af88-4ff7-acdd-7474b8a163d8",
"__expectedType__": "cc.Prefab"
},
"fileId": "70V66T2DlAP7ksTOaOBDja",
"fileId": "43WU05gfVAPr5YF69YW+M4",
"instance": {
"__id__": 153
},
@@ -3640,7 +3643,7 @@
},
{
"__type__": "cc.PrefabInstance",
"fileId": "50AgFB9kROhppT8Q5wAY6S",
"fileId": "e722VC+kdPfqeaFe1joI7B",
"prefabRootNode": {
"__id__": 1
},
@@ -3670,12 +3673,12 @@
"propertyPath": [
"_name"
],
"value": "melist"
"value": "list-me"
},
{
"__type__": "cc.TargetInfo",
"localID": [
"70V66T2DlAP7ksTOaOBDja"
"43WU05gfVAPr5YF69YW+M4"
]
},
{
@@ -4318,7 +4321,7 @@
"__expectedType__": "cc.Prefab"
},
"melist_prefab": {
"__uuid__": "0c80c911-642f-423a-8ebb-98aebbbe2ef0",
"__uuid__": "b2a3067a-af88-4ff7-acdd-7474b8a163d8",
"__expectedType__": "cc.Prefab"
},
"_id": ""

File diff suppressed because it is too large Load Diff

View File

@@ -438,8 +438,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 286,
"height": 118.66666666666667
"width": 429,
"height": 178
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -10084,7 +10084,23 @@
},
"fileId": "6514ysgfJNtIx/ak03+nce",
"instance": null,
"targetOverrides": [],
"targetOverrides": [
{
"__id__": 458
},
{
"__id__": 461
},
{
"__id__": 464
},
{
"__id__": 467
},
{
"__id__": 470
}
],
"nestedPrefabInstanceRoots": [
{
"__id__": 438
@@ -10105,5 +10121,155 @@
"__id__": 3
}
]
},
{
"__type__": "cc.TargetOverrideInfo",
"source": {
"__id__": 3
},
"sourceInfo": {
"__id__": 459
},
"propertyPath": [
"hero1"
],
"target": {
"__id__": 3
},
"targetInfo": {
"__id__": 460
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"86bacQ+jZMOokmXQGUf5Ed"
]
},
{
"__type__": "cc.TargetInfo",
"localID": [
"b7CXK9ttxDz5mrJpDB4qA4"
]
},
{
"__type__": "cc.TargetOverrideInfo",
"source": {
"__id__": 3
},
"sourceInfo": {
"__id__": 462
},
"propertyPath": [
"hero2"
],
"target": {
"__id__": 3
},
"targetInfo": {
"__id__": 463
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"86bacQ+jZMOokmXQGUf5Ed"
]
},
{
"__type__": "cc.TargetInfo",
"localID": [
"36Na+WXPtJRbtR9UQnCyaj"
]
},
{
"__type__": "cc.TargetOverrideInfo",
"source": {
"__id__": 3
},
"sourceInfo": {
"__id__": 465
},
"propertyPath": [
"hero3"
],
"target": {
"__id__": 3
},
"targetInfo": {
"__id__": 466
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"86bacQ+jZMOokmXQGUf5Ed"
]
},
{
"__type__": "cc.TargetInfo",
"localID": [
"f2j7DYVutESIJc8Q7SiM91"
]
},
{
"__type__": "cc.TargetOverrideInfo",
"source": {
"__id__": 3
},
"sourceInfo": {
"__id__": 468
},
"propertyPath": [
"hero4"
],
"target": {
"__id__": 3
},
"targetInfo": {
"__id__": 469
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"86bacQ+jZMOokmXQGUf5Ed"
]
},
{
"__type__": "cc.TargetInfo",
"localID": [
"06y3o47t5NjZX9yks6rQWL"
]
},
{
"__type__": "cc.TargetOverrideInfo",
"source": {
"__id__": 3
},
"sourceInfo": {
"__id__": 471
},
"propertyPath": [
"hero5"
],
"target": {
"__id__": 3
},
"targetInfo": {
"__id__": 472
}
},
{
"__type__": "cc.TargetInfo",
"localID": [
"86bacQ+jZMOokmXQGUf5Ed"
]
},
{
"__type__": "cc.TargetInfo",
"localID": [
"cdrD/D+WdDiZtHCrbPOfP7"
]
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -21,12 +21,12 @@
"role_power": "Power",
"role_physical": "Physical",
"role_agile": "Agile",
"fskill_name_7009": "Frost Domain",
"fskill_name_7010": "Deadly Focus",
"fskill_name_7011": "Rending Strike",
"fskill_name_7012": "Gale March",
"fskill_info_7009": "Increase all allied heroes' freeze chance by {0}%",
"fskill_info_7010": "Increase all allied heroes' critical chance by {0}%",
"fskill_info_7011": "Increase all allied heroes' critical damage by {0}%",
"fskill_info_7012": "Increase all allied heroes' attack speed by {0}%"
"fskill_name_7003": "Frost Domain",
"fskill_name_7004": "Deadly Focus",
"fskill_name_7005": "Rending Strike",
"fskill_name_7006": "Gale March",
"fskill_info_7003": "Increase all allied heroes' freeze chance by {0}%",
"fskill_info_7004": "Increase all allied heroes' critical chance by {0}%",
"fskill_info_7005": "Increase all allied heroes' critical damage by {0}%",
"fskill_info_7006": "Increase all allied heroes' attack speed by {0}%"
}

View File

@@ -144,31 +144,31 @@
"scard_info_7102": "刷新卡池,都是远程英雄",
"scard_info_7103": "刷新卡池都是3级卡池等级英雄",
"fskill_name_7001": "召唤精通",
"fskill_name_7002": "亡灵统御",
"fskill_name_7003": "先发制人",
"fskill_name_7004": "余音绕梁",
"fskill_name_7005": "理财专家",
"fskill_name_7006": "商业大亨",
"fskill_name_7007": "神圣恢复",
"fskill_name_7008": "战鼓激昂",
"fskill_name_7009": "寒霜领域",
"fskill_name_7010": "致命专注",
"fskill_name_7011": "裂伤打击",
"fskill_name_7012": "疾风战歌",
"fskill_name_7014": "召唤精通",
"fskill_name_7015": "亡灵统御",
"fskill_name_7016": "先发制人",
"fskill_name_7017": "余音绕梁",
"fskill_name_7010": "理财专家",
"fskill_name_7011": "商业大亨",
"fskill_name_7001": "神圣恢复",
"fskill_name_7002": "战鼓激昂",
"fskill_name_7003": "寒霜领域",
"fskill_name_7004": "致命专注",
"fskill_name_7005": "裂伤打击",
"fskill_name_7006": "疾风战歌",
"fskill_info_7001": "场上所有友方召唤触发技能触发次数+{0}",
"fskill_info_7002": "场上所有友方死亡触发技能触发次数+{0}",
"fskill_info_7003": "场上所有友方战斗开始触发技能触发次数+{0}",
"fskill_info_7004": "场上所有友方战斗结束触发技能触发次数+{0}",
"fskill_info_7005": "每回合结束时金币收益提升{0}",
"fskill_info_7006": "卖出英雄时金币收益提升{0}",
"fskill_info_7007": "战斗结束时全队恢复效果+{0}%",
"fskill_info_7008": "场上所有友方攻击力提升{0}%",
"fskill_info_7009": "场上所有友方冰冻概率提升{0}%",
"fskill_info_7010": "场上所有友方暴击率提升{0}%",
"fskill_info_7011": "场上所有友方暴击伤害提升{0}%",
"fskill_info_7012": "场上所有友方攻击速度提升{0}%",
"fskill_info_7014": "场上所有友方召唤触发技能触发次数+{0}",
"fskill_info_7015": "场上所有友方死亡触发技能触发次数+{0}",
"fskill_info_7016": "场上所有友方战斗开始触发技能触发次数+{0}",
"fskill_info_7017": "场上所有友方战斗结束触发技能触发次数+{0}",
"fskill_info_7010": "每回合结束时金币收益提升{0}",
"fskill_info_7011": "卖出英雄时金币收益提升{0}",
"fskill_info_7001": "战斗结束时全队恢复效果+{0}%",
"fskill_info_7002": "场上所有友方攻击力提升{0}%",
"fskill_info_7003": "场上所有友方冰冻概率提升{0}%",
"fskill_info_7004": "场上所有友方暴击率提升{0}%",
"fskill_info_7005": "场上所有友方暴击伤害提升{0}%",
"fskill_info_7006": "场上所有友方攻击速度提升{0}%",
"hl_title_CritMaster_1": "初级暴击者",
"hl_title_CritMaster_2": "暴击大师",

View File

@@ -15,8 +15,8 @@
* | mon_name | 怪物名称 | mon_name_6001 | 兽人战士 |
* | skill_name | 技能名称 | skill_name_6001 | 攻击 |
* | skill_info | 技能描述 | skill_info_6001 | 对单个目标攻击... |
* | fskill_name | 驻场技能名称 | fskill_name_7001 | 召唤精通 |
* | fskill_info | 驻场技能描述 | fskill_info_7001 | 召唤触发... |
* | fskill_name | 驻场技能名称 | fskill_name_7014 | 召唤精通 |
* | fskill_info | 驻场技能描述 | fskill_info_7014 | 召唤触发... |
* | scard_name | 特殊卡牌名称 | scard_name_7001 | 战术晋升 |
* | scard_info | 特殊卡牌描述 | scard_info_7001 | 升级场上随机... |
* | hl_name | 亮点成就名称 | hl_name_9001 | 暴击大师 |
@@ -30,8 +30,8 @@
* langf(LangPrefix.skill_info, 6001, 1, 100) // → "对单个目标攻击1次每次造成100%攻击的伤害"
*
* // 驻场技能
* lang(LangPrefix.fskill_name, 7001) // → "召唤精通"
* langf(LangPrefix.fskill_info, 7001, 1) // → "场上所有友方召唤触发技能触发次数+1"
* lang(LangPrefix.fskill_name, 7014) // → "召唤精通"
* langf(LangPrefix.fskill_info, 7014, 1) // → "场上所有友方召唤触发技能触发次数+1"
*
* // 亮点成就(与英雄/技能完全一致的调用方式)
* lang(LangPrefix.hl_title, 9011) // → "初级暴击者"

View File

@@ -283,13 +283,13 @@ export class SingletonModuleComp extends ecs.Comp {
* 在游戏载入早期调用,预加载常用图集
*/
preloadCommonAssets() {
resources.load("gui/uicons", SpriteAtlas, (err, atlas) => {
resources.load("gui/ui3", SpriteAtlas, (err, atlas) => {
if (!err && atlas) {
// 增加引用计数防止图集被引擎自动垃圾回收GC导致底层 spriteFrames 为 null
atlas.addRef();
this.uiconsAtlas = atlas;
} else {
mLogger.error(this.debugMode, 'SMC', "预加载 gui/uicons 图集失败:", err);
mLogger.error(this.debugMode, 'SMC', "预加载 gui/ui3 图集失败:", err);
}
});
}

View File

@@ -1,6 +1,6 @@
import * as exp from "constants"
import { HeroInfo, HeroList, HType } from "./heroSet"
import { FightSet } from "./GameSet"
import { FightSet, SKILL_CARD_WAVES } from "./GameSet"
import { oops } from "db://oops-framework/core/Oops"
import { SkillOverrides, TGroup } from "./SkillSet"
@@ -37,6 +37,16 @@ export enum CKind {
Potion = 4, //药水
}
/** 技能卡触发类型 */
export enum CardSkillType {
Interval = 1, // 间隔定时触发 (战斗中每隔N秒执行)
Field = 2, // 驻场技能 (被动光环)
BattleStart = 3, // 战斗开始时触发一次
BattleEnd = 4, // 战斗结束时触发一次
HeroDead = 5, // 场上己方英雄死亡时触发
HeroCall = 6, // 场上己方英雄召唤上场时触发
}
/** 卡池等级定义 */
export enum CardLV {
LV1 = 1,
@@ -46,6 +56,21 @@ export enum CardLV {
LV5 = 5,
}
/**
* 卡牌技能触发类型
* - 命名对齐英雄侧 SkillTriggerType便于跨模块认知统一
* - 枚举值从 1 开始,避免 0 的 falsy 坑if (trigger_type) 判断出错)
*/
export enum CardTriggerType {
Instant = 1, // 即时触发:使用后立即生效一次
Interval = 2, // 定时循环:战斗中按 t_inv 间隔重复触发
Field = 3, // 驻场光环:被动生效(仅显式分类,仍由 field 字段驱动)
FightStart = 4, // 战斗开始时触发
FightEnd = 5, // 战斗结束时触发(每波结束)
HeroDead = 6, // 场上己方英雄死亡时触发
HeroCall = 7, // 英雄上场时触发(主角召唤 + 技能召唤 + 复活)
}
/** 通用卡牌配置 */
export interface CardConfig {
uuid: number
@@ -61,6 +86,7 @@ export interface CardConfig {
// 技能卡扩展属性
skill?: number // 关联的技能 UUID
icon?: string // 图标ID可选优先使用未设置时按 trigger_type 从 SkillSet/FieldSkillSet 自动取)
name?: string // 卡牌名称
info?: string // 卡牌描述信息
is_inst?: boolean // 是否即时起效
@@ -69,6 +95,15 @@ export interface CardConfig {
keep_waves?: number // 维持的波次数(-1表示持续到战斗结束0或undefined表示仅本波次
overrides?: SkillOverrides // 技能参数覆写如自定义伤害ap、buff值、金币数等
field?: number[] // 驻场技能 UUID 数组,表示该卡牌提供驻场属性加成
/** 触发类型(必填,技能卡专用;功能卡/英雄卡可缺省) */
trigger_type?: CardTriggerType;
/**
* 事件型触发的全局次数上限(仅 FightStart/FightEnd/HeroDead/HeroCall 有效)
* 默认 Infinity达到上限后销毁节点
* 注意:与 t_times 语义不同——t_times 控制每波内 Interval 的次数
*/
trigger_limit?: number;
}
export const CardsUpSet: Record<number, number> = {
1: 50,
@@ -78,14 +113,12 @@ export const CardsUpSet: Record<number, number> = {
5: 250,
}
/**初始coin数 */
export const CardInitCoins = 4
/** 卡池升级每波减免金额 */
export const CARD_POOL_UPGRADE_DISCOUNT_PER_WAVE = 10
/** 卡池默认初始等级 */
export const CARD_POOL_INIT_LEVEL = CardLV.LV1
/** 卡池等级上限 */
export const CARD_POOL_MAX_LEVEL = CardLV.LV5
/** 卡池等级上限(统一由 FightSet.MAX_CARD_POOL_LEVEL 设定,保持单一数据源) */
export const CARD_POOL_MAX_LEVEL = FightSet.MAX_CARD_POOL_LEVEL as unknown as CardLV
/** 英雄最高等级限制 */
export const CARD_HERO_MAX_LEVEL = 1
/** 基础卡池(英雄、技能、功能) */
@@ -140,58 +173,82 @@ HeroList.forEach(uuid => {
});
// 添加非英雄卡牌 (技能、功能卡)
const waveToPoolLv: Record<number, number> = {
1: 1,
5: 2,
10: 3,
15: 4,
20: 5
};
// 体系wave 由 SKILL_CARD_WAVES 统一配置每档强度递增Field 靠 field uuid 区分数值Interval 靠 overrides 覆写)
// wave→pool_lv 映射由 SKILL_CARD_WAVES 索引+1 自动生成wave 1→lv1, wave 5→lv2, wave 8→lv3
const waveToPoolLv: Record<number, number> = {};
SKILL_CARD_WAVES.forEach((w, i) => { waveToPoolLv[w] = i + 1; });
const SkillCardData: any[] = [
// === 1波技能 ===
{ uuid: 8301, skill: 6301, wave: 1, name: "护盾", info: "为伙伴/自己添加护盾可抵挡3次伤害", is_inst: true, keep_waves: 15 },
{ uuid: 8302, skill: 6302, wave: 1, name: "治疗", info: "治疗伙伴/自己", is_inst: true, keep_waves: 15 },
{ uuid: 8705, skill: 0, wave: 1, name: "金币收益", info: "每回合金币收益+1", is_inst: false, keep_waves: -1, field: [7005] },
{ uuid: 8706, skill: 0, wave: 1, name: "出售强化", info: "卖出英雄金币+1", is_inst: false, keep_waves: -1, field: [7006] },
{ uuid: 8707, skill: 0, wave: 1, name: "战后恢复", info: "战斗结束生命回复量+10%", is_inst: false, keep_waves: -1, field: [7007] },
// ==================== wave 1 档(基础强度) ====================
// --- 驻场卡Field ---
// === 5波技能 ===
{ uuid: 8303, skill: 6303, wave: 5, name: "获取金币", info: "增加一定数量的金币", is_inst: true, keep_waves: 15 },
{ uuid: 8401, skill: 6401, wave: 5, name: "攻击强化", info: "全体友方攻击力提升5点持续1次", is_inst: true, keep_waves: 15 },
{ uuid: 8402, skill: 6402, wave: 5, name: "生命强化", info: "全体友方最大生命值提升20点持续1次", is_inst: true, keep_waves: 15 },
{ uuid: 8403, skill: 6403, wave: 5, name: "暴击强化", info: "全体友方暴击率提升10%持续1次", is_inst: true, keep_waves: 15 },
{ uuid: 8404, skill: 6404, wave: 5, name: "暴伤强化", info: "全体友方暴击伤害提升20%持续1次", is_inst: true, keep_waves: 15 },
{ uuid: 8405, skill: 6405, wave: 5, name: "击晕强化", info: "全体友方击晕概率提升10%持续1次", is_inst: true, keep_waves: 15 },
{ uuid: 8408, skill: 6408, wave: 5, name: "穿刺强化", info: "全体友方穿透概率提升20%持续1次", is_inst: true, keep_waves: 15 },
{ uuid: 8409, skill: 6409, wave: 5, name: "风怒强化", info: "全体友方风怒次数提升1次持续1次", is_inst: true, keep_waves: 15 },
{ uuid: 8501, skill: 6501, wave: 5, name: "复活", info: "ap 代表复活的生命值百分比", is_inst: true, keep_waves: 15 },
{ uuid: 8701, skill: 0, wave: SKILL_CARD_WAVES[0], name: "战后恢复", info: "战斗结束生命回复量+10%", is_inst: false, keep_waves: -1, field: [7001], trigger_type: CardTriggerType.Field },
{ uuid: 8702, skill: 0, wave: SKILL_CARD_WAVES[0], name: "攻击加成", info: "英雄攻击力+10%", is_inst: false, keep_waves: -1, field: [7002], trigger_type: CardTriggerType.Field },
{ uuid: 8703, skill: 0, wave: SKILL_CARD_WAVES[0], name: "击晕加成", info: "英雄击晕概率+10%", is_inst: false, keep_waves: -1, field: [7003], trigger_type: CardTriggerType.Field },
{ uuid: 8704, skill: 0, wave: SKILL_CARD_WAVES[0], name: "暴击加成", info: "英雄暴击率+10%", is_inst: false, keep_waves: -1, field: [7004], trigger_type: CardTriggerType.Field },
{ uuid: 8705, skill: 0, wave: SKILL_CARD_WAVES[0], name: "暴伤加成", info: "英雄暴击伤害+20%", is_inst: false, keep_waves: -1, field: [7005], trigger_type: CardTriggerType.Field },
{ uuid: 8706, skill: 0, wave: SKILL_CARD_WAVES[0], name: "攻速加成", info: "英雄攻击速度+10%", is_inst: false, keep_waves: -1, field: [7006], trigger_type: CardTriggerType.Field },
{ uuid: 8707, skill: 0, wave: SKILL_CARD_WAVES[0], name: "生命加成", info: "英雄最大生命+10%", is_inst: false, keep_waves: -1, field: [7007], trigger_type: CardTriggerType.Field },
{ uuid: 8708, skill: 0, wave: SKILL_CARD_WAVES[0], name: "风怒加成", info: "英雄风怒概率+10%", is_inst: false, keep_waves: -1, field: [7008], trigger_type: CardTriggerType.Field },
{ uuid: 8709, skill: 0, wave: SKILL_CARD_WAVES[0], name: "穿刺加成", info: "英雄穿刺概率+10%", is_inst: false, keep_waves: -1, field: [7009], trigger_type: CardTriggerType.Field },
// === 10波技能 ===
{ uuid: 8708, skill: 0, wave: 10, name: "攻击加成", info: "英雄攻击力+10%", is_inst: false, keep_waves: -1, field: [7008] },
{ uuid: 8709, skill: 0, wave: 10, name: "击晕加成", info: "英雄击晕概率+10%", is_inst: false, keep_waves: -1, field: [7009] },
{ uuid: 8710, skill: 0, wave: 10, name: "暴击加成", info: "英雄暴击率+10%", is_inst: false, keep_waves: -1, field: [7010] },
{ uuid: 8711, skill: 0, wave: 10, name: "暴伤加成", info: "英雄暴击伤害+20%", is_inst: false, keep_waves: -1, field: [7011] },
{ uuid: 8712, skill: 0, wave: 10, name: "攻速加成", info: "英雄攻击速度+10%", is_inst: false, keep_waves: -1, field: [7012] },
{ uuid: 8713, skill: 0, wave: 10, name: "购买优惠", info: "购买卡牌费用-1金币", is_inst: false, keep_waves: -1, field: [7013] },
{ uuid: 8714, skill: 0, wave: 10, name: "刷新优惠", info: "刷新卡牌费用-1金币", is_inst: false, keep_waves: -1, field: [7014] },
{ uuid: 8716, skill: 0, wave: 10, name: "生命加成", info: "英雄最大生命+10%", is_inst: false, keep_waves: -1, field: [7016] },
{ uuid: 8717, skill: 0, wave: 10, name: "风怒加成", info: "英雄风怒概率+10%", is_inst: false, keep_waves: -1, field: [7017] },
{ uuid: 8718, skill: 0, wave: 10, name: "穿刺加成", info: "英雄穿刺概率+10%", is_inst: false, keep_waves: -1, field: [7018] },
{ uuid: 8710, skill: 0, wave: SKILL_CARD_WAVES[0], name: "金币收益", info: "每回合金币收益+1", is_inst: false, keep_waves: -1, field: [7010], trigger_type: CardTriggerType.Field },
{ uuid: 8711, skill: 0, wave: SKILL_CARD_WAVES[0], name: "出售强化", info: "卖出英雄金币+1", is_inst: false, keep_waves: -1, field: [7011], trigger_type: CardTriggerType.Field },
// === 15波技能 ===
{ uuid: 8701, skill: 0, wave: 15, name: "召唤强化", info: "召唤触发技能次数+1", is_inst: false, keep_waves: -1, field: [7001] },
{ uuid: 8702, skill: 0, wave: 15, name: "死亡强化", info: "死亡触发技能次数+1", is_inst: false, keep_waves: -1, field: [7002] },
{ uuid: 8703, skill: 0, wave: 15, name: "开场强化", info: "战斗开始触发技能次数+1", is_inst: false, keep_waves: -1, field: [7003] },
{ uuid: 8704, skill: 0, wave: 15, name: "结束强化", info: "战斗结束触发技能次数+1", is_inst: false, keep_waves: -1, field: [7004] },
// ==================== wave 5 档(强度 ×2 ====================
// --- 驻场卡field uuid +200对应 FieldSkillSet 72xx 段) ---
{ uuid: 8751, skill: 0, wave: SKILL_CARD_WAVES[1], name: "战后恢复+", info: "战斗结束生命回复量+20%", is_inst: false, keep_waves: -1, field: [7201], trigger_type: CardTriggerType.Field },
{ uuid: 8752, skill: 0, wave: SKILL_CARD_WAVES[1], name: "攻击加成+", info: "英雄攻击力+20%", is_inst: false, keep_waves: -1, field: [7202], trigger_type: CardTriggerType.Field },
{ uuid: 8753, skill: 0, wave: SKILL_CARD_WAVES[1], name: "击晕加成+", info: "英雄击晕概率+20%", is_inst: false, keep_waves: -1, field: [7203], trigger_type: CardTriggerType.Field },
{ uuid: 8754, skill: 0, wave: SKILL_CARD_WAVES[1], name: "暴击加成+", info: "英雄暴击率+20%", is_inst: false, keep_waves: -1, field: [7204], trigger_type: CardTriggerType.Field },
{ uuid: 8755, skill: 0, wave: SKILL_CARD_WAVES[1], name: "暴伤加成+", info: "英雄暴击伤害+40%", is_inst: false, keep_waves: -1, field: [7205], trigger_type: CardTriggerType.Field },
{ uuid: 8756, skill: 0, wave: SKILL_CARD_WAVES[1], name: "攻速加成+", info: "英雄攻击速度+20%", is_inst: false, keep_waves: -1, field: [7206], trigger_type: CardTriggerType.Field },
{ uuid: 8757, skill: 0, wave: SKILL_CARD_WAVES[1], name: "生命加成+", info: "英雄最大生命+20%", is_inst: false, keep_waves: -1, field: [7207], trigger_type: CardTriggerType.Field },
{ uuid: 8758, skill: 0, wave: SKILL_CARD_WAVES[1], name: "风怒加成+", info: "英雄风怒概率+20%", is_inst: false, keep_waves: -1, field: [7208], trigger_type: CardTriggerType.Field },
{ uuid: 8759, skill: 0, wave: SKILL_CARD_WAVES[1], name: "穿刺加成+", info: "英雄穿刺概率+20%", is_inst: false, keep_waves: -1, field: [7209], trigger_type: CardTriggerType.Field },
// --- 范围攻击卡ap 递增,间隔缩短) ---
{ uuid: 8261, skill: 6201, wave: SKILL_CARD_WAVES[1], name: "雷墙+", info: "召唤雷墙阻挡敌人,有概率击晕", is_inst: false, t_inv: 5, keep_waves: -1, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
{ uuid: 8262, skill: 6202, wave: SKILL_CARD_WAVES[1], name: "火墙+", info: "召唤火墙阻挡敌人,有概率击晕", is_inst: false, t_inv: 5, keep_waves: -1, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
{ uuid: 8263, skill: 6203, wave: SKILL_CARD_WAVES[1], name: "飓风+", info: "召唤飓风攻击敌人,有概率击晕", is_inst: false, t_inv: 5, keep_waves: -1, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
{ uuid: 8264, skill: 6204, wave: SKILL_CARD_WAVES[1], name: "水墙+", info: "召唤水墙阻挡敌人,有概率击晕", is_inst: false, t_inv: 5, keep_waves: -1, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
{ uuid: 8265, skill: 6205, wave: SKILL_CARD_WAVES[1], name: "风墙+", info: "召唤风墙困住敌人,有概率击晕", is_inst: false, t_inv: 5, keep_waves: -1, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
{ uuid: 8266, skill: 6206, wave: SKILL_CARD_WAVES[1], name: "陨石术+", info: "召唤陨石范围攻击敌人,有概率击晕", is_inst: false, t_inv: 5, keep_waves: -1, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
// === 20波技能 ===
{ uuid: 8201, skill: 6201, wave: 20, name: "雷墙", info: "召唤雷墙阻挡敌人,有概率击晕", is_inst: false, t_times: 999, t_inv: 5, keep_waves: -1 },
{ uuid: 8202, skill: 6202, wave: 20, name: "火墙", info: "召唤火墙阻挡敌人,有概率击晕", is_inst: false, t_times: 999, t_inv: 5, keep_waves: -1 },
{ uuid: 8203, skill: 6203, wave: 20, name: "飓风", info: "召唤飓风攻击敌人,有概率击晕", is_inst: false, t_times: 999, t_inv: 5, keep_waves: -1 },
{ uuid: 8204, skill: 6204, wave: 20, name: "水墙", info: "召唤水墙阻挡敌人,有概率击晕", is_inst: false, t_times: 999, t_inv: 5, keep_waves: -1 },
{ uuid: 8205, skill: 6205, wave: 20, name: "风墙", info: "召唤风墙困住敌人,有概率击晕", is_inst: false, t_times: 999, t_inv: 5, keep_waves: -1 },
{ uuid: 8206, skill: 6206, wave: 20, name: "陨石术", info: "召唤陨石范围攻击敌人,有概率击晕", is_inst: false, t_times: 999, t_inv: 5, keep_waves: -1 },
{ uuid: 8760, skill: 0, wave: SKILL_CARD_WAVES[1], name: "金币收益+", info: "每回合金币收益+2", is_inst: false, keep_waves: -1, field: [7210], trigger_type: CardTriggerType.Field },
{ uuid: 8761, skill: 0, wave: SKILL_CARD_WAVES[1], name: "购买优惠+", info: "购买卡牌费用-2金币", is_inst: false, keep_waves: -1, field: [7212], trigger_type: CardTriggerType.Field },
{ uuid: 8762, skill: 0, wave: SKILL_CARD_WAVES[1], name: "刷新优惠", info: "刷新卡牌费用-1金币", is_inst: false, keep_waves: -1, field: [7213], trigger_type: CardTriggerType.Field },
// ==================== wave 8 档(强度 ×3 ====================
// --- 驻场卡field uuid +400对应 FieldSkillSet 74xx 段) ---
{ uuid: 8801, skill: 0, wave: SKILL_CARD_WAVES[2], name: "战后恢复++", info: "战斗结束生命回复量+30%", is_inst: false, keep_waves: -1, field: [7401], trigger_type: CardTriggerType.Field },
{ uuid: 8802, skill: 0, wave: SKILL_CARD_WAVES[2], name: "攻击加成++", info: "英雄攻击力+30%", is_inst: false, keep_waves: -1, field: [7402], trigger_type: CardTriggerType.Field },
{ uuid: 8803, skill: 0, wave: SKILL_CARD_WAVES[2], name: "击晕加成++", info: "英雄击晕概率+30%", is_inst: false, keep_waves: -1, field: [7403], trigger_type: CardTriggerType.Field },
{ uuid: 8804, skill: 0, wave: SKILL_CARD_WAVES[2], name: "暴击加成++", info: "英雄暴击率+30%", is_inst: false, keep_waves: -1, field: [7404], trigger_type: CardTriggerType.Field },
{ uuid: 8805, skill: 0, wave: SKILL_CARD_WAVES[2], name: "暴伤加成++", info: "英雄暴击伤害+60%", is_inst: false, keep_waves: -1, field: [7405], trigger_type: CardTriggerType.Field },
{ uuid: 8806, skill: 0, wave: SKILL_CARD_WAVES[2], name: "攻速加成++", info: "英雄攻击速度+30%", is_inst: false, keep_waves: -1, field: [7406], trigger_type: CardTriggerType.Field },
{ uuid: 8807, skill: 0, wave: SKILL_CARD_WAVES[2], name: "生命加成++", info: "英雄最大生命+30%", is_inst: false, keep_waves: -1, field: [7407], trigger_type: CardTriggerType.Field },
{ uuid: 8808, skill: 0, wave: SKILL_CARD_WAVES[2], name: "风怒加成++", info: "英雄风怒概率+30%", is_inst: false, keep_waves: -1, field: [7408], trigger_type: CardTriggerType.Field },
{ uuid: 8809, skill: 0, wave: SKILL_CARD_WAVES[2], name: "穿刺加成++", info: "英雄穿刺概率+30%", is_inst: false, keep_waves: -1, field: [7409], trigger_type: CardTriggerType.Field },
{ uuid: 8361, skill: 6201, wave: SKILL_CARD_WAVES[2], name: "雷墙++", info: "召唤雷墙阻挡敌人,有概率击晕", is_inst: false, t_inv: 4, keep_waves: -1, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
{ uuid: 8362, skill: 6202, wave: SKILL_CARD_WAVES[2], name: "火墙++", info: "召唤火墙阻挡敌人,有概率击晕", is_inst: false, t_inv: 4, keep_waves: -1, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
{ uuid: 8363, skill: 6203, wave: SKILL_CARD_WAVES[2], name: "飓风++", info: "召唤飓风攻击敌人,有概率击晕", is_inst: false, t_inv: 4, keep_waves: -1, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
{ uuid: 8364, skill: 6204, wave: SKILL_CARD_WAVES[2], name: "水墙++", info: "召唤水墙阻挡敌人,有概率击晕", is_inst: false, t_inv: 4, keep_waves: -1, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
{ uuid: 8365, skill: 6205, wave: SKILL_CARD_WAVES[2], name: "风墙++", info: "召唤风墙困住敌人,有概率击晕", is_inst: false, t_inv: 4, keep_waves: -1, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
{ uuid: 8366, skill: 6206, wave: SKILL_CARD_WAVES[2], name: "陨石术++", info: "召唤陨石范围攻击敌人,有概率击晕", is_inst: false, t_inv: 4, keep_waves: -1, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
{ uuid: 8810, skill: 0, wave: SKILL_CARD_WAVES[2], name: "金币收益++", info: "每回合金币收益+3", is_inst: false, keep_waves: -1, field: [7410], trigger_type: CardTriggerType.Field },
{ uuid: 8811, skill: 0, wave: SKILL_CARD_WAVES[2], name: "召唤强化++", info: "召唤触发技能次数+1", is_inst: false, keep_waves: -1, field: [7014], trigger_type: CardTriggerType.Field },
{ uuid: 8812, skill: 0, wave: SKILL_CARD_WAVES[2], name: "死亡强化++", info: "死亡触发技能次数+1", is_inst: false, keep_waves: -1, field: [7015], trigger_type: CardTriggerType.Field },
{ uuid: 8813, skill: 0, wave: SKILL_CARD_WAVES[2], name: "开场强化++", info: "战斗开始触发技能次数+1", is_inst: false, keep_waves: -1, field: [7016], trigger_type: CardTriggerType.Field },
{ uuid: 8814, skill: 0, wave: SKILL_CARD_WAVES[2], name: "结束强化++", info: "战斗结束触发技能次数+1", is_inst: false, keep_waves: -1, field: [7017], trigger_type: CardTriggerType.Field },
{ uuid: 8815, skill: 0, wave: SKILL_CARD_WAVES[2], name: "攻击强化++", info: "攻击触发技能次数+1", is_inst: false, keep_waves: -1, field: [7018], trigger_type: CardTriggerType.Field },
{ uuid: 8816, skill: 0, wave: SKILL_CARD_WAVES[2], name: "受击强化++", info: "被攻击触发技能次数+1", is_inst: false, keep_waves: -1, field: [7019], trigger_type: CardTriggerType.Field },
// --- 范围攻击卡ap 最高,间隔最短) ---
];
SkillCardData.forEach(data => {
@@ -204,14 +261,18 @@ SkillCardData.forEach(data => {
pool_lv: waveToPoolLv[data.wave] as CardLV,
wave: data.wave,
kind: CKind.Skill,
card_lv: 1,
card_lv: waveToPoolLv[data.wave], // wave 1→1, 5→2, 8→3
name: data.name,
info: data.info,
icon: data.icon, // 【新增】透传自定义图标ID优先级最高
is_inst: data.is_inst,
t_times: data.t_times || (data.is_inst ? 1 : 999),
t_inv: data.t_inv || 0,
keep_waves: data.keep_waves,
field: data.field
field: data.field,
overrides: data.overrides, // 【修复】原遗漏
trigger_type: data.trigger_type, // 【新增】显式触发类型
trigger_limit: data.trigger_limit, // 【新增】事件型触发次数上限
});
});
@@ -290,14 +351,19 @@ const weightedPick = (cards: CardConfig[]): CardConfig | null => {
return cards[cards.length - 1]
}
/** 连续抽取 count 张卡,允许重复 */
const pickCards = (cards: CardConfig[], count: number): CardConfig[] => {
/** 连续抽取 count 张卡,允许重复或通过 unique 剔除重复 */
const pickCards = (cards: CardConfig[], count: number, unique: boolean = false): CardConfig[] => {
if (cards.length === 0 || count <= 0) return []
const selected: CardConfig[] = []
let available = [...cards]
while (selected.length < count) {
const pick = weightedPick(cards)
if (available.length === 0) break
const pick = weightedPick(available)
if (!pick) break
selected.push(pick)
if (unique) {
available = available.filter(c => c.uuid !== pick.uuid)
}
}
return selected
}
@@ -345,6 +411,7 @@ export const drawCardsByRule = (
heroLv?: number
targetPoolLv?: number
wave?: number
unique?: boolean
} = {}
): CardConfig[] => {
const count = Math.max(0, Math.floor(options.count ?? 4))
@@ -394,6 +461,6 @@ export const drawCardsByRule = (
})
}
const picked = pickCards(pool, count)
const picked = pickCards(pool, count, options.unique)
return picked
}

View File

@@ -20,6 +20,8 @@ export enum GameEvent {
CastSkill = "CastSkill",
CardsClose = "CardsClose",
CardRefresh = "CardRefresh",
/** 单张卡牌被点击选中payload 为被点击的 CardComp 实例,用于其他卡牌联动隐藏 call_btn */
CardSelected = "CardSelected",
UseHeroCard = "UseHeroCard",
UseSkillCard = "UseSkillCard",
UseSpecialCard = "UseSpecialCard",

View File

@@ -13,7 +13,7 @@ export enum BoxSet {
LETF_END = -360,
RIGHT_END = 360,
//游戏地平线
GAME_LINE = -100,
GAME_LINE = -90,
}
export enum FacSet {
@@ -21,14 +21,14 @@ export enum FacSet {
MON = 1,
}
export enum FightSet {
WAVE_COIN_BASE = 4, // 波次金币基础奖励
WAVE_COIN_GROW = 1, // 波次金币递增值
WAVE_COIN_MAX = 10, // 波次金币最大基础奖励
WAVE_COIN_BASE = 7, // 波次金币基础奖励
WAVE_COIN_GROW = 2, // 波次金币递增值
WAVE_COIN_MAX = 17, // 波次金币最大基础奖励
CRIT_DAMAGE = 50,//暴击伤害
MORE_RC = 10,//更多次数 广告获取的次数
HEARTPOS = -320,//基地位置
HERO_MAX_NUM = 6,//英雄最大数量
MERGE_MAX = 3, //英雄最大等级
MERGE_MAX = 2, //英雄最大等级
MERGE_NEED = 3, //英雄升级需要的英雄数
// BACK_RANG=30,//后退范围
BACK_RANG = 30,//后退范围
@@ -42,11 +42,33 @@ export enum FightSet {
SHIELD_MAX = 5,
WAVE_HEAL_RATE = 0.5, // 回合结束时所有英雄恢复最大生命值的比例
PUNCTURE_DOWN = 50,
REFRESH_COST = 1,
BASE_COST=3
REFRESH_COST = 2,
BASE_COST = 5,
INIT_COIN = 7, // 初始金币数
// 刷新成本
/** 卡池等级上限(对应 CardLV 最大值) */
MAX_CARD_POOL_LEVEL = 5,
}
/**
* 卡池升级波次配置(单一数据源)。
* 索引 i 对应目标等级 = i + 2
* - 第 1 个波次 → 升至 LV2
* - 第 2 个波次 → 升至 LV3
* - 依此类推,上限为 FightSet.MAX_CARD_POOL_LEVEL
*/
export const CARD_POOL_UPGRADE_WAVES: number[] = [4, 7, 10, 13];
/**
* 技能卡牌出现的波次配置(单一数据源)。
* 数组索引 i 对应卡牌档位 card_lv = i + 1
* - 第 1 个波次 → LV1 档(基础强度)
* - 第 2 个波次 → LV2 档(强度 ×2
* - 第 3 个波次 → LV3 档(强度 ×3
* 需与 CardSet.SkillCardData 的 wave 字段严格对齐,否则抽卡时池子为空。
*/
export const SKILL_CARD_WAVES: number[] = [1, 5, 8];
export const laneIdx = {
2: [-180, 90],
1: [-180, 0],

View File

@@ -155,6 +155,7 @@ export interface SkillConfig {
bck?: number, // 额外击退概率
buff_type?: Attrs, // Buff 类型 (单一职责)
call_hero?: number, // 召唤技能召唤英雄id(可选)
is_accel?: boolean, // 是否逐渐加速飞行
info: string, // 技能描述
}
@@ -170,6 +171,7 @@ export interface SkillOverrides {
bck?: number;
buff_type?: Attrs;
call_hero?: number;
is_accel?: boolean;
}
/**
@@ -208,39 +210,33 @@ export const SkillSet: Record<number, SkillConfig> = {
* 6010 箭矢黄 击晕取向
**/
6001: {
uuid: 6001, name: "火球", sp_name: "atk_1", icon: "1026", 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,
RType: RType.bezier, EType: EType.collision, info: "造成攻击力100%的伤害",
uuid: 6001, name: "火球", sp_name: "atk_1", icon: "Stat_Attack_01", 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.linear, EType: EType.collision, info: "造成攻击力100%的伤害",
},
6002: {
uuid: 6002, name: "紫烟", sp_name: "atk_2", icon: "1126", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
uuid: 6002, name: "紫烟", sp_name: "atk_2", icon: "Stat_Attack_01", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
DTType: DTType.single, ap: 100, hit_count: 1, hitcd: 0.3, speed: 720, with: 90, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.bezier, EType: EType.collision, info: "近战普通攻击技能",
RType: RType.linear, EType: EType.collision, info: "近战普通攻击技能",
},
6003: {
uuid: 6003, name: "白球", sp_name: "atk_3", icon: "1126", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
uuid: 6003, name: "白球", sp_name: "atk_3", icon: "Stat_Attack_01", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
DTType: DTType.single, ap: 100, hit_count: 1, hitcd: 0.3, speed: 720, with: 90, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.bezier, EType: EType.collision, info: "一定几率暴击",
RType: RType.linear, EType: EType.collision, info: "一定几率暴击",
},
6004: {
uuid: 6004, name: "水球", sp_name: "atk_4", icon: "1126", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
DTType: DTType.single, ap: 100, hit_count: 1, hitcd: 0.3, speed: 720, with: 90, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.bezier, EType: EType.collision, info: "普通远程攻击",
},
6008: {
uuid: 6008, name: "箭矢", sp_name: "arrow", icon: "1135", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
uuid: 6008, name: "箭矢", sp_name: "arrow", icon: "Stat_RangedAttack", 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.remote,
RType: RType.bezier, EType: EType.collision, bezier_start_y: 20, bezier_mid_y: 140, bezier_arc: 1.05, info: "造成攻击力100%的伤害",
},
6009: {
uuid: 6009, name: "箭矢蓝", sp_name: "arrow_blue", icon: "1135", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
uuid: 6009, name: "箭矢蓝", sp_name: "arrow_blue", icon: "Stat_RangedAttack", 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.remote,
RType: RType.bezier, EType: EType.collision, bezier_start_y: 20, bezier_mid_y: 140, bezier_arc: 1.05, info: "造成攻击力100%的伤害",
},
6010: {
uuid: 6010, name: "箭矢红", sp_name: "arrow_red", icon: "1135", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
uuid: 6010, name: "箭矢红", sp_name: "arrow_red", icon: "Stat_RangedAttack", 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.remote,
RType: RType.bezier, EType: EType.collision, bezier_start_y: 20, bezier_mid_y: 140, bezier_arc: 1.05, info: "造成攻击力100%的伤害",
},
@@ -252,27 +248,22 @@ export const SkillSet: Record<number, SkillConfig> = {
* 6104 水球 溅射:分裂多个
**/
6101: {
uuid: 6101, name: "大火球", sp_name: "line_1", icon: "1126", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
uuid: 6101, name: "大火球", sp_name: "line_1", icon: "Stat_FireDamage", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
DTType: DTType.single, stun: 0, ap: 100, hit_count: 2, hitcd: 0.3, speed: 720, with: 90, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.linear, EType: EType.collision, info: "造成攻击力100%的伤害,一定几率暴击,高阶技能",
},
//怪物法师统一使用 暗影球
6102: {
uuid: 6102, name: "大紫球", sp_name: "line_2", icon: "1126", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
uuid: 6102, name: "大紫球", sp_name: "line_2", icon: "Stat_Mana", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
DTType: DTType.single, ap: 100, hit_count: 2, hitcd: 0.3, speed: 720, with: 90, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.linear, EType: EType.collision, info: "造成攻击力100%的伤害,高阶技能",
},
6103: {
uuid: 6103, name: "大白球", sp_name: "line_3", icon: "1126", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
uuid: 6103, name: "大白球", sp_name: "line_3", icon: "Stat_WaterDamage", TGroup: TGroup.Enemy, readyAnm: "", endAnm: "", act: "atk",
DTType: DTType.single, ap: 100, hit_count: 2, hitcd: 0.3, speed: 720, with: 90, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.linear, EType: EType.collision, info: "造成攻击力100%的伤害,一定几率上毒(后期加入),高阶技能 ",
},
6104: {
uuid: 6104, name: "大水球", sp_name: "line_4", icon: "1135", TGroup: TGroup.Enemy, readyAnm: "yellow", endAnm: "", act: "max",
DTType: DTType.single, crt: 20, ap: 100, hit_count: 2, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.linear, EType: EType.collision, info: "射出强力箭矢最多穿透6个敌人附带20%额外暴击率",
},
/*** ======高阶范围攻击技能 ====
* 都是3*3 范围攻击 不是英雄技能是技能卡20波技能
* 6201 雷墙 击晕向
@@ -283,105 +274,105 @@ export const SkillSet: Record<number, SkillConfig> = {
* 6206 陨石术 暴击向
**/
6201: {
uuid: 6201, name: "雷墙", sp_name: "box_1", icon: "1173", TGroup: TGroup.Enemy, readyAnm: "blues", endAnm: "", act: "max",
uuid: 6201, name: "雷墙", sp_name: "box_1", icon: "Stat_LightningDamag", TGroup: TGroup.Enemy, readyAnm: "blues", endAnm: "", act: "max",
DTType: DTType.aoe_grid, stun: 0, ap: 150, hit_count: 6, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.fixed, EType: EType.animationEnd, info: "召唤雷墙阻挡敌人,有概率击晕",
},
6202: {
uuid: 6202, name: "火墙", sp_name: "box_2", icon: "1173", TGroup: TGroup.Enemy, readyAnm: "blues", endAnm: "", act: "max",
uuid: 6202, name: "火墙", sp_name: "box_2", icon: "Stat_FireDamage", TGroup: TGroup.Enemy, readyAnm: "blues", endAnm: "", act: "max",
DTType: DTType.aoe_grid, stun: 0, ap: 150, hit_count: 6, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.fixed, EType: EType.animationEnd, info: "召唤雷墙阻挡敌人,有概率击晕",
},
6203: {
uuid: 6203, name: "飓风", sp_name: "box_3", icon: "1173", TGroup: TGroup.Enemy, readyAnm: "blues", endAnm: "", act: "max",
uuid: 6203, name: "飓风", sp_name: "box_3", icon: "Stat_Stun_01", TGroup: TGroup.Enemy, readyAnm: "blues", endAnm: "", act: "max",
DTType: DTType.aoe_grid, stun: 0, ap: 150, hit_count: 6, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.fixed, EType: EType.animationEnd, info: "召唤雷墙阻挡敌人,有概率击晕",
},
6204: {
uuid: 6204, name: "水墙", sp_name: "box_4", icon: "1173", TGroup: TGroup.Enemy, readyAnm: "blues", endAnm: "", act: "max",
uuid: 6204, name: "水墙", sp_name: "box_4", icon: "Stat_Mana", TGroup: TGroup.Enemy, readyAnm: "blues", endAnm: "", act: "max",
DTType: DTType.aoe_grid, stun: 0, ap: 150, hit_count: 6, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.fixed, EType: EType.animationEnd, info: "召唤雷墙阻挡敌人,有概率击晕",
},
6205: {
uuid: 6205, name: "风墙", sp_name: "box_5", icon: "1173", TGroup: TGroup.Enemy, readyAnm: "blues", endAnm: "", act: "max",
uuid: 6205, name: "风墙", sp_name: "box_5", icon: "Stat_Stun_01", TGroup: TGroup.Enemy, readyAnm: "blues", endAnm: "", act: "max",
DTType: DTType.aoe_grid, stun: 0, ap: 150, hit_count: 6, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.fixed, EType: EType.animationEnd, info: "召唤风墙困住敌人,有概率击晕",
},
6206: {
uuid: 6206, name: "陨石术", sp_name: "box_6", icon: "1173", TGroup: TGroup.Enemy, readyAnm: "blues", endAnm: "", act: "max",
uuid: 6206, name: "陨石术", sp_name: "box_6", icon: "Stat_Tripleshot", TGroup: TGroup.Enemy, readyAnm: "blues", endAnm: "", act: "max",
DTType: DTType.aoe_grid, stun: 0, ap: 150, hit_count: 6, hitcd: 0.2, speed: 720, with: 0, ready: 0.2, EAnm: 0, DAnm: "", IType: IType.remote,
RType: RType.fixed, EType: EType.animationEnd, info: "召唤陨石范围攻击敌人,有概率击晕",
},
//============================= ====== 辅助技能 技能卡牌 1 波技能 ====== ==========================
6301: {
uuid: 6301, name: "护盾", sp_name: "buff_wind", icon: "1255", TGroup: TGroup.Self, readyAnm: "up_blue", endAnm: "", act: "atk",
uuid: 6301, name: "护盾", sp_name: "buff_wind", icon: "Stat_Defense", TGroup: TGroup.Self, readyAnm: "up_blue", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Shield, ap: 3, 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: "为伙伴/自己添加护盾可抵挡3次伤害",
},
6302: {
uuid: 6302, name: "治疗", sp_name: "buff_wind", icon: "1292", TGroup: TGroup.Team, readyAnm: "up_green", endAnm: "", act: "atk",
uuid: 6302, name: "治疗", sp_name: "buff_wind", icon: "Stat_Hp_01", TGroup: TGroup.Team, readyAnm: "up_green", endAnm: "", act: "atk",
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: "治疗伙伴/自己",
},
//==========================buff 技能 也是 技能卡牌 5 波技能
6303: {
uuid: 6303, name: "获取金币", sp_name: "buff_wind", icon: "1255", TGroup: TGroup.Self, readyAnm: "up_blue", endAnm: "gold", act: "atk",
uuid: 6303, name: "获取金币", sp_name: "buff_wind", icon: "Stat_GoldGainIncrease_01", TGroup: TGroup.Self, readyAnm: "up_blue", endAnm: "gold", act: "atk",
DTType: DTType.single, kind: SkillKind.Gold, ap: 0, gold: 1, 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: "增加一定数量的金币",
},
//==========================buff 技能=====================
6401: {
uuid: 6401, name: "攻击强化", sp_name: "buff_wind", icon: "1255", TGroup: TGroup.Team, readyAnm: "up_ap", endAnm: "", act: "atk",
uuid: 6401, name: "攻击强化", sp_name: "buff_wind", icon: "Stat_Attack_03", 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次",
},
6402: {
uuid: 6402, name: "生命强化", sp_name: "buff_wind", icon: "1255", TGroup: TGroup.Team, readyAnm: "up_hp", endAnm: "", act: "atk",
uuid: 6402, name: "生命强化", sp_name: "buff_wind", icon: "Stat_Hp_02", 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次",
},
6403: {
uuid: 6403, name: "暴击强化", sp_name: "buff_wind", icon: "1255", TGroup: TGroup.Team, readyAnm: "up_ap", endAnm: "", act: "atk",
uuid: 6403, name: "暴击强化", sp_name: "buff_wind", icon: "Stat_CriticalChance_02", TGroup: TGroup.Team, readyAnm: "up_ap", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Support, ap: 1, 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.critical, info: "全体友方暴击率提升10%持续1次",
},
6404: {
uuid: 6404, name: "暴伤强化", sp_name: "buff_wind", icon: "1255", TGroup: TGroup.Team, readyAnm: "up_ap", endAnm: "", act: "atk",
uuid: 6404, name: "暴伤强化", sp_name: "buff_wind", icon: "Stat_Critical_01", TGroup: TGroup.Team, readyAnm: "up_ap", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Support, ap: 1, 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.critical_damage, info: "全体友方暴击伤害提升20%持续1次",
},
6405: {
uuid: 6405, name: "击晕强化", sp_name: "buff_wind", icon: "1255", TGroup: TGroup.Team, readyAnm: "up_blue", endAnm: "", act: "atk",
uuid: 6405, name: "击晕强化", sp_name: "buff_wind", icon: "Stat_Stun_01", TGroup: TGroup.Team, readyAnm: "up_blue", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Support, ap: 1, 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.stun_chance, info: "全体友方击晕概率提升10%持续1次",
},
6406: {
uuid: 6406, name: "击退强化", sp_name: "buff_wind", icon: "1255", TGroup: TGroup.Team, readyAnm: "up_blue", endAnm: "", act: "atk",
uuid: 6406, name: "击退强化", sp_name: "buff_wind", icon: "Stat_Stun_01", TGroup: TGroup.Team, readyAnm: "up_blue", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Support, ap: 1, 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: "暂未使用",
},
6407: {
uuid: 6407, name: "距推强化", sp_name: "buff_wind", icon: "1255", TGroup: TGroup.Team, readyAnm: "up_blue", endAnm: "", act: "atk",
uuid: 6407, name: "距推强化", sp_name: "buff_wind", icon: "Stat_Stun_01", TGroup: TGroup.Team, readyAnm: "up_blue", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Support, ap: 1, 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: "暂未使用",
},
6408: {
uuid: 6408, name: "穿刺强化", sp_name: "buff_wind", icon: "1255", TGroup: TGroup.Team, readyAnm: "up_ap", endAnm: "", act: "atk",
uuid: 6408, name: "穿刺强化", sp_name: "buff_wind", icon: "Stat_Tripleshot", TGroup: TGroup.Team, readyAnm: "up_ap", 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.puncture_chance, info: "全体友方穿透概率提升20%持续1次",
},
6409: {
uuid: 6409, name: "风怒强化", sp_name: "buff_wind", icon: "1255", TGroup: TGroup.Team, readyAnm: "up_ap", endAnm: "", act: "atk",
uuid: 6409, name: "风怒强化", sp_name: "buff_wind", icon: "Stat_CriticalComboChance", TGroup: TGroup.Team, readyAnm: "up_ap", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Support, ap: 1, 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.wfuny, info: "全体友方风怒次数提升1次持续1次",
},
6501: {
uuid: 6501, name: "复活", sp_name: "buff_wind", icon: "1255", TGroup: TGroup.Self, readyAnm: "up_ap", endAnm: "", act: "atk",
uuid: 6501, name: "复活", sp_name: "buff_wind", icon: "Stat_HolyDamage", TGroup: TGroup.Self, readyAnm: "up_ap", endAnm: "", act: "atk",
DTType: DTType.single, kind: SkillKind.Support, ap: 50, 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, info: "ap 代表复活的生命值百分比",
}
@@ -408,34 +399,77 @@ export enum FieldSkillType {
HeroHp = 16, // 英雄最大生命加成
HeroWindFury = 17, // 英雄风怒概率加成
HeroPuncture = 18, // 英雄穿刺概率加成
AtkCount = 19, // 攻击触发技能次数提升
BeAtkCount = 20, // 被攻击触发技能次数提升
}
export interface FieldSkillConfig {
uuid: number;
name: string;
icon: string; // 新增图标字段
type: FieldSkillType;
value: number; // 提升的数值
info: string;
}
export const FieldSkillSet: Record<number, FieldSkillConfig> = {
7001: { uuid: 7001, name: "召唤强化", type: FieldSkillType.SummonCount, value: 1, info: "召唤触发技能次数+1" }, //15 波技能
7002: { uuid: 7002, name: "死亡强化", type: FieldSkillType.DeadCount, value: 1, info: "死亡触发技能次数+1" }, //15 波技能
7003: { uuid: 7003, name: "开场强化", type: FieldSkillType.StartCount, value: 1, info: "战斗开始触发技能次数+1" }, //15 波技能
7004: { uuid: 7004, name: "结束强化", type: FieldSkillType.EndCount, value: 1, info: "战斗结束触发技能次数+1" }, //15 波技能
7005: { uuid: 7005, name: "金币收益", type: FieldSkillType.WaveGold, value: 1, info: "每回合金币收益+1" }, //1 波技能
7006: { uuid: 7006, name: "出售强化", type: FieldSkillType.SellGold, value: 1, info: "卖出英雄金币+1" }, //1 波技能
7007: { uuid: 7007, name: "战后恢复", type: FieldSkillType.WaveHeal, value: 0.1, info: "战斗结束生命回复量+10%" }, //1 波技能
7008: { uuid: 7008, name: "攻加成", type: FieldSkillType.HeroAtk, value: 0.1, info: "英雄攻击+10%" }, //10 波技能
7009: { uuid: 7009, name: "击晕加成", type: FieldSkillType.HeroStun, value: 0.1, info: "英雄击晕概率+10%" }, //10 波技能
7010: { uuid: 7010, name: "暴击加成", type: FieldSkillType.HeroCrit, value: 0.1, info: "英雄暴击率+10%" }, //10 波技能
7011: { uuid: 7011, name: "暴伤加成", type: FieldSkillType.HeroCritDamage, value: 0.2, info: "英雄暴击伤害+20%" }, //10 波技能
7012: { uuid: 7012, name: "攻速加成", type: FieldSkillType.HeroSpeed, value: 0.1, info: "英雄攻击速度+10%" }, //10 波技能
// ---- 13~18 来自原 TalentSet统一为驻场百分比 / 绝对值口径 ----
// 出售返还由原生 SellGold 承担SellBonus 不再单独配置
7013: { uuid: 7013, name: "购买优惠", type: FieldSkillType.BuyDiscount, value: 1, info: "购买卡牌费用-1金币" }, //10 波技能
7014: { uuid: 7014, name: "刷新优惠", type: FieldSkillType.RefreshDiscount, value: 1, info: "刷新卡牌费用-1金币" }, //10 波技能
7016: { uuid: 7016, name: "生命加成", type: FieldSkillType.HeroHp, value: 0.1, info: "英雄最大生命+10%" }, //10 波技能
7017: { uuid: 7017, name: "风怒加成", type: FieldSkillType.HeroWindFury, value: 0.1, info: "英雄风怒概率+10%" }, //10 波技能
7018: { uuid: 7018, name: "穿刺加成", type: FieldSkillType.HeroPuncture, value: 0.1, info: "英雄穿刺概率+10%" }, //10 波技能
7001: { uuid: 7001, name: "战后恢复", icon: "Stat_PotionBoost", type: FieldSkillType.WaveHeal, value: 0.1, info: "战斗结束生命回复量+10%" },
7002: { uuid: 7002, name: "攻击加成", icon: "Stat_Attack_03", type: FieldSkillType.HeroAtk, value: 0.1, info: "英雄攻击力+10%" },
7003: { uuid: 7003, name: "击晕加成", icon: "Stat_Stun_01", type: FieldSkillType.HeroStun, value: 0.1, info: "英雄击晕概率+10%" },
7004: { uuid: 7004, name: "暴击加成", icon: "Stat_CriticalChance_02", type: FieldSkillType.HeroCrit, value: 0.1, info: "英雄暴击率+10%" },
7005: { uuid: 7005, name: "暴伤加成", icon: "Stat_Critical_01", type: FieldSkillType.HeroCritDamage, value: 0.2, info: "英雄暴击伤害+20%" },
7006: { uuid: 7006, name: "攻加成", icon: "Stat_AttackSpeed_02", type: FieldSkillType.HeroSpeed, value: 0.1, info: "英雄攻击速度+10%" },
7007: { uuid: 7007, name: "生命加成", icon: "Stat_Hp_02", type: FieldSkillType.HeroHp, value: 0.1, info: "英雄最大生命+10%" },
7008: { uuid: 7008, name: "风怒加成", icon: "Stat_CriticalComboChance", type: FieldSkillType.HeroWindFury, value: 0.1, info: "英雄风怒概率+10%" },
7009: { uuid: 7009, name: "穿刺加成", icon: "Stat_Tripleshot", type: FieldSkillType.HeroPuncture, value: 0.1, info: "英雄穿刺概率+10%" },
7010: { uuid: 7010, name: "金币收益", icon: "Stat_InventorySlotIncrease", type: FieldSkillType.WaveGold, value: 1, info: "每回合金币收益+1" },
7011: { uuid: 7011, name: "出售强化", icon: "Stat_GoldGainIncrease_01", type: FieldSkillType.SellGold, value: 1, info: "卖出英雄金币+1" },
7012: { uuid: 7012, name: "购买优惠", icon: "Stat_KeyCapacityIncrease", type: FieldSkillType.BuyDiscount, value: 1, info: "购买卡牌费用-1金币" },
7013: { uuid: 7013, name: "刷新优惠", icon: "Stat_RandomBonus", type: FieldSkillType.RefreshDiscount, value: 1, info: "刷新卡牌费用-1金币" },
7014: { uuid: 7014, name: "召唤强化", icon: "Stat_UnitSummonIncrease_02", type: FieldSkillType.SummonCount, value: 1, info: "召唤触发技能次数+1" },
7015: { uuid: 7015, name: "死亡强化", icon: "Stat_PoisonChanceIncrease", type: FieldSkillType.DeadCount, value: 1, info: "死亡触发技能次数+1" },
7016: { uuid: 7016, name: "开场强化", icon: "Stat_AttackRangeIncrease_01", type: FieldSkillType.StartCount, value: 1, info: "战斗开始触发技能次数+1" },
7017: { uuid: 7017, name: "结束强化", icon: "Stat_UnitSummonIncrease_01", type: FieldSkillType.EndCount, value: 1, info: "战斗结束触发技能次数+1" },
7018: { uuid: 7018, name: "攻击强化", icon: "Stat_Attack_03", type: FieldSkillType.AtkCount, value: 1, info: "攻击触发技能次数+1" },
7019: { uuid: 7019, name: "受击强化", icon: "Stat_Armor_01", type: FieldSkillType.BeAtkCount, value: 1, info: "被攻击触发技能次数+1" },
// ============ wave5 档(原值 ×2 ============
7201: { uuid: 7201, name: "战后恢复+", icon: "Stat_PotionBoost", type: FieldSkillType.WaveHeal, value: 0.2, info: "战斗结束生命回复量+20%" },
7202: { uuid: 7202, name: "攻击加成+", icon: "Stat_Attack_03", type: FieldSkillType.HeroAtk, value: 0.2, info: "英雄攻击力+20%" },
7203: { uuid: 7203, name: "击晕加成+", icon: "Stat_Stun_01", type: FieldSkillType.HeroStun, value: 0.2, info: "英雄击晕概率+20%" },
7204: { uuid: 7204, name: "暴击加成+", icon: "Stat_CriticalChance_02", type: FieldSkillType.HeroCrit, value: 0.2, info: "英雄暴击率+20%" },
7205: { uuid: 7205, name: "暴伤加成+", icon: "Stat_Critical_01", type: FieldSkillType.HeroCritDamage, value: 0.4, info: "英雄暴击伤害+40%" },
7206: { uuid: 7206, name: "攻速加成+", icon: "Stat_AttackSpeed_02", type: FieldSkillType.HeroSpeed, value: 0.2, info: "英雄攻击速度+20%" },
7207: { uuid: 7207, name: "生命加成+", icon: "Stat_Hp_02", type: FieldSkillType.HeroHp, value: 0.2, info: "英雄最大生命+20%" },
7208: { uuid: 7208, name: "风怒加成+", icon: "Stat_CriticalComboChance", type: FieldSkillType.HeroWindFury, value: 0.2, info: "英雄风怒概率+20%" },
7209: { uuid: 7209, name: "穿刺加成+", icon: "Stat_Tripleshot", type: FieldSkillType.HeroPuncture, value: 0.2, info: "英雄穿刺概率+20%" },
7210: { uuid: 7210, name: "金币收益+", icon: "Stat_InventorySlotIncrease", type: FieldSkillType.WaveGold, value: 2, info: "每回合金币收益+2" },
7211: { uuid: 7211, name: "出售强化+", icon: "Stat_GoldGainIncrease_01", type: FieldSkillType.SellGold, value: 2, info: "卖出英雄金币+2" },
7212: { uuid: 7212, name: "购买优惠+", icon: "Stat_KeyCapacityIncrease", type: FieldSkillType.BuyDiscount, value: 2, info: "购买卡牌费用-2金币" },
7213: { uuid: 7213, name: "刷新优惠+", icon: "Stat_RandomBonus", type: FieldSkillType.RefreshDiscount, value: 2, info: "刷新卡牌费用-2金币" },
7401: { uuid: 7401, name: "战后恢复++", icon: "Stat_PotionBoost", type: FieldSkillType.WaveHeal, value: 0.3, info: "战斗结束生命回复量+30%" },
7402: { uuid: 7402, name: "攻击加成++", icon: "Stat_Attack_03", type: FieldSkillType.HeroAtk, value: 0.3, info: "英雄攻击力+30%" },
7403: { uuid: 7403, name: "击晕加成++", icon: "Stat_Stun_01", type: FieldSkillType.HeroStun, value: 0.3, info: "英雄击晕概率+30%" },
7404: { uuid: 7404, name: "暴击加成++", icon: "Stat_CriticalChance_02", type: FieldSkillType.HeroCrit, value: 0.3, info: "英雄暴击率+30%" },
7405: { uuid: 7405, name: "暴伤加成++", icon: "Stat_Critical_01", type: FieldSkillType.HeroCritDamage, value: 0.6, info: "英雄暴击伤害+60%" },
7406: { uuid: 7406, name: "攻速加成++", icon: "Stat_AttackSpeed_02", type: FieldSkillType.HeroSpeed, value: 0.3, info: "英雄攻击速度+30%" },
7407: { uuid: 7407, name: "生命加成++", icon: "Stat_Hp_02", type: FieldSkillType.HeroHp, value: 0.3, info: "英雄最大生命+30%" },
7408: { uuid: 7408, name: "风怒加成++", icon: "Stat_CriticalComboChance", type: FieldSkillType.HeroWindFury, value: 0.3, info: "英雄风怒概率+30%" },
7409: { uuid: 7409, name: "穿刺加成++", icon: "Stat_Tripleshot", type: FieldSkillType.HeroPuncture, value: 0.3, info: "英雄穿刺概率+30%" },
7410: { uuid: 7410, name: "金币收益++", icon: "Stat_InventorySlotIncrease", type: FieldSkillType.WaveGold, value: 3, info: "每回合金币收益+3" },
7411: { uuid: 7411, name: "出售强化++", icon: "Stat_GoldGainIncrease_01", type: FieldSkillType.SellGold, value: 3, info: "卖出英雄金币+3" },
7412: { uuid: 7412, name: "购买优惠++", icon: "Stat_KeyCapacityIncrease", type: FieldSkillType.BuyDiscount, value: 3, info: "购买卡牌费用-3金币" },
7413: { uuid: 7413, name: "刷新优惠++", icon: "Stat_RandomBonus", type: FieldSkillType.RefreshDiscount, value: 3, info: "刷新卡牌费用-3金币" },
};

View File

@@ -100,14 +100,14 @@ export enum SkillTriggerType {
}
export const SkillTriggerName = {
[SkillTriggerType.Call]: "降临",
[SkillTriggerType.Dead]: "遗志",
[SkillTriggerType.FStart]: "手",
[SkillTriggerType.FEnd]: "终战",
[SkillTriggerType.Call]: "召唤",
[SkillTriggerType.Dead]: "亡语",
[SkillTriggerType.FStart]: "手",
[SkillTriggerType.FEnd]: "生息",
[SkillTriggerType.Field]: "光环",
[SkillTriggerType.Atking]: "击",
[SkillTriggerType.Atked]: "击",
[SkillTriggerType.Revive]: "涅槃",
[SkillTriggerType.Atking]: "击",
[SkillTriggerType.Atked]: "击",
[SkillTriggerType.Revive]: "复活",
}
export const SkillTriggerDesc = {
@@ -198,30 +198,30 @@ export interface HeroEvolve {
export const HeroInfo: Record<number, heroInfo> = {
// ========== atked 类(战士 · 自身强化) ==========
5011:{uuid:5011,name:"小铁卫",path:"hk1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Melee,hp:400,ap:20,
5011:{uuid:5011,name:"小铁卫",path:"hk1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Melee,hp:300,ap:28,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
atked:[{s_uuid:6301,t_num:3,overrides:{TGroup:TGroup.Self,ap:4}}],
info:"每受击3次为自身添加4层护盾"},
5012:{uuid:5012,name:"不死小强",path:"hk2", fac:FacSet.HERO,pool_lv:2,lv:1,type:HType.Melee,hp:350,ap:25,
5012:{uuid:5012,name:"不死小强",path:"hk2", fac:FacSet.HERO,pool_lv:2,lv:1,type:HType.Melee,hp:600,ap:57,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
atked:[{s_uuid:6302,t_num:3,overrides:{TGroup:TGroup.Self,ap:250}}],
info:"每受击3次为自身回复攻击力250%的生命值"},
5013:{uuid:5013,name:"铁骨头",path:"hk3", fac:FacSet.HERO,pool_lv:2,lv:1,type:HType.Melee,hp:300,ap:20,
5013:{uuid:5013,name:"铁骨头",path:"hk3", fac:FacSet.HERO,pool_lv:2,lv:1,type:HType.Melee,hp:600,ap:57,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
atked:[{s_uuid:6402,t_num:5,overrides:{TGroup:TGroup.Self,ap:100}}],
info:"每受击5次永久提升自身最大生命值100点"},
5014:{uuid:5014,name:"怒火武者",path:"hk4", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Melee,hp:320,ap:30,
5014:{uuid:5014,name:"怒火武者",path:"hk4", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Melee,hp:900,ap:85,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow1].cd,ccd:0}},
atked:[{s_uuid:6401,t_num:3,overrides:{TGroup:TGroup.Self,ap:12}}],
info:"每受击3次永久提升自身攻击力12点"},
5015:{uuid:5015,name:"血刃武者",path:"hk5", fac:FacSet.HERO,pool_lv:4,lv:1,type:HType.Melee,hp:450,ap:35,
5015:{uuid:5015,name:"血刃武者",path:"hk5", fac:FacSet.HERO,pool_lv:4,lv:1,type:HType.Melee,hp:1200,ap:113,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
atked:[
{s_uuid:6301,t_num:3,overrides:{TGroup:TGroup.Self,ap:3}},
{s_uuid:6401,t_num:5,overrides:{TGroup:TGroup.Self,ap:15}}
],
info:"每受击3次加3层护盾每受击5次永久+15攻击力"},
5016:{uuid:5016,name:"狂血战士",path:"hc1", fac:FacSet.HERO,pool_lv:5,lv:1,type:HType.Melee,hp:380,ap:45,
5016:{uuid:5016,name:"狂血战士",path:"hc1", fac:FacSet.HERO,pool_lv:5,lv:1,type:HType.Melee,hp:1500,ap:142,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
atked:[
{s_uuid:6401,t_num:3,overrides:{TGroup:TGroup.Self,ap:10}},
@@ -230,18 +230,18 @@ export const HeroInfo: Record<number, heroInfo> = {
info:"每受击3次永久+10攻击力每受击5次永久+15%暴击率"},
// ========== atking 类 — 刺客(自身强化) ==========
5021:{uuid:5021,name:"小刺客",path:"hc1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Melee,hp:200,ap:40,
5021:{uuid:5021,name:"小刺客",path:"hc1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Melee,hp:300,ap:28,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Fast2].cd,ccd:0}},
atking:[{s_uuid:6401,t_num:5,overrides:{TGroup:TGroup.Self,ap:8}}],
info:"每攻击5次永久提升自身攻击力8点"},
5022:{uuid:5022,name:"嗜血剑客",path:"hc2", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Melee,hp:240,ap:60,
5022:{uuid:5022,name:"嗜血剑客",path:"hc2", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Melee,hp:900,ap:85,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Fast2].cd,ccd:0}},
atking:[
{s_uuid:6403,t_num:5,overrides:{TGroup:TGroup.Self,ap:10}},
{s_uuid:6401,t_num:7,overrides:{TGroup:TGroup.Self,ap:12}}
],
info:"每攻击5次永久+10%暴击率每攻击7次永久+12攻击力"},
5023:{uuid:5023,name:"暗影杀手",path:"hc3", fac:FacSet.HERO,pool_lv:5,lv:1,type:HType.Melee,hp:280,ap:85,
5023:{uuid:5023,name:"暗影杀手",path:"hc3", fac:FacSet.HERO,pool_lv:5,lv:1,type:HType.Melee,hp:1500,ap:142,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Fast1].cd,ccd:0}},
atking:[
{s_uuid:6403,t_num:5,overrides:{TGroup:TGroup.Self,ap:10}},
@@ -250,15 +250,15 @@ export const HeroInfo: Record<number, heroInfo> = {
info:"每攻击5次永久+10%暴击率每攻击7次永久+15%暴伤"},
// ========== atking 类 — 射手队友强化hit_count 控制目标数) ==========
5031:{uuid:5031,name:"援护弓手",path:"ha1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Long,hp:160,ap:45,
5031:{uuid:5031,name:"援护弓手",path:"ha1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Long,hp:143,ap:40,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Normal2].cd,ccd:0}},
atking:[{s_uuid:6401,t_num:5,overrides:{TGroup:TGroup.Team,hit_count:1,ap:8}}],
info:"每攻击5次为随机1名队友永久提升攻击力8点"},
5032:{uuid:5032,name:"战术弓手",path:"ha2", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Long,hp:190,ap:60,
5032:{uuid:5032,name:"战术弓手",path:"ha2", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Long,hp:430,ap:120,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Normal1].cd,ccd:0}},
atking:[{s_uuid:6403,t_num:5,overrides:{TGroup:TGroup.Team,hit_count:3,ap:10}}],
info:"每攻击5次为随机3名队友永久提升暴击率10%"},
5033:{uuid:5033,name:"鹰眼弓将",path:"ha3", fac:FacSet.HERO,pool_lv:5,lv:1,type:HType.Long,hp:220,ap:75,
5033:{uuid:5033,name:"鹰眼弓将",path:"ha3", fac:FacSet.HERO,pool_lv:5,lv:1,type:HType.Long,hp:717,ap:200,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Fast3].cd,ccd:0}},
atking:[
{s_uuid:6401,t_num:5,overrides:{TGroup:TGroup.Team,hit_count:6,ap:8}},
@@ -267,11 +267,11 @@ export const HeroInfo: Record<number, heroInfo> = {
info:"每攻击5次为随机6名队友永久+8攻击力每攻击7次永久+12%暴伤"},
// ========== dead 类(战士+刺客 · 死亡遗产) ==========
5041:{uuid:5041,name:"殉道卫士",path:"hk1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Melee,hp:300,ap:25,
5041:{uuid:5041,name:"殉道卫士",path:"hk1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Melee,hp:300,ap:28,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
dead:[{s_uuid:6301,t_num:1,overrides:{TGroup:TGroup.Team,ap:3}}],
info:"死亡时为全队添加3层护盾"},
5042:{uuid:5042,name:"遗志将军",path:"hk2", fac:FacSet.HERO,pool_lv:2,lv:1,type:HType.Melee,hp:350,ap:30,
5042:{uuid:5042,name:"遗志将军",path:"hk2", fac:FacSet.HERO,pool_lv:2,lv:1,type:HType.Melee,hp:600,ap:57,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
dead:[
{s_uuid:6401,t_num:1,overrides:{TGroup:TGroup.Team,ap:20}},
@@ -279,18 +279,18 @@ export const HeroInfo: Record<number, heroInfo> = {
],
revive:{s_uuid:6501,r_num:1,upr:0.3},
info:"死亡时全队永久+20攻击力、+80最大生命值死后复活一次"},
5043:{uuid:5043,name:"亡魂刺客",path:"hc1", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Melee,hp:220,ap:50,
5043:{uuid:5043,name:"亡魂刺客",path:"hc1", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Melee,hp:900,ap:85,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Normal1].cd,ccd:0}},
dead:[{s_uuid:6405,t_num:1,overrides:{TGroup:TGroup.Team,ap:15}}],
info:"死亡时全队永久提升击晕概率15%"},
5044:{uuid:5044,name:"血誓剑客",path:"hc2", fac:FacSet.HERO,pool_lv:4,lv:1,type:HType.Melee,hp:250,ap:65,
5044:{uuid:5044,name:"血誓剑客",path:"hc2", fac:FacSet.HERO,pool_lv:4,lv:1,type:HType.Melee,hp:1200,ap:113,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Fast3].cd,ccd:0}},
dead:[
{s_uuid:6403,t_num:1,overrides:{TGroup:TGroup.Team,ap:15}},
{s_uuid:6404,t_num:1,overrides:{TGroup:TGroup.Team,ap:20}}
],
info:"死亡时全队永久+15%暴击率、+20%暴伤"},
5045:{uuid:5045,name:"不灭战魂",path:"hk3", fac:FacSet.HERO,pool_lv:5,lv:1,type:HType.Melee,hp:420,ap:40,
5045:{uuid:5045,name:"不灭战魂",path:"hk3", fac:FacSet.HERO,pool_lv:5,lv:1,type:HType.Melee,hp:1500,ap:142,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
dead:[
{s_uuid:6301,t_num:1,overrides:{TGroup:TGroup.Team,ap:5}},
@@ -301,30 +301,30 @@ export const HeroInfo: Record<number, heroInfo> = {
info:"死亡时全队获得5层护盾、永久+30攻击力、永久+120最大生命值死后复活一次"},
// ========== fstart 类(法师 · 战前增益) ==========
5051:{uuid:5051,name:"占卜师",path:"hm1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Long,hp:140,ap:35,
5051:{uuid:5051,name:"占卜师",path:"hm1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Long,hp:143,ap:40,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Mid3].cd,ccd:0}},
fstart:[{s_uuid:6401,t_num:1,overrides:{TGroup:TGroup.Team,ap:15}}],
info:"战斗开始时为全队永久提升攻击力15点"},
5052:{uuid:5052,name:"护盾牧师",path:"hm2", fac:FacSet.HERO,pool_lv:2,lv:1,type:HType.Long,hp:150,ap:40,
5052:{uuid:5052,name:"护盾牧师",path:"hm2", fac:FacSet.HERO,pool_lv:2,lv:1,type:HType.Long,hp:287,ap:80,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow1].cd,ccd:0}},
fstart:[{s_uuid:6301,t_num:1,overrides:{TGroup:TGroup.Team,ap:2}}],
info:"战斗开始时为全队添加2层护盾"},
5053:{uuid:5053,name:"血盟法师",path:"hm3", fac:FacSet.HERO,pool_lv:2,lv:1,type:HType.Long,hp:160,ap:45,
5053:{uuid:5053,name:"血盟法师",path:"hm3", fac:FacSet.HERO,pool_lv:2,lv:1,type:HType.Long,hp:287,ap:80,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Mid2].cd,ccd:0}},
fstart:[{s_uuid:6402,t_num:1,overrides:{TGroup:TGroup.Team,ap:100}}],
info:"战斗开始时为全队永久提升最大生命值100点"},
5054:{uuid:5054,name:"暴击法师",path:"hm4", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Long,hp:170,ap:55,
5054:{uuid:5054,name:"暴击法师",path:"hm4", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Long,hp:430,ap:120,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Mid3].cd,ccd:0}},
fstart:[{s_uuid:6403,t_num:1,overrides:{TGroup:TGroup.Team,ap:20}}],
info:"战斗开始时为全队永久提升暴击率20%"},
5055:{uuid:5055,name:"毁灭法师",path:"hm5", fac:FacSet.HERO,pool_lv:4,lv:1,type:HType.Long,hp:185,ap:65,
5055:{uuid:5055,name:"毁灭法师",path:"hm5", fac:FacSet.HERO,pool_lv:4,lv:1,type:HType.Long,hp:573,ap:160,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Mid1].cd,ccd:0}},
fstart:[
{s_uuid:6404,t_num:1,overrides:{TGroup:TGroup.Team,ap:25}},
{s_uuid:6401,t_num:1,overrides:{TGroup:TGroup.Team,ap:20}}
],
info:"战斗开始时为全队永久+25%暴伤、+20攻击力"},
5056:{uuid:5056,name:"预言法师",path:"hm6", fac:FacSet.HERO,pool_lv:5,lv:1,type:HType.Long,hp:200,ap:70,
5056:{uuid:5056,name:"预言法师",path:"hm6", fac:FacSet.HERO,pool_lv:5,lv:1,type:HType.Long,hp:717,ap:200,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Normal3].cd,ccd:0}},
fstart:[
{s_uuid:6405,t_num:1,overrides:{TGroup:TGroup.Team,ap:20}},
@@ -334,32 +334,32 @@ export const HeroInfo: Record<number, heroInfo> = {
info:"战斗开始时为全队永久+20%击晕概率、+15%暴击率、+20%暴伤"},
// ========== field 类(法师 · 驻场光环) ==========
5061:{uuid:5061,name:"亡语法师",path:"hm1", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Long,hp:160,ap:50,
5061:{uuid:5061,name:"亡语法师",path:"hm1", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Long,hp:430,ap:120,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Mid2].cd,ccd:0}},
field:[7002],
field:[7015],
info:"驻场期间全队死亡触发技能次数+1死亡后光环消失"},
// ========== fend + atking 类(辅助 · 治疗续航 + 波次增益) ==========
5071:{uuid:5071,name:"治愈牧师",path:"hh1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Long,hp:130,ap:40,
5071:{uuid:5071,name:"治愈牧师",path:"hh1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Long,hp:143,ap:40,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Normal3].cd,ccd:0}},
atking:[{s_uuid:6302,t_num:5,overrides:{TGroup:TGroup.Team,ap:200}}],
info:"每攻击5次治疗全队200%AP"},
5072:{uuid:5072,name:"小金库",path:"hh2", fac:FacSet.HERO,pool_lv:2,lv:1,type:HType.Long,hp:140,ap:35,
5072:{uuid:5072,name:"小金库",path:"hh2", fac:FacSet.HERO,pool_lv:2,lv:1,type:HType.Long,hp:287,ap:80,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Mid1].cd,ccd:0}},
atking:[{s_uuid:6302,t_num:5,overrides:{TGroup:TGroup.Team,ap:200}}],
fend:[{s_uuid:6303,t_num:1,overrides:{gold:1}}],
info:"每攻击5次治疗全队每波结束获得1金币"},
5073:{uuid:5073,name:"强化牧师",path:"hh3", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Long,hp:150,ap:45,
5073:{uuid:5073,name:"强化牧师",path:"hh3", fac:FacSet.HERO,pool_lv:3,lv:1,type:HType.Long,hp:430,ap:120,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Normal3].cd,ccd:0}},
atking:[{s_uuid:6302,t_num:5,overrides:{TGroup:TGroup.Team,ap:250}}],
fend:[{s_uuid:6401,t_num:1,overrides:{TGroup:TGroup.Team,ap:10}}],
info:"每攻击5次治疗全队每波结束全队永久+10攻击力"},
5074:{uuid:5074,name:"生命牧师",path:"hh4", fac:FacSet.HERO,pool_lv:4,lv:1,type:HType.Long,hp:160,ap:50,
5074:{uuid:5074,name:"生命牧师",path:"hh4", fac:FacSet.HERO,pool_lv:4,lv:1,type:HType.Long,hp:573,ap:160,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Normal3].cd,ccd:0}},
atking:[{s_uuid:6302,t_num:5,overrides:{TGroup:TGroup.Team,ap:250}}],
fend:[{s_uuid:6402,t_num:1,overrides:{TGroup:TGroup.Team,ap:80}}],
info:"每攻击5次治疗全队每波结束全队永久+80最大生命值"},
5075:{uuid:5075,name:"全能牧师",path:"hh5", fac:FacSet.HERO,pool_lv:5,lv:1,type:HType.Long,hp:180,ap:55,
5075:{uuid:5075,name:"全能牧师",path:"hh5", fac:FacSet.HERO,pool_lv:5,lv:1,type:HType.Long,hp:717,ap:200,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Normal2].cd,ccd:0}},
atking:[{s_uuid:6302,t_num:5,overrides:{TGroup:TGroup.Team,ap:300}}],
fend:[
@@ -381,40 +381,40 @@ export const HeroInfo: Record<number, heroInfo> = {
*/
// 基础怪物 (全部远程攻击HType仅决定站位)
// 近战位怪物 (站在前排,承受更多伤害) — v5: TD节奏CD多而弱爽感设计
// 基础怪物 (全部固定点位站桩攻击HType仅决定是前排还是后排)
// 前排怪物 (站在前排,承受更多伤害) — v5: TD节奏CD多而弱爽感设计
6001:{uuid:6001,name:"兽人战士",path:"m1", fac:FacSet.MON,lv:1,type:HType.Melee,hp:220,ap:10,speed:70,
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"基础近战位怪"},
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"基础前排怪"},
6002:{uuid:6002,name:"兽人精锐战士",path:"m2", fac:FacSet.MON,lv:1,type:HType.Melee,hp:300,ap:14,speed:110,
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"进阶近战位怪,更快更痛"},
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"进阶前排怪,更快更痛"},
6003:{uuid:6003,name:"兽人重装兵",path:"m3", fac:FacSet.MON,lv:1,type:HType.Melee,hp:850,ap:20,speed:50,
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"重型坦克怪高HP慢攻"},
// 远程位怪物 (站在后排,输出更高)
// 后排怪物 (站在后排,输出更高)
6004:{uuid:6004,name:"兽人射手",path:"m4", fac:FacSet.MON,lv:1,type:HType.Long,hp:190,ap:35,speed:70,
skills:{6008:{uuid:6008,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"远程高DPS怪"},
skills:{6008:{uuid:6008,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"后排高DPS怪"},
6005:{uuid:6005,name:"兽人刺客",path:"m5", fac:FacSet.MON,lv:1,type:HType.Long,hp:210,ap:38,speed:130,
skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"高AP快速攻击刺客"},
// 特殊位怪物
6006:{uuid:6006,name:"骷髅领主",path:"m6", fac:FacSet.MON,lv:1,type:HType.Melee,hp:5000,ap:20,speed:60,
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"MiniBoss级坦克"},
6007:{uuid:6007,name:"兽人术士",path:"m7", fac:FacSet.MON,lv:1,type:HType.Melee,hp:300,ap:24,speed:70,
skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"法师怪,远程魔法攻击"},
6008:{uuid:6008,name:"兽人火法",path:"m8", fac:FacSet.MON,lv:1,type:HType.Melee,hp:270,ap:32,speed:70,
skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"高输出法师怪"},
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"前排MiniBoss级坦克"},
6007:{uuid:6007,name:"兽人术士",path:"m7", fac:FacSet.MON,lv:1,type:HType.Long,hp:300,ap:24,speed:70,
skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"后排法师怪,魔法攻击"},
6008:{uuid:6008,name:"兽人火法",path:"m8", fac:FacSet.MON,lv:1,type:HType.Long,hp:270,ap:32,speed:70,
skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"后排高输出法师怪"},
// BOSS怪物 — Boss节奏1.2-1.5s删除不存在的6206技能
6101:{uuid:6101,name:"兽人首领-双刀战士",path:"mb1", fac:FacSet.MON,lv:6,type:HType.Long,hp:1900,ap:30,speed:120,
skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"远程Boss高攻速"},
6101:{uuid:6101,name:"兽人首领-双刀战士",path:"mb1", fac:FacSet.MON,lv:6,type:HType.Melee,hp:1900,ap:30,speed:120,
skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"前排Boss高攻速"},
6102:{uuid:6102,name:"兽人首领-斧头战士",path:"mb2", fac:FacSet.MON,lv:6,type:HType.Melee,hp:7500,ap:26,speed:60,
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"近战Boss超高HP"},
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"前排Boss超高HP"},
6103:{uuid:6103,name:"兽人首领-魔法师",path:"mb3", fac:FacSet.MON,lv:6,type:HType.Long,hp:2250,ap:38,speed:110,
skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"远程法系Boss高AP"},
6104:{uuid:6104,name:"兽人首领-射手",path:"mb4", fac:FacSet.MON,lv:6,type:HType.Melee,hp:6800,ap:30,speed:70,
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"近战位Boss均衡型"},
skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"后排法系Boss高AP"},
6104:{uuid:6104,name:"兽人首领-射手",path:"mb4", fac:FacSet.MON,lv:6,type:HType.Long,hp:6800,ap:30,speed:70,
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"后排位Boss均衡型"},
6105:{uuid:6105,name:"亡灵首领-法师",path:"mb5", fac:FacSet.MON,lv:6,type:HType.Long,hp:2600,ap:42,speed:110,
skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"远程高AP Boss"},
skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"后排高AP Boss"},
6106:{uuid:6106,name:"亡灵首领-骑马战士",path:"mb6", fac:FacSet.MON,lv:6,type:HType.Melee,hp:9000,ap:26,speed:130,
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"终极Boss最高HP+高速"},
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"前排终极Boss最高HP+高速"},
};

View File

@@ -573,13 +573,13 @@
- **职业**:法师(远程)
- **HP / AP / CD**HP 160 / AP 50 / CD 1.2
- **驻场**field:[7002] — 存活期间全队死亡触发次数 **+1**
- **驻场**field:[7015] — 存活期间全队死亡触发次数 **+1**
```
5061:{uuid:5061, name:"亡语法师", path:"hm1", fac:FacSet.HERO, pool_lv:3, lv:1, type:HType.Long,
hp:160, ap:50,
skills:{6003:{uuid:6003, lv:1, cd:1.2, ccd:0}},
field:[7002],
field:[7015],
info:"驻场期间全队死亡触发技能次数+1死亡后光环消失"}
```

View File

@@ -249,7 +249,7 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
// 触发复活动画
if (targetView && reviveSkillConf) {
targetView.playReady(reviveSkillConf.readyAnm);
targetView.skill_name('', reviveSkillConf.uuid);
targetView.skill_name('', reviveSkillConf.uuid, SkillTriggerType.Revive);
// 延迟 0.5 秒后恢复状态,让特效有时间播放,同时也能体现“正在复活”的过程
targetView.scheduleOnce(() => {
if (targetView.node && targetView.node.isValid) {
@@ -327,6 +327,10 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpd
const view = entity.get(HeroViewComp);
if (view) {
SkillTriggerHelper.trigger(SkillTriggerType.Dead, TAttrsComp, view);
// 【新增】仅英雄阵营派发全局死亡事件(怪物死亡会误触发海量卡牌效果)
if (TAttrsComp.fac === FacSet.HERO) {
oops.message.dispatchEvent(GameEvent.HeroDead, { eid: entity.eid });
}
}
}

View File

@@ -9,6 +9,7 @@ import { SkillSet,} from "../common/config/SkillSet";
import { HeroInfo } from "../common/config/heroSet";
import { oops } from "db://oops-framework/core/Oops";
import { UIID } from "../common/config/GameUIConfig";
import { GameEvent } from "../common/config/GameEvent";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { Tooltip } from "../skill/Tooltip";
import { timedCom } from "../skill/timedCom";
@@ -321,10 +322,10 @@ export class HeroViewComp extends CCComp {
Tooltip.load(pos, type, value, s_uuid, this.node);
}
/** 技能提示 */
public skill_name(value: string = "", s_uuid: number = 1001, y: number = 50) {
public skill_name(value: string = "", s_uuid: number = 1001, triggerType: string = "", y: number = 50) {
let pos = v3(0, 60);
pos.y = pos.y + y;
Tooltip.load(pos, TooltipTypes.skill, value, s_uuid, this.node);
Tooltip.load(pos, TooltipTypes.skill, value, s_uuid, this.node, 1, this.model?.fac ?? FacSet.MON, triggerType);
}
/** 血量提示(伤害数字) */
private hp_tip(type: number = 1, value: string = "", s_uuid: number = 1001, y: number = 0) {
@@ -454,6 +455,12 @@ export class HeroViewComp extends CCComp {
this.top_node.active = true;
this.status_change("idle");
// 【新增】仅英雄阵营派发复活成功事件供卡牌技能HeroCall 类型)监听
// 统一在此派发可覆盖两条复活路径:复活技能触发 + 关卡战斗准备阶段恢复
if (this.model && this.model.fac === FacSet.HERO && this.ent) {
oops.message.dispatchEvent(GameEvent.ReviveSuccess, { eid: this.ent.eid });
}
}

View File

@@ -278,7 +278,7 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
if(castTimes >1){
val = "*"+castTimes.toString
}
heroView.skill_name(val,s_uuid)
heroView.skill_name(val,s_uuid,triggerType)
for (let i = 0; i < castTimes; i++) {
if (!heroView.node || !heroView.node.isValid) return;
if (isFriendly) {

View File

@@ -101,10 +101,19 @@ export class SkillTriggerHelper {
*/
private static handleAtking(model: HeroAttrsComp, view: HeroViewComp) {
if (!model.atking || model.atking.length === 0) return;
let triggerCount = 1;
if (model.fac === FacSet.HERO) {
triggerCount += FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.AtkCount);
}
triggerCount = Math.max(1, Math.floor(triggerCount));
model.atking.forEach(atkConfig => {
// atk_count 代表已进行的普攻次数。当其余数刚好整除配置阈值时触发。
if (model.atk_count > 0 && model.atk_count % atkConfig.t_num === 0) {
this.dispatchSingle(atkConfig.s_uuid, model, view, SkillTriggerType.Atking, atkConfig.overrides);
for (let i = 0; i < triggerCount; i++) {
this.dispatchSingle(atkConfig.s_uuid, model, view, SkillTriggerType.Atking, atkConfig.overrides);
}
}
});
}
@@ -115,10 +124,19 @@ export class SkillTriggerHelper {
*/
private static handleAtked(model: HeroAttrsComp, view: HeroViewComp) {
if (!model.atked || model.atked.length === 0) return;
let triggerCount = 1;
if (model.fac === FacSet.HERO) {
triggerCount += FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.BeAtkCount);
}
triggerCount = Math.max(1, Math.floor(triggerCount));
model.atked.forEach(atkConfig => {
// atked_count 代表已承受的受击次数。当其余数刚好整除配置阈值时触发。
if (model.atked_count > 0 && model.atked_count % atkConfig.t_num === 0) {
this.dispatchSingle(atkConfig.s_uuid, model, view, SkillTriggerType.Atked, atkConfig.overrides);
for (let i = 0; i < triggerCount; i++) {
this.dispatchSingle(atkConfig.s_uuid, model, view, SkillTriggerType.Atked, atkConfig.overrides);
}
}
});
}

View File

@@ -20,7 +20,7 @@
* - smc.vmdata.mission_data —— 读写局内金币
*/
import { mLogger } from "../common/Logger";
import { _decorator, Animation, AnimationClip, EventTouch, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, resources, Light, UITransform, Widget } from "cc";
import { _decorator, Animation, AnimationClip, EventTouch, Input, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, input, resources, Light, UITransform, Widget } 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 { CardConfig, CardType, SpecialRefreshCardList, SpecialUpgradeCardList, CKind, CardPoolList } from "../common/config/CardSet";
@@ -62,6 +62,9 @@ export class CardComp extends CCComp {
/** 解锁态图标节点(显示时表示本槽位未锁定,可点击上锁) */
@property(Node)
unLock: Node = null!
@property(Node)
call_btn: Node = null!
/** 英雄卡信息面板(显示 AP / HP 废弃,改用直接绑定 */
// @property(Node)
// info_node=null!
@@ -132,10 +135,8 @@ export class CardComp extends CCComp {
* 防止快速切卡时旧回调错误覆盖新图标。
*/
private iconVisualToken: number = 0;
/** 是否触发了长按 */
private isLongPressed: boolean = false;
/** 长按触发时间(秒) */
private readonly LONG_PRESS_DURATION: number = 0.5;
/** 视为"点击"的最大位移阈值(像素),小于该值视为点击而非拖拽 */
private readonly tapMoveThreshold: number = 10;
// ======================== 生命周期 ========================
@@ -151,7 +152,8 @@ export class CardComp extends CCComp {
this.opacityComp.opacity = 255;
this.updateLockUI();
this.applyEmptyUI();
// call_btn 默认隐藏,仅在点击本卡时显示
this.hideCallBtn();
}
/** 组件销毁时解绑所有事件,防止残留回调 */
@@ -445,6 +447,7 @@ export class CardComp extends CCComp {
this.node.setScale(new Vec3(1, 1, 1));
this.updateLockUI();
this.applyEmptyUI();
this.hideCallBtn();
this.node.active = false;
}
@@ -458,6 +461,10 @@ export class CardComp extends CCComp {
this.node.on(NodeEventType.TOUCH_CANCEL, this.onCardTouchCancel, this);
this.Lock?.on(NodeEventType.TOUCH_END, this.onToggleLock, this);
this.unLock?.on(NodeEventType.TOUCH_END, this.onToggleLock, this);
// 召唤按钮点击 = 使用卡牌
this.call_btn?.on(NodeEventType.TOUCH_END, this.onCallBtnClick, this);
// 监听跨卡联动:其他卡牌被点击时隐藏本卡 call_btn
oops.message.on(GameEvent.CardSelected, this.onOtherCardSelected, this);
}
/** 解绑触控,防止节点销毁后残留回调 */
@@ -474,19 +481,22 @@ export class CardComp extends CCComp {
if (this.unLock && this.unLock.isValid) {
this.unLock.off(NodeEventType.TOUCH_END, this.onToggleLock, this);
}
if (this.call_btn && this.call_btn.isValid) {
this.call_btn.off(NodeEventType.TOUCH_END, this.onCallBtnClick, this);
}
oops.message.off(GameEvent.CardSelected, this.onOtherCardSelected, this);
// 兜底注销全局触摸监听showCallBtn 后组件销毁的场景)
input.off(Input.EventType.TOUCH_END, this.onGlobalTouchEnd, this);
}
// ======================== 触摸交互 ========================
/** 触摸开始:记录起点 Y,进入拖拽状态 */
/** 触摸开始:记录起点坐标,进入拖拽状态 */
private onCardTouchStart(event: EventTouch) {
if (!this.cardData || this.isUsing) return;
this.touchStartY = event.getUILocation().y;
this.touchStartX = event.getUILocation().x;
this.isDragging = true;
this.isLongPressed = false;
this.unschedule(this.onLongPress);
this.scheduleOnce(this.onLongPress, this.LONG_PRESS_DURATION);
}
/**
@@ -496,25 +506,15 @@ export class CardComp extends CCComp {
if (!this.isDragging || !this.cardData || this.isUsing) return;
const currentY = event.getUILocation().y;
const deltaY = Math.max(0, currentY - this.touchStartY);
if (deltaY > 10) {
this.unschedule(this.onLongPress);
if (this.isLongPressed) {
this.isLongPressed = false;
oops.gui.remove(UIID.HInfo);
}
}
this.node.setPosition(this.restPosition.x, this.restPosition.y + deltaY, this.restPosition.z);
}
/**
* 触摸结束:
* - 技能卡:点击即可使用
* - 英雄卡/其他:上拉距离 >= dragUseThreshold → 视为"使用卡牌"
* - 否则视为"点击"或者"长按结束"
* - 上拉距离 >= dragUseThreshold → 视为"使用卡牌"
* - 位移很小(点击)→ 触发 onCardTap显示 call_btn / 打开信息面板 / 通知其他卡牌联动
*/
private onCardTouchEnd(event: EventTouch) {
this.unschedule(this.onLongPress);
if (!this.isDragging || !this.cardData || this.isUsing) return;
const endY = event.getUILocation().y;
const endX = event.getUILocation().x;
@@ -522,11 +522,6 @@ export class CardComp extends CCComp {
const deltaX = endX - this.touchStartX;
this.isDragging = false;
if (this.isLongPressed) {
this.isLongPressed = false;
oops.gui.remove(UIID.HInfo);
}
// 英雄卡保持上划使用
if (deltaY >= this.dragUseThreshold) {
const used = this.useCard();
@@ -536,31 +531,93 @@ export class CardComp extends CCComp {
return;
}
// 位移小于阈值视为"点击"
if (Math.abs(deltaY) < this.tapMoveThreshold && Math.abs(deltaX) < this.tapMoveThreshold) {
this.onCardTap();
return;
}
this.playReboundAnim();
}
/** 触摸取消:回弹至原位 */
private onCardTouchCancel() {
this.unschedule(this.onLongPress);
if (!this.isDragging || this.isUsing) return;
this.isDragging = false;
if (this.isLongPressed) {
this.isLongPressed = false;
oops.gui.remove(UIID.HInfo);
}
this.playReboundAnim();
}
/** 长按触发:英雄卡打开英雄信息预览面板 */
private onLongPress() {
if (!this.isDragging || this.isUsing) return;
if (!this.cardData || this.card_type !== CardType.Hero) return;
this.isLongPressed = true;
const heroUuid = this.card_uuid;
const heroLv = Math.max(1, this.cardData.hero_lv ?? 1);
const poolLv = Math.max(1, this.cardData.base_pool_lv ?? this.cardData.pool_lv ?? 1);
oops.gui.remove(UIID.HInfo);
oops.gui.open(UIID.HInfo, { heroUuid, heroLv, poolLv });
/**
* 卡牌被点击(非拖拽):
* 1. 显示本卡的召唤按钮 call_btn。
* 2. 派发 CardSelected 事件,其他卡牌收到后隐藏各自的 call_btn。
* 3. 英雄卡打开 HInfo 信息面板(点击查看,替代原长按)。
*/
private onCardTap() {
this.playReboundAnim();
this.showCallBtn();
oops.message.dispatchEvent(GameEvent.CardSelected, this);
// 英雄卡打开信息面板(点击替代长按)
if (this.card_type === CardType.Hero) {
const heroUuid = this.card_uuid;
const heroLv = Math.max(1, this.cardData?.hero_lv ?? 1);
const poolLv = Math.max(1, this.cardData?.base_pool_lv ?? this.cardData?.pool_lv ?? 1);
oops.gui.remove(UIID.HInfo);
oops.gui.open(UIID.HInfo, { heroUuid, heroLv, poolLv });
}
}
/**
* 其他卡牌被点击时联动隐藏本卡 call_btn。
* @param event 事件名ListenerFunc 约定的第一个参数)
* @param source 被点击的 CardComp 实例
*/
private onOtherCardSelected(event: string, source: CardComp) {
if (source === this) return;
this.hideCallBtn();
}
/** 显示召唤按钮 */
private showCallBtn() {
if (this.call_btn && this.call_btn.isValid) {
this.call_btn.active = true;
// 按需监听全局触摸:点击本体以外区域时隐藏召唤按钮
input.on(Input.EventType.TOUCH_END, this.onGlobalTouchEnd, this);
}
}
/** 隐藏召唤按钮 */
private hideCallBtn() {
if (this.call_btn && this.call_btn.isValid) {
this.call_btn.active = false;
}
// 注销全局监听无论是否曾注册off 均安全)
input.off(Input.EventType.TOUCH_END, this.onGlobalTouchEnd, this);
}
/**
* 全局触摸结束:点击落点不在召唤按钮 / 卡牌本体包围盒内时隐藏召唤按钮。
* Why: 复用 HInfoComp 的"点击外部关闭"交互模式,避免 call_btn 长期滞留。
* 排除卡牌本体是防止与 onCardTap 的"点击卡牌显示 call_btn"交互相互冲突。
* @param event 全局触摸事件
*/
private onGlobalTouchEnd(event: EventTouch) {
if (!this.call_btn || !this.call_btn.active) return;
const uiPos = event.getUILocation();
// 命中召唤按钮本体 → 保留
const btnTrans = this.call_btn.getComponent(UITransform);
if (btnTrans && btnTrans.getBoundingBoxToWorld().contains(uiPos)) return;
// 命中卡牌本体 → 保留(由 onCardTap 自行管理显隐)
const cardTrans = this.node.getComponent(UITransform);
if (cardTrans && cardTrans.getBoundingBoxToWorld().contains(uiPos)) return;
// 其余区域 → 隐藏
this.hideCallBtn();
}
/** 召唤按钮点击回调:阻止冒泡后触发使用卡牌 */
private onCallBtnClick(event: EventTouch) {
event.propagationStopped = true;
this.useCard();
}
/**

View File

@@ -19,14 +19,14 @@
* - Hero —— 英雄 ECS 实体类(用于出售删除)
* - UIID.IBox —— 英雄详情弹窗 ID
*/
import { _decorator, Animation, AnimationClip, Button, Event, Label, Node, NodeEventType, Sprite, resources, CCInteger, SpriteFrame } from "cc";
import { _decorator, Animation, AnimationClip, Button, Event, EventTouch, Input, Label, Node, NodeEventType, Sprite, UITransform, input, resources, CCInteger, SpriteFrame } 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 { HeroInfo } from "../common/config/heroSet";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { smc } from "../common/SingletonModuleComp";
import { Hero } from "../hero/Hero";
import { FieldSkillType } from "../common/config/SkillSet";
import { FieldSkillType, FieldSkillSet } from "../common/config/SkillSet";
import { buildSkillDesc } from "../common/config/HeroSkillDesc";
import { GameEvent } from "../common/config/GameEvent";
import { oops } from "db://oops-framework/core/Oops";
@@ -35,7 +35,7 @@ import { mLogger } from "../common/Logger";
import { MissionHeroComp } from "./MissionHeroComp";
import { MoveComp } from "../hero/MoveComp";
import { FacSet, getLvColor } from "../common/config/GameSet";
import { CKind, CardPoolList } from "../common/config/CardSet";
import { CKind, CardPoolList, CardConfig, CardTriggerType } from "../common/config/CardSet";
import { SkillSet } from "../common/config/SkillSet";
import { CardBgComp } from "./CardBgComp";
import { MissionEconomy } from "./MissionEconomy";
@@ -54,6 +54,9 @@ export class HInfoComp extends CCComp {
/** 英雄 idle 动画图标节点 */
@property(Node)
icon_node=null!
/** 技能图标节点 */
@property(Node)
skill_icon_node=null!
/** 出售按钮节点 */
@property(Node)
sell_node=null!
@@ -153,9 +156,15 @@ export class HInfoComp extends CCComp {
const heroUuid = this.previewUuid;
const heroLv = Math.max(1, this.previewLv);
// 技能卡无等级概念,隐藏等级节点;英雄卡显示等级
if (this.lv_node) {
this.lv_node.string = `Lv.${heroLv}`;
this.lv_node.color = getLvColor(heroLv);
if (this.isSkillCard) {
this.lv_node.node.active = false;
} else {
this.lv_node.node.active = true;
this.lv_node.string = `Lv.${heroLv}`;
this.lv_node.color = getLvColor(heroLv);
}
}
const kindName = this.isSkillCard ? CKind[CKind.Skill] : CKind[CKind.Hero];
@@ -169,6 +178,10 @@ export class HInfoComp extends CCComp {
if (this.isSkillCard) {
// ================= 技能卡预览 =================
// 互斥:技能卡只显示技能图标,隐藏英雄图标
if (this.icon_node) this.icon_node.active = false;
if (this.skill_icon_node) this.skill_icon_node.active = true;
const config = CardPoolList.find(c => c.uuid === heroUuid);
if (!config) return;
@@ -192,10 +205,14 @@ export class HInfoComp extends CCComp {
if (heroUuid !== this.iconHeroUuid) {
this.iconHeroUuid = heroUuid;
this.iconVisualToken += 1;
this.updateSkillAnimation(this.icon_node, config.skill ?? heroUuid, this.iconVisualToken);
this.updateSkillAnimation(this.skill_icon_node, config, this.iconVisualToken);
}
} else {
// ================= 英雄卡预览 =================
// 互斥:英雄卡只显示英雄图标,隐藏技能图标
if (this.icon_node) this.icon_node.active = true;
if (this.skill_icon_node) this.skill_icon_node.active = false;
const hero = HeroInfo[heroUuid];
if (!hero) return;
@@ -461,31 +478,35 @@ export class HInfoComp extends CCComp {
/**
* 为技能图标加载静态图片。
* @param node 图标节点
* @param skillUuid 技能 UUID
* @param token 视觉令牌
* 图标来源优先级config.icon > 按 trigger_type 自动取Interval→SkillSetField→FieldSkillSet
* @param node 图标节点
* @param config 卡牌配置
* @param token 视觉令牌
*/
private updateSkillAnimation(node: Node, skillUuid: number, token: number) {
private updateSkillAnimation(node: Node, config: CardConfig, token: number) {
if (!node) return;
this.clearIconAnimation(node); // 停止之前的动画
this.clearIconAnimation(node);
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
if (!sprite) return;
const skillData = SkillSet[skillUuid];
if (!skillData || !skillData.icon) {
sprite.spriteFrame = null;
return;
}
sprite.spriteFrame = null;
if (smc.uiconsAtlas) {
const frame = smc.uiconsAtlas.getSpriteFrame(skillData.icon);
if (frame && token === this.iconVisualToken) {
sprite.spriteFrame = frame;
// 优先使用卡牌自定义 icon未定义则按 trigger_type 自动取
let iconId: string | undefined = config.icon;
if (!iconId) {
if (config.trigger_type === CardTriggerType.Interval) {
iconId = config.skill ? SkillSet[config.skill]?.icon : undefined;
} else if (config.trigger_type === CardTriggerType.Field) {
const fieldUuid = config.field?.[0];
iconId = fieldUuid ? FieldSkillSet[fieldUuid]?.icon : undefined;
}
} else {
const sf = oops.res.get("game/heros/cards/" + skillData.icon, SpriteFrame) as SpriteFrame;
if (sf && token === this.iconVisualToken) {
sprite.spriteFrame = sf;
}
if (!iconId) return;
// 与 CardComp / SCardComp 一致:仅信任全局缓存的 uiconsAtlas
if (smc.uiconsAtlas) {
const frame = smc.uiconsAtlas.getSpriteFrame(iconId);
if (token === this.iconVisualToken) {
sprite.spriteFrame = frame || null;
}
}
}
@@ -511,6 +532,8 @@ export class HInfoComp extends CCComp {
this.sell_node?.on(Button.EventType.CLICK, this.onSellHero, this);
this.close_node?.on(Button.EventType.CLICK, this.onClosePanel, this);
// this.node.on(NodeEventType.TOUCH_END, this.onOpenIBox, this);
// 监听全局触摸结束:点击本体节点以外区域时关闭面板
input.on(Input.EventType.TOUCH_END, this.onGlobalTouchEnd, this);
}
private unbindEvents() {
@@ -523,6 +546,28 @@ export class HInfoComp extends CCComp {
// if (this.node && this.node.isValid) {
// this.node.off(NodeEventType.TOUCH_END, this.onOpenIBox, this);
// }
input.off(Input.EventType.TOUCH_END, this.onGlobalTouchEnd, this);
}
/**
* 全局触摸结束处理:点击落点不在本体节点包围盒内时关闭面板。
* Why: 弹窗常见的"点击空白处关闭"交互,用全局 input + 包围盒命中检测实现,
* 无需额外遮罩节点,也不会误关面板内部(含子按钮)的点击。
* @param event 全局触摸事件
*/
private onGlobalTouchEnd(event: EventTouch) {
if (this.isClosing) return;
const transform = this.node.getComponent(UITransform);
if (!transform) {
// 缺少 UITransform 时保守处理为关闭
this.onClosePanel();
return;
}
const worldRect = transform.getBoundingBoxToWorld();
const uiPos = event.getUILocation();
if (!worldRect.contains(uiPos)) {
this.onClosePanel();
}
}
/**

View File

@@ -63,16 +63,16 @@ export class MissSkillsComp extends CCComp {
* 第 2 行 y=320x = -320, -240, -160, -80, 0
*/
private slots: SkillBoxSlot[] = [
{ x: -320, y: 200, used: false, node: null },
{ x: -240, y: 200, used: false, node: null },
{ x: -160, y: 200, used: false, node: null },
{ x: -80, y: 200, used: false, node: null },
{ x: 0, y: 200, used: false, node: null },
{ x: -320, y: 300, used: false, node: null },
{ x: -240, y: 300, used: false, node: null },
{ x: -160, y: 300, used: false, node: null },
{ x: -80, y: 300, used: false, node: null },
{ x: 0, y: 300, used: false, node: null },
{ x: -310, y: 200, used: false, node: null },
{ x: -220, y: 200, used: false, node: null },
{ x: -130, y: 200, used: false, node: null },
{ x: -40, y: 200, used: false, node: null },
{ x: 50, y: 200, used: false, node: null },
{ x: -310, y: 300, used: false, node: null },
{ x: -220, y: 300, used: false, node: null },
{ x: -130, y: 300, used: false, node: null },
{ x: -40, y: 300, used: false, node: null },
{ x: 50, y: 300, used: false, node: null },
];
/** 注册事件监听 */

View File

@@ -45,7 +45,7 @@ import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { smc } from "../common/SingletonModuleComp";
import { HeroInfo, HType } from "../common/config/heroSet";
import { HeroViewComp } from "../hero/HeroViewComp";
import { FacSet, FightSet } from "../common/config/GameSet";
import { FacSet, FightSet, CARD_POOL_UPGRADE_WAVES, SKILL_CARD_WAVES } from "../common/config/GameSet";
import { MoveComp } from "../hero/MoveComp";
import { MissionHeroComp } from "./MissionHeroComp";
import { MissionEconomy } from "./MissionEconomy";
@@ -235,7 +235,7 @@ export class MissionCardComp extends CCComp {
this.dispatchCardsToSlots(cards);
const wave = this.getCurrentWave();
if ([1, 5, 10, 15, 20].includes(wave)) {
if (SKILL_CARD_WAVES.includes(wave)) {
this.showSkillCardPopup();
}
@@ -426,7 +426,7 @@ export class MissionCardComp extends CCComp {
this.dispatchCardsToSlots(cards);
const wave = this.getCurrentWave();
if ([1, 5, 10, 15, 20].includes(wave)) {
if (SKILL_CARD_WAVES.includes(wave)) {
this.showSkillCardPopup();
}
}
@@ -505,6 +505,10 @@ export class MissionCardComp extends CCComp {
if (!smc.finish_guides.includes(3)) {
oops.gui.open(UIID.Guide3);
}
// 驻场技能可能影响刷新费用(如"刷新优惠"),延迟到下一帧刷新费用 UI
// 确保 MissSkillsComp 已创建 SkillBoxComp 并注册驻场效果
this.scheduleOnce(() => this.updateCoinAndCostUI(), 0);
}
/** 解除按钮监听,避免节点销毁后回调泄漏 */
@@ -906,7 +910,8 @@ export class MissionCardComp extends CCComp {
const cards = drawCardsByRule(this.poolLv, {
count: 3,
type: targetType,
wave: currentWave
wave: currentWave,
unique: true // 保证技能牌不重复
});
if (cards.length >= 3) return cards.slice(0, 3);
@@ -915,10 +920,18 @@ export class MissionCardComp extends CCComp {
const fallback = drawCardsByRule(this.poolLv, {
count: 3,
type: targetType,
wave: currentWave
wave: currentWave,
unique: true
});
if (fallback.length === 0) break;
filled.push(fallback[filled.length % fallback.length]);
// 如果池子数量不足,只能被迫允许重复,但尽量拿没被抽到的
const fPick = fallback.find(c => !filled.some(fc => fc.uuid === c.uuid));
if (fPick) {
filled.push(fPick);
} else {
filled.push(fallback[filled.length % fallback.length]);
}
}
return filled;
}
@@ -1041,26 +1054,35 @@ export class MissionCardComp extends CCComp {
if (this.poolLv >= CARD_POOL_MAX_LEVEL) {
nextLabel.string = `已满级`;
} else {
let upgradeWaves: number[] = [5, 10];
// 优先取 MissionComp 运行时配置,缺失时回退到全局常量
let upgradeWaves: number[] = CARD_POOL_UPGRADE_WAVES;
ecs.query(ecs.allOf(MissionComp)).forEach((entity) => {
const mission = entity.get(MissionComp);
if (mission && mission.cardPoolUpgradeWaves) {
if (mission && mission.cardPoolUpgradeWaves && mission.cardPoolUpgradeWaves.length > 0) {
upgradeWaves = mission.cardPoolUpgradeWaves;
}
});
// 已完成的升级次数 = 当前等级 - 初始等级
// 例poolLv=2INIT=1→ 已升 1 次 → 下一升级对应 upgradeWaves[1]
const upgradedCount = Math.max(0, Math.floor(this.poolLv) - CARD_POOL_INIT_LEVEL);
const currentWave = this.getCurrentWave();
let nextWave = -1;
for (let i = 0; i < upgradeWaves.length; i++) {
if (upgradeWaves[i] > currentWave) {
nextWave = upgradeWaves[i];
break;
}
}
if (nextWave !== -1) {
const remain = nextWave - currentWave;
nextLabel.string = `${remain} 回合后升级`;
} else {
if (upgradedCount >= upgradeWaves.length) {
// 配置已耗尽但等级未到上限(配置缺陷)
nextLabel.string = `已满级`;
} else {
const nextWave = upgradeWaves[upgradedCount];
if (nextWave > currentWave) {
const remain = nextWave - currentWave;
nextLabel.string = `${remain} 回合后升级`;
} else if (nextWave === currentWave) {
// 当前波次正好是升级波次(事件可能即将触发或刚刚触发)
nextLabel.string = `本回合升级`;
} else {
// nextWave < currentWave异常状态升级事件未按时触发
nextLabel.string = `即将升级`;
}
}
}
}

View File

@@ -39,13 +39,12 @@ import { HeroViewComp } from "../hero/HeroViewComp";
import { SkillTriggerHelper } from "../hero/SkillTriggerHelper";
import { UIID } from "../common/config/GameUIConfig";
import { SkillView } from "../skill/SkillView";
import { FacSet, FightSet } from "../common/config/GameSet";
import { FacSet, FightSet, CARD_POOL_UPGRADE_WAVES } 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";
import { Tooltip } from "../skill/Tooltip";
import { CardInitCoins } from "../common/config/CardSet";
import { Timer } from "db://oops-framework/core/common/timer/Timer";
import { FieldSkillType } from "../common/config/SkillSet";
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
@@ -88,9 +87,8 @@ export class MissionComp extends CCComp {
private maxMonsterCount: number = 80;
/** 怪物数量恢复阈值(降至此值以下恢复刷怪) */
private resumeMonsterCount: number = 45;
/** 卡池升级波次配置:达到对应波次时,推送卡池升级事件 */
@property({ type: [CCInteger], tooltip: "卡池升级波次配置,例如 [10, 20] 表示第10波升到2级第20波升到3级" })
cardPoolUpgradeWaves: number[] = [5, 10];
/** 卡池升级波次配置(默认值来自 GameSet.CARD_POOL_UPGRADE_WAVES保持全局统一 */
public cardPoolUpgradeWaves: number[] = CARD_POOL_UPGRADE_WAVES;
// ======================== 编辑器绑定节点 ========================
@@ -198,10 +196,12 @@ export class MissionComp extends CCComp {
this.mission_start();
}, 0);
smc.map.MapView.scene.mapLayer.stopAnimations();
smc.map.MapView.scene.mapLayer.node.getChildByName("fight").getChildByName("fbox").active = true;
}
onDestroy() {
smc.map.MapView.scene.mapLayer.playAnimations()
smc.map.MapView.scene.mapLayer.node.getChildByName("fight").getChildByName("fbox").active = false;
super.onDestroy();
if (this.start_btn && this.start_btn.isValid) {
this.start_btn.off(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this);
@@ -495,6 +495,9 @@ export class MissionComp extends CCComp {
// 战斗结束阶段给予所有英雄恢复70%血量的技能效果
this.healAllHeroes();
// 【新增】派发每波战斗结束事件,供卡牌技能监听(区别于整局结束的 MissionEnd
oops.message.dispatchEvent(GameEvent.FightEnd);
break;
case MissionPhase.Settle:
@@ -732,7 +735,7 @@ export class MissionComp extends CCComp {
// 重置所有的战局得分数据,防止上一局的数据污染
smc.resetScores();
smc.vmdata.mission_data.coin = Math.max(0, Math.floor(CardInitCoins));
smc.vmdata.mission_data.coin = Math.max(0, Math.floor(FightSet.INIT_COIN));
// 【评分系统 - 效率分】记录初始获得的金币收入
smc.vmdata.scores.gold_earned += smc.vmdata.mission_data.coin;
}
@@ -853,11 +856,6 @@ export class MissionComp extends CCComp {
// 怪物全灭检测:如果战斗阶段场上没有任何活着的怪物,且待刷新的怪物队列也为空,直接结束战斗进入下一波的准备阶段
const pendingCount = smc.vmdata.mission_data.pending_mon_num || 0;
if (monsterCount === 0 && pendingCount === 0 && smc.mission.play && !smc.mission.pause && this.currentPhase === MissionPhase.Battle) {
let heroesAliveRatio = heroCount / 6.0; // 假设最大 6 个站位,或者直接基于存活数算比例
// 如果能获取当前已部署英雄数最好,这里简化处理,大于 4 个就算高存活
heroesAliveRatio = Math.min(1.0, heroCount / 4.0);
spawningEngine.updateAdaptive(heroesAliveRatio, this.clearTime);
if (this.currentWave >= 15) {
// 15 波通关
this.open_Victory(null, false);

View File

@@ -55,12 +55,12 @@ export class MissionHeroComp extends CCComp {
/** 硬编码的6个英雄占位点 */
public static readonly HERO_POSITIONS: Vec3[] = [
v3(-210, BoxSet.GAME_LINE + 90, 0), // index 0 (node_index 1): Top Front
v3(-160, BoxSet.GAME_LINE, 0), // index 1 (node_index 2): Mid Front
v3(-210, BoxSet.GAME_LINE - 90, 0), // index 2 (node_index 3): Bot Front
v3(-300, BoxSet.GAME_LINE + 90, 0), // index 3 (node_index 4): Top Back
v3(-300, BoxSet.GAME_LINE, 0), // index 4 (node_index 5): Mid Back
v3(-300, BoxSet.GAME_LINE - 90, 0), // index 5 (node_index 6): Bot Back
v3(-175, BoxSet.GAME_LINE + 100, 0), // index 0 (node_index 1): Top Front
v3(-170, BoxSet.GAME_LINE, 0), // index 1 (node_index 2): Mid Front
v3(-175, BoxSet.GAME_LINE - 100, 0), // index 2 (node_index 3): Bot Front
v3(-280, BoxSet.GAME_LINE + 100, 0), // index 3 (node_index 4): Top Back
v3(-280, BoxSet.GAME_LINE, 0), // index 4 (node_index 5): Mid Back
v3(-280, BoxSet.GAME_LINE - 100, 0), // index 5 (node_index 6): Bot Back
];
/** 英雄出生时的掉落高度(从空中落到地面的像素差) */

View File

@@ -3,27 +3,13 @@
* @description 怪物Monster波次刷新管理组件逻辑层
*
* 职责:
* 1. 管理每一波怪物的 **生成计划**:根据 WaveSlotConfig 生成怪物。
* 2. 处理特殊插队刷怪请求MonQueue优先于常规刷新
* 3. 自动推进波次:当前波所有怪物被清除后自动进入下一波。
* 1. 管理每一波怪物的生成计划:根据 RogueConfig 生成怪物。
* 2. 自动推进波次在准备阶段结束时PhasePrepareEnd统一刷出怪物
*
* 关键设计:
* - 突破 5 槽限制,怪物按刷怪顺序依次从 X=280 开始向右(每隔 50排布。
* - 6 条刷怪线:在三路 Y 轴范围内随机偏移,实现 6 路进军
* - resetSlotSpawnData(wave) 在每波开始时读取配置,分配并立即生成所有怪物
* - 去除跨波 HP 继承,上一波残留怪在波次结束/开始时销毁。
*
* 怪物属性计算公式:
* ap = floor((base_ap + stage × grow_ap) × SpawnPowerBias)
* hp = floor((base_hp + stage × grow_hp) × SpawnPowerBias)
* 其中 stage = currentWave - 1
*
* 依赖:
* - RogueConfig —— 怪物类型、成长值、波次配置
* - Monsterhero/Mon.ts—— 怪物 ECS 实体类
* - HeroInfoheroSet—— 怪物基础属性配置(与英雄共用配置)
* - HeroAttrsComp / MonMoveComp —— 怪物属性和移动组件
* - BoxSet.GAME_LINE —— 地面基准 Y 坐标
* - 采用 12 个硬编码的网格位置点 (MON_POSITIONS3行x4列)
* - 每次生成最多 12 个怪物,固定在位置点
* - 上一波残留怪在波次结束/开始时统一清理
*/
import { _decorator, v3, Vec3 } from "cc";
import { mLogger } from "../common/Logger";
@@ -31,21 +17,15 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { Monster } from "../hero/Mon";
import { HeroInfo, HType } from "../common/config/heroSet";
import { smc } from "../common/SingletonModuleComp";
import { GameEvent } from "../common/config/GameEvent";
import {BoxSet, FacSet } from "../common/config/GameSet";
import { spawningEngine, GeneratedMonster, AffixType, MonType, MonList, TestModeConfig } from "./RogueConfig";
import { BoxSet, FacSet } from "../common/config/GameSet";
import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { MonMoveComp } from "../hero/MonMoveComp";
const { ccclass, property } = _decorator;
/**
* MissionMonCompComp —— 怪物波次刷新管理器
*
* 每波开始时根据 WaveSlotConfig 配置生成怪物,
* 战斗中监控数量,所有怪物消灭后自动推进到下一波。
*/
@ccclass('MissionMonCompComp')
@ecs.register('MissionMonComp', false)
export class MissionMonCompComp extends CCComp {
@@ -59,21 +39,21 @@ export class MissionMonCompComp extends CCComp {
/** 硬编码的 12 个怪物占位点 (3行4列) */
public static readonly MON_POSITIONS: Vec3[] = [
// 第 1 列 (X=60)
v3(60, BoxSet.GAME_LINE + 90, 0), // index 0: Top
v3(60, BoxSet.GAME_LINE, 0), // index 1: Mid
v3(60, BoxSet.GAME_LINE - 90, 0), // index 2: Bot
v3(0, BoxSet.GAME_LINE + 100, 0), // index 0: Top
v3(0, BoxSet.GAME_LINE, 0), // index 1: Mid
v3(0, BoxSet.GAME_LINE - 100, 0), // index 2: Bot
// 第 2 列 (X=140)
v3(140, BoxSet.GAME_LINE + 90, 0), // index 3: Top
v3(140, BoxSet.GAME_LINE, 0), // index 4: Mid
v3(140, BoxSet.GAME_LINE - 90, 0), // index 5: Bot
v3(90, BoxSet.GAME_LINE + 100, 0), // index 3: Top
v3(90, BoxSet.GAME_LINE, 0), // index 4: Mid
v3(90, BoxSet.GAME_LINE - 100, 0), // index 5: Bot
// 第 3 列 (X=220)
v3(220, BoxSet.GAME_LINE + 90, 0), // index 6: Top
v3(220, BoxSet.GAME_LINE, 0), // index 7: Mid
v3(220, BoxSet.GAME_LINE - 90, 0), // index 8: Bot
v3(180, BoxSet.GAME_LINE + 100, 0), // index 6: Top
v3(180, BoxSet.GAME_LINE, 0), // index 7: Mid
v3(180, BoxSet.GAME_LINE - 100, 0), // index 8: Bot
// 第 4 列 (X=300)
v3(300, BoxSet.GAME_LINE + 90, 0), // index 9: Top
v3(300, BoxSet.GAME_LINE, 0), // index 10: Mid
v3(300, BoxSet.GAME_LINE - 90, 0), // index 11: Bot
v3(270, BoxSet.GAME_LINE + 100, 0), // index 9: Top
v3(270, BoxSet.GAME_LINE, 0), // index 10: Mid
v3(270, BoxSet.GAME_LINE - 100, 0), // index 11: Bot
];
// ======================== 编辑器属性 ========================
@@ -81,83 +61,32 @@ export class MissionMonCompComp extends CCComp {
@property({ tooltip: "是否启用调试日志" })
private debugMode: boolean = false;
// ======================== 插队刷怪队列 ========================
/**
* 刷怪队列(优先于常规配置处理):
* 用于插队生成(如运营活动怪、技能召唤怪、剧情强制怪)。
*/
private MonQueue: Array<{
/** 怪物 UUID */
uuid: number,
/** 怪物等级 */
level: number,
/** 飞行层 */
flyLane: number,
}> = [];
// ======================== 运行时状态 ========================
/** 全局生成顺序计数器(用于渲染层级排序) */
private globalSpawnOrder: number = 0;
/** 插队刷怪处理计时器 */
private queueTimer: number = 0;
/** 当前波数 */
private currentWave: number = 0;
/** 当前波的目标怪物总数 */
private waveTargetCount: number = 0;
/** 当前波已生成的怪物数量 */
private waveSpawnedCount: number = 0;
/** 等待生成的怪物队列(由新肉鸽引擎提供) */
/** 等待生成的怪物队列 */
private pendingMonsters: GeneratedMonster[] = [];
// ======================== 生命周期 ========================
onLoad(){
this.on(GameEvent.FightReady,this.fight_ready,this)
this.on("SpawnSpecialMonster", this.onSpawnSpecialMonster, this);
onLoad() {
this.on(GameEvent.FightReady, this.fight_ready, this);
this.on("PhasePrepareEnd", this.onPhasePrepareEnd, this);
this.on("TimeUpAdvanceWave", this.onTimeUpAdvanceWave, this);
}
/**
* 帧更新:
* 1. 检查游戏是否运行中。
* 2. 处理插队刷怪队列。
* 3. 逐步从 pendingMonsters 队列中生成怪物(受 stop_spawn_mon 限制)。
*/
protected update(dt: number): void {
smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length;
if(!smc.mission.play) return
if(smc.mission.pause) return
if(smc.mission.stop_mon_action) return;
if(!smc.mission.in_fight) return;
this.updateSpecialQueue(dt);
}
// ======================== 事件处理 ========================
/**
* 接收特殊刷怪事件并入队。
* @param event 事件名
* @param args { uuid: number, level: number, flyLane?: number }
*/
private onSpawnSpecialMonster(event: string, args: any) {
if (!args) return;
mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 收到特殊刷怪指令:`, args);
this.MonQueue.push({
uuid: args.uuid,
level: args.level,
flyLane: args.flyLane || 0
});
// 加速队列消费
this.queueTimer = 1.0;
}
start() {
}
start() {}
private setupWaveData(monsters: GeneratedMonster[]) {
this.pendingMonsters = monsters.slice(0, MissionMonCompComp.MAX_MONSTERS);
@@ -166,9 +95,7 @@ export class MissionMonCompComp extends CCComp {
let hasBoss = monsters.some(m => m.isBoss);
console.log(`[MissionMonComp] 波次 ${this.currentWave} 生成怪物总数: ${this.waveTargetCount}`);
const uuids = monsters.map(m => m.uuid);
console.log(`[MissionMonComp] 波次 ${this.currentWave} 怪物 UUID 列表:`, uuids);
mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 波次 ${this.currentWave} 生成怪物总数: ${this.waveTargetCount}`);
oops.message.dispatchEvent(GameEvent.NewWave, {
wave: this.currentWave,
@@ -180,81 +107,41 @@ export class MissionMonCompComp extends CCComp {
/**
* 战斗准备:重置所有运行时状态并开始第一波。
*/
fight_ready(){
smc.vmdata.mission_data.mon_num=0
smc.mission.stop_spawn_mon = false
this.globalSpawnOrder = 0
this.queueTimer = 0
this.currentWave = 1
this.waveTargetCount = 0
this.waveSpawnedCount = 0
this.MonQueue = []
this.pendingMonsters = []
fight_ready() {
smc.vmdata.mission_data.mon_num = 0;
smc.mission.stop_spawn_mon = false;
this.globalSpawnOrder = 0;
this.currentWave = 1;
this.waveTargetCount = 0;
this.waveSpawnedCount = 0;
this.pendingMonsters = [];
// 预生成第一波数据以获取数量和 Boss 信息
const monsters = spawningEngine.generateWave(this.currentWave);
this.setupWaveData(monsters);
// 如果处于测试模式,英雄也需要限制为只产出一个,这部分通知可以配合使用
if (TestModeConfig.enable) {
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] 测试模式已开启每波仅生成1只基准怪物");
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] 测试模式已开启");
}
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System");
}
// ======================== 插队刷怪 ========================
/**
* 处理插队刷怪队列(每 0.15 秒尝试消费一个):
* 1. 找到后从队列中移除并生成怪物。
*/
private updateSpecialQueue(dt: number) {
if (this.MonQueue.length <= 0) return;
this.queueTimer += dt;
if (this.queueTimer < 0.15) return;
const item = this.MonQueue.shift()!;
this.queueTimer = 0;
const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) ||
MonList[MonType.LongBoss].includes(item.uuid);
const spawnIndex = this.waveSpawnedCount++;
const targetPosIndex = spawnIndex % MissionMonCompComp.MAX_MONSTERS;
// 构造一个模拟的 GeneratedMonster 数据传递给 addMonsterAtGrid
const base = HeroInfo[item.uuid];
const monData: GeneratedMonster = {
uuid: item.uuid,
type: MonType.Melee, // 简化的兜底,真实逻辑依赖 heroSet 配置
hp: base ? base.hp : 100,
ap: base ? base.ap : 10,
affixes: [],
isBoss: isBoss,
spawnIndex: 0
};
this.addMonsterAtGrid(targetPosIndex, monData, item.level);
}
// ======================== 波次管理 ========================
/**
* 开始下一波:
* 1. 波数 +1 并更新全局数据。
* 2. 分发 NewWave 事件(实际的生成在 resetSlotSpawnData 中触发)。
* 开始下一波:波数 +1 并预生成数据
*/
private onTimeUpAdvanceWave() {
this.currentWave += 1;
smc.vmdata.mission_data.level = this.currentWave;
// 预生成新一波数据以获取数量和 Boss 信息
const monsters = spawningEngine.generateWave(this.currentWave);
this.setupWaveData(monsters);
}
private onPhasePrepareEnd() {
this.resetSlotSpawnData(this.currentWave);
this.resetSlotSpawnData();
// 准备结束阶段,立即刷出本波所有怪物
if (this.pendingMonsters.length > 0) {
@@ -262,11 +149,9 @@ export class MissionMonCompComp extends CCComp {
for (let i = 0; i < count; i++) {
const monData = this.pendingMonsters.shift()!;
const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MAX_MONSTERS;
console.log(`[MissionMonComp] [PhasePrepareEnd] 准备生成怪物 UUID=${monData.uuid}, 当前已生成数量=${this.waveSpawnedCount}`);
this.addMonsterAtGrid(targetPosIndex, monData);
this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave);
this.waveSpawnedCount++;
}
// 生成完毕后清空 pendingMonsters
this.pendingMonsters = [];
}
}
@@ -274,14 +159,9 @@ export class MissionMonCompComp extends CCComp {
// ======================== 槽位管理 ========================
/**
* 重新分配本波所有怪物状态:
* 1. 清理上一波残留怪物。
* 2. pendingMonsters 已在 onTimeUpAdvanceWave / fight_ready 中准备好。
*
* @param wave 当前波数
* 清理上一波残留怪物,并重置生成计数
*/
private resetSlotSpawnData(wave: number = 1) {
// 1. 清理上一波残留怪物
private resetSlotSpawnData() {
ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => {
const attrs = e.get(HeroAttrsComp);
if (attrs && attrs.fac === FacSet.MON && !attrs.is_dead) {
@@ -289,18 +169,13 @@ export class MissionMonCompComp extends CCComp {
}
});
// 2. 重置排号索引
this.waveSpawnedCount = 0;
}
// ======================== 怪物生成 ========================
/**
* 在指定位置索引处生成一个怪物
*
* @param posIndex 位置索引 (0-11)
* @param monData 新引擎生成的怪物数据 (含 uuid, hp, ap, affixes 等)
* @param monLv 怪物等级 (仅对旧有的 level 参数做兼容,实际属性由 monData 决定)
* 在指定位置索引处生成一个怪物
*/
private addMonsterAtGrid(
posIndex: number,
@@ -310,51 +185,32 @@ export class MissionMonCompComp extends CCComp {
let mon = ecs.getEntity<Monster>(Monster);
let scale = -1;
// 获取硬编码的占位点坐标,不再使用随机偏移
const basePos = MissionMonCompComp.MON_POSITIONS[posIndex % MissionMonCompComp.MON_POSITIONS.length];
const spawnX = basePos.x;
const landingY = basePos.y + (monData.isBoss ? 6 : 0);
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
// 如果存在测试技能覆盖,则传递下去(修改 mon.load 逻辑或者通过预存)
// 为了避免侵入 Mon.ts 的原有逻辑,我们先预存
(mon as any)._testSkills = monData.testSkills;
if (monData.testSkills) {
(mon as any)._testSkills = monData.testSkills;
}
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, posIndex);
// 设置渲染排序
const move = mon.get(MonMoveComp);
if (move) {
move.spawnOrder = this.globalSpawnOrder;
}
// 应用新引擎计算好的最终属性和词缀
// 应用新引擎计算好的最终属性
const model = mon.get(HeroAttrsComp);
if (!model) return;
model.ap = monData.ap;
model.hp_max = monData.hp;
model.hp = model.hp_max;
// 将词缀记录到属性组件上,供战斗层使用
(model as any).affixes = monData.affixes || [];
// 解析特定的抗性词缀
if (monData.affixes) {
if (monData.affixes.includes(AffixType.CritRes)) {
model.critical_res = 50;
}
if (monData.affixes.includes(AffixType.FreezeRes)) {
model.freeze_res = 50;
}
if (monData.affixes.includes(AffixType.KnockbackRes)) {
model.knockback_res = 50;
}
if (model) {
model.ap = monData.ap;
model.hp_max = monData.hp;
model.hp = model.hp_max;
}
}
/** ECS 组件移除时触发(当前不销毁节点) */
reset() {
// this.node.destroy();
}
/** ECS 组件移除时触发 */
reset() {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { CardConfig, CardType, CKind, CardPoolList } from "../common/config/CardSet";
import { CardBgComp } from "./CardBgComp";
import { SkillSet } from "../common/config/SkillSet";
import { FieldSkillSet, SkillSet } from "../common/config/SkillSet";
import { GameEvent } from "../common/config/GameEvent";
import { oops } from "db://oops-framework/core/Oops";
import { smc } from "../common/SingletonModuleComp";
@@ -258,7 +258,22 @@ export class SCardComp extends CCComp {
const spSuffix = card_lv >= 2 ? "★".repeat(card_lv - 1) : "";
this.setLabel(this.name_node, `${spSuffix}${skillCard?.name || skill?.name || ""}${spSuffix}`);
if (this.info_node) this.info_node.active = true;
if (this.info_node) {
this.info_node.active = true;
// 驻场技能卡描述取 FieldSkillSet其他卡牌取卡牌/技能配置
let desc = "";
if (this.cardData.field && this.cardData.field.length > 0) {
desc = FieldSkillSet[this.cardData.field[0]]?.info || "";
}
if (!desc) {
desc = skillCard?.info || skill?.info || this.cardData?.info || "";
}
// 与 HlistComp 保持一致:优先查找名为 "info" 的子节点上的 Label
const infoLabel = this.info_node.getChildByName("info")?.getComponent(Label)
|| this.info_node.getComponent(Label)
|| this.info_node.getComponentInChildren(Label);
if (infoLabel) infoLabel.string = desc;
}
if (this.cost_node) {
this.cost_node.active = true;
@@ -272,7 +287,14 @@ export class SCardComp extends CCComp {
if (iconNode) {
iconNode.setScale(new Vec3(1, 1, 1));
this.clearIconAnimation(iconNode);
const iconId = skill?.icon || `${s_uuid}`;
// 驻场技能卡(skill=undefined 但有 field)使用 FieldSkillSet 中的图标
let iconId: string;
if (!this.cardData.skill && this.cardData.field && this.cardData.field.length > 0) {
const fieldUuid = this.cardData.field[0];
iconId = FieldSkillSet[fieldUuid]?.icon || `${fieldUuid}`;
} else {
iconId = skill?.icon || `${s_uuid}`;
}
this.updateIcon(iconNode, iconId);
}
}

View File

@@ -4,23 +4,27 @@
*
* 职责:
* 1. 表示一张已使用的技能卡在战场上的 **可视化实体**。
* 2. 管理技能的 **触发逻辑**:即时触发 vs 定时触发(战斗中按间隔触发)。
* 2. 按 trigger_type 类型化分发触发逻辑(即时 / 定时 / 驻场 / 事件型)。
* 3. 显示技能图标和剩余触发次数。
* 4. 触发结束后自动销毁。
*
* 关键设计
* - is_instant=true即时技能init 时立即触发一次,播放后延迟销毁。
* - is_instant=false持续技能战斗中每隔 trigger_interval 秒触发一次,
* 共触发 trigger_times 次后销毁。
* - 新一波NewWave时如果持续技能的次数已用完则销毁。
* - 销毁时通过 GameEvent.RemoveSkillBox 通知 MissSkillsComp 回收槽位。
* 触发类型CardTriggerType
* - Instant (1)init 时立即触发一次(按 t_times 控制次数,跨波次 NewWave 时再次触发)
* - Interval (2):监听 FightStart → update 帧驱动按 t_inv 间隔重复触发(按 t_times 控制每波次数)
* - Field (3):被动生效,不主动施法(实际由 FieldSkillSet 处理)
* - FightStart (4):监听 FightStart 事件,按 trigger_limit 全局累计上限
* - FightEnd (5):监听 FightEnd 事件(每波结束派发),按 trigger_limit 全局累计上限
* - HeroDead (6):监听 HeroDead 事件(仅英雄阵营派发,怪物死亡不触发)
* - HeroCall (7):监听 MasterCalled主角/技能召唤)+ ReviveSuccess复活
*
* 触发技能的方式
* - 通过 GameEvent.TriggerSkill 事件,将技能 UUID、卡牌等级、
* 触发位置等信息分发给技能系统。
* 关键设计
* - 事件型4-7统一走 onEventTrigger 入口,仅作触发信号,不读取 payload
* - 触发上限Instant/Interval 按 t_times每波内事件型按 trigger_limit全局
* - 跨波次keep_waves 控制存活;事件型 trigger_count 不随波次重置
* - 销毁时通过 GameEvent.RemoveSkillBox 通知 MissSkillsComp 回收槽位
*
* 依赖:
* - CardPoolListCardSet—— 查询技能卡的触发配置t_times / t_inv / is_inst
* - CardPoolList / CardTriggerTypeCardSet—— 查询技能卡的触发配置
* - SkillSet —— 技能静态配置icon 字段)
* - GameEvent —— 各类游戏事件
* - smc.mission —— 游戏运行状态
@@ -29,8 +33,8 @@ import { mLogger } from "../common/Logger";
import { _decorator, Node, Prefab, Sprite, Label, Vec3, resources, SpriteAtlas, tween, v3, Tween, NodeEventType } 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 { CardPoolList } from "../common/config/CardSet";
import { SkillSet, SkillOverrides } from "../common/config/SkillSet";
import { CardPoolList, CardTriggerType } from "../common/config/CardSet";
import { SkillSet, SkillOverrides, FieldSkillSet } from "../common/config/SkillSet";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { smc } from "../common/SingletonModuleComp";
@@ -53,6 +57,10 @@ export class SkillBoxComp extends CCComp {
@property({ type: Node })
private icon_node: Node = null;
/** 技能图标节点:下面有 green blue purple red yellow 5个不同颜色节点*/
@property({ type: Node })
private bg_node: Node = null;
/** 剩余次数标签 */
@property(Label)
private info_label: Label = null;
@@ -78,6 +86,17 @@ export class SkillBoxComp extends CCComp {
private overrides?: SkillOverrides;
/** 驻场技能 UUID 列表 */
public field: number[] = [];
/** 卡牌自定义图标ID优先级最高未定义则按 trigger_type 自动取) */
private card_icon?: string;
// ======================== 触发类型化扩展 ========================
/** 触发类型(默认即时,保持向后兼容) */
private trigger_type: CardTriggerType = CardTriggerType.Instant;
/** 事件型触发的全局次数上限Infinity 表示无上限) */
private trigger_limit: number = Infinity;
/** 事件型已触发次数 */
private trigger_count: number = 0;
// ======================== 运行时状态 ========================
@@ -92,9 +111,13 @@ export class SkillBoxComp extends CCComp {
// ======================== 生命周期 ========================
/** 注册战斗开始、任务结束、新一波等事件 */
/**
* 注册全局事件:
* - MissionEnd所有类型都需要监听任务结束时强制销毁
* - NewWave处理 keep_waves 跨波次逻辑(所有类型统一)
* - 其它触发事件由 registerTrigger 按 trigger_type 动态注册
*/
onLoad() {
oops.message.on(GameEvent.FightStart, this.onFightStart, this);
oops.message.on(GameEvent.MissionEnd, this.onMissionEnd, this);
this.node.on(GameEvent.NewWave, this.onNewWave, this);
oops.message.on(GameEvent.NewWave, this.onNewWaveGlobal, this);
@@ -104,8 +127,15 @@ export class SkillBoxComp extends CCComp {
/** 销毁时移除所有事件监听并通知槽位管理器回收 */
onDestroy() {
super.onDestroy();
oops.message.off(GameEvent.FightStart, this.onFightStart, this);
// 统一 off 所有可能订阅的事件(即使未订阅也无副作用)
// 注意FightStart 可能由两种回调订阅Interval→onFightStart / FightStart触发型→onEventTrigger都需要 off
oops.message.off(GameEvent.MissionEnd, this.onMissionEnd, this);
oops.message.off(GameEvent.FightStart, this.onFightStart, this);
oops.message.off(GameEvent.FightStart, this.onEventTrigger, this);
oops.message.off(GameEvent.FightEnd, this.onEventTrigger, this);
oops.message.off(GameEvent.HeroDead, this.onEventTrigger, this);
oops.message.off(GameEvent.MasterCalled, this.onEventTrigger, this);
oops.message.off(GameEvent.ReviveSuccess, this.onEventTrigger, this);
if (this.node && this.node.isValid) {
this.node.off(GameEvent.NewWave, this.onNewWave, this);
this.node.off(NodeEventType.TOUCH_END, this.onNodeClicked, this);
@@ -128,9 +158,9 @@ export class SkillBoxComp extends CCComp {
/**
* 初始化技能卡效果:
* 1. 从 CardPoolList 查询技能卡的触发配置。
* 1. 从 CardPoolList 查询技能卡的触发配置(含 trigger_type
* 2. 更新 UI 显示(图标 + 次数)。
* 3. 即时技能立即触发一次;若次数已满则延迟销毁
* 3. 按 trigger_type 注册对应事件监听并执行首次触发
*
* @param uuid 卡牌 UUID
* @param card_lv 技能卡等级
@@ -148,58 +178,208 @@ export class SkillBoxComp extends CCComp {
this.keep_waves = config.keep_waves ?? 0;
this.overrides = config.overrides;
this.field = config.field || [];
this.card_icon = config.icon; // 保存卡牌自定义图标(优先级最高)
// 读取触发类型与上限(兜底默认值,避免 undefined
this.trigger_type = config.trigger_type ?? CardTriggerType.Instant;
this.trigger_limit = config.trigger_limit ?? Infinity;
} else {
this.s_uuid = uuid;
}
this.current_trigger_times = 0;
this.trigger_count = 0;
this.timer = 0;
this.initialized = true;
this.updateUI();
if (this.is_instant) {
// 即时技能:立即触发
this.triggerSkill();
this.current_trigger_times++;
if (this.keep_waves === 0 && this.current_trigger_times >= this.trigger_times) {
// 次数已满且不跨波次维持 → 延迟 1 秒后销毁(保留短暂视觉反馈)
this.scheduleOnce(() => {
if (this.ent) {
(this.ent as ecs.Entity).destroy();
} else if (this.node && this.node.isValid) {
this.node.destroy();
}
}, 1.0);
}
// 按 trigger_type 注册事件监听 + 执行首次触发
this.registerTrigger();
}
/**
* 按 trigger_type 注册对应事件监听:
* - Instant: init 时立即触发一次(保持旧行为)
* - Interval: 监听 FightStart进入战斗后由 update 帧驱动计时
* - Field: 不主动施法(实际生效由 FieldSkillSet 处理)
* - FightStart: 监听 FightStart 事件
* - FightEnd: 监听 FightEnd 事件
* - HeroDead: 监听 HeroDead 事件(已在派发处做阵营过滤)
* - HeroCall: 监听 MasterCalled主角/技能召唤)+ ReviveSuccess复活
*
* 注意MasterCalled 各派发点 payload 不一致onEventTrigger 仅作触发信号使用。
*/
private registerTrigger(): void {
switch (this.trigger_type) {
case CardTriggerType.Instant:
// 即时技能:立即触发一次
this.onEventTrigger();
break;
case CardTriggerType.Interval:
// 定时循环:监听 FightStart 进入战斗后启动计时
oops.message.on(GameEvent.FightStart, this.onFightStart, this);
break;
case CardTriggerType.Field:
// 驻场光环:不主动施法,由 FieldSkillSet 处理
break;
case CardTriggerType.FightStart:
oops.message.on(GameEvent.FightStart, this.onEventTrigger, this);
break;
case CardTriggerType.FightEnd:
oops.message.on(GameEvent.FightEnd, this.onEventTrigger, this);
break;
case CardTriggerType.HeroDead:
oops.message.on(GameEvent.HeroDead, this.onEventTrigger, this);
break;
case CardTriggerType.HeroCall:
// 同时监听召唤和复活两类英雄上场事件
oops.message.on(GameEvent.MasterCalled, this.onEventTrigger, this);
oops.message.on(GameEvent.ReviveSuccess, this.onEventTrigger, this);
break;
default:
mLogger.warn(true, 'SkillBoxComp', `[registerTrigger] unknown trigger_type: ${this.trigger_type}, fallback to Instant`);
this.onEventTrigger();
break;
}
}
/**
* 事件型触发的统一入口:
* - Instant 类型:按 trigger_times 上限判定,复用 current_trigger_times 跟踪
* (保持与原 is_instant 行为一致,且 NewWave 中也用 current_trigger_times
* - 事件型FightStart/FightEnd/HeroDead/HeroCall按 trigger_limit 上限判定,使用 trigger_count 跟踪
*
* 注意:本方法不读取事件 payload仅作触发信号使用避免 MasterCalled 不同 payload 字段引发的兼容问题)。
*/
private onEventTrigger(): void {
if (!this.initialized) return;
if (this.trigger_type === CardTriggerType.Instant) {
// 即时触发:上限由 trigger_times 控制(保持旧行为)
if (this.current_trigger_times >= this.trigger_times) {
this.destroySelf();
return;
}
this.triggerSkill();
this.current_trigger_times++;
this.updateUI();
// 单次触发 + 不跨波次维持 → 延迟销毁(保留短暂视觉反馈)
if (this.keep_waves === 0 && this.current_trigger_times >= this.trigger_times) {
this.scheduleOnce(() => this.destroySelf(), 1.0);
}
return;
}
// 事件型:上限由 trigger_limit 控制(全局累计,跨波次不重置)
if (this.trigger_count >= this.trigger_limit) {
this.destroySelf();
return;
}
this.triggerSkill();
this.trigger_count++;
this.updateUI();
}
/**
* 统一的节点销毁封装:
* 优先通过 ECS 实体销毁;否则直接销毁节点。
*/
private destroySelf(): void {
if (this.ent) {
(this.ent as ecs.Entity).destroy();
} else if (this.node && this.node.isValid) {
this.node.destroy();
}
}
/**
* 根据技能等级切换 bg_node 下的颜色子节点显示。
* 等级与颜色对应关系(技能卡 wave 档位):
* card_lv 1 (wave 1) → green
* card_lv 2 (wave 5) → blue
* card_lv 3 (wave 8) → purple
*/
private updateBgNode() {
if (!this.bg_node) return;
const lvToColor: Record<number, string> = {
1: "green",
2: "blue",
3: "purple",
};
const targetColor = lvToColor[this.card_lv];
this.bg_node.children.forEach(child => {
child.active = (child.name === targetColor);
});
}
/**
* 更新 UI
* - 图标:从 uicons 图集获取。
* - 剩余次数:持续技能显示剩余数字,即时技能不显示。
* - 背景:根据技能等级切换对应颜色子节点显示
* - 图标:从 uicons 图集获取
* - 剩余次数标签:
* * Interval / 事件型:显示剩余次数(按各自上限计算)
* * Instant / Field不显示
* - CD 遮罩:仅 Interval 类型展示冷却进度
*/
updateUI() {
// 按技能等级切换背景颜色节点(与 getLvColor 等级配色一致)
this.updateBgNode();
// 加载技能图标
if (this.icon_node) {
const iconId = SkillSet[this.s_uuid]?.icon || `${this.s_uuid}`;
if (smc.uiconsAtlas) {
const frame = smc.uiconsAtlas.getSpriteFrame(iconId);
if (frame && this.icon_node && this.icon_node.isValid) {
let sprite = this.icon_node.getComponent(Sprite) || this.icon_node.addComponent(Sprite);
sprite.spriteFrame = frame;
// 优先使用卡牌自定义 icon未定义则按 trigger_type 自动取
let iconId: string | undefined = this.card_icon;
if (!iconId) {
if (this.trigger_type === CardTriggerType.Interval) {
iconId = this.s_uuid ? SkillSet[this.s_uuid]?.icon : undefined;
} else if (this.trigger_type === CardTriggerType.Field) {
const fieldUuid = this.field?.[0];
iconId = fieldUuid ? FieldSkillSet[fieldUuid]?.icon : undefined;
}
}
if (this.icon_node && this.icon_node.isValid) {
let sprite = this.icon_node.getComponent(Sprite) || this.icon_node.addComponent(Sprite);
if (smc.uiconsAtlas && iconId) {
const frame = smc.uiconsAtlas.getSpriteFrame(iconId);
sprite.spriteFrame = frame || null; // 取不到时清空,避免残留
} else {
sprite.spriteFrame = null;
}
}
}
// 更新剩余次数标签
// 是否需要展示剩余次数
const showRemainCount =
this.trigger_type === CardTriggerType.Interval ||
this.trigger_type === CardTriggerType.FightStart ||
this.trigger_type === CardTriggerType.FightEnd ||
this.trigger_type === CardTriggerType.HeroDead ||
this.trigger_type === CardTriggerType.HeroCall;
if (this.info_label) {
if (!this.is_instant) {
if (this.trigger_interval <= 0 && this.field && this.field.length > 0) {
this.info_label.string = ""; // 纯驻场技能不显示剩余次数
if (showRemainCount) {
// 事件型按 trigger_limitInterval 按 t_times
const isEvent =
this.trigger_type === CardTriggerType.FightStart ||
this.trigger_type === CardTriggerType.FightEnd ||
this.trigger_type === CardTriggerType.HeroDead ||
this.trigger_type === CardTriggerType.HeroCall;
const used = isEvent ? this.trigger_count : this.current_trigger_times;
const total = isEvent ? this.trigger_limit : this.trigger_times;
if (isEvent && !isFinite(total)) {
// 无上限:显示已触发次数
this.info_label.string = `${used}`;
} else {
const remain = Math.max(0, this.trigger_times - this.current_trigger_times);
const remain = Math.max(0, Math.floor(total) - used);
this.info_label.string = `${remain}`;
}
} else {
@@ -207,14 +387,14 @@ export class SkillBoxComp extends CCComp {
}
}
// 初始化或重置 CD 遮罩表现
// 初始化或重置 CD 遮罩表现(仅 Interval 类型有冷却进度)
if (this.cd_mask && this.cd_mask.isValid) {
let sprite = this.cd_mask.getComponent(Sprite);
if (sprite) {
if (this.is_instant || this.trigger_interval <= 0) {
sprite.fillRange = 0; // 无需冷却(包括驻场光环卡),直接归 0
} else {
if (this.trigger_type === CardTriggerType.Interval && this.trigger_interval > 0) {
sprite.fillRange = Math.max(0, 1 - (this.timer / this.trigger_interval));
} else {
sprite.fillRange = 0; // 非冷却类型直接归 0
}
}
}
@@ -222,14 +402,17 @@ export class SkillBoxComp extends CCComp {
// ======================== 战斗状态事件 ========================
/** 战斗开始:标记进入战斗状态,持续技能开始计时 */
/**
* 战斗开始回调:
* - 仅 Interval 类型在 registerTrigger 中订阅此事件
* - 标记进入战斗状态,启动计时器(实际触发由 update 帧驱动)
*
* 注意FightStart 触发型CardTriggerType.FightStart的事件回调是 onEventTrigger不是本方法。
*/
private onFightStart() {
if (!this.initialized) return;
this.in_combat = true;
if (!this.is_instant) {
this.timer = 0; // 重置计时器
}
this.timer = 0; // 重置计时器
}
/** 节点级新一波事件处理 */
@@ -245,76 +428,79 @@ export class SkillBoxComp extends CCComp {
/**
* 新一波:退出战斗状态。
* 处理维持波次逻辑:递减剩余波次,或者重置触发次数。
*
* 各类型在新一波的行为:
* - Instant/Interval/FightStart/FightEnd按 keep_waves 决定维持/销毁,并在新一波开始时重置本地计数
* - Field被动生效跟随 keep_waves 决定存活
* - HeroDead/HeroCall跨波次触发的事件型trigger_count全局不重置仅 keep_waves 控制存活
*/
private handleNewWave() {
if (!this.initialized) return;
this.in_combat = false;
// 事件型触发HeroDead / HeroCalltrigger_count 全局累计,不随波次重置
const isGlobalEventType =
this.trigger_type === CardTriggerType.HeroDead ||
this.trigger_type === CardTriggerType.HeroCall;
if (this.keep_waves !== 0) {
if (this.keep_waves > 0) {
this.keep_waves--;
if (this.keep_waves <= 0) {
if (this.ent) {
(this.ent as ecs.Entity).destroy();
} else if (this.node && this.node.isValid) {
this.node.destroy();
}
this.destroySelf();
return;
}
}
// 能够跨波次维持重置触发次数和计时器,以便新一波继续触发
this.current_trigger_times = 0;
// 跨波次维持重置本地计数与计时器(事件型 trigger_count 不重置)
if (!isGlobalEventType) {
this.current_trigger_times = 0;
this.trigger_count = 0;
}
this.timer = 0;
// 即时技能在新一波开始立即触发一次
if (this.is_instant) {
// 即时/事件型触发一次保持旧行为Instant 在新一波开始立即触发一次
if (this.trigger_type === CardTriggerType.Instant) {
this.triggerSkill();
this.current_trigger_times++;
}
this.updateUI();
} else {
// 默认逻辑:不跨波次维持
if (!this.is_instant) {
if (this.current_trigger_times >= this.trigger_times) {
if (this.ent) {
(this.ent as ecs.Entity).destroy();
} else if (this.node && this.node.isValid) {
this.node.destroy();
}
}
// 不跨波次维持:达到上限即销毁
// - Interval / Instant按 t_times 判定
// - 事件型:按 trigger_limit 判定
const reachedLimit = isGlobalEventType
? this.trigger_count >= this.trigger_limit
: this.current_trigger_times >= this.trigger_times;
if (reachedLimit) {
this.destroySelf();
}
}
}
/** 任务结束:强制销毁 */
private onMissionEnd() {
if (this.ent) {
(this.ent as ecs.Entity).destroy();
} else if (this.node && this.node.isValid) {
this.node.destroy();
}
this.destroySelf();
}
// ======================== 帧更新 ========================
/**
* 每帧更新(仅对持续技能生效)
* - 累加计时器,达到 trigger_interval 时触发一次技能。
* - 触发后重置计时器并更新 UI。
* - 总次数用完后延迟销毁。
* 每帧更新:
* - 仅 Interval 类型走帧驱动计时逻辑(其它类型提前 return
* - 累加计时器,达到 trigger_interval 时触发一次技能
* - 触发后重置计时器并更新 UI
* - 总次数用完后延迟销毁
*/
update(dt: number) {
if (!this.initialized || !this.in_combat || this.is_instant) return;
// 收窄:仅 Interval 类型走帧驱动
if (this.trigger_type !== CardTriggerType.Interval) return;
if (!this.initialized || !this.in_combat) return;
if (!smc.mission.play || smc.mission.pause) return;
// 如果是纯驻场光环技能且无触发间隔,则不执行定期触发逻辑
if (this.trigger_interval <= 0 && this.field && this.field.length > 0) {
return;
}
if (this.current_trigger_times < this.trigger_times) {
this.timer += dt;
// 更新 CD 遮罩 (fillRange 从 1 降到 0)
if (this.cd_mask && this.cd_mask.isValid && this.trigger_interval > 0) {
let sprite = this.cd_mask.getComponent(Sprite);
@@ -331,13 +517,7 @@ export class SkillBoxComp extends CCComp {
// 次数用完且不跨波次维持 → 延迟销毁
if (this.keep_waves === 0 && this.current_trigger_times >= this.trigger_times) {
this.scheduleOnce(() => {
if (this.ent) {
(this.ent as ecs.Entity).destroy();
} else if (this.node && this.node.isValid) {
this.node.destroy();
}
}, 0.5);
this.scheduleOnce(() => this.destroySelf(), 0.5);
}
}
}

View File

@@ -15,7 +15,7 @@ import { _decorator, Label, Sprite, SpriteFrame } 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 { CardConfig } from "../common/config/CardSet";
import { SkillSet } from "../common/config/SkillSet";
import { FieldSkillSet, SkillSet } from "../common/config/SkillSet";
import { CardBgComp } from "./CardBgComp";
import { smc } from "../common/SingletonModuleComp";
import { oops } from "db://oops-framework/core/Oops";
@@ -45,38 +45,35 @@ export class TalentItemComp extends CCComp {
*/
public updateItem(config: CardConfig): void {
if (!config) return;
if (this.lbl_name) this.lbl_name.string = config.name ?? "";
if (this.lbl_info) this.lbl_info.string = config.info ?? "";
// 根据 wave 映射背景颜色
// 1=绿色(poolLv=1) 5=蓝色(poolLv=2) 10=紫色(poolLv=3) 15=黄色(poolLv=4) 20=红色(poolLv=5)
// 直接复用 CardSet 中已映射好的 pool_lv保证与技能卡牌背景一致
// CardSet 通过 waveToPoolLv[wave] 由 SKILL_CARD_WAVES 索引推导wave 1→1, 5→2, 8→3
if (this.bg) {
let poolLv = 1;
const wave = config.wave || 1;
if (wave >= 20) poolLv = 5;
else if (wave >= 15) poolLv = 4;
else if (wave >= 10) poolLv = 3;
else if (wave >= 5) poolLv = 2;
else poolLv = 1;
this.bg.apply(poolLv);
this.bg.apply(config.pool_lv || 1);
}
// 设置图标
if (this.icon && config.skill) {
const skillData = SkillSet[config.skill];
if (skillData && skillData.icon) {
// 设置图标:驻场卡(skill=undefined 但有 field)走 FieldSkillSet否则走 SkillSet
// 与 SCardComp 保持一致,避免驻场卡无 icon 显示
if (this.icon) {
let iconId: string | undefined;
if (!config.skill && config.field && config.field.length > 0) {
// 驻场卡:用 FieldSkillSet[field[0]].icon
const fieldUuid = config.field[0];
iconId = FieldSkillSet[fieldUuid]?.icon || `${fieldUuid}`;
} else if (config.skill) {
// 技能卡:用 SkillSet[skill].icon
iconId = SkillSet[config.skill]?.icon;
}
if (iconId) {
if (smc.uiconsAtlas) {
const frame = smc.uiconsAtlas.getSpriteFrame(skillData.icon);
if (frame) {
this.icon.spriteFrame = frame;
}
const frame = smc.uiconsAtlas.getSpriteFrame(iconId);
if (frame) this.icon.spriteFrame = frame;
} else {
const sf = oops.res.get("game/heros/cards/" + skillData.icon, SpriteFrame) as SpriteFrame;
if (sf) {
this.icon.spriteFrame = sf;
}
const sf = oops.res.get("game/heros/cards/" + iconId, SpriteFrame) as SpriteFrame;
if (sf) this.icon.spriteFrame = sf;
}
}
}

View File

@@ -69,6 +69,9 @@ export class SMoveDataComp extends ecs.Comp {
bezierMidHeight: number = 200;
bezierArc: number = 1;
/** 是否逐渐加速 (ease-in) */
isAccelerate: boolean = false;
/** 是否自动销毁(到达目标后) */
autoDestroy: boolean = true;
@@ -80,6 +83,7 @@ export class SMoveDataComp extends ecs.Comp {
this.bezierStartHeight = 30;
this.bezierMidHeight = 200;
this.bezierArc = 1;
this.isAccelerate = false;
this.speed = 500;
this.progress = 0;
this.scale = 1;
@@ -181,14 +185,20 @@ export class SMoveDataComp extends ecs.Comp {
* 根据移动类型计算当前位置
*/
private calculateCurrentPosition() {
// 如果开启了逐渐加速,混合线性与二次方曲线 (如 0.7 * t^2 + 0.3 * t)
// 这样起步拥有 30% 的基础速度,不会显得完全静止,随后逐渐加速
const t = this.isAccelerate ?
(this.progress * this.progress * 0.7 + this.progress * 0.3) :
this.progress;
switch (this.runType) {
case RType.linear:
// 直线运动
Vec3.lerp(this.currentPos, this.startPos, this.targetPos, this.progress);
Vec3.lerp(this.currentPos, this.startPos, this.targetPos, t);
break;
case RType.bezier:
this.calculateBezierPosition(this.progress);
this.calculateBezierPosition(t);
break;
case RType.fixed:
@@ -198,7 +208,7 @@ export class SMoveDataComp extends ecs.Comp {
break;
default:
Vec3.lerp(this.currentPos, this.startPos, this.targetPos, this.progress);
Vec3.lerp(this.currentPos, this.startPos, this.targetPos, t);
break;
}
}

View File

@@ -40,10 +40,13 @@ export class SMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
return;
}
// 根据配置设置移动速度
// 根据配置设置移动速度与加速度
if (skillConfig.speed > 0) {
moveComp.speed = skillConfig.speed;
}
if (skillConfig.is_accel) {
moveComp.isAccelerate = true;
}
// 根据runType设置初始位置
this.initializePosition(moveComp, skillView);
@@ -190,10 +193,12 @@ export class SMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
if (moveComp.progress < 1) {
// 计算下一帧的位置来确定方向
const nextProgress = Math.min(moveComp.progress + 0.01, 1);
const t = moveComp.isAccelerate ?
(nextProgress * nextProgress * 0.7 + nextProgress * 0.3) :
nextProgress;
const nextPos = v3(0, 0, 0);
// 计算下一个位置
const t = nextProgress;
const oneMinusT = 1 - t;
const oneMinusTSquared = oneMinusT * oneMinusT;
const tSquared = t * t;
@@ -245,6 +250,7 @@ export class SMoveHelper {
if (skillConfig) {
moveComp.runType = skillConfig.RType || RType.linear;
moveComp.speed = skillConfig.speed || 500;
if (skillConfig.is_accel) moveComp.isAccelerate = true;
}
}

View File

@@ -60,7 +60,7 @@ export class Tooltip extends ecs.Entity {
this.remove(TooltipCom);
super.destroy();
}
static load(pos: Vec3 = Vec3.ZERO,type:number=1,vaule:string="",s_uuid:number=1001,parent:any=null,cd:number=1,fac:number=FacSet.MON) {
static load(pos: Vec3 = Vec3.ZERO,type:number=1,vaule:string="",s_uuid:number=1001,parent:any=null,cd:number=1,fac:number=FacSet.MON,triggerType:string="") {
let node: Node;
if (Tooltip.pool.size() > 0) {
node = Tooltip.pool.get()!;
@@ -75,7 +75,7 @@ export class Tooltip extends ecs.Entity {
node.active = true;
var sv = node.getComponent(TooltipCom)!;
sv.init(type, vaule, s_uuid, fac);
sv.init(type, vaule, s_uuid, fac, triggerType);
// this.add(sv); // 不要添加到单例实体上,否则会覆盖或导致单例被销毁
}

View File

@@ -1,8 +1,9 @@
import { _decorator, Collider2D, Contact2DType, v3, IPhysics2DContact, Vec3, tween, Label, resources, SpriteFrame, Sprite, UIOpacity, Color, math, Tween } from "cc";
import { _decorator, Collider2D, Contact2DType, v3, IPhysics2DContact, Vec3, tween, Label, resources, SpriteFrame, Sprite, UIOpacity, Color, math, Tween,Node } 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 { SkillSet } from "../common/config/SkillSet";
import { FacSet, TooltipTypes } from "../common/config/GameSet";
import { SkillTriggerType } from "../common/config/heroSet";
import { Tooltip } from "./Tooltip";
const { ccclass, property } = _decorator;
@@ -11,11 +12,26 @@ const { ccclass, property } = _decorator;
@ccclass('TooltipCom')
@ecs.register('TooltipView', false)
export class TooltipCom extends CCComp {
@property(Node)
atking_bg: Node = null;
@property(Node)
atked_bg: Node = null;
@property(Node)
fstart_bg: Node = null;
@property(Node)
fend_bg: Node = null;
@property(Node)
dead_bg: Node = null;
@property(Node)
revive_bg: Node = null;
stype: number = 1; // 1:减少生命值2增加生命值3技能图标
value: string = "";
s_uuid: number = 1001;
fac: number = FacSet.MON;
/** 当前技能喊话对应的触发类型(空字符串表示普通主动技能) */
triggerType: string = "";
// 动画参数配置
private readonly popDuration = 0.15;
private readonly driftDuration = 0.5;
@@ -29,11 +45,12 @@ export class TooltipCom extends CCComp {
}
/** 初始化并播放动画 */
init(type: number, value: string, uuid: number, fac: number = FacSet.MON) {
init(type: number, value: string, uuid: number, fac: number = FacSet.MON, triggerType: string = "") {
this.stype = type;
this.value = value;
this.s_uuid = uuid;
this.fac = fac;
this.triggerType = triggerType;
// 初始化或获取 UIOpacity 组件
this._uiOpacity = this.node.getComponent(UIOpacity);
@@ -106,6 +123,8 @@ export class TooltipCom extends CCComp {
this.setupLabel("skill", "name", skillName+this.value);
// this.node.setPosition(v3(this.node.position.x, currentY));
this.node.setSiblingIndex(topSiblingIndex);
// 根据触发类型激活对应的背景标识(追击/反击/起手/生息/亡语/复活)
this.setupTriggerBg(this.triggerType);
break;
case TooltipTypes.uskill:
this.setupLabel("uskill", "name", this.value);
@@ -146,6 +165,34 @@ export class TooltipCom extends CCComp {
}
}
/**
* 根据技能触发类型激活对应的背景标识节点
* 仅 TooltipTypes.skill 走此分支,其他飘字类型不受影响
* 对象池复用场景下先统一关闭所有 _bg避免上一次的状态残留
*/
private setupTriggerBg(triggerType: string) {
// 先关闭所有触发类型背景,防止节点池复用时残留
this.atking_bg && (this.atking_bg.active = false);
this.atked_bg && (this.atked_bg.active = false);
this.fstart_bg && (this.fstart_bg.active = false);
this.fend_bg && (this.fend_bg.active = false);
this.dead_bg && (this.dead_bg.active = false);
this.revive_bg && (this.revive_bg.active = false);
if (!triggerType) return;
const bgMap: Record<string, Node | null> = {
[SkillTriggerType.Atking]: this.atking_bg,
[SkillTriggerType.Atked]: this.atked_bg,
[SkillTriggerType.FStart]: this.fstart_bg,
[SkillTriggerType.FEnd]: this.fend_bg,
[SkillTriggerType.Dead]: this.dead_bg,
[SkillTriggerType.Revive]: this.revive_bg,
};
const bg = bgMap[triggerType];
if (bg) bg.active = true;
}
playAnimation(scaleMax: number, isCrit: boolean, isHeal: boolean, sx: number = 1) {
// 随机 X 轴偏移 (防止重叠)
const randomX = (Math.random() - 0.5) * 60;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
# 技能卡触发机制重构方案(已废弃)
> ⚠️ **本文档已废弃**,被以下执行计划取代:
> [`2026-06-19-card-skill-trigger-type-refactor-plan.md`](./2026-06-19-card-skill-trigger-type-refactor-plan.md)
>
> 废弃原因:本草案存在 3 处关键错误,已在新计划中修正:
> 1. 缺少 `Instant` 类型,导致现有即时卡牌无法归类
> 2. `BattleEnd` 错误映射到 `MissionEnd`(整局结束),应为 `FightEnd`(每波结束)
> 3. `HeroDead` 未提阵营过滤,会导致怪物死亡误触发
>
> 保留本文档仅作历史记录,请勿参考。
---
## 旧草案内容(仅供参考)
### 原始需求背景
当前技能卡SkillCardData`CardSet.ts` 中仅通过 `is_inst`(是否即时)和 `t_inv`(触发间隔)隐式区分类型。随着技能丰富,需要:
1. 明确定义卡牌技能的触发类型(如:驻场、定时)。
2. 新增类似于英雄生命周期的触发时机:战斗开始、战斗结束、场上英雄死亡、英雄召唤上场。
### 原始修改方案
#### 1. 明确技能触发类型 (`CardSet.ts`)
新增枚举 `CardSkillType`,用于明确区分卡牌技能的触发时机:
```typescript
export enum CardSkillType {
Interval = 1, // 间隔定时触发 (战斗中每隔N秒执行)
Field = 2, // 驻场技能 (被动光环)
BattleStart = 3, // 战斗开始时触发一次
BattleEnd = 4, // 战斗结束时触发一次
HeroDead = 5, // 场上己方英雄死亡时触发
HeroCall = 6, // 场上己方英雄召唤上场时触发
}
```
> ❌ **错误 1**:缺少 `Instant` 类型,现有 `is_inst: true` 的卡牌8301护盾、8302治疗等无法归类。
#### 2. 完善事件派发机制
为支持新的触发类型,确保相关事件被正确派发:
- **英雄召唤上场 (`GameEvent.MasterCalled`)**:已在 `Hero.ts` 中实现。
- **英雄死亡 (`GameEvent.HeroDead`)**:需在 `HeroAtkSystem.ts` 中的英雄死亡逻辑里,补充派发 `GameEvent.HeroDead` 事件,供技能盒子监听。
- **战斗开始/结束 (`GameEvent.FightStart` / `GameEvent.MissionEnd`)**:已支持。
> ❌ **错误 2**`BattleEnd` 映射到 `MissionEnd` 是错的——MissionEnd 是整局任务结束,不是每波战斗结束。
> ❌ **错误 3**HeroDead 未提阵营过滤,怪物死亡会误触发。
#### 3. 重构技能盒子逻辑 (`SkillBoxComp.ts`)
修改 `SkillBoxComp`,使其根据 `trigger_type` 进行不同的监听与触发:
- **属性定义**:新增解析并保存 `trigger_type`
- **事件监听**:在 `onLoad``init` 后根据 `trigger_type` 注册相应的监听:
- `CardSkillType.BattleStart`: 监听 `GameEvent.FightStart`
- `CardSkillType.BattleEnd`: 监听 `GameEvent.MissionEnd`
- `CardSkillType.HeroDead`: 监听 `GameEvent.HeroDead`
- `CardSkillType.HeroCall`: 监听 `GameEvent.MasterCalled`
- **触发处理**
- 每当监听到对应事件,调用 `triggerSkill()` 释放技能,并累加触发次数。
- 若已达最大触发次数,则销毁节点。

View File

@@ -0,0 +1,332 @@
# 卡牌技能触发类型化改造执行计划
> 状态Accepted
> 日期2026-06-19
> 关联文档:`skill_card_trigger_refactor.md`(旧草案,已废弃,被本计划取代)
> 关联设计:`2026-05-22-skill-template-refactor-design.md`(技能 overrides 机制,本计划复用)
---
## 一、背景与目标
### 1.1 现状问题
当前技能卡([SkillCardData](file:///d:/game/pixelheros/assets/script/game/common/config/CardSet.ts#L151))通过 `is_inst` / `t_inv` / `field` 三个字段**隐式组合**推断触发模式:
| 隐式模式 | 判定条件 | 痛点 |
|---------|---------|------|
| 即时一次性 | `is_inst: true` | 类型不直观,新人需交叉对比 3 个字段 |
| 战斗中定时 | `is_inst: false && t_inv > 0` | 同上 |
| 纯驻场光环 | `field.length > 0 && t_inv <= 0` | 同上 |
且**无法表达**事件驱动型触发(战斗开始/结束、英雄死亡/召唤)。
### 1.2 改造目标
1. **显式类型化**:新增 `trigger_type` 字段,一张卡一个类型,强制必填
2. **事件驱动扩展**:新增 4 种事件触发类型,对齐英雄侧 [SkillTriggerType](file:///d:/game/pixelheros/assets/script/game/common/config/heroSet.ts#L91-L101)
3. **复用现有事件**:直接监听 `GameEvent.FightStart` / `FightEnd` / `HeroDead` / `MasterCalled` / `ReviveSuccess`
4. **零破坏迁移**:一次性批改所有 SkillCardData 配置,不保留向后兼容推断逻辑
### 1.3 关键决策(已确认)
| 决策点 | 选择 | 理由 |
|--------|------|------|
| 向后兼容策略 | **强制显式声明** | 一次性迁移到位,避免推断逻辑长期残留 |
| FightEnd 事件 | **新增 FightEnd 派发** | MissionEnd 是整局结束语义不符FightEnd 才是每波战斗结束 |
| HeroCall 覆盖范围 | **所有英雄上场** | MasterCalled主角+技能召唤)+ ReviveSuccess复活 |
| Field 类型改造 | **仅显式分类** | 实际生效仍由 FieldSkillSet 处理SkillBoxComp 不主动施法 |
---
## 二、最终设计(融合修正版)
### 2.1 CardTriggerType 枚举定义
> 融合说明吸收旧草案的命名规范Field/Interval/从1开始规避其事件映射错误
```typescript
/** 卡牌技能触发类型 */
export enum CardTriggerType {
Instant = 1, // 即时触发:使用后立即生效一次
Interval = 2, // 定时循环:战斗中按 t_inv 间隔重复触发
Field = 3, // 驻场光环:被动生效(仅显式分类,仍由 field 字段驱动)
FightStart = 4, // 战斗开始时触发
FightEnd = 5, // 战斗结束时触发(每波结束)
HeroDead = 6, // 场上己方英雄死亡时触发
HeroCall = 7, // 英雄上场时触发(主角召唤 + 技能召唤 + 复活)
}
```
**命名对齐说明**
- `Field` 对齐英雄侧 [SkillTriggerType.Field](file:///d:/game/pixelheros/assets/script/game/common/config/heroSet.ts#L96)
- `Interval` 对齐现有 `t_inv`interval命名
- 枚举值从 1 开始,避免 `0` 的 falsy 坑(`if (trigger_type)` 判断出错)
### 2.2 事件映射表(核心设计)
| trigger_type | 监听事件 | 派发点现状 | 需补派发 |
|--------------|---------|-----------|---------|
| `Instant` | 无init 时立即触发) | — | — |
| `Interval` | `FightStart`(启动计时) | ✅ [MissionComp.ts:458](file:///d:/game/pixelheros/assets/script/game/map/MissionComp.ts#L458) | — |
| `Field` | 无(不主动施法) | — | — |
| `FightStart` | `FightStart` | ✅ 已派发 | — |
| `FightEnd` | `FightEnd` | ❌ **未派发** | ✅ [MissionComp.ts:494](file:///d:/game/pixelheros/assets/script/game/map/MissionComp.ts#L494) 之后 |
| `HeroDead` | `HeroDead` | ❌ **未派发**(死代码) | ✅ [HeroAtkSystem.ts:329](file:///d:/game/pixelheros/assets/script/game/hero/HeroAtkSystem.ts#L329) 内(带阵营过滤) |
| `HeroCall` | `MasterCalled` + `ReviveSuccess` | MasterCalled ✅ 已派发ReviveSuccess ❌ 未派发 | ✅ 复活成功处补 ReviveSuccess |
### 2.3 CardConfig 接口扩展
```typescript
export interface CardConfig {
// ... 既有字段 ...
/** 触发类型(必填) */
trigger_type: CardTriggerType;
/** 事件型触发的全局次数上限(仅 FightStart/FightEnd/HeroDead/HeroCall 有效)
* 默认 Infinity达到上限后销毁节点
* 注意:与 t_times 语义不同——t_times 控制每波内 Interval 的次数 */
trigger_limit?: number;
}
```
### 2.4 t_times vs trigger_limit 语义区分
| 字段 | 适用类型 | 含义 | 重置时机 |
|------|---------|------|---------|
| `t_times` | `Interval` | 每波内的触发次数上限 | 每波 NewWave 时重置 |
| `trigger_limit` | `FightStart/FightEnd/HeroDead/HeroCall` | 整局全局触发总次数 | 不重置,达上限销毁 |
---
## 三、分阶段执行计划
### 阶段 1补齐事件派发缺口基础设施
**目标**:确保所有新触发类型依赖的事件都能正确派发
#### 任务 1.1MissionComp 补派发 FightEnd
- **文件**[MissionComp.ts](file:///d:/game/pixelheros/assets/script/game/map/MissionComp.ts)
- **位置**`BattleEnd` case`triggerHeroBattleSkills(false)` + `healAllHeroes()` 之后
- **改动**
```typescript
case MissionPhase.BattleEnd:
// ... 既有评分逻辑 ...
this.triggerHeroBattleSkills(false);
this.healAllHeroes();
// 【新增】派发战斗结束事件,供卡牌技能监听
oops.message.dispatchEvent(GameEvent.FightEnd);
break;
```
#### 任务 1.2HeroAtkSystem 补派发 HeroDead带阵营过滤
- **文件**[HeroAtkSystem.ts](file:///d:/game/pixelheros/assets/script/game/hero/HeroAtkSystem.ts)
- **位置**`triggerDeadSkills` 方法L329 附近)
- **改动**
```typescript
private triggerDeadSkills(entity: ecs.Entity): void {
const TAttrsComp = entity.get(HeroAttrsComp);
if (!TAttrsComp) return;
const view = entity.get(HeroViewComp);
if (view) {
SkillTriggerHelper.trigger(SkillTriggerType.Dead, TAttrsComp, view);
// 【新增】仅英雄阵营派发全局死亡事件(怪物死亡不触发卡牌效果)
if (TAttrsComp.fac === FacSet.HERO) {
oops.message.dispatchEvent(GameEvent.HeroDead, { eid: entity.eid });
}
}
}
```
#### 任务 1.3:复活逻辑补派发 ReviveSuccess
- **文件**:需先定位复活成功处理点(搜索 `is_reviving` 置 false 的位置)
- **改动**:复活成功时派发 `oops.message.dispatchEvent(GameEvent.ReviveSuccess, { eid })`
- **注意**:需先执行任务:全局搜索复活成功逻辑位置
---
### 阶段 2CardSet 配置层改造
**目标**:定义枚举 + 扩展接口 + 修复字段透传 + 批量迁移配置
#### 任务 2.1:新增 CardTriggerType 枚举
- **文件**[CardSet.ts](file:///d:/game/pixelheros/assets/script/game/common/config/CardSet.ts)
- **位置**`CardLV` 枚举之后
- **内容**:见 [2.1 节](#21-cardtriggertype-枚举定义)
#### 任务 2.2CardConfig 接口扩展
- **文件**:同上
- **位置**`CardConfig` 接口
- **内容**:见 [2.3 节](#23-cardconfig-接口扩展)
#### 任务 2.3:修复 SkillCardData.forEach 字段透传断点
- **文件**:同上
- **位置**[L220-L240](file:///d:/game/pixelheros/assets/script/game/common/config/CardSet.ts#L220-L240) `SkillCardData.forEach`
- **改动**:补充 `overrides``trigger_type` 透传:
```typescript
SkillCardData.forEach(data => {
CardPoolList.push({
// ... 既有字段 ...
keep_waves: data.keep_waves,
field: data.field,
overrides: data.overrides, // 【修复】原遗漏
trigger_type: data.trigger_type, // 【新增】
trigger_limit: data.trigger_limit, // 【新增】
});
});
```
#### 任务 2.4SkillCardData 批量补 trigger_type30 张卡牌)
- **文件**:同上
- **迁移对照表**
| 卡牌区间 | 旧字段特征 | 新增 trigger_type |
|---------|-----------|------------------|
| 8301, 8302, 8303, 8401-8409, 8501`is_inst: true` | 即时技能 | `CardTriggerType.Instant` |
| 8705, 8706, 8707, 8708-8718, 8701-8704`field` | 驻场光环 | `CardTriggerType.Field` |
| 8201-8206`is_inst: false, t_inv: 5` | 定时循环 | `CardTriggerType.Interval` |
---
### 阶段 3SkillBoxComp 核心重构
**目标**:按 trigger_type 分发事件监听与触发
#### 任务 3.1:新增成员变量
- **文件**[SkillBoxComp.ts](file:///d:/game/pixelheros/assets/script/game/map/SkillBoxComp.ts)
- **位置**`// ======================== 技能配置 ========================` 区块
```typescript
/** 触发类型 */
private trigger_type: CardTriggerType = CardTriggerType.Instant;
/** 事件型触发次数上限 */
private trigger_limit: number = Infinity;
/** 事件型已触发次数 */
private trigger_count: number = 0;
```
#### 任务 3.2init 读取 trigger_type
- **位置**`init()` 方法内,读取 config 之后
```typescript
this.trigger_type = config.trigger_type ?? CardTriggerType.Instant;
this.trigger_limit = config.trigger_limit ?? Infinity;
```
#### 任务 3.3:新增 registerTrigger 方法
按 trigger_type 注册对应事件监听,见下方完整代码。
#### 任务 3.4:新增 onEventTrigger 统一入口
事件型触发的统一处理:检查 trigger_limit → triggerSkill → 累加计数 → 检查销毁。
#### 任务 3.5onLoad / onDestroy 调整
- `onLoad`:移除原 FightStart / NewWave 硬编码监听,改为 `init` 后调用 `registerTrigger` 动态注册
- `onDestroy`:统一 off 所有可能订阅的事件(即使没订阅也无副作用)
#### 任务 3.6update 方法收窄
`Interval` 类型走帧驱动计时逻辑,其他类型提前 return。
---
### 阶段 4验证与回归
#### 任务 4.1:编译检查
- 确认所有 CardTriggerType 引用正确
- 确认无 TS 类型错误
#### 任务 4.2:功能验证清单
- [ ] 即时卡8301 护盾):使用后立即触发,每波重置
- [ ] 定时卡8201 雷墙):战斗中每 5 秒触发,跨波次维持
- [ ] 驻场卡8705 金币收益):被动生效,不主动施法
- [ ] 新增 FightStart 卡:每波战斗开始时触发
- [ ] 新增 FightEnd 卡:每波战斗结束时触发
- [ ] 新增 HeroDead 卡:英雄死亡时触发,怪物死亡不触发
- [ ] 新增 HeroCall 卡:主角召唤/技能召唤/复活都触发
#### 任务 4.3:边界场景
- [ ] trigger_limit 达上限后节点正确销毁
- [ ] keep_waves 与 trigger_type 的组合行为正确
- [ ] 节点销毁时所有事件监听正确注销(无内存泄漏)
---
## 四、风险与注意事项
### 4.1 高风险点
1. **HeroDead 必须加阵营过滤**
- [HeroAtkSystem.triggerDeadSkills](file:///d:/game/pixelheros/assets/script/game/hero/HeroAtkSystem.ts#L319) 是英雄和怪物共用
- 不加 `fac === FacSet.HERO` 过滤 → 每波几百只怪物死亡 = 海量误触发
2. **FightEnd vs MissionEnd 不可混淆**
- MissionEnd = 整局任务结束(通关/失败)
- FightEnd = 每波战斗结束BattleEnd 阶段)
- 文档草案错误地把 BattleEnd 映射到 MissionEnd本计划已修正
3. **MasterCalled 携带数据不一致**
- 3 个派发点([Hero.ts:206](file:///d:/game/pixelheros/assets/script/game/hero/Hero.ts#L206)、[MissionHeroComp.ts:223](file:///d:/game/pixelheros/assets/script/game/map/MissionHeroComp.ts#L223)、[273](file:///d:/game/pixelheros/assets/script/game/map/MissionHeroComp.ts#L273)payload 字段不同
- SkillBoxComp 的 `onEventTrigger` **不要读 payload 字段**,仅作触发信号
### 4.2 不破坏的现有逻辑
- ✅ Field 类型完全复用现有 [FieldSkillSet](file:///d:/game/pixelheros/assets/script/game/common/config/SkillSet.ts#L414-L421) 机制
- ✅ Interval 类型完全复用现有 `update` 帧驱动 + cd_mask 表现
- ✅ [forceCastCardSkill](file:///d:/game/pixelheros/assets/script/game/hero/SCastSystem.ts#L75) 施法入口零改动
- ✅ [SBox.ts](file:///d:/game/pixelheros/assets/script/game/map/SBox.ts) 节点工厂零改动
### 4.3 keep_waves 跨类型语义
| trigger_type | keep_waves 默认值 | 行为 |
|--------------|-----------------|------|
| `Instant` | 0 = 用完即销 | `-1` = 每波重置再触发一次 |
| `Interval` | -1 = 跨波次维持 | 每波重置 timer 和 trigger_count |
| `Field` | -1 = 全程存活 | 不主动触发 |
| 事件型 | 由 `trigger_limit` 控制 | 达上限销毁 |
---
## 五、文件改动清单
| 文件 | 改动类型 | 阶段 |
|------|---------|------|
| [MissionComp.ts](file:///d:/game/pixelheros/assets/script/game/map/MissionComp.ts) | 补 FightEnd 派发 | 1 |
| [HeroAtkSystem.ts](file:///d:/game/pixelheros/assets/script/game/hero/HeroAtkSystem.ts) | 补 HeroDead 派发(带过滤) | 1 |
| 复活逻辑文件(待定位) | 补 ReviveSuccess 派发 | 1 |
| [CardSet.ts](file:///d:/game/pixelheros/assets/script/game/common/config/CardSet.ts) | 枚举+接口+透传+30张卡迁移 | 2 |
| [SkillBoxComp.ts](file:///d:/game/pixelheros/assets/script/game/map/SkillBoxComp.ts) | 核心重构 | 3 |
| [SBox.ts](file:///d:/game/pixelheros/assets/script/game/map/SBox.ts) | **零改动** | — |
---
## 六、新增卡牌配置示例
```typescript
// 战斗开始护盾(整局每波开始都给全队加盾)
{ uuid: 8310, skill: 6301, wave: 5, name: "起手护盾",
trigger_type: CardTriggerType.FightStart, keep_waves: -1,
overrides: { TGroup: TGroup.Team, ap: 3 },
info: "每波战斗开始时为全体友方添加护盾", is_inst: false }
// 英雄死亡治疗(整局最多触发 3 次)
{ uuid: 8311, skill: 6302, wave: 10, name: "亡语治疗",
trigger_type: CardTriggerType.HeroDead, trigger_limit: 3, keep_waves: -1,
overrides: { TGroup: TGroup.Team, ap: 200 },
info: "己方英雄死亡时治疗全体友方整局最多触发3次", is_inst: false }
// 英雄上场攻击强化(每次有新英雄上场都触发,最多 5 次)
{ uuid: 8312, skill: 6401, wave: 15, name: "召唤强化",
trigger_type: CardTriggerType.HeroCall, trigger_limit: 5, keep_waves: -1,
info: "有英雄上场时触发攻击强化整局最多触发5次", is_inst: false }
```

View File

@@ -0,0 +1,351 @@
# 通用英雄/技能配置编辑器Cocos Creator 扩展)— 设计规格
- 日期2026-06-20
- 作者brainstorm 协作产出
- 目标引擎Cocos Creator **3.8.6**
- 部署位置:`extensions/pixelhero-config-editor/`
- 关联配置源:`assets/script/game/common/config/{heroSet,SkillSet,HeroAttrs,HeroSkillDesc}.ts`
---
## 1. 概述Overview
构建一个 Cocos Creator 编辑器扩展,提供**可视化**的英雄与技能配置编辑能力。扩展以 **schema 驱动 + TypeScript AST 往返**为核心:直接读写现有 `Record<number,X>` 形态的 `.ts` 配置文件,**不改动任何游戏运行时代码**。通过声明式 schema 描述每张表的字段、枚举、引用关系UI 由 schema 自动生成,从而实现"通用"——未来新增驻场技能表、卡牌表等只需追加 schema无需编写新 UI。
## 2. 目标与非目标
### 目标
- 可视化编辑 `HeroInfo`(英雄 50xx + 怪物 60xx/61xx`SkillSet`(技能 60xx/63xx/64xx/65xx`FieldSkillSet`(驻场 70xx/72xx/74xx三张表。
- 原地回写对应 `.ts` 文件,**保留符号表达式**(如 `AtkSpeedSet[AtkSpeedLv.Slow3].cd`)与手工注释/分节标题。
- 异构触发槽、技能引用覆盖(`SkillOverrides`)、进化配置的可视化编辑。
- 实时校验 + 实时描述预览(与游戏内 `buildSkillDesc` 一致)。
- 新建/复制/删除/保存/还原,并保持 `HeroList``HeroInfo` 一致。
- 纯逻辑层schema/IO/校验)有自动化单元测试,作为 BLOCKING 证据。
### 非目标v1
- 不重构游戏代码、不把数据迁移到 JSON。
- 不做运行时热重载游戏逻辑(仅通过 asset-db 刷新让编辑器与编辑器内预览生效)。
- 撤销/重做Undo/Redo列为 v1.1。
- 不编辑 `CardSet / HighlightSet / GameSet / ScoreSet`(架构允许后续以新增 schema 方式扩展,但不在本次范围)。
- 不做多人协作/版本对比。
## 3. 背景事实(已核实)
| 事实 | 影响 |
|---|---|
| 引擎 Cocos Creator 3.8.6 | 使用 `package_version:2` 扩展格式;面板经 `Editor.Panel.define({...})`;消息经 `contributions.messages` |
| 配置运行时只读、仅按 key 访问、无动态加载 | 回写安全;输出只需是合法 TS 且 `HeroList` 一致 |
| `skills[n].cd` 为符号表达式 | IO 必须基于 TS Compiler API识别并保号往返 |
| 现有 `oops-plugin-framework` 仅运行时框架 | 无面板示例可抄;需自建打包 |
| `HeroList` 被运行时迭代(`CardSet.ts` | 写英雄表后必须同步 `HeroList = 排序后的英雄(HERO) uuid` |
| `HeroSkillDesc.buildSkillDesc` 生成游戏内描述 | 移植为 JS 用于面板实时预览 |
## 4. 架构(五层)
```
┌────────────────────────────────────────────────────────────┐
│ UI 层 Vue3 面板 (dist/panels/default.js) │
│ master-detail + 嵌套编辑器 + 校验面板 + 描述预览 │
└───────────────────────────────┬────────────────────────────┘
Editor.Message.request │ broadcast 'record-changed'
┌───────────────────────────────┴────────────────────────────┐
│ 主进程 dist/main.js 内存真理源 + 消息处理 + 广播 │
└──────┬──────────────────────┬──────────────────────┬───────┘
│ │ │
┌──────▼───────┐ ┌───────────▼──────────┐ ┌─────────▼────────┐
│ 校验层 │ │ Schema 注册表 │ │ IO 层 (TS 往返) │
│ validate() │ │ tables/fields/enums │ │ TsConfigFile │
│ → Issue[] │ │ → 驱动 UI 生成 │ │ load/patch/save │
└──────────────┘ └──────────────────────┘ └──────────┬────────┘
│ 写回 .ts
Editor.Message.request('asset-db','refresh-asset')
```
层次依赖单向UI → 主进程 → {校验, schema, IO}。校验与 schema 为纯逻辑可独立测试。IO 依赖 `typescript` 包。
## 5. 组件详设
### 5.1 Schema 注册表(`src/shared/schema/`
"通用"的核心。每个数据表用一份 `TableSchema` 描述:
```ts
interface TableSchema {
id: 'hero' | 'skill' | 'field'; // 表标识
label: string; // "英雄/怪物"
sourceFile: string; // 相对 assets 配置目录,如 'heroSet.ts'
exportName: string; // 'HeroInfo' | 'SkillSet' | 'FieldSkillSet'
keyType: 'number';
idSegments: { label: string; min: number; max: number; note?: string }[];
listExportName?: string; // 仅 hero'HeroList'(需同步)
fields: FieldSchema[]; // 记录字段(顺序即 UI 顺序)
}
interface FieldSchema {
key: string; // 字段名(对应 .ts 对象键)
label: string; // 中文标签
type: FieldType; // 见下
required?: boolean;
default?: unknown;
group?: string; // UI 分组("基础"/"触发技能"/...
help?: string;
// type 相关的可选元数据:
enumRef?: string; // type=enum → enums.ts 中的枚举键
refTable?: TableId; // type=ref → 引用哪张表
overlayKeys?: string[]; // type=overrides → 可覆盖键集合
showIf?: { field: string; in: unknown[] };// 条件显示
}
type FieldType =
| 'number' | 'string' | 'boolean'
| 'enum' // 下拉,选项来自 enumRef
| 'ref' // 引用另一表的 uuid下拉显示 目标.name
| 'speedExpr' // 攻速符号表达式,下拉=AtkSpeedLv 档位
| 'skillMap' // Record<number,HSkillInfo>(英雄专用)
| 'triggerSlots' // 6 种数组触发槽(英雄专用)
| 'fieldList' // number[] 驻场技能列表(英雄专用)
| 'reviveSlot' // 单对象 {s_uuid,r_num,upr}(英雄专用)
| 'overrides'; // SkillOverrides 覆盖层(出现在 triggerSlots 内部)
```
**枚举源 `src/shared/schema/enums.ts`**:镜像游戏枚举为 `{label,value}[]` 字典——`HType, FacSet, TGroup, DTType, SkillKind, DType, IType, RType, EType, FieldSkillType, Attrs(buff_type), AtkSpeedLv`。此文件为编辑器侧单一事实源;并提供 `assertEnumsMatchGame()` 调试期检查(读取游戏 `.ts` 枚举定义比对,不一致则告警),避免漂移。
**三张表的字段清单v1**
- **hero (`HeroInfo`)** — 分组:
- 基础:`uuid`(number,必填), `name`(string,必填), `path`(string,必填), `icon`?(string), `fac`(enum FacSet,必填), `pool_lv`?(number), `lv`(number,必填,默认1), `type`(enum HType,必填), `hp`(number,必填), `ap`(number,必填), `dis`?(number), `speed`?(number), `info`(string,必填)
- 技能:`skills`(skillMap,必填), 触发槽组 `call/dead/fstart/fend/atking/atked`(triggerSlots), `field`(fieldList), `revive`(reviveSlot), `evolve`(evolveMap — v1 只读展示,标注"v1.1 编辑")
- ID 段:英雄 [5000,5999];怪物 [6000,6999]6101-6106 为 Boss
- **skill (`SkillSet``SkillConfig`)**
- 基础:`uuid`(number,必填), `name`(string,必填), `sp_name`(string,必填), `icon`(string,必填), `act`(string,必填), `info`(string,必填)
- 目标/类型:`TGroup`(enum,必填), `DTType`(enum,必填), `IType`(enum,必填), `RType`(enum,必填), `EType`(enum,必填), `kind`?(enum SkillKind), `DType`?(enum,默认 ATK)
- 数值:`ap`(number,必填), `gold`?(number), `hit_count`(number,必填), `hitcd`(number,必填), `speed`(number,必填), `ready`(number,必填), `with`(number,必填,默认0)
- 动画/特效:`readyAnm`,`endAnm`,`DAnm`(string), `EAnm`(number)
- 高级:`crt?`,`stun?`,`frz?`,`bck?`(number), `buff_type`?(enum Attrs), `call_hero`?(ref hero), `time`?, `bezier_start_y?`,`bezier_mid_y?`,`bezier_arc?`(number)
- ID 段6001-6999
- **field (`FieldSkillSet``FieldSkillConfig`)**
- `uuid`(number,必填), `name`(string,必填), `icon`(string,必填), `type`(enum FieldSkillType,必填), `value`(number,必填), `info`(string,必填)
- ID 段7001-7999
> 字段清单来源 = 直接对照 `heroSet.ts`/`SkillSet.ts` 的 interface 定义,已逐字段核对类型与必填性。
### 5.2 IO 层(`src/main/io/TsConfigFile.ts`
基于 `typescript`npm在扩展主进程 Node 上下文中 `require`)。
```ts
class TsConfigFile {
load(file, exportName): void; // 解析并缓存 SourceFile + 目标 VariableDeclaration
getKeys(): number[]; // 该 const 的所有键
read(key): RecordValue; // AST → 结构化值
patch(key, value: RecordValue): void; // AST 区间替换该条目
add(key, value): void; // 在 const 末尾插入新条目
delete(key): void; // 删除条目区间
serialize(value): string; // 单条目确定性序列化
save(): { ok: boolean; error?: string }; // 写 .bak → 校验可解析 → 落盘 → 触发 asset-db refresh
reload(): void;
}
```
**值模型 `RecordValue`**
- 标量:`{kind:'num',value}` / `{kind:'str',value}` / `{kind:'bool',value}`
- 符号表达式:`{kind:'speed', level:'Slow3'}` ⇄ 源 `AtkSpeedSet[AtkSpeedLv.Slow3].cd`AST 形态:`PropertyAccessExpression(ElementAccessExpression(Ident,ElementAccessExpression(Ident,Ident)),Ident)`,固定匹配该模式;不匹配则降级为 `{kind:'num',value:<节点文本>}` 并标记需人工确认)
- 对象:`{kind:'obj', props: {key: RecordValue}}`
- 数组:`{kind:'arr', items: RecordValue[]}`
**回写策略 = 条目级 AST 区间替换**
1. 解析文件 → 定位目标 `exportName``ObjectLiteralExpression`
2. 在其中按 key 定位单个 `PropertyAssignment` 的完整文本区间(含其尾随逗号)。
3. 对该条目调用 `serialize()` 生成新文本。**确定性格式(固定,无歧义)**受控多行对象字面量每字段一行、4 空格缩进、字段顺序按 schema `fields` 顺序、尾随逗号;`cd` 用符号形式 `AtkSpeedSet[AtkSpeedLv.X].cd``info` 非空时在条目上方输出一行 `// {info}` 注释。示例:
```ts
// 每受击3次为自身添加4层护盾
5011:{uuid:5011,name:"小铁卫",path:"hk1",fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Melee,hp:400,ap:20,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
atked:[{s_uuid:6301,t_num:3,overrides:{TGroup:TGroup.Self,ap:4}}],
info:"每受击3次为自身添加4层护盾"},
```
4. 用新文本替换该区间;其余条目、分节注释、其他 const 原样不动 → **diff 最小**。
5. 新增:在 `}` 前插入;删除:移除区间。
6. 落盘前:`createProgram` 对改动文件做语法检查;失败则回滚 `.bak` 并报错。
7. 落盘后:`await Editor.Message.request('asset-db','refresh-asset', url)`,使编辑器重新导入该脚本。
**降级与安全**
- 任一步骤异常 → 不写文件,返回结构化错误,面板展示。
- 始终先写 `*.bak`;校验通过后再覆盖原文件。
- 若条目内出现未识别的初始化表达式(非上述 RecordValue 形态),该条目标记为"只读(含未支持表达式)",可编辑其他条目但不改动它,避免破坏。
### 5.3 校验层(`src/shared/validation/`
纯函数 `validate(tableId, allRecords): Issue[]`,每次改动后对受影响表运行。规则:
| 规则 | 级别 |
|---|---|
| uuid 在表内唯一 | error |
| uuid 落在该实体声明的 idSegments 内 | error |
| 必填字段非空 | error |
| enum 字段值 ∈ 枚举集合 | error |
| ref 字段目标在引用表中存在(英雄触发槽/技能图引用 → SkillSetfield 列表 → FieldSkillSetcall_hero → HeroInfo | error |
| overrides 仅含 `SkillOverrides` 允许键TGroup,ap,gold,hit_count,hitcd,crt,frz,stun,bck,buff_type,call_hero | error |
| 英雄表:`HeroList` 与 `HeroInfo` 一致(每 HeroList 项存在且 fac=HERO每 fac=HERO 条目都在 HeroList | error |
**`HeroList` 同步策略(最小 diff**:保留现有数组顺序与分节注释,仅在新增英雄时把新 uuid 追加到数组末尾、删除英雄时移除其 uuid。**不重排、不重生成**,以避免破坏手工分节注释。同步后再次运行一致性校验。
| 怪物无 `pool_lv`/`evolve`(语义警告) | warn |
| `info` 文案长度 >0 | warn |
`Issue = {tableId, key, fieldPath, severity:'error'|'warn', code, message}`。面板据此在列表行显红点、字段内联报错;存在 error 时禁用"保存"。
### 5.4 主进程(`src/main/index.ts`
扩展入口。在内存持有三张表的 `TsConfigFile` 实例与 schema 注册表,作为唯一真理源。处理消息(见 §7。任何写入先校验error 则拒绝并返回 Issue 列表;成功后广播 `record-changed {tableId, key}`,所有打开面板据此刷新。
### 5.5 UI 层(`src/panels/default/`Vue 3
- **打包**`esbuild` 将 `src/panels/default/index.ts`(含 Vue 3 runtime+compiler打成单文件 `dist/panels/default.js`。Vue 用**字符串模板**`compiler` 在线编译),避免 SFC 工具链。
- **面板入口**`Editor.Panel.define``template`=`<div id="app"></div>``ready()` 中 `createApp({…}).mount(this.$.app)``close()` 卸载。
- **布局**:左 master表切换 + 搜索 + 列表),右 detailschema 驱动表单 + 嵌套编辑器 + 预览),底部校验条。
- **控件映射**number→`<ui-num-input>`string→`<ui-input>`boolean→`<ui-checkbox>`enum→`<ui-select>`ref→`<ui-select>`(选项=目标表 `{uuid:name}`,空选项=清除speedExpr→`<ui-select>`(选项=AtkSpeedLv 档位,含"自定义数值"兜底)。
- **嵌套编辑器**
- `TriggerSlotsEditor`:对 6 种触发类型,每组可增删行 `{s_uuid(技能 ref 选择器), t_num(number), overrides(可折叠)}`行内显示该技能基础信息name/kind
- `SkillMapEditor`:英雄 `skills`;每项 `uuid + lv + cd(speedExpr)`;至少 1 项index 0=普攻)。
- `FieldListEditor``field:number[]` 多选驻场技能。
- `ReviveEditor``revive` 单对象 `{s_uuid,r_num,upr}` 或空。
- `OverrideEditor`:依据所选基础技能 `kind`,仅渲染相关覆盖键(如 Damage→ap/hit_count/crt/stun…Support→TGroup/ap/buff_type…
- **实时预览**`PreviewPane` 调主进程 `query-preview-desc`(移植 `buildSkillDesc`),随编辑即时刷新。
- **图标/特效预览**`icon`、`sp_name` 变更时 `query-asset` 取贴图,旁置缩略图。
- **操作栏**:新建(自动取 idSegment 内下一个可用 uuid、复制uuid+2 或手动指定、删除、还原reload、保存并刷新触发 §5.2 save。未保存改动用 `*` 标记;切换记录前若有未保存改动,弹确认。
- **原生观感**:尽量用 Cocos `<ui-*>` 元素Vue 仅做状态与组合。
## 6. 内存与持久化
- 真理源 = 磁盘 `.ts` 文件。主进程首次 `query-*` 时懒加载并缓存 `TsConfigFile`。
- 编辑改动先作用于内存 AST"保存"才落盘。还原=丢弃内存改动重载。
- 多面板实例:主进程单例,广播保证一致。
## 7. 消息协议(`contributions.messages`
| 消息 | 方向 | 载荷 | 返回 |
|---|---|---|---|
| `query-schema` | panel→main | `tableId?` | schema全部或单表 |
| `query-enums` | panel→main | — | 枚举字典 |
| `query-keys` | panel→main | `tableId` | `number[]` |
| `query-record` | panel→main | `tableId,key` | `RecordValue` |
| `query-preview-desc` | panel→main | `hero RecordValue` | `string`(描述) |
| `query-asset` | panel→main | `name, type` | 贴图 url 或 null |
| `validate` | panel→main | `tableId` | `Issue[]` |
| `save-record` | panel→main | `tableId,key,RecordValue` | `{ok, issues?}` |
| `add-record` | panel→main | `tableId,key,RecordValue` | `{ok, issues?}` |
| `delete-record` | panel→main | `tableId,key` | `{ok, issues?}` |
| `revert-record` | panel→main | `tableId,key` | `RecordValue`(重载后值) |
| `record-changed` | main→broadcast | `tableId,key` | — |
`save/add/delete` 成功后主进程自动广播 `record-changed`。
## 8. 数据流(保存一条英雄)
```
面板改字段 → save-record(hero,5011,value)
→ 主进程 validate(hero)error? 返回 {ok:false,issues}
→ TsConfigFile.patch(5011, value) // AST 区间替换
→ TsConfigFile.save():写 .bak → createProgram 语法校验 → 覆盖原 .ts → asset-db refresh
→ 广播 record-changed(hero,5011)
→ 面板重查 → UI 刷新(含 HeroList 若变动)
```
## 9. 模块/文件布局
```
extensions/pixelhero-config-editor/
├── package.json # package_version:2, panels, contributions.messages/menu, deps: typescript, vue, esbuild
├── tsconfig.json
├── esbuild.config.mjs # 打包 main + 面板
├── i18n/{en,zh}.js
├── static/
│ ├── template/default/index.html
│ └── style/default/index.css
├── src/
│ ├── main/
│ │ ├── index.ts # 扩展入口 + onMessage 注册
│ │ └── store.ts # 三张表 TsConfigFile 单例 + 广播
│ ├── io/
│ │ ├── TsConfigFile.ts # 解析/序列化/patch/save
│ │ ├── recordValue.ts # RecordValue 类型与归一化(含 speedExpr
│ │ └── serializer.ts # serialize(entry) 确定性文本生成
│ ├── shared/ # 纯逻辑(无 Cocos/Editor 依赖,可独立测试)
│ │ ├── schema/
│ │ │ ├── types.ts # TableSchema/FieldSchema 定义
│ │ │ ├── registry.ts # 三张表 schema 注册
│ │ │ ├── hero.ts
│ │ │ ├── skill.ts
│ │ │ ├── field.ts
│ │ │ └── enums.ts # 枚举镜像 + assertEnumsMatchGame
│ │ ├── validation/
│ │ │ └── index.ts # validate() 规则
│ │ └── desc/
│ │ └── buildSkillDesc.ts # HeroSkillDesc 的 JS 移植(预览用)
│ └── panels/default/
│ ├── index.ts # Editor.Panel.define + Vue mount
│ └── app/ # Vue 组件App, MasterList, DetailForm, TriggerSlots, SkillMap, FieldList, Revive, Override, PreviewPane, ValidationBar
├── dist/ # 打包产物main.js, panels/default.js
└── __tests__/ # node:test 纯逻辑测试
├── tsConfigFile.roundtrip.test.ts
├── serializer.test.ts
├── validation.test.ts
├── speedExpr.test.ts
├── schema.test.ts
└── fixtures/ # 真实配置副本
```
## 10. 构建与开发
- `npm run build``esbuild` 同时打包 `main`platform=node, format=cjs与 `panels/default`platform=browser, format=iife, bundle vue输出到 `dist/`。
- 开发:改完 `build` → 在 Cocos"扩展管理器"重载扩展。
- 依赖:`typescript`IO 用)、`vue`(面板用)、`esbuild`(打包)、`fs-extra`(可选,读模板)。`typescript` 体积大但必要;打 main 时 bundle 进 dist。
## 11. 测试策略
遵循项目 `coding-standards.md`
- **逻辑层BLOCKING自动化`node:test`**
- `tsConfigFile.roundtrip`:用真实 `heroSet.ts`/`SkillSet.ts` 副本作 fixture → load → 读全部 → 不改动 → save → 与原文件逐字节相等(验证保号保注释)。
- 改动往返patch 一条英雄 → save → 重新 load → 读回值 == 改入值;且产物经 `createProgram` 语法合法。
- `serializer`:给定结构化值 → 序列化文本 → 再解析 → 等价。
- `speedExpr``AtkSpeedSet[AtkSpeedLv.Slow3].cd` ⇄ `{kind:'speed',level:'Slow3'}` 双向。
- `validation`:构造各类非法数据 → 断言 Issue 正确。
- `schema`:所有表 schema 字段 key 与对应 interface 一致;枚举镜像与游戏 `.ts` 枚举一致(`assertEnumsMatchGame`)。
- 命名/隔离/无外部依赖遵循测试规范fixture 为常量文件副本,不内联魔法数。
- **UIADVISORY**`production/qa/evidence/` 下手动走查文档(覆盖三表增删改查、校验阻断、预览一致性)+ 编辑器内截图。
## 12. 分阶段实施(供 writing-plans 细化)
| 阶段 | 产出 | 验证 |
|---|---|---|
| P0 脚手架 | package.json/panels/menu/build面板可从菜单打开显示 "hello" | 截图 |
| P1 IO 层 | TsConfigFile + RecordValue + serializer + roundtrip 测试 | 测试通过 |
| P2 schema+枚举+校验 | 三表 schema、enums、validate + 测试 | 测试通过 |
| P3 主进程 | store + 全部消息处理(先只读类) | 手动 query 验证 |
| P4 面板基础 | master 列表 + 标量/枚举/ref 字段编辑 + 保存往返 | 端到端改一个英雄保存 |
| P5 嵌套与预览 | 触发槽/技能图/驻场/复活/覆盖 + 描述预览 + 缩略图 | 编辑复杂英雄保存往返一致 |
| P6 收尾 | 新建/复制/删除/HeroList 同步/还原/打磨/QA 文档 | QA 走查通过 |
## 13. 风险与缓解
| 风险 | 缓解 |
|---|---|
| TS AST 往返破坏文件 | 条目级区间替换 + `.bak` + `createProgram` 校验 + 未识别表达式标记只读 |
| 符号表达式形态多样 | 固定匹配 `AtkSpeedSet[AtkSpeedLv.X].cd` 模式;其余降级为只读数值并提示 |
| 枚举镜像与游戏漂移 | `assertEnumsMatchGame` 调试期比对告警 |
| `typescript` 打包体积 | 仅 bundle 进 mainnode面板不包含可接受 |
| esbuild/Vue 在扩展环境运行 | P0 先打通最小面板Vue mount 成功)再扩展 |
| 多面板状态不一致 | 主进程单例 + 广播 record-changed |
| `HeroList` 失同步 | 英雄表写入后由 store 强制同步并校验 |
## 14. 验收标准
1. 扩展可从"面板"菜单打开,三张表可切换浏览、搜索。
2. 对三张表任一记录:可视化查看/编辑所有字段(含触发槽、技能图、覆盖、驻场、复活),保存后磁盘 `.ts` 被更新且为合法 TSasset-db 已刷新。
3. **保号往返**:仅查看后保存,文件零字节变化(测试断言)。
4. 实时校验:违反任一 error 规则时"保存"被阻断并列出问题;行/字段级可见。
5. 描述预览与游戏内 `buildSkillDesc` 输出一致。
6. 新建/复制/删除可用;`HeroList` 与 `HeroInfo` 始终一致。
7. 逻辑层单元测试全部通过BLOCKING
8. 未改动 `assets/script/game/**` 任何游戏运行时代码git diff 可证)。

View File

@@ -0,0 +1,3 @@
node_modules/
dist/
*.bak

View File

@@ -0,0 +1 @@
.tmp/

View File

@@ -0,0 +1,25 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { buildSkillDesc } from '../src/shared/desc/buildSkillDesc';
import { RecordValue } from '../src/io/recordValue';
const skillSet: Record<number, RecordValue> = {
6301: { kind: 'obj', props: { name: { kind: 'str', value: '护盾' }, kind: { kind: 'enumRef', qualifier: 'SkillKind', member: 'Shield' }, ap: { kind: 'num', value: 4 } } },
};
const fieldSet: Record<number, RecordValue> = {
7015: { kind: 'obj', props: { name: { kind: 'str', value: '死亡强化' }, info: { kind: 'str', value: '死亡触发技能次数+1' } } },
};
const hero: RecordValue = { kind: 'obj', props: {
atked: { kind: 'arr', items: [{ kind: 'obj', props: { s_uuid: { kind: 'num', value: 6301 }, t_num: { kind: 'num', value: 3 }, overrides: { kind: 'obj', props: { ap: { kind: 'num', value: 4 } } } } }] },
field: { kind: 'arr', items: [{ kind: 'num', value: 7015 }] },
} };
test('renders atked trigger with shield effect', () => {
const out = buildSkillDesc(hero, skillSet, fieldSet);
assert.match(out, /受击3次:护盾/);
assert.match(out, /护盾4次/);
});
test('renders field aura', () => {
const out = buildSkillDesc(hero, skillSet, fieldSet);
assert.match(out, /场上存活:死亡强化 死亡触发技能次数\+1/);
});

View File

@@ -0,0 +1,13 @@
// 测试夹具:镜像真实 heroSet.ts 片段speed 表达式 + 枚举引用 + 触发槽 + 驻场 + 复活)
export const HeroInfo: Record<number, any> = {
5011:{uuid:5011,name:"小铁卫",path:"hk1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Melee,hp:400,ap:20,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
atked:[{s_uuid:6301,t_num:3,overrides:{TGroup:TGroup.Self,ap:4}}],
field:[7015],
revive:{s_uuid:6501,r_num:1,upr:0.3},
info:"每受击3次为自身添加4层护盾"},
6001:{uuid:6001,name:"兽人战士",path:"m1", fac:FacSet.MON,lv:1,type:HType.Melee,hp:220,ap:10,speed:70,
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"基础近战位怪"},
};
export const HeroList: number[] = [5011];

View File

@@ -0,0 +1,16 @@
// 测试夹具:镜像真实 SkillSet.ts 片段(含 SkillSet + FieldSkillSet
export const SkillSet: Record<number, any> = {
6001:{uuid:6001,name:"火球",sp_name:"atk_1",icon:"Stat_Attack_01",TGroup:TGroup.Enemy,act:"atk",
DTType:DTType.single,ap:100,hit_count:1,hitcd:0.2,speed:720,with:0,ready:0.2,
IType:IType.Melee,RType:RType.bezier,EType:EType.collision,info:"造成攻击力100%的伤害"},
6301:{uuid:6301,name:"护盾",sp_name:"buff_wind",icon:"Stat_Defense",TGroup:TGroup.Self,act:"atk",
DTType:DTType.single,kind:SkillKind.Shield,ap:3,hit_count:1,hitcd:0.2,speed:720,with:0,ready:0.2,
IType:IType.support,RType:RType.fixed,EType:EType.animationEnd,info:"添加护盾"},
6501:{uuid:6501,name:"复活",sp_name:"buff_wind",icon:"Stat_HolyDamage",TGroup:TGroup.Self,act:"atk",
DTType:DTType.single,kind:SkillKind.Support,ap:50,hit_count:3,hitcd:0.2,speed:720,with:0,ready:0.2,
IType:IType.support,RType:RType.fixed,EType:EType.animationEnd,info:"复活百分比"},
};
export const FieldSkillSet: Record<number, any> = {
7015:{uuid:7015,name:"死亡强化",icon:"Stat_PoisonChanceIncrease",type:FieldSkillType.DeadCount,value:1,info:"死亡触发技能次数+1"},
};

View File

@@ -0,0 +1,47 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as ts from 'typescript';
import { parseExpression, findExportObjectLiteral } from '../src/io/parser';
function parse(text: string): ts.Expression {
const src = ts.createSourceFile('x.ts', `const _ = ${text};`, ts.ScriptTarget.Latest, true);
const decl = src.statements[0] as ts.VariableStatement;
return decl.declarationList.declarations[0].initializer!;
}
test('num / str / bool', () => {
assert.deepEqual(parseExpression(parse('400')), { kind: 'num', value: 400 });
assert.deepEqual(parseExpression(parse('"小铁卫"')), { kind: 'str', value: '小铁卫' });
assert.deepEqual(parseExpression(parse('true')), { kind: 'bool', value: true });
});
test('enumRef: FacSet.HERO', () => {
assert.deepEqual(parseExpression(parse('FacSet.HERO')), { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' });
});
test('speed: AtkSpeedSet[AtkSpeedLv.Slow3].cd', () => {
assert.deepEqual(parseExpression(parse('AtkSpeedSet[AtkSpeedLv.Slow3].cd')), { kind: 'speed', level: 'Slow3' });
});
test('non-Speed two-segment access falls back to enumRef', () => {
assert.deepEqual(parseExpression(parse('Foo.bar')), { kind: 'enumRef', qualifier: 'Foo', member: 'bar' });
});
test('arr', () => {
assert.deepEqual(parseExpression(parse('[1,2,3]')), { kind: 'arr', items: [
{ kind: 'num', value: 1 }, { kind: 'num', value: 2 }, { kind: 'num', value: 3 }] });
});
test('obj with numeric + identifier keys', () => {
const v = parseExpression(parse('{6001:{uuid:6001},name:"x"}'));
assert.equal(v.kind, 'obj');
assert.deepEqual(v.props['6001'], { kind: 'obj', props: { uuid: { kind: 'num', value: 6001 } } });
assert.deepEqual(v.props['name'], { kind: 'str', value: 'x' });
});
test('raw: unsupported expression preserved verbatim', () => {
const v = parseExpression(parse('a + b'));
assert.equal(v.kind, 'raw');
assert.equal((v as any).text, 'a + b');
});
test('findExportObjectLiteral locates HeroInfo', () => {
const src = ts.createSourceFile('x.ts',
`export const HeroInfo: Record<number, any> = { 5011:{uuid:5011} };`, ts.ScriptTarget.Latest, true);
const node = findExportObjectLiteral(src, 'HeroInfo');
assert.ok(node);
assert.equal(node!.properties.length, 1);
});

View File

@@ -0,0 +1,44 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { serializeValue, serializeEntry } from '../src/io/serializer';
import { RecordValue } from '../src/io/recordValue';
test('serializeValue: num/str/bool', () => {
assert.equal(serializeValue({ kind: 'num', value: 400 }), '400');
assert.equal(serializeValue({ kind: 'str', value: '小铁卫' }), '"小铁卫"');
assert.equal(serializeValue({ kind: 'bool', value: true }), 'true');
});
test('serializeValue: enumRef', () => {
const v: RecordValue = { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' };
assert.equal(serializeValue(v), 'FacSet.HERO');
});
test('serializeValue: speed', () => {
const v: RecordValue = { kind: 'speed', level: 'Slow3' };
assert.equal(serializeValue(v), 'AtkSpeedSet[AtkSpeedLv.Slow3].cd');
});
test('serializeValue: arr', () => {
const v: RecordValue = { kind: 'arr', items: [{ kind: 'num', value: 1 }, { kind: 'num', value: 2 }] };
assert.equal(serializeValue(v), '[1,2]');
});
test('serializeValue: obj', () => {
const v: RecordValue = { kind: 'obj', props: { a: { kind: 'num', value: 1 }, b: { kind: 'str', value: 'x' } } };
assert.equal(serializeValue(v), '{a:1,b:"x"}');
});
test('serializeValue: raw passthrough', () => {
const v: RecordValue = { kind: 'raw', text: 'a + b' };
assert.equal(serializeValue(v), 'a + b');
});
test('serializeEntry: all-scalar one line with trailing comma', () => {
const v: RecordValue = { kind: 'obj', props: { uuid: { kind: 'num', value: 1 }, name: { kind: 'str', value: 'x' } } };
assert.equal(serializeEntry('1', v), '1:{uuid:1,name:"x"},');
});
test('serializeEntry: nested on continuation lines', () => {
const v: RecordValue = { kind: 'obj', props: {
uuid: { kind: 'num', value: 5011 },
skills: { kind: 'obj', props: { 6001: { kind: 'obj', props: { uuid: { kind: 'num', value: 6001 } } } } },
} };
const out = serializeEntry('5011', v);
assert.match(out, /^5011:\{uuid:5011,$/m); // 首行:键 + 标量
assert.match(out, / skills:\{6001:\{uuid:6001\}\},$/m); // 续行 8 空格
assert.match(out, /^ \},$/m); // 闭合 4 空格
});

View File

@@ -0,0 +1,101 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, copyFileSync, mkdtempSync, readdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { TsConfigFile } from '../src/io/TsConfigFile';
const here = dirname(fileURLToPath(import.meta.url));
const fixture = join(here, 'fixtures', 'heroSet.sample.ts');
test('load + getKeys', () => {
const f = new TsConfigFile(fixture, 'HeroInfo');
f.load();
assert.deepEqual(f.getKeys(), ['5011', '6001']);
});
test('read returns structured value with speed/enumRef preserved', () => {
const f = new TsConfigFile(fixture, 'HeroInfo');
f.load();
const v = f.read('5011')!;
assert.equal(v.kind, 'obj');
assert.deepEqual(v.props['fac'], { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' });
const skill = v.props['skills'].props['6001'];
assert.deepEqual(skill.props['cd'], { kind: 'speed', level: 'Slow3' });
});
test('getText after load equals original file (no mutation on load)', () => {
const f = new TsConfigFile(fixture, 'HeroInfo');
f.load();
assert.equal(f.getText(), readFileSync(fixture, 'utf8'));
});
test('read missing key returns null', () => {
const f = new TsConfigFile(fixture, 'HeroInfo');
f.load();
assert.equal(f.read('9999'), null);
});
function withTempFixture(): { dir: string; file: string } {
const dir = mkdtempSync(join(tmpdir(), 'phcfg-'));
const file = join(dir, 'heroSet.sample.ts');
copyFileSync(fixture, file);
return { dir, file };
}
test('patch updates one entry; reload reads new value; other entries intact', () => {
const { file } = withTempFixture();
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
const v = f.read('5011')!;
v.props['ap'] = { kind: 'num', value: 99 };
f.patch('5011', v);
assert.equal(f.isDirty(), true);
assert.equal(f.save().ok, true);
const f2 = new TsConfigFile(file, 'HeroInfo'); f2.load();
assert.deepEqual(f2.read('5011')!.props['ap'], { kind: 'num', value: 99 });
assert.equal(f2.read('6001')!.props['name'].value, '兽人战士'); // 另一条未破坏
});
test('patch preserves speed + enumRef on reload', () => {
const { file } = withTempFixture();
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
const v = f.read('5011')!;
v.props['hp'] = { kind: 'num', value: 500 };
f.patch('5011', v); f.save();
const f2 = new TsConfigFile(file, 'HeroInfo'); f2.load();
const again = f2.read('5011')!;
assert.deepEqual(again.props['fac'], { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' });
assert.deepEqual(again.props['skills'].props['6001'].props['cd'], { kind: 'speed', level: 'Slow3' });
});
test('add appends entry readable after save+reload', () => {
const { file } = withTempFixture();
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
f.add('5099', { kind: 'obj', props: { uuid: { kind: 'num', value: 5099 }, name: { kind: 'str', value: '新英雄' } } });
f.save();
const f2 = new TsConfigFile(file, 'HeroInfo'); f2.load();
assert.ok(f2.getKeys().includes('5099'));
assert.equal(f2.read('5099')!.props['name'].value, '新英雄');
});
test('delete removes entry', () => {
const { file } = withTempFixture();
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
f.delete('6001'); f.save();
const f2 = new TsConfigFile(file, 'HeroInfo'); f2.load();
assert.ok(!f2.getKeys().includes('6001'));
assert.ok(f2.getKeys().includes('5011'));
});
test('save writes a .bak backup of the pre-save file', () => {
const { file } = withTempFixture();
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
const v = f.read('5011')!; v.props['hp'] = { kind: 'num', value: 1 }; f.patch('5011', v);
f.save();
assert.equal(readFileSync(file + '.bak', 'utf8'), readFileSync(fixture, 'utf8'));
});
test('save with no edits is a no-op (ok, no .bak written)', () => {
const { file, dir } = withTempFixture();
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
assert.equal(f.save().ok, true);
assert.equal(readdirSync(dir).some(p => p.endsWith('.bak')), false);
});

View File

@@ -0,0 +1,67 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { validate } from '../src/shared/validation';
import { RecordValue } from '../src/io/recordValue';
const ctx = {
hasSkill: (u: number) => [6001, 6301, 6501].includes(u),
hasField: (u: number) => [7015].includes(u),
hasHero: (u: number) => [5011, 6001].includes(u),
heroListKeys: new Set<string>(['5011']),
};
function heroObj(extra: Record<string, RecordValue> = {}): RecordValue {
return { kind: 'obj', props: {
uuid: { kind: 'num', value: 5011 }, name: { kind: 'str', value: 'x' }, path: { kind: 'str', value: 'p' },
fac: { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' }, lv: { kind: 'num', value: 1 },
type: { kind: 'enumRef', qualifier: 'HType', member: 'Melee' }, hp: { kind: 'num', value: 400 },
ap: { kind: 'num', value: 20 },
skills: { kind: 'obj', props: { 6001: { kind: 'obj', props: { uuid: { kind: 'num', value: 6001 }, lv: { kind: 'num', value: 1 }, cd: { kind: 'speed', level: 'Slow3' }, ccd: { kind: 'num', value: 0 } } } } },
info: { kind: 'str', value: 'ok' },
...extra,
} };
}
test('valid hero → no errors', () => {
const issues = validate('hero', new Map([['5011', heroObj()]]), ctx);
assert.equal(issues.filter(i => i.severity === 'error').length, 0);
});
test('duplicate uuid → error', () => {
// 两条记录携带相同的 uuid 字段值Map 键不同uuid 字段相同)
const dup = heroObj(); // uuid=5011
const issues = validate('hero', new Map([['5011', heroObj()], ['5012', dup]]), ctx);
assert.ok(issues.some(i => i.severity === 'error' && i.code === 'dup-uuid'));
});
test('missing required field → error', () => {
const h = heroObj(); delete h.props['name'];
const issues = validate('hero', new Map([['5011', h]]), ctx);
assert.ok(issues.some(i => i.code === 'missing-required' && i.fieldPath === 'name'));
});
test('trigger slot references missing skill → error', () => {
const h = heroObj({ atked: { kind: 'arr', items: [{ kind: 'obj', props: {
s_uuid: { kind: 'num', value: 9999 }, t_num: { kind: 'num', value: 3 } } }] } });
const issues = validate('hero', new Map([['5011', h]]), ctx);
assert.ok(issues.some(i => i.code === 'dangling-ref'));
});
test('field list references missing field skill → error', () => {
const h = heroObj({ field: { kind: 'arr', items: [{ kind: 'num', value: 8888 }] } });
const issues = validate('hero', new Map([['5011', h]]), ctx);
assert.ok(issues.some(i => i.code === 'dangling-ref'));
});
test('HeroList/hero consistency: fac=HERO entry missing from HeroList → error', () => {
const ctx2 = { ...ctx, heroListKeys: new Set<string>([]) };
const issues = validate('hero', new Map([['5011', heroObj()]]), ctx2);
assert.ok(issues.some(i => i.code === 'herolist-inconsistent'));
});
test('invalid enum value → error', () => {
const h = heroObj({ fac: { kind: 'enumRef', qualifier: 'FacSet', member: 'NOPE' } });
const issues = validate('hero', new Map([['5011', h]]), ctx);
assert.ok(issues.some(i => i.code === 'bad-enum'));
});
test('unknown overrides key → error', () => {
const h = heroObj({ atked: { kind: 'arr', items: [{ kind: 'obj', props: {
s_uuid: { kind: 'num', value: 6301 }, t_num: { kind: 'num', value: 3 },
overrides: { kind: 'obj', props: { bogus: { kind: 'num', value: 1 } } } } }] } });
const issues = validate('hero', new Map([['5011', h]]), ctx);
assert.ok(issues.some(i => i.code === 'bad-override-key'));
});

View File

@@ -0,0 +1,29 @@
import { build, context } from 'esbuild';
import { readFileSync } from 'fs';
import { join } from 'path';
const watch = process.argv.includes('--watch');
const common = {
bundle: true,
sourcemap: false,
logLevel: 'info',
alias: { 'vue': 'vue/dist/vue.esm-bundler.js' },
};
// 面板进程在 Cocos 的 Electron 渲染层运行,可访问 Node 内建fs/path但 vue 必须
// 打进浏览器侧 IIFE。因此面板项用 platform:'browser' + 把 node: 内建标为 external
// 交由运行时解析;这样 esbuild 既不抱怨,又保持 plan 的"运行时读 static 文件"语义。
const entries = [
{ entryPoints: ['src/main/index.ts'], outfile: 'dist/main.js', platform: 'node', format: 'cjs', external: [] },
{ entryPoints: ['src/panels/default/index.ts'], outfile: 'dist/panels/default.js', platform: 'browser', format: 'iife', external: ['node:fs', 'node:path'] },
];
if (watch) {
for (const e of entries) {
const ctx = await context({ ...common, ...e });
await ctx.watch();
}
} else {
for (const e of entries) await build({ ...common, ...e });
}

View File

@@ -0,0 +1,4 @@
exports.en = {
'pixelhero-config-editor': { description: 'Hero/Skill Config Editor', title: 'Hero/Skill Config' },
'menu': { 'panel/英雄技能配置': 'Hero/Skill Config' },
};

View File

@@ -0,0 +1,4 @@
exports.zh = {
'pixelhero-config-editor': { description: '英雄/技能配置编辑器', title: '英雄/技能配置' },
'menu': { 'panel/英雄技能配置': '英雄技能配置' },
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
{
"package_version": 2,
"name": "pixelhero-config-editor",
"version": "0.1.0",
"description": "英雄/技能配置编辑器",
"main": "./dist/main.js",
"author": "pixelhero",
"editor": ">=3.8.0",
"scripts": {
"build": "node esbuild.config.mjs",
"watch": "node esbuild.config.mjs --watch",
"test": "tsx --test __tests__/*.test.ts"
},
"panels": {
"default": {
"title": "英雄/技能配置",
"type": "dockable",
"main": "./dist/panels/default.js",
"size": { "width": 960, "height": 640, "min-width": 640, "min-height": 480 }
}
},
"contributions": {
"menu": [
{ "path": "PixelHero/英雄技能配置", "message": "open-panel" }
],
"messages": {
"open-panel": { "methods": ["open-panel"] },
"query-schema": { "methods": ["query-schema"] },
"query-enums": { "methods": ["query-enums"] },
"query-keys": { "methods": ["query-keys"] },
"query-record": { "methods": ["query-record"] },
"query-preview-desc": { "methods": ["query-preview-desc"] },
"validate": { "methods": ["validate"] },
"save-record": { "methods": ["save-record"] },
"revert-record": { "methods": ["revert-record"] }
}
},
"dependencies": {
"typescript": "^5.4.0",
"vue": "^3.4.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"esbuild": "^0.20.0",
"fs-extra": "^11.0.0",
"tsx": "^4.7.0"
}
}

View File

@@ -0,0 +1,110 @@
import * as fs from 'node:fs';
import * as ts from 'typescript';
import { RecordValue } from './recordValue';
import { findExportObjectLiteral, parseExpression } from './parser';
import { serializeEntry } from './serializer';
/**
* 以"条目级 AST 区间替换"方式读写一个 `export const <name>: Record<number,X> = {...}` 配置文件。
* 保留符号表达式speed与枚举引用enumRef未识别表达式原样保留raw
* save() 纯逻辑:仅做 fs 读写 + 语法校验(写 .bakasset-db 刷新由主进程在 save 成功后调用。
*/
export class TsConfigFile {
private sourceText = '';
private sourceFile!: ts.SourceFile;
private targetNode!: ts.ObjectLiteralExpression;
private dirty = false;
constructor(public readonly filePath: string, public readonly exportName: string) {}
load(): void {
this.sourceText = fs.readFileSync(this.filePath, 'utf8');
this.reparse();
this.dirty = false;
}
private reparse(): void {
// 第 4 参数 setParentNodes 必须为 true否则 node.getStart()/getEnd() 不可用。
this.sourceFile = ts.createSourceFile(this.filePath, this.sourceText, ts.ScriptTarget.Latest, true);
const node = findExportObjectLiteral(this.sourceFile, this.exportName);
if (!node) throw new Error(`export const ${this.exportName} not found or not an object literal in ${this.filePath}`);
this.targetNode = node;
}
getKeys(): string[] {
return this.targetNode.properties.map(p => (p.name as ts.PropertyName).getText());
}
read(key: string): RecordValue | null {
const entry = this.findEntry(key);
if (!entry) return null;
return parseExpression(entry.initializer);
}
private findEntry(key: string): ts.PropertyAssignment | undefined {
return this.targetNode.properties.find(p => (p.name as ts.PropertyName).getText() === key) as ts.PropertyAssignment | undefined;
}
getText(): string { return this.sourceText; }
isDirty(): boolean { return this.dirty; }
patch(key: string, value: RecordValue): void {
const entry = this.findEntry(key);
const newText = serializeEntry(key, value);
if (entry) {
const { start, end } = this.entrySpan(entry);
this.sourceText = this.sourceText.slice(0, start) + newText + this.sourceText.slice(end);
} else {
this.insertEntry(newText);
}
this.reparse();
this.dirty = true;
}
add(key: string, value: RecordValue): void {
if (this.findEntry(key)) throw new Error(`key ${key} already exists`);
this.insertEntry(serializeEntry(key, value));
this.reparse();
this.dirty = true;
}
delete(key: string): void {
const entry = this.findEntry(key);
if (!entry) return;
const { start, end } = this.entrySpan(entry);
this.sourceText = this.sourceText.slice(0, start) + this.sourceText.slice(end);
this.reparse();
this.dirty = true;
}
save(): { ok: true } | { ok: false; error: string } {
if (!this.dirty) return { ok: true };
const check = ts.createSourceFile(this.filePath, this.sourceText, ts.ScriptTarget.Latest, true);
if (check.parseDiagnostics.length > 0) {
const msg = check.parseDiagnostics.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n')).join('; ');
return { ok: false, error: `syntax error after edit: ${msg}` };
}
// .bak 取自磁盘原始内容(落盘前的当前文件),保证可回滚。
fs.writeFileSync(this.filePath + '.bak', fs.readFileSync(this.filePath));
fs.writeFileSync(this.filePath, this.sourceText);
this.dirty = false;
return { ok: true };
}
/** 条目文本区间:从 key 词法起始(不含前导缩进 trivia避免吞掉上一行换行到尾随逗号止。 */
private entrySpan(entry: ts.PropertyAssignment): { start: number; end: number } {
const start = entry.getStart();
let end = entry.getEnd();
const comma = /^\s*,/.exec(this.sourceText.slice(end));
if (comma) end += comma[0].length;
return { start, end };
}
/** 在闭合 `}` 前追加一条新记录4 空格缩进)。 */
private insertEntry(entryText: string): void {
const closeBrace = this.targetNode.getLastToken();
if (!closeBrace) throw new Error('object literal has no closing brace');
const pos = closeBrace.getStart();
this.sourceText = this.sourceText.slice(0, pos) + ` ${entryText}\n ` + this.sourceText.slice(pos);
}
}

View File

@@ -0,0 +1,65 @@
import * as ts from 'typescript';
import { RecordValue } from './recordValue';
/** 在 SourceFile 中查找 `export const <name> = {...}`,返回其对象字面量节点。 */
export function findExportObjectLiteral(src: ts.SourceFile, name: string): ts.ObjectLiteralExpression | null {
let result: ts.ObjectLiteralExpression | null = null;
const visit = (node: ts.Node) => {
if (result) return;
if (ts.isVariableStatement(node)) {
for (const d of node.declarationList.declarations) {
if (d.name.getText() === name && d.initializer && ts.isObjectLiteralExpression(d.initializer)) {
result = d.initializer;
return;
}
}
}
ts.forEachChild(node, visit);
};
visit(src);
return result;
}
/** 识别 `AtkSpeedSet[AtkSpeedLv.<level>].cd`,否则 null。 */
function tryParseSpeedExpr(node: ts.PropertyAccessExpression): RecordValue | null {
if (node.name.text !== 'cd') return null;
const inner = node.expression;
if (!ts.isElementAccessExpression(inner)) return null;
if (!ts.isIdentifier(inner.expression) || inner.expression.text !== 'AtkSpeedSet') return null;
const arg = inner.argumentExpression;
if (!ts.isPropertyAccessExpression(arg)) return null;
if (!ts.isIdentifier(arg.expression) || arg.expression.text !== 'AtkSpeedLv') return null;
return { kind: 'speed', level: arg.name.text };
}
export function parseExpression(node: ts.Expression): RecordValue {
if (ts.isNumericLiteral(node)) return { kind: 'num', value: Number(node.text) };
if (ts.isStringLiteral(node)) return { kind: 'str', value: node.text };
if (node.kind === ts.SyntaxKind.TrueKeyword) return { kind: 'bool', value: true };
if (node.kind === ts.SyntaxKind.FalseKeyword) return { kind: 'bool', value: false };
if (ts.isPropertyAccessExpression(node)) {
const speed = tryParseSpeedExpr(node);
if (speed) return speed;
if (ts.isIdentifier(node.expression)) {
return { kind: 'enumRef', qualifier: node.expression.text, member: node.name.text };
}
return { kind: 'raw', text: node.getText() }; // 多级 a.b.c 等
}
if (ts.isArrayLiteralExpression(node)) {
return { kind: 'arr', items: node.elements.map(e => parseExpression(e)) };
}
if (ts.isObjectLiteralExpression(node)) {
const props: Record<string, RecordValue> = {};
for (const m of node.properties) {
if (ts.isPropertyAssignment(m)) {
props[m.name.getText()] = parseExpression(m.initializer);
}
}
return { kind: 'obj', props };
}
return { kind: 'raw', text: node.getText() }; // 二元/模板/调用/其他元素访问
}

View File

@@ -0,0 +1,15 @@
/** IO 层对配置对象字面量值的结构化表示。零依赖。 */
export type RecordValue =
| { kind: 'num'; value: number }
| { kind: 'str'; value: string }
| { kind: 'bool'; value: boolean }
| { kind: 'enumRef'; qualifier: string; member: string } // 如 FacSet.HERO
| { kind: 'speed'; level: string } // AtkSpeedSet[AtkSpeedLv.X].cd
| { kind: 'obj'; props: Record<string, RecordValue> }
| { kind: 'arr'; items: RecordValue[] }
| { kind: 'raw'; text: string }; // 未识别表达式,原样保留
export function isScalar(v: RecordValue): boolean {
return v.kind === 'num' || v.kind === 'str' || v.kind === 'bool'
|| v.kind === 'enumRef' || v.kind === 'speed' || v.kind === 'raw';
}

View File

@@ -0,0 +1,32 @@
import { RecordValue, isScalar } from './recordValue';
export function serializeValue(v: RecordValue): string {
switch (v.kind) {
case 'num': return String(v.value);
case 'str': return JSON.stringify(v.value);
case 'bool': return String(v.value);
case 'enumRef': return `${v.qualifier}.${v.member}`;
case 'speed': return `AtkSpeedSet[AtkSpeedLv.${v.level}].cd`;
case 'arr': return `[${v.items.map(serializeValue).join(',')}]`;
case 'obj': {
const body = Object.entries(v.props).map(([k, val]) => `${k}:${serializeValue(val)}`).join(',');
return `{${body}}`;
}
case 'raw': return v.text;
}
}
/** 序列化一条顶层记录。首行无缩进(缩进由调用处保留的 trivia 提供);嵌套字段 8 空格续行;闭合 4 空格。 */
export function serializeEntry(key: string, obj: RecordValue): string {
if (obj.kind !== 'obj') return `${key}:${serializeValue(obj)},`;
const props = obj.props;
const keys = Object.keys(props);
const scalars = keys.filter(k => isScalar(props[k]));
const nested = keys.filter(k => !isScalar(props[k]));
const scalarTxt = scalars.map(k => `${k}:${serializeValue(props[k])}`).join(',');
if (nested.length === 0) return `${key}:{${scalarTxt}},`;
const lines = [`${key}:{${scalarTxt},`];
for (const k of nested) lines.push(` ${k}:${serializeValue(props[k])},`);
lines.push(' },');
return lines.join('\n');
}

View File

@@ -0,0 +1,48 @@
import { store } from './store';
/**
* Cocos Creator 3.8 扩展主进程入口。
* 关键约定(与官方 first/message 文档一致):
* - 处理函数必须挂在 `methods` 对象里Cocos 读取 module.exports.methods
* methods 的 key 必须与 package.json 中 contributions.messages[*].methods 引用的名字一致。
* - 生命周期钩子为 `load`/`unload`(不是 onLoad
* - 处理函数直接接收消息参数(无 event返回值即 Editor.Message.request 的 resolve 结果。
*/
function load() {
store.reloadAll();
}
function unload() {
// 进程级单例随扩展卸载自然销毁,无需显式清理。
}
const methods: Record<string, (...args: any[]) => any> = {
'open-panel'() {
Editor.Panel.open('pixelhero-config-editor');
},
'query-schema'(id?: string) {
return store.querySchema(id as any);
},
'query-enums'() {
return store.queryEnums();
},
'query-keys'(id: string) {
return store.queryKeys(id as any);
},
'query-record'(id: string, key: string) {
return store.queryRecord(id as any, key);
},
'query-preview-desc'(hero: any) {
return store.queryPreviewDesc(hero);
},
'validate'(id: string) {
return store.validate(id as any);
},
'save-record'(id: string, key: string, value: any) {
return store.saveRecord(id as any, key, value);
},
'revert-record'(id: string, key: string) {
return store.revertRecord(id as any, key);
},
};
module.exports = { methods, load, unload };

View File

@@ -0,0 +1,85 @@
import { join } from 'node:path';
import { TsConfigFile } from '../io/TsConfigFile';
import { allSchemas } from '../shared/schema/registry';
import { ENUMS } from '../shared/schema/enums';
import { validate, ValidationContext } from '../shared/validation';
import { buildSkillDesc } from '../shared/desc/buildSkillDesc';
import { RecordValue } from '../io/recordValue';
import { TableId } from '../shared/schema/types';
/** 游戏配置目录(相对项目根)。主进程用 Editor.Project.path 解析到项目根的 assets 配置目录。 */
const CONFIG_DIR = join(Editor.Project.path, 'assets/script/game/common/config');
interface Entry { file: TsConfigFile; }
const tables: Partial<Record<TableId, Entry>> = {};
// skill 与 field 共享 SkillSet.ts但用不同 exportName 的 TsConfigFile 实例
const fileCache: Record<string, TsConfigFile> = {};
function getTable(id: TableId): Entry {
if (tables[id]) return tables[id]!;
const schema = allSchemas().find(s => s.id === id)!;
const cacheKey = `${schema.sourceFile}:${schema.exportName}`;
let file = fileCache[cacheKey];
if (!file) { file = new TsConfigFile(join(CONFIG_DIR, schema.sourceFile), schema.exportName); file.load(); fileCache[cacheKey] = file; }
const entry = { file };
tables[id] = entry;
return entry;
}
function recordsOf(id: TableId): Map<string, RecordValue> {
const { file } = getTable(id);
const m = new Map<string, RecordValue>();
for (const k of file.getKeys()) { const v = file.read(k); if (v) m.set(k, v); }
return m;
}
function buildContext(): ValidationContext {
const hero = recordsOf('hero');
const skill = recordsOf('skill');
const field = recordsOf('field');
const heroKeys = Array.from(hero.keys()).map(Number);
const skillKeys = Array.from(skill.keys()).map(Number);
const fieldKeys = Array.from(field.keys()).map(Number);
// HeroList 读取hero 表的 listExportName
const heroList = new TsConfigFile(join(CONFIG_DIR, 'heroSet.ts'), 'HeroList');
heroList.load();
const listRaw = heroList.getText();
const heroListKeys = new Set<string>(
(listRaw.match(/HeroList[\s\S]*?=\s*\[([^\]]*)\]/)?.[1].match(/-?\d+/g) ?? []).map(String)
);
return {
hasHero: (u) => heroKeys.includes(u),
hasSkill: (u) => skillKeys.includes(u),
hasField: (u) => fieldKeys.includes(u),
heroListKeys,
};
}
export const store = {
queryEnums: () => ENUMS,
querySchema: (id?: TableId) => id ? allSchemas().find(s => s.id === id) : allSchemas(),
queryKeys: (id: TableId) => getTable(id).file.getKeys(),
queryRecord: (id: TableId, key: string) => getTable(id).file.read(key),
queryPreviewDesc: (hero: RecordValue) => {
const skill = recordsOf('skill'); const field = recordsOf('field');
const skillSet: Record<number, RecordValue> = {};
for (const [k, v] of skill) skillSet[Number(k)] = v;
const fieldSet: Record<number, RecordValue> = {};
for (const [k, v] of field) fieldSet[Number(k)] = v;
return buildSkillDesc(hero, skillSet, fieldSet);
},
validate: (id: TableId) => validate(id, recordsOf(id), buildContext()),
saveRecord: (id: TableId, key: string, value: RecordValue) => {
// 先在副本上试写并校验
const { file } = getTable(id);
file.patch(key, value);
const issues = validate(id, recordsOf(id), buildContext()).filter(i => i.key === key && i.severity === 'error');
if (issues.length) { file.load(); return { ok: false as const, issues }; } // 回滚内存
const r = file.save();
if (!r.ok) { file.load(); return { ok: false as const, issues: [] }; }
void Editor.Message.request('asset-db', 'refresh-asset', `db://assets/script/game/common/config/${allSchemas().find(s => s.id === id)!.sourceFile}`);
return { ok: true as const, issues: [] };
},
revertRecord: (id: TableId, key: string) => { getTable(id).file.load(); return getTable(id).file.read(key); },
reloadAll: () => { for (const k of Object.keys(fileCache)) fileCache[k].load(); },
};

View File

@@ -0,0 +1,38 @@
import { createApp, defineComponent, reactive } from 'vue';
export const App = defineComponent({
setup() {
const state = reactive({ table: 'hero' as string, keys: [] as string[], picked: '' as string, detail: '' as string });
async function load() {
const keys = await Editor.Message.request('pixelhero-config-editor', 'query-keys', state.table);
state.keys = keys || [];
state.picked = ''; state.detail = '';
}
async function pick(k: string) {
state.picked = k;
const v = await Editor.Message.request('pixelhero-config-editor', 'query-record', state.table, k);
state.detail = JSON.stringify(v, null, 2);
}
load();
return { state, load, pick };
},
template: `
<div>
<div class="row">
<label>表:</label>
<select v-model="state.table" @change="load">
<option value="hero">英雄/怪物</option>
<option value="skill">技能</option>
<option value="field">驻场技能</option>
</select>
<span style="margin-left:12px;color:#888">共 {{ state.keys.length }} 条(端到端 IPC 已打通)</span>
</div>
<ul>
<li v-for="k in state.keys" :key="k" @click="pick(k)" :style="state.picked===k ? 'font-weight:bold' : ''">{{ k }}</li>
</ul>
<pre v-if="state.detail" style="white-space:pre-wrap;background:var(--color-normal-fill);padding:8px">{{ state.detail }}</pre>
</div>
`,
});
export function mount(el: HTMLElement) { createApp(App).mount(el); }

View File

@@ -0,0 +1,14 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { mount } from './app';
const template = readFileSync(join(__dirname, '../../../static/template/default/index.html'), 'utf-8');
const style = readFileSync(join(__dirname, '../../../static/style/default/index.css'), 'utf-8');
module.exports = Editor.Panel.define({
template,
style,
$: { app: '#app' },
ready() { mount(this.$.app); },
close() { /* Vue 卸载随面板进程退出自动清理 */ },
});

View File

@@ -0,0 +1,63 @@
import { RecordValue } from '../../io/recordValue';
const TRIGGER_KEYS = ['call', 'dead', 'fstart', 'fend', 'atking', 'atked'] as const;
const TRIGGER_DESC: Record<string, string> = {
call: '召唤时', dead: '死亡时', fstart: '战斗开始时', fend: '战斗结束时',
field: '场上存活', atking: '攻击n次', atked: '受击n次', revive: '复活时',
};
function num(v: RecordValue | undefined): number | undefined { return v && v.kind === 'num' ? v.value : undefined; }
function str(v: RecordValue | undefined): string | undefined { return v && v.kind === 'str' ? v.value : undefined; }
function member(v: RecordValue | undefined): string | undefined { return v && v.kind === 'enumRef' ? v.member : undefined; }
function buildEffect(merged: RecordValue): string {
if (merged.kind !== 'obj') return '';
const kind = member(merged.props['kind']);
const ap = num(merged.props['ap']) ?? 0;
const parts: string[] = [];
if (kind === 'Heal') parts.push(`治疗伙伴${ap}`);
else if (kind === 'Shield') parts.push(`护盾${ap}`);
else if (kind === 'Gold') parts.push(`金币+${num(merged.props['gold']) ?? 0}`);
else if (kind === 'Support') parts.push(String(str(merged.props['info']) ?? ''));
else { // Damage / undefined
parts.push(`伤害${ap}%`);
if ((num(merged.props['hit_count']) ?? 1) > 1) parts.push(`${num(merged.props['hit_count'])}`);
if (num(merged.props['crt'])) parts.push(`暴击+${num(merged.props['crt'])}%`);
if (num(merged.props['stun'])) parts.push(`击晕+${num(merged.props['stun'])}%`);
}
return parts.join(' ');
}
/** 合并 overrides 到 base浅合并对象 props */
function merge(base: RecordValue, overrides: RecordValue | undefined): RecordValue {
if (!overrides || overrides.kind !== 'obj' || base.kind !== 'obj') return base;
return { kind: 'obj', props: { ...base.props, ...overrides.props } };
}
export function buildSkillDesc(hero: RecordValue, skillSet: Record<number, RecordValue>, fieldSet: Record<number, RecordValue>): string {
if (hero.kind !== 'obj') return '';
const lines: string[] = [];
for (const key of TRIGGER_KEYS) {
const arr = hero.props[key];
if (!arr || arr.kind !== 'arr') continue;
const tpl = TRIGGER_DESC[key] ?? key;
for (const it of arr.items) {
if (it.kind !== 'obj') continue;
const su = num(it.props['s_uuid']); if (su === undefined) continue;
const base = skillSet[su]; if (!base) continue;
const merged = merge(base, it.props['overrides']);
const trigger = tpl.includes('n') ? tpl.replace('n', String(num(it.props['t_num']) ?? '')) : tpl;
lines.push(`${trigger}:${str(base.props['name'])} ${buildEffect(merged)}`);
}
}
const fl = hero.props['field'];
if (fl && fl.kind === 'arr') {
for (const it of fl.items) { const u = num(it); if (u === undefined) continue; const fs = fieldSet[u]; if (fs) lines.push(`${TRIGGER_DESC.field}:${str(fs.props['name'])} ${str(fs.props['info'])}`); }
}
const rv = hero.props['revive'];
if (rv && rv.kind === 'obj') {
const su = num(rv.props['s_uuid']); const base = su !== undefined ? skillSet[su] : undefined;
if (base) lines.push(`${TRIGGER_DESC.revive} : ${str(base.props['name'])} ${buildEffect(merge(base, undefined))}`);
}
return lines.join('\n');
}

View File

@@ -0,0 +1,55 @@
export interface EnumMember { label: string; value: number | string; }
export interface EnumDef { qualifier: string; members: EnumMember[]; }
export const ENUMS: Record<string, EnumDef> = {
HType: { qualifier: 'HType', members: [
{ label: '近战 Melee', value: 0 }, { label: '中程 Mid', value: 1 }, { label: '远程 Long', value: 2 } ] },
FacSet: { qualifier: 'FacSet', members: [
{ label: '英雄 HERO', value: 0 }, { label: '怪物 MON', value: 1 } ] },
TGroup: { qualifier: 'TGroup', members: [
{ label: '自身 Self', value: 0 }, { label: '友方含己 Ally', value: 1 },
{ label: '友方 Team', value: 2 }, { label: '敌方 Enemy', value: 3 }, { label: '全体 All', value: 4 } ] },
DTType: { qualifier: 'DTType', members: [
{ label: '单体 single', value: 0 }, { label: '范围 range', value: 1 }, { label: '3x3 aoe_grid', value: 2 } ] },
SkillKind: { qualifier: 'SkillKind', members: [
{ label: '伤害 Damage', value: 0 }, { label: '治疗 Heal', value: 1 }, { label: '护盾 Shield', value: 2 },
{ label: '辅助 Support', value: 3 }, { label: '金币 Gold', value: 4 } ] },
DType: { qualifier: 'DType', members: [
{ label: '物理 ATK', value: 0 }, { label: '冰 ICE', value: 1 }, { label: '火 FIRE', value: 2 }, { label: '风 WIND', value: 3 } ] },
IType: { qualifier: 'IType', members: [
{ label: '近战 Melee', value: 0 }, { label: '远程 remote', value: 1 }, { label: '辅助 support', value: 2 } ] },
RType: { qualifier: 'RType', members: [
{ label: '直线 linear', value: 0 }, { label: '贝塞尔 bezier', value: 1 },
{ label: '固定起点 fixed', value: 2 }, { label: '固定终点 fixedEnd', value: 3 } ] },
EType: { qualifier: 'EType', members: [
{ label: '动画结束 animationEnd', value: 0 }, { label: '时间结束 timeEnd', value: 1 }, { label: '碰撞 collision', value: 2 } ] },
FieldSkillType: { qualifier: 'FieldSkillType', members: [
{ label: '召唤次数 SummonCount', value: 1 }, { label: '死亡次数 DeadCount', value: 2 },
{ label: '开场次数 StartCount', value: 3 }, { label: '结束次数 EndCount', value: 4 },
{ label: '每波金币 WaveGold', value: 5 }, { label: '卖出金币 SellGold', value: 6 },
{ label: '战后回复 WaveHeal', value: 7 }, { label: '英雄攻击 HeroAtk', value: 8 },
{ label: '英雄击晕 HeroStun', value: 9 }, { label: '英雄暴击 HeroCrit', value: 10 },
{ label: '英雄暴伤 HeroCritDamage', value: 11 }, { label: '英雄攻速 HeroSpeed', value: 12 },
{ label: '购买优惠 BuyDiscount', value: 13 }, { label: '刷新优惠 RefreshDiscount', value: 14 },
{ label: '英雄生命 HeroHp', value: 16 }, { label: '英雄风怒 HeroWindFury', value: 17 },
{ label: '英雄穿刺 HeroPuncture', value: 18 }, { label: '攻击次数 AtkCount', value: 19 },
{ label: '受击次数 BeAtkCount', value: 20 } ] },
AtkSpeedLv: { qualifier: 'AtkSpeedLv', members: [
{ label: '极速++ VeryFast1', value: 1 }, { label: '极速+ VeryFast2', value: 2 }, { label: '极速 VeryFast3', value: 3 },
{ label: '快速++ Fast1', value: 4 }, { label: '快速+ Fast2', value: 5 }, { label: '快速 Fast3', value: 6 },
{ label: '中速++ Normal1', value: 7 }, { label: '中速+ Normal2', value: 8 }, { label: '中速 Normal3', value: 9 },
{ label: '一般+ Mid1', value: 10 }, { label: '一般 Mid2', value: 11 }, { label: '一般- Mid3', value: 12 },
{ label: '慢 Slow1', value: 13 }, { label: '慢+ Slow2', value: 14 }, { label: '慢++ Slow3', value: 15 },
{ label: '很慢 VerySlow1', value: 16 }, { label: '很慢+ VerySlow2', value: 17 }, { label: '很慢++ VerySlow3', value: 18 } ] },
Attrs: { qualifier: 'Attrs', members: [
{ label: 'ap', value: 'ap' }, { label: 'hp', value: 'hp' }, { label: 'hp_max', value: 'hp_max' },
{ label: 'critical', value: 'critical' }, { label: 'critical_damage', value: 'critical_damage' },
{ label: 'stun_chance', value: 'stun_chance' }, { label: 'puncture_chance', value: 'puncture_chance' },
{ label: 'wfuny', value: 'wfuny' }, { label: 'freeze_chance', value: 'freeze_chance' },
{ label: 'knockback_chance', value: 'knockback_chance' } ] },
};
/** qualifier如 'FacSet')→ 对应 enumId如 'FacSet')。当前两者同名。 */
export const QUALIFIER_TO_ID: Record<string, string> = Object.fromEntries(
Object.entries(ENUMS).map(([id, def]) => [def.qualifier, id])
);

View File

@@ -0,0 +1,14 @@
import { TableSchema } from './types';
export const fieldSchema: TableSchema = {
id: 'field', label: '驻场技能',
sourceFile: 'SkillSet.ts', exportName: 'FieldSkillSet',
idSegments: [{ label: '驻场技能', min: 7000, max: 7999 }],
fields: [
{ key: 'uuid', label: 'UUID', type: 'number', required: true, group: '基础' },
{ key: 'name', label: '名称', type: 'string', required: true, group: '基础' },
{ key: 'icon', label: '图标', type: 'string', required: true, group: '基础' },
{ key: 'type', label: '类型', type: 'enum', enumRef: 'FieldSkillType', required: true, group: '效果' },
{ key: 'value', label: '数值', type: 'number', required: true, group: '效果' },
{ key: 'info', label: '描述', type: 'string', required: true, group: '基础' },
],
};

View File

@@ -0,0 +1,33 @@
import { TableSchema } from './types';
export const heroSchema: TableSchema = {
id: 'hero', label: '英雄/怪物',
sourceFile: 'heroSet.ts', exportName: 'HeroInfo', listExportName: 'HeroList',
idSegments: [
{ label: '英雄', min: 5000, max: 5999 },
{ label: '怪物', min: 6000, max: 6999 },
],
fields: [
{ key: 'uuid', label: 'UUID', type: 'number', required: true, group: '基础' },
{ key: 'name', label: '名称', type: 'string', required: true, group: '基础' },
{ key: 'path', label: '资源路径', type: 'string', required: true, group: '基础' },
{ key: 'icon', label: '图标', type: 'string', group: '基础' },
{ key: 'fac', label: '阵营', type: 'enum', enumRef: 'FacSet', required: true, group: '基础' },
{ key: 'pool_lv', label: '卡片等级', type: 'number', group: '基础' },
{ key: 'lv', label: '英雄等级', type: 'number', required: true, default: 1, group: '基础' },
{ key: 'type', label: '攻击定位', type: 'enum', enumRef: 'HType', required: true, group: '基础' },
{ key: 'hp', label: '生命上限', type: 'number', required: true, group: '基础' },
{ key: 'ap', label: '攻击力', type: 'number', required: true, group: '基础' },
{ key: 'dis', label: '攻击距离', type: 'number', group: '基础' },
{ key: 'speed', label: '移动速度', type: 'number', group: '基础' },
{ key: 'skills', label: '携带技能', type: 'skillMap', required: true, group: '技能' },
{ key: 'call', label: '召唤触发', type: 'triggerSlots', group: '触发技能' },
{ key: 'dead', label: '死亡触发', type: 'triggerSlots', group: '触发技能' },
{ key: 'fstart', label: '战斗开始触发', type: 'triggerSlots', group: '触发技能' },
{ key: 'fend', label: '战斗结束触发', type: 'triggerSlots', group: '触发技能' },
{ key: 'atking', label: '攻击触发', type: 'triggerSlots', group: '触发技能' },
{ key: 'atked', label: '受击触发', type: 'triggerSlots', group: '触发技能' },
{ key: 'field', label: '驻场技能', type: 'fieldList', group: '触发技能' },
{ key: 'revive', label: '复活触发', type: 'reviveSlot', group: '触发技能' },
{ key: 'info', label: '描述文案', type: 'string', required: true, group: '基础' },
],
};

View File

@@ -0,0 +1,10 @@
import { TableId, TableSchema } from './types';
import { heroSchema } from './hero';
import { skillSchema } from './skill';
import { fieldSchema } from './field';
export const REGISTRY: Record<TableId, TableSchema> = {
hero: heroSchema, skill: skillSchema, field: fieldSchema,
};
export function getSchema(id: TableId): TableSchema { return REGISTRY[id]; }
export function allSchemas(): TableSchema[] { return Object.values(REGISTRY); }

View File

@@ -0,0 +1,41 @@
import { TableSchema } from './types';
export const skillSchema: TableSchema = {
id: 'skill', label: '技能',
sourceFile: 'SkillSet.ts', exportName: 'SkillSet',
idSegments: [{ label: '技能', min: 6000, max: 6999 }],
fields: [
{ key: 'uuid', label: 'UUID', type: 'number', required: true, group: '基础' },
{ key: 'name', label: '名称', type: 'string', required: true, group: '基础' },
{ key: 'sp_name', label: '特效名', type: 'string', required: true, group: '基础' },
{ key: 'icon', label: '图标ID', type: 'string', required: true, group: '基础' },
{ key: 'act', label: '执行动画', type: 'string', required: true, group: '基础' },
{ key: 'TGroup', label: '目标群体', type: 'enum', enumRef: 'TGroup', required: true, group: '目标' },
{ key: 'DTType', label: '伤害类型', type: 'enum', enumRef: 'DTType', required: true, group: '目标' },
{ key: 'IType', label: '技能类型', type: 'enum', enumRef: 'IType', required: true, group: '目标' },
{ key: 'RType', label: '运行类型', type: 'enum', enumRef: 'RType', required: true, group: '目标' },
{ key: 'EType', label: '结束条件', type: 'enum', enumRef: 'EType', required: true, group: '目标' },
{ key: 'kind', label: '主效果', type: 'enum', enumRef: 'SkillKind', group: '目标' },
{ key: 'ap', label: 'ap(伤害%/护盾次)', type: 'number', required: true, group: '数值' },
{ key: 'gold', label: '金币值', type: 'number', group: '数值' },
{ key: 'hit_count', label: '可命中次数', type: 'number', required: true, group: '数值' },
{ key: 'hitcd', label: '伤害间隔', type: 'number', required: true, group: '数值' },
{ key: 'speed', label: '移动速度', type: 'number', required: true, group: '数值' },
{ key: 'ready', label: '前摇时间', type: 'number', required: true, group: '数值' },
{ key: 'with', label: '宽度', type: 'number', default: 0, group: '数值' },
{ key: 'readyAnm', label: '前摇动画', type: 'string', group: '动画' },
{ key: 'endAnm', label: '结束动画', type: 'string', group: '动画' },
{ key: 'DAnm', label: '命中动画ID', type: 'string', group: '动画' },
{ key: 'EAnm', label: '结束动画ID', type: 'number', group: '动画' },
{ key: 'crt', label: '额外暴击率', type: 'number', group: '高级' },
{ key: 'stun', label: '额外击晕概率', type: 'number', group: '高级' },
{ key: 'frz', label: '额外冰冻概率', type: 'number', group: '高级' },
{ key: 'bck', label: '额外击退概率', type: 'number', group: '高级' },
{ key: 'buff_type', label: 'Buff类型', type: 'enum', enumRef: 'Attrs', group: '高级' },
{ key: 'call_hero', label: '召唤英雄', type: 'ref', refTable: 'hero', group: '高级' },
{ key: 'time', label: '持续时间', type: 'number', group: '高级' },
{ key: 'bezier_start_y', label: '贝塞尔起始Y', type: 'number', group: '高级' },
{ key: 'bezier_mid_y', label: '贝塞尔中点Y', type: 'number', group: '高级' },
{ key: 'bezier_arc', label: '贝塞尔弧度', type: 'number', group: '高级' },
{ key: 'info', label: '描述', type: 'string', required: true, group: '基础' },
],
};

View File

@@ -0,0 +1,31 @@
export type TableId = 'hero' | 'skill' | 'field';
export type FieldType =
| 'number' | 'string' | 'boolean'
| 'enum' // 下拉,选项来自 enumRef
| 'ref' // 引用另一表 uuid
| 'speedExpr' // 攻速符号表达式
| 'skillMap' | 'triggerSlots' | 'fieldList' | 'reviveSlot' | 'overrides';
export interface FieldSchema {
key: string;
label: string;
type: FieldType;
required?: boolean;
default?: unknown;
group?: string;
enumRef?: string; // type='enum'
refTable?: TableId; // type='ref'
}
export interface IdSegment { label: string; min: number; max: number; }
export interface TableSchema {
id: TableId;
label: string;
sourceFile: string; // 相对 assets/script/game/common/config/
exportName: string; // 'HeroInfo' | 'SkillSet' | 'FieldSkillSet'
listExportName?: string; // 'HeroList'(仅 hero
idSegments: IdSegment[];
fields: FieldSchema[];
}

View File

@@ -0,0 +1,107 @@
import { RecordValue } from '../../io/recordValue';
import { TableId } from '../schema/types';
import { ENUMS } from '../schema/enums';
export type Severity = 'error' | 'warn';
export interface Issue {
tableId: TableId; key: string; fieldPath: string;
severity: Severity; code: string; message: string;
}
export interface ValidationContext {
hasSkill: (u: number) => boolean;
hasField: (u: number) => boolean;
hasHero: (u: number) => boolean;
heroListKeys: Set<string>;
}
const OVERRIDE_KEYS = new Set(['TGroup', 'ap', 'gold', 'hit_count', 'hitcd', 'crt', 'frz', 'stun', 'bck', 'buff_type', 'call_hero']);
const TRIGGER_ARRAY_KEYS = ['call', 'dead', 'fstart', 'fend', 'atking', 'atked'];
function num(v: RecordValue | undefined): number | undefined {
return v && v.kind === 'num' ? v.value : undefined;
}
function isKnownEnum(qualifier: string, member: string): boolean {
const def = ENUMS[qualifier] ?? Object.values(ENUMS).find(e => e.qualifier === qualifier);
return !!def && def.members.some(m => String(m.value) === member || m.label.split(' ').pop() === member);
}
export function validate(tableId: TableId, records: Map<string, RecordValue>, ctx: ValidationContext): Issue[] {
const issues: Issue[] = [];
// 跟踪 uuid 字段值(而非 Map 键)—— JS Map 会折叠重复键,但两条记录可能携带相同的 uuid 字段值。
const seenUuids = new Set<string>();
for (const [key, rec] of records) {
const push = (fieldPath: string, severity: Severity, code: string, message: string) =>
issues.push({ tableId, key, fieldPath, severity, code, message });
if (rec.kind !== 'obj') { push('', 'error', 'not-object', '记录不是对象'); continue; }
const p = rec.props;
const uVal = num(p['uuid']);
if (uVal !== undefined) {
const uKey = String(uVal);
if (seenUuids.has(uKey)) push('uuid', 'error', 'dup-uuid', `重复 uuid: ${uKey}`);
seenUuids.add(uKey);
}
// 必填(按表最小集合)
const required: Record<TableId, string[]> = {
hero: ['uuid', 'name', 'path', 'fac', 'lv', 'type', 'hp', 'ap', 'skills', 'info'],
skill: ['uuid', 'name', 'sp_name', 'icon', 'act', 'TGroup', 'DTType', 'IType', 'RType', 'EType', 'ap', 'hit_count', 'hitcd', 'speed', 'ready', 'info'],
field: ['uuid', 'name', 'icon', 'type', 'value', 'info'],
};
for (const f of required[tableId]) {
if (p[f] === undefined) push(f, 'error', 'missing-required', `缺少必填字段 ${f}`);
}
// 枚举值合法
for (const [f, v] of Object.entries(p)) {
if (v.kind === 'enumRef' && !isKnownEnum(v.qualifier, v.member)) {
push(f, 'error', 'bad-enum', `非法枚举值 ${v.qualifier}.${v.member}`);
}
}
if (tableId === 'hero') {
// 触发槽数组引用 + overrides 键
for (const tk of TRIGGER_ARRAY_KEYS) {
const arr = p[tk];
if (!arr || arr.kind !== 'arr') continue;
for (const it of arr.items) {
if (it.kind !== 'obj') continue;
const su = num(it.props['s_uuid']);
if (su !== undefined && !ctx.hasSkill(su)) push(tk, 'error', 'dangling-ref', `引用了不存在的技能 ${su}`);
const ov = it.props['overrides'];
if (ov && ov.kind === 'obj') {
for (const k of Object.keys(ov.props)) {
if (!OVERRIDE_KEYS.has(k)) push(`${tk}.overrides`, 'error', 'bad-override-key', `非法覆盖键 ${k}`);
}
}
}
}
// field 列表
const fl = p['field'];
if (fl && fl.kind === 'arr') {
for (const it of fl.items) { const u = num(it); if (u !== undefined && !ctx.hasField(u)) push('field', 'error', 'dangling-ref', `引用了不存在的驻场技能 ${u}`); }
}
// revive
const rv = p['revive'];
if (rv && rv.kind === 'obj') {
const su = num(rv.props['s_uuid']); if (su !== undefined && !ctx.hasSkill(su)) push('revive', 'error', 'dangling-ref', `引用了不存在的技能 ${su}`);
}
// call_hero技能表里的 ref 也用 hasHero
}
if (tableId === 'skill') {
const ch = num(p['call_hero']); if (ch !== undefined && !ctx.hasHero(ch)) push('call_hero', 'error', 'dangling-ref', `引用了不存在的英雄 ${ch}`);
}
// HeroList 一致性(仅 hero 表)
if (tableId === 'hero') {
const u = num(p['uuid']); const fac = p['fac'];
const isHero = fac && fac.kind === 'enumRef' && fac.member === 'HERO';
if (isHero && u !== undefined && !ctx.heroListKeys.has(String(u))) {
push('uuid', 'error', 'herolist-inconsistent', `英雄 ${u} 不在 HeroList 中`);
}
}
}
return issues;
}

View File

@@ -0,0 +1,7 @@
#app { color: var(--color-font-normal); font-size: 12px; }
.row { margin: 6px 0; }
button { margin-right: 8px; }
ul { list-style: none; padding: 0; max-height: 360px; overflow:auto; }
li { padding: 3px 6px; cursor: pointer; }
li:hover { background: var(--color-hover-bg); }
.err { color: var(--color-warn); }

View File

@@ -0,0 +1 @@
<div id="app" style="padding:12px;"></div>

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"types": ["node"],
"lib": ["ES2020", "DOM"]
},
"include": ["src/**/*", "__tests__/**/*"]
}

View File

@@ -0,0 +1,93 @@
# 配置编辑器 Plan A — 验证证据
- **Plan**: `docs/superpowers/plans/2026-06-20-config-editor-foundation.md`
- **Branch**: `card0614`
- **Date**: 2026-06-21
- **Tasks implemented**: 113 (full plan)
---
## 自动化测试BLOCKING 门槛)
- **命令**: `npx tsx --test __tests__/*.test.ts`(在 `extensions/pixelhero-config-editor/` 下运行)
- **结果**: **36 / 36 PASS**0 fail0 skip
- **耗时**: ~560600ms
覆盖范围:
| 测试文件 | 用例数 | 覆盖点 |
|---|---|---|
| `recordValue.serializer.test.ts` | 8 | num/str/bool/enumRef/speed/arr/obj/raw 序列化;`serializeEntry` 单行与嵌套续行 |
| `parser.test.ts` | 8 | AST → RecordValuespeed 表达式识别enumRef fallbackraw 保留;`findExportObjectLiteral` |
| `tsConfigFile.test.ts` | 10 | load/getKeys/readpatch/add/delete`.bak` 备份语法校验阻断dirty/no-op savespeed+enumRef 往返不丢 |
| `validation.test.ts` | 8 | dup-uuid、missing-required、bad-enum、dangling-ref触发槽/field/revive、bad-override-key、herolist-inconsistent |
| `buildSkillDesc.test.ts` | 2 | 受击触发盾效、驻场光环描述 |
> **说明**Task 11`store.ts`)与 Task 12`index.ts`/面板)依赖 Cocos `Editor` 全局,无法在 Node 中 import/运行,故无单元测试——按计划设计。自动化门槛以纯逻辑层 36 用例为准。
---
## 构建BLOCKING 门槛)
- **命令**: `npm run build`(在 `extensions/pixelhero-config-editor/` 下运行)
- **结果**: 成功两个产物均生成esbuild 无错误。
| 产物 | 大小(字节) | 说明 |
|---|---|---|
| `dist/main.js` | 9,982,310 (~9.5 MB) | node/cjs内联 `typescript` 编译器 API`Editor` 为运行时全局(无 esbuild 报错) |
| `dist/panels/default.js` | 642,825 (~628 KB) | browser/iife内联 `vue/dist/vue.esm-bundler.js`(含运行时模板编译器) |
`dist/` 已被 `extensions/pixelhero-config-editor/.gitignore` 忽略,未提交。
### 构建过程中的必要修正(已记入 Task 12 commit message
Plan 给出的 `esbuild.config.mjs` 将面板入口设为 `platform: 'browser'``external: []`。但 Plan 的面板 `src/panels/default/index.ts` 在运行时用 `node:fs`/`node:path` 读取 `static/template/default/index.html``static/style/default/index.css`。esbuild 在 browser 平台下拒绝打包 `node:` 内建,报错:
```
X [ERROR] Could not resolve "node:fs"
X [ERROR] Could not resolve "node:path"
```
**修正**:将面板入口的 `external` 改为 `['node:fs', 'node:path']`。理由Cocos 面板进程在 Electron 渲染层运行,能访问 Node 内建vue 仍按 plan 打进 IIFE。`dist/main.js` 不受影响platform:'node' 本就允许 `node:` 内建)。
---
## 编辑器内集成ADVISORY — 待人工完成)
> 以下为人工在 Cocos Creator 3.8.6 中执行的验证清单。完成后请勾选并补截图路径。
- [ ] 1. 打开 Cocos Creator 3.8.6 项目 `d:\game\pixelheros`
- [ ] 2. 扩展管理器Extension Manager→ 项目扩展 → 启用/重载 `pixelhero-config-editor`;控制台无报错。
- [ ] 3. 主菜单 → 面板Panel→ 英雄技能配置Hero/Skill Config面板打开。
- [ ] 4. 表下拉切换 `英雄/怪物` → 列表显示真实 uuid应含 5011/5012/.../6106 等英雄条目)。
- [ ] 5. 表下拉切换 `技能` → 列表显示 6xxx 系列技能 uuid`驻场技能` → 7xxx 系列。
- [ ] 6. 点选 hero 表任一条 → 右侧 `<pre>` 显示结构化 JSON
- [ ] 6a. `fac` 字段为 `{ "kind": "enumRef", "qualifier": "FacSet", "member": "HERO" }`(或 `MON`)。
- [ ] 6b. 技能 `skills.<uuid>.cd``{ "kind": "speed", "level": "Slow3" }`(或其它 AtkSpeedLv 成员)。
- [ ] 6c. 数值/字符串字段为 `{ "kind": "num"|"str", "value": ... }`
- [ ] 7. 点选 skill 表任一条 → JSON 中 `TGroup`/`DTType`/`IType`/`RType`/`EType` 等字段均为 `enumRef``call_hero`(若有)为 `num``enumRef`
- [ ] 8. 结论:端到端 IPC 打通IO 层正确解析真实 `assets/script/game/common/config/heroSet.ts``SkillSet.ts`
- [ ] 9. 截图存档路径(填写):`production/qa/evidence/screenshots/config-editor-plan-a-YYYYMMDD.png`
---
## 提交记录
| Task | Commit SHA | 标题 |
|---|---|---|
| 11 | `e3102c63` | feat(config-editor): main-process store (in-memory truth + message impls + asset-db refresh) |
| 12 | `24b5c498` | feat(config-editor): extension entry + minimal Vue panel proving end-to-end IPC |
| 13 | (本提交) | test(config-editor): record Plan A verification evidence |
Tasks 110 由前序批次完成并提交SHAs 详见 `git log --oneline extensions/pixelhero-config-editor/`
---
## Definition of Done 对照
1. **扩展可被 Cocos 3.8.6 加载,菜单可打开面板** — 待人工确认(见上节步骤 23
2. **IO 层对真实配置正确解析(含 speed 表达式与枚举引用)** — 单元测试覆盖fixtures 镜像真实形态),人工确认真实文件见步骤 6。
3. **patch 一条英雄保存后:磁盘文件更新且为合法 TS其他条目与符号表达式原样保留`.bak` 已生成asset-db 已刷新** — TsConfigFile 单元测试覆盖文件行为patch/add/delete/save/.bak/语法校验asset-db refresh 在 `store.saveRecord` 中调用,待人工在编辑器内验证一次保存。
4. **校验层对各类非法数据正确报错error 级阻断保存)** — 8 个 validation 单元测试覆盖;`saveRecord` 在 error 时回滚不落盘。
5. **全部单元测试 PASSBLOCKING****36/36 PASS**
6. **未改动 `assets/script/game/**` 任何游戏运行时代码** — 本批次仅改动 `extensions/pixelhero-config-editor/**` 与本证据文件。