Compare commits
21 Commits
b7388615ed
...
card0614
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c2f1defa9 | ||
|
|
46fa481607 | ||
|
|
72cdf32a75 | ||
|
|
a8642cb788 | ||
|
|
d60b66350a | ||
|
|
bfa434634c | ||
|
|
fb65fa79c8 | ||
|
|
24b5c49891 | ||
|
|
e3102c63ff | ||
|
|
6a81630f6f | ||
|
|
4a5659b7ec | ||
|
|
4df88c1c90 | ||
|
|
0d28ad7a5e | ||
|
|
0a960b737c | ||
|
|
7bb5f8bacc | ||
|
|
c0755b3b8d | ||
|
|
88c1a28c80 | ||
|
|
acb038a70a | ||
|
|
315a1a6af9 | ||
|
|
5170b2d0dc | ||
|
|
d8f02b568b |
@@ -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
3
.gitignore
vendored
@@ -20,7 +20,8 @@ native
|
||||
# WebStorm
|
||||
#//////////////////////////
|
||||
.idea/
|
||||
extensions/
|
||||
extensions/*
|
||||
!extensions/pixelhero-config-editor/
|
||||
extensions/oops-plugin-framework
|
||||
# === IDE and Editor ===
|
||||
.vs/
|
||||
|
||||
@@ -2602,8 +2602,8 @@
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -1.378,
|
||||
"y": 1.378,
|
||||
"x": 2.438,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -2643,8 +2643,8 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 17.90380859375,
|
||||
"height": 35.5
|
||||
"width": 34.748356206795606,
|
||||
"height": 67
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
@@ -2682,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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -747,7 +747,7 @@
|
||||
"b": 255,
|
||||
"a": 255
|
||||
},
|
||||
"_string": "选择一个战斗技能",
|
||||
"_string": "选择一个战斗「天赋」",
|
||||
"_horizontalAlign": 1,
|
||||
"_verticalAlign": 1,
|
||||
"_actualFontSize": 46,
|
||||
|
||||
@@ -7263,8 +7263,8 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 40,
|
||||
"height": 40
|
||||
"width": 30,
|
||||
"height": 30
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
@@ -7365,8 +7365,8 @@
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -2.313,
|
||||
"y": 2.149,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -7604,7 +7604,7 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 20.6845703125,
|
||||
"width": 27.0612671550967,
|
||||
"height": 54.4
|
||||
},
|
||||
"_anchorPoint": {
|
||||
@@ -7652,7 +7652,7 @@
|
||||
"_font": null,
|
||||
"_isSystemFontUsed": true,
|
||||
"_spacingX": 0,
|
||||
"_isItalic": false,
|
||||
"_isItalic": true,
|
||||
"_isBold": true,
|
||||
"_isUnderline": false,
|
||||
"_underlineHeight": 2,
|
||||
@@ -16834,8 +16834,8 @@
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -1.378,
|
||||
"y": 1.378,
|
||||
"x": 2.438,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -16875,8 +16875,8 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 17.90380859375,
|
||||
"height": 35.5
|
||||
"width": 34.748356206795606,
|
||||
"height": 67
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
@@ -16914,16 +16914,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,
|
||||
@@ -20788,8 +20788,8 @@
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -1.378,
|
||||
"y": 1.378,
|
||||
"x": 2.438,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -20829,8 +20829,8 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 17.90380859375,
|
||||
"height": 35.5
|
||||
"width": 34.748356206795606,
|
||||
"height": 67
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
@@ -20868,16 +20868,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,
|
||||
@@ -24742,8 +24742,8 @@
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -1.378,
|
||||
"y": 1.378,
|
||||
"x": 2.438,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -24783,8 +24783,8 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 17.90380859375,
|
||||
"height": 35.5
|
||||
"width": 34.748356206795606,
|
||||
"height": 67
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
@@ -24822,16 +24822,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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -271,8 +271,8 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 218,
|
||||
"height": 218
|
||||
"width": 168,
|
||||
"height": 168
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
@@ -396,8 +396,8 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 420,
|
||||
"height": 420
|
||||
"width": 320,
|
||||
"height": 320
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
@@ -1958,8 +1958,8 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 100,
|
||||
"height": 100
|
||||
"width": 80,
|
||||
"height": 80
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
@@ -2892,8 +2892,8 @@
|
||||
},
|
||||
"_lscale": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 0.9,
|
||||
"y": 0.9,
|
||||
"x": 0.6,
|
||||
"y": 0.6,
|
||||
"z": 1
|
||||
},
|
||||
"_mobility": 0,
|
||||
@@ -3062,8 +3062,8 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 74,
|
||||
"height": 74
|
||||
"width": 55,
|
||||
"height": 55
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
@@ -3111,7 +3111,7 @@
|
||||
"y": 0.5
|
||||
},
|
||||
"_fillStart": 0,
|
||||
"_fillRange": 0,
|
||||
"_fillRange": 1,
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": {
|
||||
@@ -3138,10 +3138,10 @@
|
||||
},
|
||||
"_alignFlags": 45,
|
||||
"_target": null,
|
||||
"_left": 3,
|
||||
"_right": 3,
|
||||
"_top": 3,
|
||||
"_bottom": 3,
|
||||
"_left": 12.5,
|
||||
"_right": 12.5,
|
||||
"_top": 12.5,
|
||||
"_bottom": 12.5,
|
||||
"_horizontalCenter": 0,
|
||||
"_verticalCenter": 0,
|
||||
"_isAbsLeft": true,
|
||||
|
||||
@@ -1111,788 +1111,6 @@
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"536b9": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@536b9",
|
||||
"displayName": "",
|
||||
"id": "536b9",
|
||||
"name": "CardFrame_Rectangle_01_Blue_Bg",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": true,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 65,
|
||||
"trimY": 1861,
|
||||
"width": 180,
|
||||
"height": 221,
|
||||
"rawWidth": 180,
|
||||
"rawHeight": 221,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"cb195": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@cb195",
|
||||
"displayName": "",
|
||||
"id": "cb195",
|
||||
"name": "CardFrame_Rectangle_01_Blue_Border",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": true,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 895,
|
||||
"trimY": 673,
|
||||
"width": 204,
|
||||
"height": 253,
|
||||
"rawWidth": 204,
|
||||
"rawHeight": 253,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"4cf00": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@4cf00",
|
||||
"displayName": "",
|
||||
"id": "4cf00",
|
||||
"name": "CardFrame_Rectangle_01_Blue_BorderGem",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": true,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 1314,
|
||||
"trimY": 439,
|
||||
"width": 204,
|
||||
"height": 258,
|
||||
"rawWidth": 204,
|
||||
"rawHeight": 258,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"1ed54": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@1ed54",
|
||||
"displayName": "",
|
||||
"id": "1ed54",
|
||||
"name": "CardFrame_Rectangle_01_Green_Bg",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 1670,
|
||||
"trimY": 645,
|
||||
"width": 180,
|
||||
"height": 221,
|
||||
"rawWidth": 180,
|
||||
"rawHeight": 221,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"d5c11": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@d5c11",
|
||||
"displayName": "",
|
||||
"id": "d5c11",
|
||||
"name": "CardFrame_Rectangle_01_Green_Border",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": true,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 1574,
|
||||
"trimY": 439,
|
||||
"width": 204,
|
||||
"height": 253,
|
||||
"rawWidth": 204,
|
||||
"rawHeight": 253,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"df52c": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@df52c",
|
||||
"displayName": "",
|
||||
"id": "df52c",
|
||||
"name": "CardFrame_Rectangle_01_Green_BorderGem",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": true,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 468,
|
||||
"trimY": 933,
|
||||
"width": 204,
|
||||
"height": 258,
|
||||
"rawWidth": 204,
|
||||
"rawHeight": 258,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"19b1d": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@19b1d",
|
||||
"displayName": "",
|
||||
"id": "19b1d",
|
||||
"name": "CardFrame_Rectangle_01_Purple_Bg",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": true,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 1660,
|
||||
"trimY": 868,
|
||||
"width": 180,
|
||||
"height": 221,
|
||||
"rawWidth": 180,
|
||||
"rawHeight": 221,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"8de73": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@8de73",
|
||||
"displayName": "",
|
||||
"id": "8de73",
|
||||
"name": "CardFrame_Rectangle_01_Purple_Border",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 454,
|
||||
"trimY": 1524,
|
||||
"width": 204,
|
||||
"height": 253,
|
||||
"rawWidth": 204,
|
||||
"rawHeight": 253,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"e19cf": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@e19cf",
|
||||
"displayName": "",
|
||||
"id": "e19cf",
|
||||
"name": "CardFrame_Rectangle_01_Purple_BorderGem",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 454,
|
||||
"trimY": 1264,
|
||||
"width": 204,
|
||||
"height": 258,
|
||||
"rawWidth": 204,
|
||||
"rawHeight": 258,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"487ca": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@487ca",
|
||||
"displayName": "",
|
||||
"id": "487ca",
|
||||
"name": "CardFrame_Rectangle_01_Red_Bg",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 728,
|
||||
"trimY": 933,
|
||||
"width": 180,
|
||||
"height": 221,
|
||||
"rawWidth": 180,
|
||||
"rawHeight": 221,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"fc735": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@fc735",
|
||||
"displayName": "",
|
||||
"id": "fc735",
|
||||
"name": "CardFrame_Rectangle_01_Red_Border",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": true,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 1150,
|
||||
"trimY": 851,
|
||||
"width": 204,
|
||||
"height": 253,
|
||||
"rawWidth": 204,
|
||||
"rawHeight": 253,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"d5f55": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@d5f55",
|
||||
"displayName": "",
|
||||
"id": "d5f55",
|
||||
"name": "CardFrame_Rectangle_01_Red_BorderGem",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": true,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 1150,
|
||||
"trimY": 645,
|
||||
"width": 204,
|
||||
"height": 258,
|
||||
"rawWidth": 204,
|
||||
"rawHeight": 258,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"9d4d9": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@9d4d9",
|
||||
"displayName": "",
|
||||
"id": "9d4d9",
|
||||
"name": "CardFrame_Rectangle_01_Yellow_Bg",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 910,
|
||||
"trimY": 879,
|
||||
"width": 180,
|
||||
"height": 221,
|
||||
"rawWidth": 180,
|
||||
"rawHeight": 221,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"4fde0": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@4fde0",
|
||||
"displayName": "",
|
||||
"id": "4fde0",
|
||||
"name": "CardFrame_Rectangle_01_Yellow_Border",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": true,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 1405,
|
||||
"trimY": 851,
|
||||
"width": 204,
|
||||
"height": 253,
|
||||
"rawWidth": 204,
|
||||
"rawHeight": 253,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"e9809": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@e9809",
|
||||
"displayName": "",
|
||||
"id": "e9809",
|
||||
"name": "CardFrame_Rectangle_01_Yellow_BorderGem",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": true,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 1410,
|
||||
"trimY": 645,
|
||||
"width": 204,
|
||||
"height": 258,
|
||||
"rawWidth": 204,
|
||||
"rawHeight": 258,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"3c27d": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@3c27d",
|
||||
"displayName": "",
|
||||
"id": "3c27d",
|
||||
"name": "Reward",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": true,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 65,
|
||||
"trimY": 1319,
|
||||
"width": 540,
|
||||
"height": 108,
|
||||
"rawWidth": 540,
|
||||
"rawHeight": 108,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"abb23": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@abb23",
|
||||
"displayName": "",
|
||||
"id": "abb23",
|
||||
"name": "btn_yellow2",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": true,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 913,
|
||||
"trimY": 214,
|
||||
"width": 457,
|
||||
"height": 196,
|
||||
"rawWidth": 457,
|
||||
"rawHeight": 196,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [],
|
||||
"indexes": [],
|
||||
"uv": [],
|
||||
"nuv": [],
|
||||
"minPos": [],
|
||||
"maxPos": []
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "0c35d8c7-1528-42ce-b7c4-20fbb1baab11@6c48a",
|
||||
"atlasUuid": "cb93c900-b440-4571-91d1-7da1636e3d73",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"b9538": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "cb93c900-b440-4571-91d1-7da1636e3d73@b9538",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,18 +211,18 @@ export const SkillSet: Record<number, SkillConfig> = {
|
||||
**/
|
||||
6001: {
|
||||
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,
|
||||
RType: RType.bezier, EType: EType.collision, info: "造成攻击力100%的伤害",
|
||||
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: "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: "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: "一定几率暴击",
|
||||
},
|
||||
|
||||
6008: {
|
||||
|
||||
@@ -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:[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+高速"},
|
||||
|
||||
|
||||
};
|
||||
|
||||
@@ -856,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);
|
||||
|
||||
@@ -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 —— 怪物类型、成长值、波次配置
|
||||
* - Monster(hero/Mon.ts)—— 怪物 ECS 实体类
|
||||
* - HeroInfo(heroSet)—— 怪物基础属性配置(与英雄共用配置)
|
||||
* - HeroAttrsComp / MonMoveComp —— 怪物属性和移动组件
|
||||
* - BoxSet.GAME_LINE —— 地面基准 Y 坐标
|
||||
* - 采用 12 个硬编码的网格位置点 (MON_POSITIONS,3行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 {
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1741
docs/superpowers/plans/2026-06-20-config-editor-foundation.md
Normal file
1741
docs/superpowers/plans/2026-06-20-config-editor-foundation.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 字段目标在引用表中存在(英雄触发槽/技能图引用 → SkillSet;field 列表 → FieldSkillSet;call_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(表切换 + 搜索 + 列表),右 detail(schema 驱动表单 + 嵌套编辑器 + 预览),底部校验条。
|
||||
- **控件映射**: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 为常量文件副本,不内联魔法数。
|
||||
- **UI(ADVISORY)**:`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 进 main(node),面板不包含;可接受 |
|
||||
| esbuild/Vue 在扩展环境运行 | P0 先打通最小面板(Vue mount 成功)再扩展 |
|
||||
| 多面板状态不一致 | 主进程单例 + 广播 record-changed |
|
||||
| `HeroList` 失同步 | 英雄表写入后由 store 强制同步并校验 |
|
||||
|
||||
## 14. 验收标准
|
||||
|
||||
1. 扩展可从"面板"菜单打开,三张表可切换浏览、搜索。
|
||||
2. 对三张表任一记录:可视化查看/编辑所有字段(含触发槽、技能图、覆盖、驻场、复活),保存后磁盘 `.ts` 被更新且为合法 TS,asset-db 已刷新。
|
||||
3. **保号往返**:仅查看后保存,文件零字节变化(测试断言)。
|
||||
4. 实时校验:违反任一 error 规则时"保存"被阻断并列出问题;行/字段级可见。
|
||||
5. 描述预览与游戏内 `buildSkillDesc` 输出一致。
|
||||
6. 新建/复制/删除可用;`HeroList` 与 `HeroInfo` 始终一致。
|
||||
7. 逻辑层单元测试全部通过(BLOCKING)。
|
||||
8. 未改动 `assets/script/game/**` 任何游戏运行时代码(git diff 可证)。
|
||||
3
extensions/pixelhero-config-editor/.gitignore
vendored
Normal file
3
extensions/pixelhero-config-editor/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.bak
|
||||
1
extensions/pixelhero-config-editor/__tests__/.gitignore
vendored
Normal file
1
extensions/pixelhero-config-editor/__tests__/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.tmp/
|
||||
@@ -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/);
|
||||
});
|
||||
@@ -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];
|
||||
@@ -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"},
|
||||
};
|
||||
47
extensions/pixelhero-config-editor/__tests__/parser.test.ts
Normal file
47
extensions/pixelhero-config-editor/__tests__/parser.test.ts
Normal 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);
|
||||
});
|
||||
@@ -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 空格
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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'));
|
||||
});
|
||||
29
extensions/pixelhero-config-editor/esbuild.config.mjs
Normal file
29
extensions/pixelhero-config-editor/esbuild.config.mjs
Normal 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 });
|
||||
}
|
||||
4
extensions/pixelhero-config-editor/i18n/en.js
Normal file
4
extensions/pixelhero-config-editor/i18n/en.js
Normal file
@@ -0,0 +1,4 @@
|
||||
exports.en = {
|
||||
'pixelhero-config-editor': { description: 'Hero/Skill Config Editor', title: 'Hero/Skill Config' },
|
||||
'menu': { 'panel/英雄技能配置': 'Hero/Skill Config' },
|
||||
};
|
||||
4
extensions/pixelhero-config-editor/i18n/zh.js
Normal file
4
extensions/pixelhero-config-editor/i18n/zh.js
Normal file
@@ -0,0 +1,4 @@
|
||||
exports.zh = {
|
||||
'pixelhero-config-editor': { description: '英雄/技能配置编辑器', title: '英雄/技能配置' },
|
||||
'menu': { 'panel/英雄技能配置': '英雄技能配置' },
|
||||
};
|
||||
1312
extensions/pixelhero-config-editor/package-lock.json
generated
Normal file
1312
extensions/pixelhero-config-editor/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
extensions/pixelhero-config-editor/package.json
Normal file
48
extensions/pixelhero-config-editor/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
110
extensions/pixelhero-config-editor/src/io/TsConfigFile.ts
Normal file
110
extensions/pixelhero-config-editor/src/io/TsConfigFile.ts
Normal 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 读写 + 语法校验(写 .bak);asset-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);
|
||||
}
|
||||
}
|
||||
65
extensions/pixelhero-config-editor/src/io/parser.ts
Normal file
65
extensions/pixelhero-config-editor/src/io/parser.ts
Normal 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() }; // 二元/模板/调用/其他元素访问
|
||||
}
|
||||
15
extensions/pixelhero-config-editor/src/io/recordValue.ts
Normal file
15
extensions/pixelhero-config-editor/src/io/recordValue.ts
Normal 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';
|
||||
}
|
||||
32
extensions/pixelhero-config-editor/src/io/serializer.ts
Normal file
32
extensions/pixelhero-config-editor/src/io/serializer.ts
Normal 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');
|
||||
}
|
||||
48
extensions/pixelhero-config-editor/src/main/index.ts
Normal file
48
extensions/pixelhero-config-editor/src/main/index.ts
Normal 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 };
|
||||
85
extensions/pixelhero-config-editor/src/main/store.ts
Normal file
85
extensions/pixelhero-config-editor/src/main/store.ts
Normal 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(); },
|
||||
};
|
||||
38
extensions/pixelhero-config-editor/src/panels/default/app.ts
Normal file
38
extensions/pixelhero-config-editor/src/panels/default/app.ts
Normal 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); }
|
||||
@@ -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 卸载随面板进程退出自动清理 */ },
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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])
|
||||
);
|
||||
@@ -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: '基础' },
|
||||
],
|
||||
};
|
||||
33
extensions/pixelhero-config-editor/src/shared/schema/hero.ts
Normal file
33
extensions/pixelhero-config-editor/src/shared/schema/hero.ts
Normal 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: '基础' },
|
||||
],
|
||||
};
|
||||
@@ -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); }
|
||||
@@ -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: '基础' },
|
||||
],
|
||||
};
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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); }
|
||||
@@ -0,0 +1 @@
|
||||
<div id="app" style="padding:12px;"></div>
|
||||
15
extensions/pixelhero-config-editor/tsconfig.json
Normal file
15
extensions/pixelhero-config-editor/tsconfig.json
Normal 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__/**/*"]
|
||||
}
|
||||
93
production/qa/evidence/2026-06-20-config-editor-plan-a.md
Normal file
93
production/qa/evidence/2026-06-20-config-editor-plan-a.md
Normal 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**: 1–13 (full plan)
|
||||
|
||||
---
|
||||
|
||||
## 自动化测试(BLOCKING 门槛)
|
||||
|
||||
- **命令**: `npx tsx --test __tests__/*.test.ts`(在 `extensions/pixelhero-config-editor/` 下运行)
|
||||
- **结果**: **36 / 36 PASS**,0 fail,0 skip
|
||||
- **耗时**: ~560–600ms
|
||||
|
||||
覆盖范围:
|
||||
|
||||
| 测试文件 | 用例数 | 覆盖点 |
|
||||
|---|---|---|
|
||||
| `recordValue.serializer.test.ts` | 8 | num/str/bool/enumRef/speed/arr/obj/raw 序列化;`serializeEntry` 单行与嵌套续行 |
|
||||
| `parser.test.ts` | 8 | AST → RecordValue;speed 表达式识别;enumRef fallback;raw 保留;`findExportObjectLiteral` |
|
||||
| `tsConfigFile.test.ts` | 10 | load/getKeys/read;patch/add/delete;`.bak` 备份;语法校验阻断;dirty/no-op save;speed+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 1–10 由前序批次完成并提交,SHAs 详见 `git log --oneline extensions/pixelhero-config-editor/`。
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done 对照
|
||||
|
||||
1. **扩展可被 Cocos 3.8.6 加载,菜单可打开面板** — 待人工确认(见上节步骤 2–3)。
|
||||
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. **全部单元测试 PASS(BLOCKING)** — **36/36 PASS**。
|
||||
6. **未改动 `assets/script/game/**` 任何游戏运行时代码** — 本批次仅改动 `extensions/pixelhero-config-editor/**` 与本证据文件。
|
||||
Reference in New Issue
Block a user