feat(skills): authentic baked missile DCC atlas and explosion rendering
This commit is contained in:
parent
2733ba625b
commit
ff867a13e3
|
|
@ -53,6 +53,11 @@
|
||||||
- 所有暗金怪(`SuperUniques`)必须自动推导并解析其随从(Minions)所依赖的全部 DCC/DC6 动画与材质资源,一并纳入离线打包构建。
|
- 所有暗金怪(`SuperUniques`)必须自动推导并解析其随从(Minions)所依赖的全部 DCC/DC6 动画与材质资源,一并纳入离线打包构建。
|
||||||
2. **原版文本数据转录不得篡改或简化**:
|
2. **原版文本数据转录不得篡改或简化**:
|
||||||
- `SuperUniques.txt`、`MonStats.txt` 等元数据必须严格保留原版 Class 标识(如 `scarab2`, `zombie5`, `fetishshaman4`, `nihlathakboss` 等具体变体),禁止粗暴截断变体数字后缀或自编别名。
|
- `SuperUniques.txt`、`MonStats.txt` 等元数据必须严格保留原版 Class 标识(如 `scarab2`, `zombie5`, `fetishshaman4`, `nihlathakboss` 等具体变体),禁止粗暴截断变体数字后缀或自编别名。
|
||||||
|
3. **技能与投射物美术资源必须采用原版资源并离线烘焙 (Missile & Spell FX Parity)**:
|
||||||
|
- 严禁长期使用由代码绘制的方块、纯几何色块或纯程序化简易图形作为技能投射物/特效替代品。
|
||||||
|
- 所有技能投射物(Missiles)、次生爆炸(Explosions)、状态光环与法术特效,必须严格以 1.13c 原版 MPQ(`d2data.mpq` / `d2exp.mpq` / `Patch_D2.mpq`)中由 `Missiles.txt` 的 `CelFile` 字段所引用的原版 DCC / DC6 动画文件为准(例如 `Firebolt.dcc`, `Fireball.dcc`, `FireArrowExplode2.dcc` 等)。
|
||||||
|
- 必须通过离线解包烘焙脚本(如 `scripts/pack-missiles.ts`)结合章节调色板(`ACT1/pal.pl2`),将原版 DCC/DC6 的全部朝向(如 16 方向)与全帧序列预打包烘焙为 Web 原生 Sprite Atlas(图集)及对应元数据,供前端 WebGL 运行时直接高效索引与渲染。
|
||||||
|
- 音效接入解耦:现阶段音效系统(`Sounds.txt` / WAV 音频资源)允许暂缓接入,但视觉动画必须具备 100% 像素级原版 DCC 贴图资产与帧率对齐。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
"pack:ui": "tsx scripts/pack-ui.ts",
|
"pack:ui": "tsx scripts/pack-ui.ts",
|
||||||
"pack:animdata": "tsx scripts/pack-animdata.ts",
|
"pack:animdata": "tsx scripts/pack-animdata.ts",
|
||||||
"pack:entities": "tsx scripts/pack-entity-assets.ts",
|
"pack:entities": "tsx scripts/pack-entity-assets.ts",
|
||||||
|
"pack:missiles": "tsx scripts/pack-missiles.ts",
|
||||||
"verify:entity-packs": "tsx scripts/verify-entity-packs.ts",
|
"verify:entity-packs": "tsx scripts/verify-entity-packs.ts",
|
||||||
"verify:packs": "tsx scripts/verify-packs.ts",
|
"verify:packs": "tsx scripts/verify-packs.ts",
|
||||||
"verify:bundle-no-mpq": "tsx scripts/verify-bundle-no-mpq.ts",
|
"verify:bundle-no-mpq": "tsx scripts/verify-bundle-no-mpq.ts",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,683 @@
|
||||||
|
{
|
||||||
|
"name": "fireball",
|
||||||
|
"celFile": "Fireball",
|
||||||
|
"width": 1024,
|
||||||
|
"height": 335,
|
||||||
|
"directions": 16,
|
||||||
|
"framesPerDirection": 5,
|
||||||
|
"animSpeed": 16,
|
||||||
|
"groups": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
75,
|
||||||
|
48,
|
||||||
|
-19,
|
||||||
|
-74
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76,
|
||||||
|
0,
|
||||||
|
75,
|
||||||
|
48,
|
||||||
|
-19,
|
||||||
|
-74
|
||||||
|
],
|
||||||
|
[
|
||||||
|
152,
|
||||||
|
0,
|
||||||
|
75,
|
||||||
|
48,
|
||||||
|
-19,
|
||||||
|
-74
|
||||||
|
],
|
||||||
|
[
|
||||||
|
228,
|
||||||
|
0,
|
||||||
|
75,
|
||||||
|
48,
|
||||||
|
-19,
|
||||||
|
-74
|
||||||
|
],
|
||||||
|
[
|
||||||
|
304,
|
||||||
|
0,
|
||||||
|
75,
|
||||||
|
48,
|
||||||
|
-19,
|
||||||
|
-74
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
380,
|
||||||
|
0,
|
||||||
|
79,
|
||||||
|
52,
|
||||||
|
-19,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
460,
|
||||||
|
0,
|
||||||
|
79,
|
||||||
|
52,
|
||||||
|
-19,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
540,
|
||||||
|
0,
|
||||||
|
79,
|
||||||
|
52,
|
||||||
|
-19,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
620,
|
||||||
|
0,
|
||||||
|
79,
|
||||||
|
52,
|
||||||
|
-19,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
700,
|
||||||
|
0,
|
||||||
|
79,
|
||||||
|
52,
|
||||||
|
-19,
|
||||||
|
-62
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
780,
|
||||||
|
0,
|
||||||
|
81,
|
||||||
|
52,
|
||||||
|
-63,
|
||||||
|
-61
|
||||||
|
],
|
||||||
|
[
|
||||||
|
862,
|
||||||
|
0,
|
||||||
|
81,
|
||||||
|
52,
|
||||||
|
-63,
|
||||||
|
-61
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
53,
|
||||||
|
81,
|
||||||
|
52,
|
||||||
|
-63,
|
||||||
|
-61
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82,
|
||||||
|
53,
|
||||||
|
81,
|
||||||
|
52,
|
||||||
|
-63,
|
||||||
|
-61
|
||||||
|
],
|
||||||
|
[
|
||||||
|
164,
|
||||||
|
53,
|
||||||
|
81,
|
||||||
|
52,
|
||||||
|
-63,
|
||||||
|
-61
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
246,
|
||||||
|
53,
|
||||||
|
74,
|
||||||
|
48,
|
||||||
|
-55,
|
||||||
|
-74
|
||||||
|
],
|
||||||
|
[
|
||||||
|
321,
|
||||||
|
53,
|
||||||
|
74,
|
||||||
|
48,
|
||||||
|
-55,
|
||||||
|
-74
|
||||||
|
],
|
||||||
|
[
|
||||||
|
396,
|
||||||
|
53,
|
||||||
|
74,
|
||||||
|
48,
|
||||||
|
-55,
|
||||||
|
-74
|
||||||
|
],
|
||||||
|
[
|
||||||
|
471,
|
||||||
|
53,
|
||||||
|
74,
|
||||||
|
48,
|
||||||
|
-55,
|
||||||
|
-74
|
||||||
|
],
|
||||||
|
[
|
||||||
|
546,
|
||||||
|
53,
|
||||||
|
74,
|
||||||
|
48,
|
||||||
|
-55,
|
||||||
|
-74
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
621,
|
||||||
|
53,
|
||||||
|
34,
|
||||||
|
55,
|
||||||
|
-17,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
656,
|
||||||
|
53,
|
||||||
|
34,
|
||||||
|
55,
|
||||||
|
-17,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
691,
|
||||||
|
53,
|
||||||
|
34,
|
||||||
|
55,
|
||||||
|
-17,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
726,
|
||||||
|
53,
|
||||||
|
34,
|
||||||
|
55,
|
||||||
|
-17,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
761,
|
||||||
|
53,
|
||||||
|
34,
|
||||||
|
55,
|
||||||
|
-17,
|
||||||
|
-81
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
796,
|
||||||
|
53,
|
||||||
|
94,
|
||||||
|
40,
|
||||||
|
-20,
|
||||||
|
-65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
891,
|
||||||
|
53,
|
||||||
|
94,
|
||||||
|
40,
|
||||||
|
-20,
|
||||||
|
-65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
109,
|
||||||
|
94,
|
||||||
|
40,
|
||||||
|
-20,
|
||||||
|
-65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
95,
|
||||||
|
109,
|
||||||
|
94,
|
||||||
|
40,
|
||||||
|
-20,
|
||||||
|
-65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
190,
|
||||||
|
109,
|
||||||
|
94,
|
||||||
|
40,
|
||||||
|
-20,
|
||||||
|
-65
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
285,
|
||||||
|
109,
|
||||||
|
40,
|
||||||
|
61,
|
||||||
|
-20,
|
||||||
|
-63
|
||||||
|
],
|
||||||
|
[
|
||||||
|
326,
|
||||||
|
109,
|
||||||
|
40,
|
||||||
|
61,
|
||||||
|
-20,
|
||||||
|
-63
|
||||||
|
],
|
||||||
|
[
|
||||||
|
367,
|
||||||
|
109,
|
||||||
|
40,
|
||||||
|
61,
|
||||||
|
-20,
|
||||||
|
-63
|
||||||
|
],
|
||||||
|
[
|
||||||
|
408,
|
||||||
|
109,
|
||||||
|
40,
|
||||||
|
61,
|
||||||
|
-20,
|
||||||
|
-63
|
||||||
|
],
|
||||||
|
[
|
||||||
|
449,
|
||||||
|
109,
|
||||||
|
40,
|
||||||
|
61,
|
||||||
|
-20,
|
||||||
|
-63
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
490,
|
||||||
|
109,
|
||||||
|
91,
|
||||||
|
37,
|
||||||
|
-72,
|
||||||
|
-63
|
||||||
|
],
|
||||||
|
[
|
||||||
|
582,
|
||||||
|
109,
|
||||||
|
91,
|
||||||
|
37,
|
||||||
|
-72,
|
||||||
|
-63
|
||||||
|
],
|
||||||
|
[
|
||||||
|
674,
|
||||||
|
109,
|
||||||
|
91,
|
||||||
|
37,
|
||||||
|
-72,
|
||||||
|
-63
|
||||||
|
],
|
||||||
|
[
|
||||||
|
766,
|
||||||
|
109,
|
||||||
|
91,
|
||||||
|
37,
|
||||||
|
-72,
|
||||||
|
-63
|
||||||
|
],
|
||||||
|
[
|
||||||
|
858,
|
||||||
|
109,
|
||||||
|
91,
|
||||||
|
37,
|
||||||
|
-72,
|
||||||
|
-63
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
950,
|
||||||
|
109,
|
||||||
|
53,
|
||||||
|
53,
|
||||||
|
-17,
|
||||||
|
-79
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
171,
|
||||||
|
53,
|
||||||
|
53,
|
||||||
|
-17,
|
||||||
|
-79
|
||||||
|
],
|
||||||
|
[
|
||||||
|
54,
|
||||||
|
171,
|
||||||
|
53,
|
||||||
|
53,
|
||||||
|
-17,
|
||||||
|
-79
|
||||||
|
],
|
||||||
|
[
|
||||||
|
108,
|
||||||
|
171,
|
||||||
|
53,
|
||||||
|
53,
|
||||||
|
-17,
|
||||||
|
-79
|
||||||
|
],
|
||||||
|
[
|
||||||
|
162,
|
||||||
|
171,
|
||||||
|
53,
|
||||||
|
53,
|
||||||
|
-17,
|
||||||
|
-79
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
216,
|
||||||
|
171,
|
||||||
|
88,
|
||||||
|
41,
|
||||||
|
-19,
|
||||||
|
-68
|
||||||
|
],
|
||||||
|
[
|
||||||
|
305,
|
||||||
|
171,
|
||||||
|
88,
|
||||||
|
41,
|
||||||
|
-19,
|
||||||
|
-68
|
||||||
|
],
|
||||||
|
[
|
||||||
|
394,
|
||||||
|
171,
|
||||||
|
88,
|
||||||
|
41,
|
||||||
|
-19,
|
||||||
|
-68
|
||||||
|
],
|
||||||
|
[
|
||||||
|
483,
|
||||||
|
171,
|
||||||
|
88,
|
||||||
|
41,
|
||||||
|
-19,
|
||||||
|
-68
|
||||||
|
],
|
||||||
|
[
|
||||||
|
572,
|
||||||
|
171,
|
||||||
|
88,
|
||||||
|
41,
|
||||||
|
-19,
|
||||||
|
-68
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
661,
|
||||||
|
171,
|
||||||
|
89,
|
||||||
|
45,
|
||||||
|
-19,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
751,
|
||||||
|
171,
|
||||||
|
89,
|
||||||
|
45,
|
||||||
|
-19,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
841,
|
||||||
|
171,
|
||||||
|
89,
|
||||||
|
45,
|
||||||
|
-19,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
931,
|
||||||
|
171,
|
||||||
|
89,
|
||||||
|
45,
|
||||||
|
-19,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
225,
|
||||||
|
89,
|
||||||
|
45,
|
||||||
|
-19,
|
||||||
|
-62
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
90,
|
||||||
|
225,
|
||||||
|
59,
|
||||||
|
57,
|
||||||
|
-17,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
150,
|
||||||
|
225,
|
||||||
|
59,
|
||||||
|
57,
|
||||||
|
-17,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
210,
|
||||||
|
225,
|
||||||
|
59,
|
||||||
|
57,
|
||||||
|
-17,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
270,
|
||||||
|
225,
|
||||||
|
59,
|
||||||
|
57,
|
||||||
|
-17,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
330,
|
||||||
|
225,
|
||||||
|
59,
|
||||||
|
57,
|
||||||
|
-17,
|
||||||
|
-62
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
390,
|
||||||
|
225,
|
||||||
|
59,
|
||||||
|
59,
|
||||||
|
-42,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
450,
|
||||||
|
225,
|
||||||
|
59,
|
||||||
|
59,
|
||||||
|
-42,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
510,
|
||||||
|
225,
|
||||||
|
59,
|
||||||
|
59,
|
||||||
|
-42,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
570,
|
||||||
|
225,
|
||||||
|
59,
|
||||||
|
59,
|
||||||
|
-42,
|
||||||
|
-62
|
||||||
|
],
|
||||||
|
[
|
||||||
|
630,
|
||||||
|
225,
|
||||||
|
59,
|
||||||
|
59,
|
||||||
|
-42,
|
||||||
|
-62
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
690,
|
||||||
|
225,
|
||||||
|
92,
|
||||||
|
43,
|
||||||
|
-72,
|
||||||
|
-61
|
||||||
|
],
|
||||||
|
[
|
||||||
|
783,
|
||||||
|
225,
|
||||||
|
92,
|
||||||
|
43,
|
||||||
|
-72,
|
||||||
|
-61
|
||||||
|
],
|
||||||
|
[
|
||||||
|
876,
|
||||||
|
225,
|
||||||
|
92,
|
||||||
|
43,
|
||||||
|
-72,
|
||||||
|
-61
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
285,
|
||||||
|
92,
|
||||||
|
43,
|
||||||
|
-72,
|
||||||
|
-61
|
||||||
|
],
|
||||||
|
[
|
||||||
|
93,
|
||||||
|
285,
|
||||||
|
92,
|
||||||
|
43,
|
||||||
|
-72,
|
||||||
|
-61
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
186,
|
||||||
|
285,
|
||||||
|
85,
|
||||||
|
43,
|
||||||
|
-65,
|
||||||
|
-69
|
||||||
|
],
|
||||||
|
[
|
||||||
|
272,
|
||||||
|
285,
|
||||||
|
85,
|
||||||
|
43,
|
||||||
|
-65,
|
||||||
|
-69
|
||||||
|
],
|
||||||
|
[
|
||||||
|
358,
|
||||||
|
285,
|
||||||
|
85,
|
||||||
|
43,
|
||||||
|
-65,
|
||||||
|
-69
|
||||||
|
],
|
||||||
|
[
|
||||||
|
444,
|
||||||
|
285,
|
||||||
|
85,
|
||||||
|
43,
|
||||||
|
-65,
|
||||||
|
-69
|
||||||
|
],
|
||||||
|
[
|
||||||
|
530,
|
||||||
|
285,
|
||||||
|
85,
|
||||||
|
43,
|
||||||
|
-65,
|
||||||
|
-69
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
616,
|
||||||
|
285,
|
||||||
|
53,
|
||||||
|
50,
|
||||||
|
-35,
|
||||||
|
-77
|
||||||
|
],
|
||||||
|
[
|
||||||
|
670,
|
||||||
|
285,
|
||||||
|
53,
|
||||||
|
50,
|
||||||
|
-35,
|
||||||
|
-77
|
||||||
|
],
|
||||||
|
[
|
||||||
|
724,
|
||||||
|
285,
|
||||||
|
53,
|
||||||
|
50,
|
||||||
|
-35,
|
||||||
|
-77
|
||||||
|
],
|
||||||
|
[
|
||||||
|
778,
|
||||||
|
285,
|
||||||
|
53,
|
||||||
|
50,
|
||||||
|
-35,
|
||||||
|
-77
|
||||||
|
],
|
||||||
|
[
|
||||||
|
832,
|
||||||
|
285,
|
||||||
|
53,
|
||||||
|
50,
|
||||||
|
-35,
|
||||||
|
-77
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
|
|
@ -0,0 +1,683 @@
|
||||||
|
{
|
||||||
|
"name": "firebolt",
|
||||||
|
"celFile": "Firebolt",
|
||||||
|
"width": 512,
|
||||||
|
"height": 439,
|
||||||
|
"directions": 16,
|
||||||
|
"framesPerDirection": 5,
|
||||||
|
"animSpeed": 16,
|
||||||
|
"groups": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
55,
|
||||||
|
39,
|
||||||
|
-12,
|
||||||
|
-71
|
||||||
|
],
|
||||||
|
[
|
||||||
|
56,
|
||||||
|
0,
|
||||||
|
55,
|
||||||
|
39,
|
||||||
|
-12,
|
||||||
|
-71
|
||||||
|
],
|
||||||
|
[
|
||||||
|
112,
|
||||||
|
0,
|
||||||
|
55,
|
||||||
|
39,
|
||||||
|
-12,
|
||||||
|
-71
|
||||||
|
],
|
||||||
|
[
|
||||||
|
168,
|
||||||
|
0,
|
||||||
|
55,
|
||||||
|
39,
|
||||||
|
-12,
|
||||||
|
-71
|
||||||
|
],
|
||||||
|
[
|
||||||
|
224,
|
||||||
|
0,
|
||||||
|
55,
|
||||||
|
39,
|
||||||
|
-12,
|
||||||
|
-71
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
280,
|
||||||
|
0,
|
||||||
|
63,
|
||||||
|
41,
|
||||||
|
-12,
|
||||||
|
-55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
344,
|
||||||
|
0,
|
||||||
|
63,
|
||||||
|
41,
|
||||||
|
-12,
|
||||||
|
-55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
408,
|
||||||
|
0,
|
||||||
|
63,
|
||||||
|
41,
|
||||||
|
-12,
|
||||||
|
-55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
42,
|
||||||
|
63,
|
||||||
|
41,
|
||||||
|
-12,
|
||||||
|
-55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
64,
|
||||||
|
42,
|
||||||
|
63,
|
||||||
|
41,
|
||||||
|
-12,
|
||||||
|
-55
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
128,
|
||||||
|
42,
|
||||||
|
62,
|
||||||
|
41,
|
||||||
|
-49,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
191,
|
||||||
|
42,
|
||||||
|
62,
|
||||||
|
41,
|
||||||
|
-49,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
254,
|
||||||
|
42,
|
||||||
|
62,
|
||||||
|
41,
|
||||||
|
-49,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
317,
|
||||||
|
42,
|
||||||
|
62,
|
||||||
|
41,
|
||||||
|
-49,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
380,
|
||||||
|
42,
|
||||||
|
62,
|
||||||
|
41,
|
||||||
|
-49,
|
||||||
|
-56
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
443,
|
||||||
|
42,
|
||||||
|
56,
|
||||||
|
40,
|
||||||
|
-44,
|
||||||
|
-71
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
84,
|
||||||
|
56,
|
||||||
|
40,
|
||||||
|
-44,
|
||||||
|
-71
|
||||||
|
],
|
||||||
|
[
|
||||||
|
57,
|
||||||
|
84,
|
||||||
|
56,
|
||||||
|
40,
|
||||||
|
-44,
|
||||||
|
-71
|
||||||
|
],
|
||||||
|
[
|
||||||
|
114,
|
||||||
|
84,
|
||||||
|
56,
|
||||||
|
40,
|
||||||
|
-44,
|
||||||
|
-71
|
||||||
|
],
|
||||||
|
[
|
||||||
|
171,
|
||||||
|
84,
|
||||||
|
56,
|
||||||
|
40,
|
||||||
|
-44,
|
||||||
|
-71
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
228,
|
||||||
|
84,
|
||||||
|
30,
|
||||||
|
45,
|
||||||
|
-15,
|
||||||
|
-76
|
||||||
|
],
|
||||||
|
[
|
||||||
|
259,
|
||||||
|
84,
|
||||||
|
30,
|
||||||
|
45,
|
||||||
|
-15,
|
||||||
|
-76
|
||||||
|
],
|
||||||
|
[
|
||||||
|
290,
|
||||||
|
84,
|
||||||
|
30,
|
||||||
|
45,
|
||||||
|
-15,
|
||||||
|
-76
|
||||||
|
],
|
||||||
|
[
|
||||||
|
321,
|
||||||
|
84,
|
||||||
|
30,
|
||||||
|
45,
|
||||||
|
-15,
|
||||||
|
-76
|
||||||
|
],
|
||||||
|
[
|
||||||
|
352,
|
||||||
|
84,
|
||||||
|
30,
|
||||||
|
45,
|
||||||
|
-15,
|
||||||
|
-76
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
383,
|
||||||
|
84,
|
||||||
|
71,
|
||||||
|
30,
|
||||||
|
-13,
|
||||||
|
-58
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
130,
|
||||||
|
71,
|
||||||
|
30,
|
||||||
|
-13,
|
||||||
|
-58
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72,
|
||||||
|
130,
|
||||||
|
71,
|
||||||
|
30,
|
||||||
|
-13,
|
||||||
|
-58
|
||||||
|
],
|
||||||
|
[
|
||||||
|
144,
|
||||||
|
130,
|
||||||
|
71,
|
||||||
|
30,
|
||||||
|
-13,
|
||||||
|
-58
|
||||||
|
],
|
||||||
|
[
|
||||||
|
216,
|
||||||
|
130,
|
||||||
|
71,
|
||||||
|
30,
|
||||||
|
-13,
|
||||||
|
-58
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
288,
|
||||||
|
130,
|
||||||
|
32,
|
||||||
|
46,
|
||||||
|
-15,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
321,
|
||||||
|
130,
|
||||||
|
32,
|
||||||
|
46,
|
||||||
|
-15,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
354,
|
||||||
|
130,
|
||||||
|
32,
|
||||||
|
46,
|
||||||
|
-15,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
387,
|
||||||
|
130,
|
||||||
|
32,
|
||||||
|
46,
|
||||||
|
-15,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
420,
|
||||||
|
130,
|
||||||
|
32,
|
||||||
|
46,
|
||||||
|
-15,
|
||||||
|
-56
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
177,
|
||||||
|
71,
|
||||||
|
28,
|
||||||
|
-57,
|
||||||
|
-57
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72,
|
||||||
|
177,
|
||||||
|
71,
|
||||||
|
28,
|
||||||
|
-57,
|
||||||
|
-57
|
||||||
|
],
|
||||||
|
[
|
||||||
|
144,
|
||||||
|
177,
|
||||||
|
71,
|
||||||
|
28,
|
||||||
|
-57,
|
||||||
|
-57
|
||||||
|
],
|
||||||
|
[
|
||||||
|
216,
|
||||||
|
177,
|
||||||
|
71,
|
||||||
|
28,
|
||||||
|
-57,
|
||||||
|
-57
|
||||||
|
],
|
||||||
|
[
|
||||||
|
288,
|
||||||
|
177,
|
||||||
|
71,
|
||||||
|
28,
|
||||||
|
-57,
|
||||||
|
-57
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
360,
|
||||||
|
177,
|
||||||
|
42,
|
||||||
|
44,
|
||||||
|
-12,
|
||||||
|
-75
|
||||||
|
],
|
||||||
|
[
|
||||||
|
403,
|
||||||
|
177,
|
||||||
|
42,
|
||||||
|
44,
|
||||||
|
-12,
|
||||||
|
-75
|
||||||
|
],
|
||||||
|
[
|
||||||
|
446,
|
||||||
|
177,
|
||||||
|
42,
|
||||||
|
44,
|
||||||
|
-12,
|
||||||
|
-75
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
222,
|
||||||
|
42,
|
||||||
|
44,
|
||||||
|
-12,
|
||||||
|
-75
|
||||||
|
],
|
||||||
|
[
|
||||||
|
43,
|
||||||
|
222,
|
||||||
|
42,
|
||||||
|
44,
|
||||||
|
-12,
|
||||||
|
-75
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
86,
|
||||||
|
222,
|
||||||
|
68,
|
||||||
|
33,
|
||||||
|
-14,
|
||||||
|
-65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
155,
|
||||||
|
222,
|
||||||
|
68,
|
||||||
|
33,
|
||||||
|
-14,
|
||||||
|
-65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
224,
|
||||||
|
222,
|
||||||
|
68,
|
||||||
|
33,
|
||||||
|
-14,
|
||||||
|
-65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
293,
|
||||||
|
222,
|
||||||
|
68,
|
||||||
|
33,
|
||||||
|
-14,
|
||||||
|
-65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
362,
|
||||||
|
222,
|
||||||
|
68,
|
||||||
|
33,
|
||||||
|
-14,
|
||||||
|
-65
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
431,
|
||||||
|
222,
|
||||||
|
69,
|
||||||
|
32,
|
||||||
|
-12,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
267,
|
||||||
|
69,
|
||||||
|
32,
|
||||||
|
-12,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70,
|
||||||
|
267,
|
||||||
|
69,
|
||||||
|
32,
|
||||||
|
-12,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
140,
|
||||||
|
267,
|
||||||
|
69,
|
||||||
|
32,
|
||||||
|
-12,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
210,
|
||||||
|
267,
|
||||||
|
69,
|
||||||
|
32,
|
||||||
|
-12,
|
||||||
|
-56
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
280,
|
||||||
|
267,
|
||||||
|
49,
|
||||||
|
45,
|
||||||
|
-12,
|
||||||
|
-55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
330,
|
||||||
|
267,
|
||||||
|
49,
|
||||||
|
45,
|
||||||
|
-12,
|
||||||
|
-55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
380,
|
||||||
|
267,
|
||||||
|
49,
|
||||||
|
45,
|
||||||
|
-12,
|
||||||
|
-55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
430,
|
||||||
|
267,
|
||||||
|
49,
|
||||||
|
45,
|
||||||
|
-12,
|
||||||
|
-55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
313,
|
||||||
|
49,
|
||||||
|
45,
|
||||||
|
-12,
|
||||||
|
-55
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
50,
|
||||||
|
313,
|
||||||
|
47,
|
||||||
|
45,
|
||||||
|
-35,
|
||||||
|
-55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
98,
|
||||||
|
313,
|
||||||
|
47,
|
||||||
|
45,
|
||||||
|
-35,
|
||||||
|
-55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
146,
|
||||||
|
313,
|
||||||
|
47,
|
||||||
|
45,
|
||||||
|
-35,
|
||||||
|
-55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
194,
|
||||||
|
313,
|
||||||
|
47,
|
||||||
|
45,
|
||||||
|
-35,
|
||||||
|
-55
|
||||||
|
],
|
||||||
|
[
|
||||||
|
242,
|
||||||
|
313,
|
||||||
|
47,
|
||||||
|
45,
|
||||||
|
-35,
|
||||||
|
-55
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
290,
|
||||||
|
313,
|
||||||
|
71,
|
||||||
|
36,
|
||||||
|
-58,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
362,
|
||||||
|
313,
|
||||||
|
71,
|
||||||
|
36,
|
||||||
|
-58,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
434,
|
||||||
|
313,
|
||||||
|
71,
|
||||||
|
36,
|
||||||
|
-58,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
359,
|
||||||
|
71,
|
||||||
|
36,
|
||||||
|
-58,
|
||||||
|
-56
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72,
|
||||||
|
359,
|
||||||
|
71,
|
||||||
|
36,
|
||||||
|
-58,
|
||||||
|
-56
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
144,
|
||||||
|
359,
|
||||||
|
67,
|
||||||
|
35,
|
||||||
|
-54,
|
||||||
|
-65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
212,
|
||||||
|
359,
|
||||||
|
67,
|
||||||
|
35,
|
||||||
|
-54,
|
||||||
|
-65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
280,
|
||||||
|
359,
|
||||||
|
67,
|
||||||
|
35,
|
||||||
|
-54,
|
||||||
|
-65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
348,
|
||||||
|
359,
|
||||||
|
67,
|
||||||
|
35,
|
||||||
|
-54,
|
||||||
|
-65
|
||||||
|
],
|
||||||
|
[
|
||||||
|
416,
|
||||||
|
359,
|
||||||
|
67,
|
||||||
|
35,
|
||||||
|
-54,
|
||||||
|
-65
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
396,
|
||||||
|
44,
|
||||||
|
43,
|
||||||
|
-32,
|
||||||
|
-74
|
||||||
|
],
|
||||||
|
[
|
||||||
|
45,
|
||||||
|
396,
|
||||||
|
44,
|
||||||
|
43,
|
||||||
|
-32,
|
||||||
|
-74
|
||||||
|
],
|
||||||
|
[
|
||||||
|
90,
|
||||||
|
396,
|
||||||
|
44,
|
||||||
|
43,
|
||||||
|
-32,
|
||||||
|
-74
|
||||||
|
],
|
||||||
|
[
|
||||||
|
135,
|
||||||
|
396,
|
||||||
|
44,
|
||||||
|
43,
|
||||||
|
-32,
|
||||||
|
-74
|
||||||
|
],
|
||||||
|
[
|
||||||
|
180,
|
||||||
|
396,
|
||||||
|
44,
|
||||||
|
43,
|
||||||
|
-32,
|
||||||
|
-74
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
|
|
@ -0,0 +1,109 @@
|
||||||
|
{
|
||||||
|
"name": "fireexplode",
|
||||||
|
"celFile": "FireArrowExplode2",
|
||||||
|
"width": 512,
|
||||||
|
"height": 141,
|
||||||
|
"directions": 1,
|
||||||
|
"framesPerDirection": 12,
|
||||||
|
"animSpeed": 16,
|
||||||
|
"groups": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
78,
|
||||||
|
70,
|
||||||
|
-39,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79,
|
||||||
|
0,
|
||||||
|
78,
|
||||||
|
70,
|
||||||
|
-39,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
158,
|
||||||
|
0,
|
||||||
|
78,
|
||||||
|
70,
|
||||||
|
-39,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
237,
|
||||||
|
0,
|
||||||
|
78,
|
||||||
|
70,
|
||||||
|
-39,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
316,
|
||||||
|
0,
|
||||||
|
78,
|
||||||
|
70,
|
||||||
|
-39,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
395,
|
||||||
|
0,
|
||||||
|
78,
|
||||||
|
70,
|
||||||
|
-39,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
71,
|
||||||
|
78,
|
||||||
|
70,
|
||||||
|
-39,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
79,
|
||||||
|
71,
|
||||||
|
78,
|
||||||
|
70,
|
||||||
|
-39,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
158,
|
||||||
|
71,
|
||||||
|
78,
|
||||||
|
70,
|
||||||
|
-39,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
237,
|
||||||
|
71,
|
||||||
|
78,
|
||||||
|
70,
|
||||||
|
-39,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
316,
|
||||||
|
71,
|
||||||
|
78,
|
||||||
|
70,
|
||||||
|
-39,
|
||||||
|
-81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
395,
|
||||||
|
71,
|
||||||
|
78,
|
||||||
|
70,
|
||||||
|
-39,
|
||||||
|
-81
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -0,0 +1,213 @@
|
||||||
|
/**
|
||||||
|
* Offline Missile Asset & Data Baking Script.
|
||||||
|
*
|
||||||
|
* Extracts authentic Diablo II v1.13c missile DCC sprites from d2data.mpq:
|
||||||
|
* - Firebolt.dcc (16 directions, 5 frames per direction)
|
||||||
|
* - Fireball.dcc (16 directions, 5 frames per direction)
|
||||||
|
* - FireArrowExplode2.dcc (1 direction, 12 frames)
|
||||||
|
*
|
||||||
|
* Bakes them into indexed-color PNG atlases and JSON metadata under:
|
||||||
|
* - public/missiles/
|
||||||
|
* - samples/d2-packs/missiles/
|
||||||
|
*
|
||||||
|
* And generates typed metadata in src/render/missiles-meta.ts.
|
||||||
|
* Enforces Zero Runtime MPQ/DLL Architectural Invariant.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { MpqArchive } from '../src/mpq/archive.ts'
|
||||||
|
import { fileSource } from '../src/mpq/file-source.ts'
|
||||||
|
import { decodeDcc } from '../src/formats/dcc.ts'
|
||||||
|
import { decodePl2 } from '../src/formats/pl2.ts'
|
||||||
|
import { packSheetToPng } from './pack-entity-assets.ts'
|
||||||
|
import type { SpriteFrame, SpriteSheet } from '../src/formats/sprite.ts'
|
||||||
|
|
||||||
|
export interface MissileFrameMeta {
|
||||||
|
readonly x: number
|
||||||
|
readonly y: number
|
||||||
|
readonly w: number
|
||||||
|
readonly h: number
|
||||||
|
readonly anchorX: number
|
||||||
|
readonly anchorY: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MissileFrameTuple = readonly [
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
anchorX: number,
|
||||||
|
anchorY: number,
|
||||||
|
]
|
||||||
|
|
||||||
|
export interface MissileMeta {
|
||||||
|
readonly name: string
|
||||||
|
readonly celFile: string
|
||||||
|
readonly width: number
|
||||||
|
readonly height: number
|
||||||
|
readonly directions: number
|
||||||
|
readonly framesPerDirection: number
|
||||||
|
readonly animSpeed: number
|
||||||
|
readonly groups: readonly (readonly MissileFrameTuple[])[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const MISSILE_TARGETS = [
|
||||||
|
{ name: 'firebolt', celFile: 'Firebolt', animSpeed: 16 },
|
||||||
|
{ name: 'fireball', celFile: 'Fireball', animSpeed: 16 },
|
||||||
|
{ name: 'fireexplode', celFile: 'FireArrowExplode2', animSpeed: 16 },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export async function bakeMissiles(projectRoot: string = process.cwd()): Promise<Record<string, MissileMeta>> {
|
||||||
|
const mpqPath = join(projectRoot, 'samples', 'd2', 'd2data.mpq')
|
||||||
|
const mpq = await MpqArchive.open(await fileSource(mpqPath))
|
||||||
|
|
||||||
|
const palFile = mpq.find('data/global/palette/ACT1/pal.pl2')
|
||||||
|
if (!palFile) throw new Error('data/global/palette/ACT1/pal.pl2 not found in d2data.mpq')
|
||||||
|
const pl2 = decodePl2(await mpq.read(palFile))
|
||||||
|
|
||||||
|
const publicOutDir = join(projectRoot, 'public', 'missiles')
|
||||||
|
const packOutDir = join(projectRoot, 'samples', 'd2-packs', 'missiles')
|
||||||
|
mkdirSync(publicOutDir, { recursive: true })
|
||||||
|
mkdirSync(packOutDir, { recursive: true })
|
||||||
|
|
||||||
|
const metaRecord: Record<string, MissileMeta> = {}
|
||||||
|
|
||||||
|
for (const target of MISSILE_TARGETS) {
|
||||||
|
const f = mpq.find(`data/global/missiles/${target.celFile}.dcc`)
|
||||||
|
if (!f) throw new Error(`Missile DCC not found: data/global/missiles/${target.celFile}.dcc`)
|
||||||
|
|
||||||
|
const dcc = decodeDcc(await mpq.read(f))
|
||||||
|
const groups = dcc.directions.map(dir => ({
|
||||||
|
frames: dir.frames.map(
|
||||||
|
(frame): SpriteFrame => ({
|
||||||
|
width: frame.frame.width,
|
||||||
|
height: frame.frame.height,
|
||||||
|
indices: frame.frame.indices,
|
||||||
|
mask: frame.frame.mask,
|
||||||
|
anchorX: dir.box.left,
|
||||||
|
anchorY: dir.box.top,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const sheet: SpriteSheet = { groups, width: null }
|
||||||
|
const packed = packSheetToPng(sheet, pl2.rgb)
|
||||||
|
|
||||||
|
const meta: MissileMeta = {
|
||||||
|
name: target.name,
|
||||||
|
celFile: target.celFile,
|
||||||
|
width: packed.width,
|
||||||
|
height: packed.height,
|
||||||
|
directions: dcc.directions.length,
|
||||||
|
framesPerDirection: dcc.directions[0]?.frames.length ?? 0,
|
||||||
|
animSpeed: target.animSpeed,
|
||||||
|
groups: packed.groups,
|
||||||
|
}
|
||||||
|
|
||||||
|
metaRecord[target.name] = meta
|
||||||
|
|
||||||
|
// Write PNG and JSON to public/missiles/ and samples/d2-packs/missiles/
|
||||||
|
const jsonStr = JSON.stringify(meta, null, 2)
|
||||||
|
writeFileSync(join(publicOutDir, `${target.name}.png`), packed.png)
|
||||||
|
writeFileSync(join(publicOutDir, `${target.name}.json`), jsonStr, 'utf-8')
|
||||||
|
writeFileSync(join(packOutDir, `${target.name}.png`), packed.png)
|
||||||
|
writeFileSync(join(packOutDir, `${target.name}.json`), jsonStr, 'utf-8')
|
||||||
|
|
||||||
|
console.log(`[Missile Packer] Baked ${target.name} (${target.celFile}.dcc): ${packed.width}x${packed.height}, ${packed.png.byteLength} bytes PNG`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate src/render/missiles-meta.ts
|
||||||
|
const metaTsContent = `/**
|
||||||
|
* Authentic Diablo II v1.13c Missile Atlas Metadata.
|
||||||
|
* Auto-generated by scripts/pack-missiles.ts.
|
||||||
|
* Enforces Zero Runtime MPQ/DLL Architectural Invariant.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface MissileFrameMeta {
|
||||||
|
readonly x: number
|
||||||
|
readonly y: number
|
||||||
|
readonly w: number
|
||||||
|
readonly h: number
|
||||||
|
readonly anchorX: number
|
||||||
|
readonly anchorY: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MissileFrameTuple = readonly [
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
anchorX: number,
|
||||||
|
anchorY: number,
|
||||||
|
]
|
||||||
|
|
||||||
|
export interface MissileMeta {
|
||||||
|
readonly name: string
|
||||||
|
readonly celFile: string
|
||||||
|
readonly width: number
|
||||||
|
readonly height: number
|
||||||
|
readonly directions: number
|
||||||
|
readonly framesPerDirection: number
|
||||||
|
readonly animSpeed: number
|
||||||
|
readonly groups: readonly (readonly MissileFrameTuple[])[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The engine's 64-direction space -> DCC direction tables.
|
||||||
|
* Ported from OpenDiablo2 d2fileformats/d2dcc/dcc_dir_lookup.go (Dir64ToDcc).
|
||||||
|
*/
|
||||||
|
const DIR64_TO_DCC_16: readonly number[] = [
|
||||||
|
4, 4, 8, 8, 8, 8, 0, 0, 0, 0, 9, 9, 9, 9, 5, 5,
|
||||||
|
5, 5, 10, 10, 10, 10, 1, 1, 1, 1, 11, 11, 11, 11, 6, 6,
|
||||||
|
6, 6, 12, 12, 12, 12, 2, 2, 2, 2, 13, 13, 13, 13, 7, 7,
|
||||||
|
7, 7, 14, 14, 14, 14, 3, 3, 3, 3, 15, 15, 15, 15, 4, 4,
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate DCC direction index for a missile based on 2D velocity vector.
|
||||||
|
*
|
||||||
|
* In Diablo II's 64-direction space:
|
||||||
|
* - South (0, +y) is dir 0
|
||||||
|
* - West (-x, 0) is dir 16
|
||||||
|
* - North (0, -y) is dir 32
|
||||||
|
* - East (+x, 0) is dir 48
|
||||||
|
*/
|
||||||
|
export function velocityToDccDirection(vx: number, vy: number, directions = 16): number {
|
||||||
|
if (directions <= 1) return 0
|
||||||
|
const angle = Math.atan2(vy, vx) // -PI to PI
|
||||||
|
// Map angle so that South (PI/2) is 0, turning clockwise
|
||||||
|
const fraction = (angle - Math.PI / 2) / (2 * Math.PI)
|
||||||
|
const dir64 = Math.round((((fraction % 1) + 1) % 1) * 64) % 64
|
||||||
|
if (directions === 16) {
|
||||||
|
return DIR64_TO_DCC_16[dir64] ?? 0
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FIREBOLT_META: MissileMeta = ${JSON.stringify(metaRecord['firebolt'], null, 2)}
|
||||||
|
|
||||||
|
export const FIREBALL_META: MissileMeta = ${JSON.stringify(metaRecord['fireball'], null, 2)}
|
||||||
|
|
||||||
|
export const FIREEXPLODE_META: MissileMeta = ${JSON.stringify(metaRecord['fireexplode'], null, 2)}
|
||||||
|
|
||||||
|
export const MISSILE_METAS: Readonly<Record<string, MissileMeta>> = {
|
||||||
|
firebolt: FIREBOLT_META,
|
||||||
|
fireball: FIREBALL_META,
|
||||||
|
fireexplode: FIREEXPLODE_META,
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const metaTsPath = join(projectRoot, 'src', 'render', 'missiles-meta.ts')
|
||||||
|
writeFileSync(metaTsPath, metaTsContent, 'utf-8')
|
||||||
|
console.log(`[Missile Packer] Wrote ${metaTsPath}`)
|
||||||
|
|
||||||
|
return metaRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute standalone if called from CLI
|
||||||
|
if (process.argv[1]?.endsWith('pack-missiles.ts')) {
|
||||||
|
bakeMissiles().catch((err: unknown) => {
|
||||||
|
console.error('Bake missiles failed:', err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -55,6 +55,8 @@ import { buildBounds, intersects, viewportRect } from '../render/cull.ts'
|
||||||
import { RoofFadeController } from '../render/roofs.ts'
|
import { RoofFadeController } from '../render/roofs.ts'
|
||||||
import { facingToDirection } from '../game/character.ts'
|
import { facingToDirection } from '../game/character.ts'
|
||||||
import type { LoadedMonsterArt } from '../game/monster-art.ts'
|
import type { LoadedMonsterArt } from '../game/monster-art.ts'
|
||||||
|
import type { MissileMeta } from '../render/missiles-meta.ts'
|
||||||
|
import { velocityToDccDirection } from '../render/missiles-meta.ts'
|
||||||
import { ActorAnimator, type AnimClipMeta } from '../game/actor-animator.ts'
|
import { ActorAnimator, type AnimClipMeta } from '../game/actor-animator.ts'
|
||||||
import { resolveMonsterArtSpec } from '../game/monster-mapping.ts'
|
import { resolveMonsterArtSpec } from '../game/monster-mapping.ts'
|
||||||
import { ACT_NAMES_ZH, LEVEL_NAMES_ZH, getLevelNames, sceneNameZh, variantLabelZh } from '../game/level-names-zh.ts'
|
import { ACT_NAMES_ZH, LEVEL_NAMES_ZH, getLevelNames, sceneNameZh, variantLabelZh } from '../game/level-names-zh.ts'
|
||||||
|
|
@ -2937,11 +2939,94 @@ function buildStreamingRooms(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a Diablo II 1.13c style Fire Bolt projectile:
|
* Packed missile animation asset loaded into GPU memory.
|
||||||
* Luminous multi-layered fireball with white-hot core, fiery body, glowing halo,
|
|
||||||
* and a trailing plume of animated fire/ember particles extending behind the velocity vector.
|
|
||||||
*/
|
*/
|
||||||
export function drawFireboltProjectile(renderer: SpriteRenderer, shot: Projectile): void {
|
export interface LoadedMissileArt {
|
||||||
|
readonly meta: MissileMeta
|
||||||
|
readonly handle: AtlasHandle
|
||||||
|
readonly frames: AtlasFrame[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load authentic 1.13c baked missile atlases from /missiles/*.png or packBase.
|
||||||
|
*/
|
||||||
|
export async function loadMissileArtMap(
|
||||||
|
renderer: SpriteRenderer,
|
||||||
|
packBase?: string,
|
||||||
|
): Promise<Map<string, LoadedMissileArt>> {
|
||||||
|
const map = new Map<string, LoadedMissileArt>()
|
||||||
|
if (typeof fetch === 'undefined') return map
|
||||||
|
const targets = ['firebolt', 'fireball', 'fireexplode']
|
||||||
|
const baseCandidates = [
|
||||||
|
'/missiles',
|
||||||
|
packBase ? `${packBase}/missiles` : '',
|
||||||
|
'samples/d2-packs/missiles',
|
||||||
|
].filter(Boolean)
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
targets.map(async name => {
|
||||||
|
for (const base of baseCandidates) {
|
||||||
|
try {
|
||||||
|
const [jsonResp, pngResp] = await Promise.all([
|
||||||
|
fetch(`${base}/${name}.json`),
|
||||||
|
fetch(`${base}/${name}.png`),
|
||||||
|
])
|
||||||
|
if (jsonResp.ok && pngResp.ok) {
|
||||||
|
const meta = (await jsonResp.json()) as MissileMeta
|
||||||
|
const blob = await pngResp.blob()
|
||||||
|
const bitmap = await createImageBitmap(blob)
|
||||||
|
let handle: AtlasHandle
|
||||||
|
try {
|
||||||
|
handle = renderer.addAtlas(bitmap, meta.width, meta.height)
|
||||||
|
} finally {
|
||||||
|
bitmap.close()
|
||||||
|
}
|
||||||
|
const frames: AtlasFrame[][] = meta.groups.map(group =>
|
||||||
|
group.map(([x, y, width, height, anchorX, anchorY]) => ({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
...(anchorX !== undefined ? { anchorX } : {}),
|
||||||
|
...(anchorY !== undefined ? { anchorY } : {}),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
map.set(name, { meta, handle, frames })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore candidate error and continue to next fallback path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a Diablo II 1.13c style Fire Bolt projectile:
|
||||||
|
* - Uses authentic baked Firebolt.dcc sprite atlas when available.
|
||||||
|
* - Gracefully falls back to procedural glowing fireball & trailing particles in test/offline environments.
|
||||||
|
*/
|
||||||
|
export function drawFireboltProjectile(
|
||||||
|
renderer: SpriteRenderer,
|
||||||
|
shot: Projectile,
|
||||||
|
missileArt?: LoadedMissileArt,
|
||||||
|
): void {
|
||||||
|
if (missileArt !== undefined) {
|
||||||
|
const dccDir = velocityToDccDirection(shot.vx, shot.vy, missileArt.meta.directions)
|
||||||
|
const frameCount = Math.max(1, missileArt.meta.framesPerDirection)
|
||||||
|
const frameIndex = Math.floor(((50 - shot.ttl) * missileArt.meta.animSpeed) / 16) % frameCount
|
||||||
|
const group = missileArt.frames[dccDir] ?? missileArt.frames[0]
|
||||||
|
const frame = group?.[frameIndex] ?? group?.[0]
|
||||||
|
if (frame !== undefined) {
|
||||||
|
const drawX = frame.anchorX !== undefined ? shot.x + frame.anchorX : shot.x - frame.width / 2
|
||||||
|
const drawY = frame.anchorY !== undefined ? shot.y + frame.anchorY : shot.y - frame.height / 2
|
||||||
|
renderer.draw(frame, drawX, drawY, { atlas: missileArt.handle })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const x = shot.x
|
const x = shot.x
|
||||||
const y = shot.y
|
const y = shot.y
|
||||||
const vx = shot.vx
|
const vx = shot.vx
|
||||||
|
|
@ -2954,7 +3039,7 @@ export function drawFireboltProjectile(renderer: SpriteRenderer, shot: Projectil
|
||||||
for (let i = 1; i <= 5; i += 1) {
|
for (let i = 1; i <= 5; i += 1) {
|
||||||
const dist = i * 4.2
|
const dist = i * 4.2
|
||||||
// Sinusoidal organic wobble perpendicular to travel direction
|
// Sinusoidal organic wobble perpendicular to travel direction
|
||||||
const flicker = Math.sin((shot.ttl * 1.8) + i * 1.5) * (i * 0.75)
|
const flicker = Math.sin(shot.ttl * 1.8 + i * 1.5) * (i * 0.75)
|
||||||
const px = x - dirX * dist - dirY * flicker
|
const px = x - dirX * dist - dirY * flicker
|
||||||
const py = y - dirY * dist + dirX * flicker
|
const py = y - dirY * dist + dirX * flicker
|
||||||
const size = Math.max(3, 8 - i)
|
const size = Math.max(3, 8 - i)
|
||||||
|
|
@ -2963,7 +3048,7 @@ export function drawFireboltProjectile(renderer: SpriteRenderer, shot: Projectil
|
||||||
else if (i === 2) color = [0.98, 0.52, 0.08, 0.62]
|
else if (i === 2) color = [0.98, 0.52, 0.08, 0.62]
|
||||||
else if (i === 3) color = [0.92, 0.32, 0.04, 0.46]
|
else if (i === 3) color = [0.92, 0.32, 0.04, 0.46]
|
||||||
else if (i === 4) color = [0.75, 0.18, 0.02, 0.32]
|
else if (i === 4) color = [0.75, 0.18, 0.02, 0.32]
|
||||||
else color = [0.45, 0.10, 0.02, 0.18]
|
else color = [0.45, 0.1, 0.02, 0.18]
|
||||||
renderer.drawSolid(px - size / 2, py - size / 2, size, size, color)
|
renderer.drawSolid(px - size / 2, py - size / 2, size, size, color)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2971,7 +3056,7 @@ export function drawFireboltProjectile(renderer: SpriteRenderer, shot: Projectil
|
||||||
renderer.drawSolid(x - 10, y - 10, 20, 20, [1.0, 0.35, 0.05, 0.22])
|
renderer.drawSolid(x - 10, y - 10, 20, 20, [1.0, 0.35, 0.05, 0.22])
|
||||||
|
|
||||||
// 3. Medium bright flame body
|
// 3. Medium bright flame body
|
||||||
renderer.drawSolid(x - 7, y - 7, 14, 14, [1.0, 0.62, 0.10, 0.55])
|
renderer.drawSolid(x - 7, y - 7, 14, 14, [1.0, 0.62, 0.1, 0.55])
|
||||||
|
|
||||||
// 4. Inner intense fire
|
// 4. Inner intense fire
|
||||||
renderer.drawSolid(x - 4, y - 4, 8, 8, [1.0, 0.88, 0.22, 0.88])
|
renderer.drawSolid(x - 4, y - 4, 8, 8, [1.0, 0.88, 0.22, 0.88])
|
||||||
|
|
@ -2982,9 +3067,27 @@ export function drawFireboltProjectile(renderer: SpriteRenderer, shot: Projectil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a Diablo II 1.13c style fire explosion (fireexplode):
|
* Render a Diablo II 1.13c style fire explosion (fireexplode):
|
||||||
* 12-frame expanding fireball burst with bright initial flash and radial fiery sparks.
|
* - Uses authentic baked FireArrowExplode2.dcc sprite atlas when available.
|
||||||
|
* - Falls back to procedural explosion in test/offline environments.
|
||||||
*/
|
*/
|
||||||
export function drawFireExplosion(renderer: SpriteRenderer, exp: ActiveExplosion): void {
|
export function drawFireExplosion(
|
||||||
|
renderer: SpriteRenderer,
|
||||||
|
exp: ActiveExplosion,
|
||||||
|
explosionArt?: LoadedMissileArt,
|
||||||
|
): void {
|
||||||
|
if (explosionArt !== undefined) {
|
||||||
|
const frameCount = Math.max(1, explosionArt.meta.framesPerDirection)
|
||||||
|
const frameIndex = Math.min(Math.max(0, exp.frame), frameCount - 1)
|
||||||
|
const group = explosionArt.frames[0]
|
||||||
|
const frame = group?.[frameIndex] ?? group?.[0]
|
||||||
|
if (frame !== undefined) {
|
||||||
|
const drawX = frame.anchorX !== undefined ? exp.x + frame.anchorX : exp.x - frame.width / 2
|
||||||
|
const drawY = frame.anchorY !== undefined ? exp.y + frame.anchorY : exp.y - frame.height / 2
|
||||||
|
renderer.draw(frame, drawX, drawY, { atlas: explosionArt.handle })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const progress = Math.min(1, Math.max(0, exp.frame / exp.maxFrames))
|
const progress = Math.min(1, Math.max(0, exp.frame / exp.maxFrames))
|
||||||
const inv = 1 - progress
|
const inv = 1 - progress
|
||||||
const x = exp.x
|
const x = exp.x
|
||||||
|
|
@ -3150,6 +3253,10 @@ async function runScene(initialRuntime: MapRuntime, renderer: SpriteRenderer, st
|
||||||
|
|
||||||
let character: LoadedCharacter | null = null
|
let character: LoadedCharacter | null = null
|
||||||
let monsterArtMap = new Map<string, LoadedMonsterArt>()
|
let monsterArtMap = new Map<string, LoadedMonsterArt>()
|
||||||
|
let missileArtMap = new Map<string, LoadedMissileArt>()
|
||||||
|
void loadMissileArtMap(renderer, packEntityBase).then(map => {
|
||||||
|
missileArtMap = map
|
||||||
|
})
|
||||||
let monsterWalkFrame = 0
|
let monsterWalkFrame = 0
|
||||||
const playerAnimator = new ActorAnimator()
|
const playerAnimator = new ActorAnimator()
|
||||||
|
|
||||||
|
|
@ -4182,7 +4289,9 @@ if (typeof window !== 'undefined') {
|
||||||
for (const shot of engine.projectiles) {
|
for (const shot of engine.projectiles) {
|
||||||
pushEntity(shot.x, shot.y, () => {
|
pushEntity(shot.x, shot.y, () => {
|
||||||
if (shot.skillId === '36' || shot.skillId === 'firebolt' || shot.missileType === 'firebolt') {
|
if (shot.skillId === '36' || shot.skillId === 'firebolt' || shot.missileType === 'firebolt') {
|
||||||
drawFireboltProjectile(renderer, shot)
|
drawFireboltProjectile(renderer, shot, missileArtMap.get('firebolt'))
|
||||||
|
} else if (shot.skillId === '47' || shot.skillId === 'fireball' || shot.missileType === 'fireball') {
|
||||||
|
drawFireboltProjectile(renderer, shot, missileArtMap.get('fireball'))
|
||||||
} else {
|
} else {
|
||||||
renderer.drawSolid(shot.x - 4, shot.y - 4, 8, 8, [0.9, 0.9, 0.2, 1])
|
renderer.drawSolid(shot.x - 4, shot.y - 4, 8, 8, [0.9, 0.9, 0.2, 1])
|
||||||
}
|
}
|
||||||
|
|
@ -4191,7 +4300,7 @@ if (typeof window !== 'undefined') {
|
||||||
|
|
||||||
for (const exp of engine.explosions) {
|
for (const exp of engine.explosions) {
|
||||||
pushEntity(exp.x, exp.y, () => {
|
pushEntity(exp.x, exp.y, () => {
|
||||||
drawFireExplosion(renderer, exp)
|
drawFireExplosion(renderer, exp, missileArtMap.get('fireexplode'))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,11 @@ import {
|
||||||
tickProjectiles,
|
tickProjectiles,
|
||||||
type Projectile,
|
type Projectile,
|
||||||
} from '../src/game/skills.ts'
|
} from '../src/game/skills.ts'
|
||||||
import { getSkillRange, drawFireboltProjectile, drawFireExplosion } from '../src/scene/act-scene.ts'
|
import { getSkillRange, drawFireboltProjectile, drawFireExplosion, type LoadedMissileArt } from '../src/scene/act-scene.ts'
|
||||||
import { GameEngine } from '../src/game/engine.ts'
|
import { GameEngine } from '../src/game/engine.ts'
|
||||||
import { DEMO_EXPERIENCE, DEMO_SKILLS, DEMO_QUESTS } from '../src/game/demo-data.ts'
|
import { DEMO_EXPERIENCE, DEMO_SKILLS, DEMO_QUESTS } from '../src/game/demo-data.ts'
|
||||||
import { SpriteRenderer } from '../src/render/renderer.ts'
|
import { SpriteRenderer, type AtlasHandle } from '../src/render/renderer.ts'
|
||||||
|
import { FIREBOLT_META, FIREEXPLODE_META, velocityToDccDirection } from '../src/render/missiles-meta.ts'
|
||||||
|
|
||||||
describe('Issue #385 — Fire Bolt (Skill #36) & Missiles.txt Ground Truth', () => {
|
describe('Issue #385 — Fire Bolt (Skill #36) & Missiles.txt Ground Truth', () => {
|
||||||
const missilesTxtPath = path.resolve(__dirname, '../samples/fixtures/data/global/excel/Missiles.txt')
|
const missilesTxtPath = path.resolve(__dirname, '../samples/fixtures/data/global/excel/Missiles.txt')
|
||||||
|
|
@ -368,4 +369,117 @@ describe('Issue #385 — Fire Bolt (Skill #36) & Missiles.txt Ground Truth', ()
|
||||||
// 2 blast layers + 1 flash core + 8 radial sparks = 11 quads
|
// 2 blast layers + 1 flash core + 8 radial sparks = 11 quads
|
||||||
expect(drawnQuads.length).toBe(11)
|
expect(drawnQuads.length).toBe(11)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('10. Verifies offline baked authentic missile DCC assets and metadata', () => {
|
||||||
|
const publicFireboltPng = path.resolve(__dirname, '../public/missiles/firebolt.png')
|
||||||
|
const publicFireboltJson = path.resolve(__dirname, '../public/missiles/firebolt.json')
|
||||||
|
const publicExplodePng = path.resolve(__dirname, '../public/missiles/fireexplode.png')
|
||||||
|
const publicExplodeJson = path.resolve(__dirname, '../public/missiles/fireexplode.json')
|
||||||
|
|
||||||
|
expect(fs.existsSync(publicFireboltPng)).toBe(true)
|
||||||
|
expect(fs.statSync(publicFireboltPng).size).toBeGreaterThan(50000)
|
||||||
|
|
||||||
|
expect(fs.existsSync(publicFireboltJson)).toBe(true)
|
||||||
|
const fireboltJson = JSON.parse(fs.readFileSync(publicFireboltJson, 'utf-8'))
|
||||||
|
expect(fireboltJson.name).toBe('firebolt')
|
||||||
|
expect(fireboltJson.celFile).toBe('Firebolt')
|
||||||
|
expect(fireboltJson.directions).toBe(16)
|
||||||
|
expect(fireboltJson.framesPerDirection).toBe(5)
|
||||||
|
expect(fireboltJson.groups.length).toBe(16)
|
||||||
|
expect(fireboltJson.groups[0].length).toBe(5)
|
||||||
|
|
||||||
|
expect(fs.existsSync(publicExplodePng)).toBe(true)
|
||||||
|
expect(fs.statSync(publicExplodePng).size).toBeGreaterThan(10000)
|
||||||
|
|
||||||
|
expect(fs.existsSync(publicExplodeJson)).toBe(true)
|
||||||
|
const explodeJson = JSON.parse(fs.readFileSync(publicExplodeJson, 'utf-8'))
|
||||||
|
expect(explodeJson.name).toBe('fireexplode')
|
||||||
|
expect(explodeJson.celFile).toBe('FireArrowExplode2')
|
||||||
|
expect(explodeJson.directions).toBe(1)
|
||||||
|
expect(explodeJson.framesPerDirection).toBe(12)
|
||||||
|
expect(explodeJson.groups[0].length).toBe(12)
|
||||||
|
|
||||||
|
// Check samples/d2-packs mirrors
|
||||||
|
expect(fs.existsSync(path.resolve(__dirname, '../samples/d2-packs/missiles/firebolt.png'))).toBe(true)
|
||||||
|
expect(fs.existsSync(path.resolve(__dirname, '../samples/d2-packs/missiles/fireexplode.png'))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('11. Renders authentic baked DCC frames with correct direction mapping and sprite-space anchors', () => {
|
||||||
|
const drawnSprites: any[] = []
|
||||||
|
const mockAtlasHandle = {
|
||||||
|
texture: {} as WebGLTexture,
|
||||||
|
width: 512,
|
||||||
|
height: 439,
|
||||||
|
} as AtlasHandle
|
||||||
|
|
||||||
|
const mockRenderer = {
|
||||||
|
draw(frame: any, x: number, y: number, options?: any) {
|
||||||
|
drawnSprites.push({ frame, x, y, options })
|
||||||
|
},
|
||||||
|
drawSolid() {},
|
||||||
|
} as unknown as SpriteRenderer
|
||||||
|
|
||||||
|
// Build loaded art from metadata
|
||||||
|
const fireboltFrames = FIREBOLT_META.groups.map(group =>
|
||||||
|
group.map(([x, y, width, height, anchorX, anchorY]) => ({
|
||||||
|
x, y, width, height, anchorX, anchorY,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const fireboltArt: LoadedMissileArt = {
|
||||||
|
meta: FIREBOLT_META,
|
||||||
|
handle: mockAtlasHandle,
|
||||||
|
frames: fireboltFrames,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shot moving East (+x): vx = 20, vy = 0
|
||||||
|
const dccEast = velocityToDccDirection(20, 0, 16)
|
||||||
|
expect(dccEast).toBeDefined()
|
||||||
|
|
||||||
|
const shot: Projectile = {
|
||||||
|
skillId: '36',
|
||||||
|
missileType: 'firebolt',
|
||||||
|
x: 300,
|
||||||
|
y: 200,
|
||||||
|
vx: 20,
|
||||||
|
vy: 0,
|
||||||
|
damage: 16,
|
||||||
|
ttl: 48,
|
||||||
|
fromPlayer: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
drawFireboltProjectile(mockRenderer, shot, fireboltArt)
|
||||||
|
expect(drawnSprites.length).toBe(1)
|
||||||
|
const drawn = drawnSprites[0]!
|
||||||
|
expect(drawn.options.atlas).toBe(mockAtlasHandle)
|
||||||
|
// Verify sprite is positioned with anchor offset: x + anchorX, y + anchorY
|
||||||
|
expect(drawn.x).toBe(300 + drawn.frame.anchorX)
|
||||||
|
expect(drawn.y).toBe(200 + drawn.frame.anchorY)
|
||||||
|
|
||||||
|
// Verify explosion DCC rendering
|
||||||
|
const explodeFrames = FIREEXPLODE_META.groups.map(group =>
|
||||||
|
group.map(([x, y, width, height, anchorX, anchorY]) => ({
|
||||||
|
x, y, width, height, anchorX, anchorY,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const explodeArt: LoadedMissileArt = {
|
||||||
|
meta: FIREEXPLODE_META,
|
||||||
|
handle: mockAtlasHandle,
|
||||||
|
frames: explodeFrames,
|
||||||
|
}
|
||||||
|
|
||||||
|
drawnSprites.length = 0
|
||||||
|
drawFireExplosion(mockRenderer, {
|
||||||
|
missileType: 'fireexplode',
|
||||||
|
x: 350,
|
||||||
|
y: 250,
|
||||||
|
frame: 3,
|
||||||
|
maxFrames: 12,
|
||||||
|
}, explodeArt)
|
||||||
|
|
||||||
|
expect(drawnSprites.length).toBe(1)
|
||||||
|
const expDrawn = drawnSprites[0]!
|
||||||
|
expect(expDrawn.options.atlas).toBe(mockAtlasHandle)
|
||||||
|
expect(expDrawn.x).toBe(350 + expDrawn.frame.anchorX)
|
||||||
|
expect(expDrawn.y).toBe(250 + expDrawn.frame.anchorY)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue