fix(ground-items): authentic ground item icon sprites and eliminate residual white dot
19
AGENTS.md
|
|
@ -58,6 +58,11 @@
|
|||
- 所有技能投射物(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 贴图资产与帧率对齐。
|
||||
4. **法术投射物透明度与黑边消除铁律 (Missile Transparency & Additive Blending Parity)**:
|
||||
- 依据 1.13c `Missiles.txt`,所有魔法投射物与爆炸(除普通物理箭矢 `arrow` 外)均声明 `Trans: 1`(Alpha / Additive 加法半透明)。
|
||||
- MPQ 原版 DCC 动画帧在提取时背景或抗锯齿过渡区存在深黑底色(如调色板索引 172 对应 RGB `(4, 4, 4)`)。若直接生成仅以索引 0 透明的调色板 PNG 并使用标准混合渲染,会在画面上产生明显的黑色方框或黑色毛边瑕疵(Issue #385)。
|
||||
- **材质级透明处理**:投射物图集必须离线烘焙为 32-bit RGBA PNG(`colorType = 6`),且对 `Trans: 1` 法术图集严格执行零黑边过滤——凡底色或暗色过渡像素(`max(r, g, b) <= 4`)其 Alpha 通道必须强行置为 0,暗部边缘平滑过渡,确保图集中非透明黑像素数量严格为 0。
|
||||
- **着色器级混合模式**:WebGL `SpriteRenderer` 必须支持并采用加法混合(`blendMode: 'additive'`,即 `gl.blendFunc(gl.SRC_ALPHA, gl.ONE)`),实现法术发光光晕叠加与 100% 零黑边渲染;普通物理投射物(`Trans: 0`)采用常规 Alpha 混合(`gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA`)。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -68,3 +73,17 @@
|
|||
2. **严苛静态类型与全量单测**:
|
||||
- `npm run typecheck` 必须保持 **0 errors**。
|
||||
- `npx vitest run` 必须保持全量套件 100% 通过(包括 `global-dt1-load.test.ts`, `iso-spawn-void-guard.test.ts`, `variant-distribution.test.ts`, `superuniques-fidelity.test.ts` 等防退化单测)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 斜视角与地图物理坐标 vs 渲染图标铁律 (Isometric Perspective & Coordinate Disambiguation)
|
||||
|
||||
1. **2:1 斜视角投影下地面圆形的几何特征 (2:1 Isometric Projection Parity)**:
|
||||
- 暗黑破坏神 2 采用 2:1 二等角投影(Isometric / Dimetric Projection,菱形单元格宽 80、高 40,子图元 Sub-tile 宽 16、高 8,垂直/水平轴比 `ISO_GROUND_ASPECT_RATIO = 0.5`)。
|
||||
- 任何在游戏世界地面平面(Ground Plane)上的圆形或径向扩张技能(如祝福之槌 Blessed Hammer 的阿基米德螺旋线、剧毒新星 Poison Nova 的 360 度圆环扩散、法术爆炸范围 AoE 伤害判定等),在投影到场景/屏幕像素坐标空间时,其几何形态在视觉上**绝对是椭圆**(长轴为水平 $X$,短轴为垂直 $Y$,且短半轴长为长半轴长的 $0.5$ 倍)。
|
||||
- 严禁在场景坐标中使用各向同性的 1:1 正圆公式计算地面运动轨迹,否则在视觉上会呈现出立在镜头正前方的“空中竖立圆圈”,与 2:1 倾斜地貌产生严重透视断层。
|
||||
2. **地图物理坐标与渲染图标严格区分 (Map Coordinates vs. Rendered Sprite Icons)**:
|
||||
- **地图物理坐标(Map / World Simulation Coordinates)**:`projectile.x`, `projectile.y` 以及速度向量 `vx, vy` 计算出的是投射物在地图平面上的物理世界坐标,负责与怪物碰撞盒、地形阻挡掩码(`COLLIDE_MASK_MISSILE`)进行物理检测与命中判定。
|
||||
- **渲染图标(Rendered Sprite Billboard)**:投射物的视觉图元是基于原版 DCC 导出的 2.5D 公告板(Billboard),其在渲染时由 `drawMissileProjectile` 通过 `shot.x + frame.anchorX`, `shot.y + frame.anchorY` 锚点对齐绘制,并依据切线速度向量 `(vx, vy)` 的朝向动态选取 16/32 方向的 DCC 预烘焙视角图。
|
||||
- **严禁混淆两者的职责**:绝对禁止通过拉伸/变形渲染图标去强行模拟椭圆,也绝对禁止把渲染图标的像素尺寸误当作物理坐标。渲染图标保持原版 DCC 原始比例,而物理轨迹坐标严格遵循 2:1 地面椭圆数学方程。
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
## 暗黑破坏神 II (v1.13c) 施法视觉效果 (Cast Overlay) 底层调查与实现设计规范
|
||||
|
||||
### 一、 机制调查与 1.13c 真实底层数据 (Ground Truth Invariant)
|
||||
|
||||
在暗黑破坏神 II v1.13c 引擎体系(`D2Common.dll`、`D2Client.dll`)中,释放法术时角色脚下涌现并包裹全身的动画效果并非角色自身动作(Action Frame)或飞弹(Missile),而是属于 **Unit Overlay(单位覆盖动画系统)**。
|
||||
|
||||
#### 1. 数据驱动关系
|
||||
1. **`Skills.txt`**:包含列 **`castoverlay`**,定义技能施展时触发的覆盖效果标识(例如技能 36 火弹绑定 `fire_cast_1`)。
|
||||
2. **`Overlay.txt`**:集中定义所有覆盖效果的物理与渲染属性:
|
||||
- **`Filename`**:指向 DCC 动画资源路径(位于 `d2data.mpq: data/global/overlays/{Filename}.dcc`)。
|
||||
- **`Frames`**:动画总帧数。
|
||||
- **`AnimRate`**:动画播放速率。法术施法特效原版均为 **16**(代表在 25 FPS 逻辑刻下,每 25 tick 播放 16 帧动画,即步长 `16 / 25 = 0.64` 帧/刻,总时长约 0.88 秒)。
|
||||
- **`Trans`**:混合渲染模式。原版为 **3**(**Additive Blending / 增色叠加混合**),呈现火焰、电光与寒冰的自发光半透明视觉质感。
|
||||
- **`PreDraw`**:渲染层级。原版均为 **0**(**Foreground / 前景层**),先绘制施法者角色精灵,再在前景叠加该 Overlay,形成火焰包裹躯干的立体层次。
|
||||
- **`Xoffset` / `Yoffset` / `Height1..4`**:均为 **0**,严格以施法者单位脚底地面中心点 `(0, 0)` 为基准。
|
||||
- **`InitRadius` / `Radius` / `Red, Green, Blue`**:动态点光源参数(半径均为 9 个子网格)。Overlay 激活期间会在角色周围投射对应属性的动态光晕。
|
||||
|
||||
---
|
||||
|
||||
### 二、 核心法术原版 Overlay 映射与 DCC 规格表
|
||||
|
||||
经解析 `samples/d2/Patch_D2.mpq` 与 `d2data.mpq`,法师三大元素系法术完整映射如下:
|
||||
|
||||
| 技能类别 | 技能名称 (Id) | `Skills.txt` `castoverlay` | DCC 文件名 (`data/global/overlays/`) | 帧数 | DCC 碰撞盒 (Left, Top, W, H) | 光照颜色 (R, G, B, Radius) | 视觉特征 |
|
||||
| :--- | :--- | :--- | :--- | :---: | :--- | :--- | :--- |
|
||||
| **火焰系** | **火弹 Fire Bolt (36)** | `fire_cast_1` | `FireCast_for_Sorceress.dcc` | 14 | `[-55, -127, 117, 164]` | (255, 178, 64, 9) | **从脚底升腾直窜头顶的火焰柱 (即 Issue 附件效果)** |
|
||||
| **火焰系** | 火球 (47), 强化/火焰强化 (46), 火墙 (51), 陨石 (56), 九头海蛇 (62) | `fire_cast_2` | `FireCast2.dcc` | 16 | `[-74, -89, 145, 133]` | (255, 178, 64, 9) | 范围更大、旋转包裹的炽热烈焰 |
|
||||
| **冰霜系** | 冰弹 (39), 冰封甲 (40), 冰风暴 (45) | `ice_cast_1` | `IceCastNew01.dcc` | 15 | `[-49, -90, 97, 55]` | (81, 81, 255, 9) | 脚下微弱旋起的寒冰碎屑与白霜冷雾 |
|
||||
| **冰霜系** | 霜之新星 (44), 碎冰甲 (50), 冰尖柱 (55) | `ice_cast_2` | `IceCastNew02.dcc` | 15 | `[-58, -100, 115, 123]` | (81, 81, 255, 9) | 向上旋转升腾的深蓝冰柱与霜雾 |
|
||||
| **冰霜系** | 暴风雪 (59), 寒冰甲 (60), 冰封球 (64) | `ice_cast_3` | `IceCastNew03.dcc` | 16 | `[-63, -113, 127, 148]` | (81, 81, 255, 9) | 强烈的暴风雪霜华与立体冰凌环绕 |
|
||||
| **闪电系** | 充能弹 (38), 静电场 (42), 新星 (48), 闪电 (49), 连锁闪电 (53) | `light_cast_1` | `LightningCast.dcc` | 10 | `[-79, -107, 164, 144]` | (255, 255, 255, 9) | 脚底向外炸裂并包裹全身的电弧跳跃 |
|
||||
| **闪电系** | 心灵传动 (43), 雷云风暴 (57), 能量护盾 (58) | `light_cast_2` | `LightningCastRunesFront.dcc` | 10 | `[-73, -154, 147, 190]` | (255, 255, 255, 9) | 闪电符文闪耀与高耸电光柱 |
|
||||
| **特殊** | 传送 Teleport (54) | `teleport` | `Teleport.dcc` | 18 | `[-61, -108, 136, 154]` | (255, 255, 200, 5) | 金黄色空间坍缩与残影粒子 |
|
||||
|
||||
---
|
||||
|
||||
### 三、 系统架构与实现方案 (diablo2-web)
|
||||
|
||||
遵循项目 **Zero Runtime MPQ Invariant** 及 **Anti-Silent Failures** 原则:
|
||||
|
||||
#### 1. 离线资源烘焙流水线 (`scripts/pack-overlays.ts`)
|
||||
- 提取 `d2data.mpq` 中上述 8 个关键施法 Overlay DCC 精灵文件。
|
||||
- 读取 `data/global/palette/ACT1/pal.pl2` 进行真实调色板色彩映射。
|
||||
- 将单向动画打包成 PNG 纹理图集并导出 JSON 元数据至 `public/overlays/` 与 `samples/d2-packs/overlays/`。
|
||||
- 自动生成类型定义 `src/render/overlays-meta.ts`,提供精确的 `box.left, box.top, width, height, animRate, trans` 常量。
|
||||
|
||||
#### 2. 运行时数据契约与实体抽象 (`src/game/skills.ts` & `src/game/engine.ts`)
|
||||
- 扩展 `SkillDef` 接口,包含 `castOverlay?: string`。
|
||||
- 建立 `CANONICAL_113C_OVERLAYS` 真值表,包含 overlay 标识到对应资源与物理属性的映射。
|
||||
- 在 `GameEngine` 中引入 `ActiveOverlay` 活跃实体管理:
|
||||
```typescript
|
||||
export interface ActiveOverlay {
|
||||
readonly id: string
|
||||
readonly overlayName: string
|
||||
readonly casterId: string
|
||||
x: number
|
||||
y: number
|
||||
frame: number
|
||||
readonly maxFrames: number
|
||||
readonly animRate: number
|
||||
readonly preDraw: boolean
|
||||
readonly trans: number
|
||||
readonly lightRadius: number
|
||||
readonly lightColor: readonly [number, number, number]
|
||||
expired: boolean
|
||||
}
|
||||
```
|
||||
- **生命周期规则**:
|
||||
- 玩家或怪物在 `executePlayerSkillCast` 触发施法动作时,根据技能 `castoverlay` 生成 `ActiveOverlay`。
|
||||
- 每个逻辑 tick(25 FPS):`overlay.frame += overlay.animRate / 25`。
|
||||
- 当 `Math.floor(overlay.frame) >= overlay.maxFrames` 时,标记 `expired = true` 并在 tick 结束时销毁(单次播放不循环)。
|
||||
- Overlay 坐标实时同步施法者当前脚下位置 `(caster.x, caster.y)`。
|
||||
|
||||
#### 3. 渲染管线集成 (`src/scene/act-scene.ts`)
|
||||
- 资源载入:通过 `loadOverlayArtMap` 预载 `overlays/*.png`。
|
||||
- 深度层级(Z-Order):
|
||||
- 角色绘制完成后、飞弹绘制前,执行前景 Overlay 渲染。
|
||||
- 混合模式(Blend Mode):
|
||||
- 遇到 `trans === 3`,切换 Canvas2D `ctx.globalCompositeOperation = 'lighter'` 或 WebGL Additive 混合状态,绘制完成后恢复 `source-over`。
|
||||
- 锚点投影(Anchor Projection):
|
||||
- 精确应用 DCC 包围盒偏移:`drawX = overlay.x + frameMeta.anchorX`,`drawY = overlay.y + frameMeta.anchorY`。
|
||||
- 动态光照反馈:
|
||||
- 在 Overlay 存活期间,将其注册为动态点光源(如火焰投射 9 码的 RGB(255, 178, 64) 暖黄光晕)。
|
||||
|
||||
---
|
||||
|
||||
### 四、 验证与回归测试策略
|
||||
|
||||
1. **表格对齐测试**:验证 `parseSkillsTxt` 与 `Skills.txt`、`Overlay.txt` 的 `castoverlay` 字段及参数完全一致。
|
||||
2. **生命周期测试**:验证 14 帧的 `fire_cast_1` 在 25 FPS 模拟下,在第 22 个 tick 精确结束并移除。
|
||||
3. **渲染与图集测试**:验证 `FireCast_for_Sorceress`、`IceCastNew01`、`LightningCast` 纹理图集无缺失、锚点边界无裁切、Additive 模式下正确渲染。
|
||||
4. **全场景稳定性**:自动化 Headless 运行,保障全 136 个关卡环境施法 Overlay 无跨法术贴图污染或未定义崩溃。
|
||||
|
|
@ -0,0 +1,331 @@
|
|||
{
|
||||
"name": "arrow",
|
||||
"celFile": "Arrow",
|
||||
"width": 512,
|
||||
"height": 36,
|
||||
"directions": 32,
|
||||
"framesPerDirection": 1,
|
||||
"animSpeed": 16,
|
||||
"groups": [
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
31,
|
||||
13,
|
||||
-16,
|
||||
-56
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
32,
|
||||
0,
|
||||
31,
|
||||
13,
|
||||
-15,
|
||||
-55
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
64,
|
||||
0,
|
||||
30,
|
||||
14,
|
||||
-15,
|
||||
-55
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
95,
|
||||
0,
|
||||
31,
|
||||
14,
|
||||
-15,
|
||||
-56
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
127,
|
||||
0,
|
||||
4,
|
||||
18,
|
||||
-2,
|
||||
-58
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
132,
|
||||
0,
|
||||
42,
|
||||
5,
|
||||
-22,
|
||||
-52
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
175,
|
||||
0,
|
||||
4,
|
||||
17,
|
||||
-2,
|
||||
-57
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
180,
|
||||
0,
|
||||
41,
|
||||
5,
|
||||
-20,
|
||||
-51
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
222,
|
||||
0,
|
||||
18,
|
||||
17,
|
||||
-9,
|
||||
-58
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
241,
|
||||
0,
|
||||
39,
|
||||
9,
|
||||
-20,
|
||||
-54
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
281,
|
||||
0,
|
||||
39,
|
||||
8,
|
||||
-20,
|
||||
-53
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
321,
|
||||
0,
|
||||
18,
|
||||
16,
|
||||
-8,
|
||||
-57
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
340,
|
||||
0,
|
||||
18,
|
||||
17,
|
||||
-9,
|
||||
-57
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
359,
|
||||
0,
|
||||
39,
|
||||
9,
|
||||
-19,
|
||||
-53
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
399,
|
||||
0,
|
||||
39,
|
||||
8,
|
||||
-19,
|
||||
-53
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
439,
|
||||
0,
|
||||
18,
|
||||
16,
|
||||
-9,
|
||||
-57
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
458,
|
||||
0,
|
||||
11,
|
||||
18,
|
||||
-6,
|
||||
-58
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
470,
|
||||
0,
|
||||
25,
|
||||
15,
|
||||
-13,
|
||||
-57
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
19,
|
||||
35,
|
||||
12,
|
||||
-18,
|
||||
-56
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
36,
|
||||
19,
|
||||
41,
|
||||
7,
|
||||
-21,
|
||||
-53
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
78,
|
||||
19,
|
||||
41,
|
||||
6,
|
||||
-21,
|
||||
-52
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
120,
|
||||
19,
|
||||
35,
|
||||
11,
|
||||
-17,
|
||||
-54
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
156,
|
||||
19,
|
||||
25,
|
||||
15,
|
||||
-12,
|
||||
-56
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
182,
|
||||
19,
|
||||
11,
|
||||
17,
|
||||
-5,
|
||||
-57
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
194,
|
||||
19,
|
||||
11,
|
||||
17,
|
||||
-6,
|
||||
-57
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
206,
|
||||
19,
|
||||
25,
|
||||
16,
|
||||
-13,
|
||||
-56
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
232,
|
||||
19,
|
||||
35,
|
||||
12,
|
||||
-17,
|
||||
-54
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
268,
|
||||
19,
|
||||
41,
|
||||
7,
|
||||
-20,
|
||||
-52
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
310,
|
||||
19,
|
||||
41,
|
||||
6,
|
||||
-20,
|
||||
-52
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
352,
|
||||
19,
|
||||
35,
|
||||
12,
|
||||
-17,
|
||||
-55
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
388,
|
||||
19,
|
||||
24,
|
||||
16,
|
||||
-12,
|
||||
-57
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
413,
|
||||
19,
|
||||
10,
|
||||
17,
|
||||
-5,
|
||||
-57
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
|
|
@ -0,0 +1,811 @@
|
|||
{
|
||||
"name": "blessedhammer",
|
||||
"celFile": "blessedhammer",
|
||||
"width": 512,
|
||||
"height": 192,
|
||||
"directions": 16,
|
||||
"framesPerDirection": 6,
|
||||
"animSpeed": 16,
|
||||
"groups": [
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
27,
|
||||
38,
|
||||
-13,
|
||||
-56
|
||||
],
|
||||
[
|
||||
28,
|
||||
0,
|
||||
27,
|
||||
38,
|
||||
-13,
|
||||
-56
|
||||
],
|
||||
[
|
||||
56,
|
||||
0,
|
||||
27,
|
||||
38,
|
||||
-13,
|
||||
-56
|
||||
],
|
||||
[
|
||||
84,
|
||||
0,
|
||||
27,
|
||||
38,
|
||||
-13,
|
||||
-56
|
||||
],
|
||||
[
|
||||
112,
|
||||
0,
|
||||
27,
|
||||
38,
|
||||
-13,
|
||||
-56
|
||||
],
|
||||
[
|
||||
140,
|
||||
0,
|
||||
27,
|
||||
38,
|
||||
-13,
|
||||
-56
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
168,
|
||||
0,
|
||||
26,
|
||||
33,
|
||||
-13,
|
||||
-53
|
||||
],
|
||||
[
|
||||
195,
|
||||
0,
|
||||
26,
|
||||
33,
|
||||
-13,
|
||||
-53
|
||||
],
|
||||
[
|
||||
222,
|
||||
0,
|
||||
26,
|
||||
33,
|
||||
-13,
|
||||
-53
|
||||
],
|
||||
[
|
||||
249,
|
||||
0,
|
||||
26,
|
||||
33,
|
||||
-13,
|
||||
-53
|
||||
],
|
||||
[
|
||||
276,
|
||||
0,
|
||||
26,
|
||||
33,
|
||||
-13,
|
||||
-53
|
||||
],
|
||||
[
|
||||
303,
|
||||
0,
|
||||
26,
|
||||
33,
|
||||
-13,
|
||||
-53
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
330,
|
||||
0,
|
||||
27,
|
||||
33,
|
||||
-14,
|
||||
-54
|
||||
],
|
||||
[
|
||||
358,
|
||||
0,
|
||||
27,
|
||||
33,
|
||||
-14,
|
||||
-54
|
||||
],
|
||||
[
|
||||
386,
|
||||
0,
|
||||
27,
|
||||
33,
|
||||
-14,
|
||||
-54
|
||||
],
|
||||
[
|
||||
414,
|
||||
0,
|
||||
27,
|
||||
33,
|
||||
-14,
|
||||
-54
|
||||
],
|
||||
[
|
||||
442,
|
||||
0,
|
||||
27,
|
||||
33,
|
||||
-14,
|
||||
-54
|
||||
],
|
||||
[
|
||||
470,
|
||||
0,
|
||||
27,
|
||||
33,
|
||||
-14,
|
||||
-54
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
39,
|
||||
27,
|
||||
38,
|
||||
-13,
|
||||
-56
|
||||
],
|
||||
[
|
||||
28,
|
||||
39,
|
||||
27,
|
||||
38,
|
||||
-13,
|
||||
-56
|
||||
],
|
||||
[
|
||||
56,
|
||||
39,
|
||||
27,
|
||||
38,
|
||||
-13,
|
||||
-56
|
||||
],
|
||||
[
|
||||
84,
|
||||
39,
|
||||
27,
|
||||
38,
|
||||
-13,
|
||||
-56
|
||||
],
|
||||
[
|
||||
112,
|
||||
39,
|
||||
27,
|
||||
38,
|
||||
-13,
|
||||
-56
|
||||
],
|
||||
[
|
||||
140,
|
||||
39,
|
||||
27,
|
||||
38,
|
||||
-13,
|
||||
-56
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
168,
|
||||
39,
|
||||
7,
|
||||
38,
|
||||
-3,
|
||||
-56
|
||||
],
|
||||
[
|
||||
176,
|
||||
39,
|
||||
7,
|
||||
38,
|
||||
-3,
|
||||
-56
|
||||
],
|
||||
[
|
||||
184,
|
||||
39,
|
||||
7,
|
||||
38,
|
||||
-3,
|
||||
-56
|
||||
],
|
||||
[
|
||||
192,
|
||||
39,
|
||||
7,
|
||||
38,
|
||||
-3,
|
||||
-56
|
||||
],
|
||||
[
|
||||
200,
|
||||
39,
|
||||
7,
|
||||
38,
|
||||
-3,
|
||||
-56
|
||||
],
|
||||
[
|
||||
208,
|
||||
39,
|
||||
7,
|
||||
38,
|
||||
-3,
|
||||
-56
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
216,
|
||||
39,
|
||||
36,
|
||||
35,
|
||||
-18,
|
||||
-54
|
||||
],
|
||||
[
|
||||
253,
|
||||
39,
|
||||
36,
|
||||
35,
|
||||
-18,
|
||||
-54
|
||||
],
|
||||
[
|
||||
290,
|
||||
39,
|
||||
36,
|
||||
35,
|
||||
-18,
|
||||
-54
|
||||
],
|
||||
[
|
||||
327,
|
||||
39,
|
||||
36,
|
||||
35,
|
||||
-18,
|
||||
-54
|
||||
],
|
||||
[
|
||||
364,
|
||||
39,
|
||||
36,
|
||||
35,
|
||||
-18,
|
||||
-54
|
||||
],
|
||||
[
|
||||
401,
|
||||
39,
|
||||
36,
|
||||
35,
|
||||
-18,
|
||||
-54
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
438,
|
||||
39,
|
||||
7,
|
||||
32,
|
||||
-4,
|
||||
-53
|
||||
],
|
||||
[
|
||||
446,
|
||||
39,
|
||||
7,
|
||||
32,
|
||||
-4,
|
||||
-53
|
||||
],
|
||||
[
|
||||
454,
|
||||
39,
|
||||
7,
|
||||
32,
|
||||
-4,
|
||||
-53
|
||||
],
|
||||
[
|
||||
462,
|
||||
39,
|
||||
7,
|
||||
32,
|
||||
-4,
|
||||
-53
|
||||
],
|
||||
[
|
||||
470,
|
||||
39,
|
||||
7,
|
||||
32,
|
||||
-4,
|
||||
-53
|
||||
],
|
||||
[
|
||||
478,
|
||||
39,
|
||||
7,
|
||||
32,
|
||||
-4,
|
||||
-53
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
78,
|
||||
36,
|
||||
35,
|
||||
-18,
|
||||
-55
|
||||
],
|
||||
[
|
||||
37,
|
||||
78,
|
||||
36,
|
||||
35,
|
||||
-18,
|
||||
-55
|
||||
],
|
||||
[
|
||||
74,
|
||||
78,
|
||||
36,
|
||||
35,
|
||||
-18,
|
||||
-55
|
||||
],
|
||||
[
|
||||
111,
|
||||
78,
|
||||
36,
|
||||
35,
|
||||
-18,
|
||||
-55
|
||||
],
|
||||
[
|
||||
148,
|
||||
78,
|
||||
36,
|
||||
35,
|
||||
-18,
|
||||
-55
|
||||
],
|
||||
[
|
||||
185,
|
||||
78,
|
||||
36,
|
||||
35,
|
||||
-18,
|
||||
-55
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
222,
|
||||
78,
|
||||
16,
|
||||
38,
|
||||
-8,
|
||||
-56
|
||||
],
|
||||
[
|
||||
239,
|
||||
78,
|
||||
16,
|
||||
38,
|
||||
-8,
|
||||
-56
|
||||
],
|
||||
[
|
||||
256,
|
||||
78,
|
||||
16,
|
||||
38,
|
||||
-8,
|
||||
-56
|
||||
],
|
||||
[
|
||||
273,
|
||||
78,
|
||||
16,
|
||||
38,
|
||||
-8,
|
||||
-56
|
||||
],
|
||||
[
|
||||
290,
|
||||
78,
|
||||
16,
|
||||
38,
|
||||
-8,
|
||||
-56
|
||||
],
|
||||
[
|
||||
307,
|
||||
78,
|
||||
16,
|
||||
38,
|
||||
-8,
|
||||
-56
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
324,
|
||||
78,
|
||||
34,
|
||||
36,
|
||||
-17,
|
||||
-55
|
||||
],
|
||||
[
|
||||
359,
|
||||
78,
|
||||
34,
|
||||
36,
|
||||
-17,
|
||||
-55
|
||||
],
|
||||
[
|
||||
394,
|
||||
78,
|
||||
34,
|
||||
36,
|
||||
-17,
|
||||
-55
|
||||
],
|
||||
[
|
||||
429,
|
||||
78,
|
||||
34,
|
||||
36,
|
||||
-17,
|
||||
-55
|
||||
],
|
||||
[
|
||||
464,
|
||||
78,
|
||||
34,
|
||||
36,
|
||||
-17,
|
||||
-55
|
||||
],
|
||||
[
|
||||
0,
|
||||
117,
|
||||
34,
|
||||
36,
|
||||
-17,
|
||||
-55
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
35,
|
||||
117,
|
||||
34,
|
||||
34,
|
||||
-17,
|
||||
-54
|
||||
],
|
||||
[
|
||||
70,
|
||||
117,
|
||||
34,
|
||||
34,
|
||||
-17,
|
||||
-54
|
||||
],
|
||||
[
|
||||
105,
|
||||
117,
|
||||
34,
|
||||
34,
|
||||
-17,
|
||||
-54
|
||||
],
|
||||
[
|
||||
140,
|
||||
117,
|
||||
34,
|
||||
34,
|
||||
-17,
|
||||
-54
|
||||
],
|
||||
[
|
||||
175,
|
||||
117,
|
||||
34,
|
||||
34,
|
||||
-17,
|
||||
-54
|
||||
],
|
||||
[
|
||||
210,
|
||||
117,
|
||||
34,
|
||||
34,
|
||||
-17,
|
||||
-54
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
245,
|
||||
117,
|
||||
16,
|
||||
32,
|
||||
-8,
|
||||
-53
|
||||
],
|
||||
[
|
||||
262,
|
||||
117,
|
||||
16,
|
||||
32,
|
||||
-8,
|
||||
-53
|
||||
],
|
||||
[
|
||||
279,
|
||||
117,
|
||||
16,
|
||||
32,
|
||||
-8,
|
||||
-53
|
||||
],
|
||||
[
|
||||
296,
|
||||
117,
|
||||
16,
|
||||
32,
|
||||
-8,
|
||||
-53
|
||||
],
|
||||
[
|
||||
313,
|
||||
117,
|
||||
16,
|
||||
32,
|
||||
-8,
|
||||
-53
|
||||
],
|
||||
[
|
||||
330,
|
||||
117,
|
||||
16,
|
||||
32,
|
||||
-8,
|
||||
-53
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
347,
|
||||
117,
|
||||
16,
|
||||
32,
|
||||
-9,
|
||||
-53
|
||||
],
|
||||
[
|
||||
364,
|
||||
117,
|
||||
16,
|
||||
32,
|
||||
-9,
|
||||
-53
|
||||
],
|
||||
[
|
||||
381,
|
||||
117,
|
||||
16,
|
||||
32,
|
||||
-9,
|
||||
-53
|
||||
],
|
||||
[
|
||||
398,
|
||||
117,
|
||||
16,
|
||||
32,
|
||||
-9,
|
||||
-53
|
||||
],
|
||||
[
|
||||
415,
|
||||
117,
|
||||
16,
|
||||
32,
|
||||
-9,
|
||||
-53
|
||||
],
|
||||
[
|
||||
432,
|
||||
117,
|
||||
16,
|
||||
32,
|
||||
-9,
|
||||
-53
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
449,
|
||||
117,
|
||||
33,
|
||||
34,
|
||||
-17,
|
||||
-54
|
||||
],
|
||||
[
|
||||
0,
|
||||
154,
|
||||
33,
|
||||
34,
|
||||
-17,
|
||||
-54
|
||||
],
|
||||
[
|
||||
34,
|
||||
154,
|
||||
33,
|
||||
34,
|
||||
-17,
|
||||
-54
|
||||
],
|
||||
[
|
||||
68,
|
||||
154,
|
||||
33,
|
||||
34,
|
||||
-17,
|
||||
-54
|
||||
],
|
||||
[
|
||||
102,
|
||||
154,
|
||||
33,
|
||||
34,
|
||||
-17,
|
||||
-54
|
||||
],
|
||||
[
|
||||
136,
|
||||
154,
|
||||
33,
|
||||
34,
|
||||
-17,
|
||||
-54
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
170,
|
||||
154,
|
||||
34,
|
||||
36,
|
||||
-17,
|
||||
-55
|
||||
],
|
||||
[
|
||||
205,
|
||||
154,
|
||||
34,
|
||||
36,
|
||||
-17,
|
||||
-55
|
||||
],
|
||||
[
|
||||
240,
|
||||
154,
|
||||
34,
|
||||
36,
|
||||
-17,
|
||||
-55
|
||||
],
|
||||
[
|
||||
275,
|
||||
154,
|
||||
34,
|
||||
36,
|
||||
-17,
|
||||
-55
|
||||
],
|
||||
[
|
||||
310,
|
||||
154,
|
||||
34,
|
||||
36,
|
||||
-17,
|
||||
-55
|
||||
],
|
||||
[
|
||||
345,
|
||||
154,
|
||||
34,
|
||||
36,
|
||||
-17,
|
||||
-55
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
380,
|
||||
154,
|
||||
16,
|
||||
38,
|
||||
-7,
|
||||
-56
|
||||
],
|
||||
[
|
||||
397,
|
||||
154,
|
||||
16,
|
||||
38,
|
||||
-7,
|
||||
-56
|
||||
],
|
||||
[
|
||||
414,
|
||||
154,
|
||||
16,
|
||||
38,
|
||||
-7,
|
||||
-56
|
||||
],
|
||||
[
|
||||
431,
|
||||
154,
|
||||
16,
|
||||
38,
|
||||
-7,
|
||||
-56
|
||||
],
|
||||
[
|
||||
448,
|
||||
154,
|
||||
16,
|
||||
38,
|
||||
-7,
|
||||
-56
|
||||
],
|
||||
[
|
||||
465,
|
||||
154,
|
||||
16,
|
||||
38,
|
||||
-7,
|
||||
-56
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -0,0 +1,811 @@
|
|||
{
|
||||
"name": "bonespear",
|
||||
"celFile": "BoneSpear",
|
||||
"width": 1024,
|
||||
"height": 347,
|
||||
"directions": 16,
|
||||
"framesPerDirection": 6,
|
||||
"animSpeed": 16,
|
||||
"groups": [
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
58,
|
||||
0,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
116,
|
||||
0,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
174,
|
||||
0,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
232,
|
||||
0,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
290,
|
||||
0,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
348,
|
||||
0,
|
||||
58,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
407,
|
||||
0,
|
||||
58,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
466,
|
||||
0,
|
||||
58,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
525,
|
||||
0,
|
||||
58,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
584,
|
||||
0,
|
||||
58,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
643,
|
||||
0,
|
||||
58,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
702,
|
||||
0,
|
||||
60,
|
||||
57,
|
||||
-31,
|
||||
-68
|
||||
],
|
||||
[
|
||||
763,
|
||||
0,
|
||||
60,
|
||||
57,
|
||||
-31,
|
||||
-68
|
||||
],
|
||||
[
|
||||
824,
|
||||
0,
|
||||
60,
|
||||
57,
|
||||
-31,
|
||||
-68
|
||||
],
|
||||
[
|
||||
885,
|
||||
0,
|
||||
60,
|
||||
57,
|
||||
-31,
|
||||
-68
|
||||
],
|
||||
[
|
||||
946,
|
||||
0,
|
||||
60,
|
||||
57,
|
||||
-31,
|
||||
-68
|
||||
],
|
||||
[
|
||||
0,
|
||||
58,
|
||||
60,
|
||||
57,
|
||||
-31,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
61,
|
||||
58,
|
||||
58,
|
||||
57,
|
||||
-29,
|
||||
-68
|
||||
],
|
||||
[
|
||||
120,
|
||||
58,
|
||||
58,
|
||||
57,
|
||||
-29,
|
||||
-68
|
||||
],
|
||||
[
|
||||
179,
|
||||
58,
|
||||
58,
|
||||
57,
|
||||
-29,
|
||||
-68
|
||||
],
|
||||
[
|
||||
238,
|
||||
58,
|
||||
58,
|
||||
57,
|
||||
-29,
|
||||
-68
|
||||
],
|
||||
[
|
||||
297,
|
||||
58,
|
||||
58,
|
||||
57,
|
||||
-29,
|
||||
-68
|
||||
],
|
||||
[
|
||||
356,
|
||||
58,
|
||||
58,
|
||||
57,
|
||||
-29,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
415,
|
||||
58,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
473,
|
||||
58,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
531,
|
||||
58,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
589,
|
||||
58,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
647,
|
||||
58,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
705,
|
||||
58,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
763,
|
||||
58,
|
||||
68,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
832,
|
||||
58,
|
||||
68,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
901,
|
||||
58,
|
||||
68,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
0,
|
||||
116,
|
||||
68,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
69,
|
||||
116,
|
||||
68,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
138,
|
||||
116,
|
||||
68,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
207,
|
||||
116,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
264,
|
||||
116,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
321,
|
||||
116,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
378,
|
||||
116,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
435,
|
||||
116,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
492,
|
||||
116,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
549,
|
||||
116,
|
||||
69,
|
||||
57,
|
||||
-40,
|
||||
-68
|
||||
],
|
||||
[
|
||||
619,
|
||||
116,
|
||||
69,
|
||||
57,
|
||||
-40,
|
||||
-68
|
||||
],
|
||||
[
|
||||
689,
|
||||
116,
|
||||
69,
|
||||
57,
|
||||
-40,
|
||||
-68
|
||||
],
|
||||
[
|
||||
759,
|
||||
116,
|
||||
69,
|
||||
57,
|
||||
-40,
|
||||
-68
|
||||
],
|
||||
[
|
||||
829,
|
||||
116,
|
||||
69,
|
||||
57,
|
||||
-40,
|
||||
-68
|
||||
],
|
||||
[
|
||||
899,
|
||||
116,
|
||||
69,
|
||||
57,
|
||||
-40,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
174,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
58,
|
||||
174,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
116,
|
||||
174,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
174,
|
||||
174,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
232,
|
||||
174,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
290,
|
||||
174,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
348,
|
||||
174,
|
||||
65,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
414,
|
||||
174,
|
||||
65,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
480,
|
||||
174,
|
||||
65,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
546,
|
||||
174,
|
||||
65,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
612,
|
||||
174,
|
||||
65,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
678,
|
||||
174,
|
||||
65,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
744,
|
||||
174,
|
||||
66,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
811,
|
||||
174,
|
||||
66,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
878,
|
||||
174,
|
||||
66,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
945,
|
||||
174,
|
||||
66,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
0,
|
||||
232,
|
||||
66,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
67,
|
||||
232,
|
||||
66,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
134,
|
||||
232,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
191,
|
||||
232,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
248,
|
||||
232,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
305,
|
||||
232,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
362,
|
||||
232,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
419,
|
||||
232,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
476,
|
||||
232,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
533,
|
||||
232,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
590,
|
||||
232,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
647,
|
||||
232,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
704,
|
||||
232,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
],
|
||||
[
|
||||
761,
|
||||
232,
|
||||
56,
|
||||
57,
|
||||
-27,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
818,
|
||||
232,
|
||||
67,
|
||||
57,
|
||||
-38,
|
||||
-68
|
||||
],
|
||||
[
|
||||
886,
|
||||
232,
|
||||
67,
|
||||
57,
|
||||
-38,
|
||||
-68
|
||||
],
|
||||
[
|
||||
954,
|
||||
232,
|
||||
67,
|
||||
57,
|
||||
-38,
|
||||
-68
|
||||
],
|
||||
[
|
||||
0,
|
||||
290,
|
||||
67,
|
||||
57,
|
||||
-38,
|
||||
-68
|
||||
],
|
||||
[
|
||||
68,
|
||||
290,
|
||||
67,
|
||||
57,
|
||||
-38,
|
||||
-68
|
||||
],
|
||||
[
|
||||
136,
|
||||
290,
|
||||
67,
|
||||
57,
|
||||
-38,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
204,
|
||||
290,
|
||||
66,
|
||||
57,
|
||||
-37,
|
||||
-68
|
||||
],
|
||||
[
|
||||
271,
|
||||
290,
|
||||
66,
|
||||
57,
|
||||
-37,
|
||||
-68
|
||||
],
|
||||
[
|
||||
338,
|
||||
290,
|
||||
66,
|
||||
57,
|
||||
-37,
|
||||
-68
|
||||
],
|
||||
[
|
||||
405,
|
||||
290,
|
||||
66,
|
||||
57,
|
||||
-37,
|
||||
-68
|
||||
],
|
||||
[
|
||||
472,
|
||||
290,
|
||||
66,
|
||||
57,
|
||||
-37,
|
||||
-68
|
||||
],
|
||||
[
|
||||
539,
|
||||
290,
|
||||
66,
|
||||
57,
|
||||
-37,
|
||||
-68
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
606,
|
||||
290,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
664,
|
||||
290,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
722,
|
||||
290,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
780,
|
||||
290,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
838,
|
||||
290,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
],
|
||||
[
|
||||
896,
|
||||
290,
|
||||
57,
|
||||
57,
|
||||
-28,
|
||||
-68
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 129 KiB |
|
|
@ -0,0 +1,539 @@
|
|||
{
|
||||
"name": "bonespirit",
|
||||
"celFile": "BoneSpirit",
|
||||
"width": 2048,
|
||||
"height": 499,
|
||||
"directions": 8,
|
||||
"framesPerDirection": 8,
|
||||
"animSpeed": 16,
|
||||
"groups": [
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
123,
|
||||
122,
|
||||
-56,
|
||||
-60
|
||||
],
|
||||
[
|
||||
124,
|
||||
0,
|
||||
123,
|
||||
122,
|
||||
-56,
|
||||
-60
|
||||
],
|
||||
[
|
||||
248,
|
||||
0,
|
||||
123,
|
||||
122,
|
||||
-56,
|
||||
-60
|
||||
],
|
||||
[
|
||||
372,
|
||||
0,
|
||||
123,
|
||||
122,
|
||||
-56,
|
||||
-60
|
||||
],
|
||||
[
|
||||
496,
|
||||
0,
|
||||
123,
|
||||
122,
|
||||
-56,
|
||||
-60
|
||||
],
|
||||
[
|
||||
620,
|
||||
0,
|
||||
123,
|
||||
122,
|
||||
-56,
|
||||
-60
|
||||
],
|
||||
[
|
||||
744,
|
||||
0,
|
||||
123,
|
||||
122,
|
||||
-56,
|
||||
-60
|
||||
],
|
||||
[
|
||||
868,
|
||||
0,
|
||||
123,
|
||||
122,
|
||||
-56,
|
||||
-60
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
992,
|
||||
0,
|
||||
125,
|
||||
124,
|
||||
-57,
|
||||
-55
|
||||
],
|
||||
[
|
||||
1118,
|
||||
0,
|
||||
125,
|
||||
124,
|
||||
-57,
|
||||
-55
|
||||
],
|
||||
[
|
||||
1244,
|
||||
0,
|
||||
125,
|
||||
124,
|
||||
-57,
|
||||
-55
|
||||
],
|
||||
[
|
||||
1370,
|
||||
0,
|
||||
125,
|
||||
124,
|
||||
-57,
|
||||
-55
|
||||
],
|
||||
[
|
||||
1496,
|
||||
0,
|
||||
125,
|
||||
124,
|
||||
-57,
|
||||
-55
|
||||
],
|
||||
[
|
||||
1622,
|
||||
0,
|
||||
125,
|
||||
124,
|
||||
-57,
|
||||
-55
|
||||
],
|
||||
[
|
||||
1748,
|
||||
0,
|
||||
125,
|
||||
124,
|
||||
-57,
|
||||
-55
|
||||
],
|
||||
[
|
||||
1874,
|
||||
0,
|
||||
125,
|
||||
124,
|
||||
-57,
|
||||
-55
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
125,
|
||||
126,
|
||||
124,
|
||||
-69,
|
||||
-55
|
||||
],
|
||||
[
|
||||
127,
|
||||
125,
|
||||
126,
|
||||
124,
|
||||
-69,
|
||||
-55
|
||||
],
|
||||
[
|
||||
254,
|
||||
125,
|
||||
126,
|
||||
124,
|
||||
-69,
|
||||
-55
|
||||
],
|
||||
[
|
||||
381,
|
||||
125,
|
||||
126,
|
||||
124,
|
||||
-69,
|
||||
-55
|
||||
],
|
||||
[
|
||||
508,
|
||||
125,
|
||||
126,
|
||||
124,
|
||||
-69,
|
||||
-55
|
||||
],
|
||||
[
|
||||
635,
|
||||
125,
|
||||
126,
|
||||
124,
|
||||
-69,
|
||||
-55
|
||||
],
|
||||
[
|
||||
762,
|
||||
125,
|
||||
126,
|
||||
124,
|
||||
-69,
|
||||
-55
|
||||
],
|
||||
[
|
||||
889,
|
||||
125,
|
||||
126,
|
||||
124,
|
||||
-69,
|
||||
-55
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
1016,
|
||||
125,
|
||||
124,
|
||||
122,
|
||||
-68,
|
||||
-60
|
||||
],
|
||||
[
|
||||
1141,
|
||||
125,
|
||||
124,
|
||||
122,
|
||||
-68,
|
||||
-60
|
||||
],
|
||||
[
|
||||
1266,
|
||||
125,
|
||||
124,
|
||||
122,
|
||||
-68,
|
||||
-60
|
||||
],
|
||||
[
|
||||
1391,
|
||||
125,
|
||||
124,
|
||||
122,
|
||||
-68,
|
||||
-60
|
||||
],
|
||||
[
|
||||
1516,
|
||||
125,
|
||||
124,
|
||||
122,
|
||||
-68,
|
||||
-60
|
||||
],
|
||||
[
|
||||
1641,
|
||||
125,
|
||||
124,
|
||||
122,
|
||||
-68,
|
||||
-60
|
||||
],
|
||||
[
|
||||
1766,
|
||||
125,
|
||||
124,
|
||||
122,
|
||||
-68,
|
||||
-60
|
||||
],
|
||||
[
|
||||
1891,
|
||||
125,
|
||||
124,
|
||||
122,
|
||||
-68,
|
||||
-60
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
250,
|
||||
123,
|
||||
121,
|
||||
-62,
|
||||
-61
|
||||
],
|
||||
[
|
||||
124,
|
||||
250,
|
||||
123,
|
||||
121,
|
||||
-62,
|
||||
-61
|
||||
],
|
||||
[
|
||||
248,
|
||||
250,
|
||||
123,
|
||||
121,
|
||||
-62,
|
||||
-61
|
||||
],
|
||||
[
|
||||
372,
|
||||
250,
|
||||
123,
|
||||
121,
|
||||
-62,
|
||||
-61
|
||||
],
|
||||
[
|
||||
496,
|
||||
250,
|
||||
123,
|
||||
121,
|
||||
-62,
|
||||
-61
|
||||
],
|
||||
[
|
||||
620,
|
||||
250,
|
||||
123,
|
||||
121,
|
||||
-62,
|
||||
-61
|
||||
],
|
||||
[
|
||||
744,
|
||||
250,
|
||||
123,
|
||||
121,
|
||||
-62,
|
||||
-61
|
||||
],
|
||||
[
|
||||
868,
|
||||
250,
|
||||
123,
|
||||
121,
|
||||
-62,
|
||||
-61
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
992,
|
||||
250,
|
||||
124,
|
||||
123,
|
||||
-54,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1117,
|
||||
250,
|
||||
124,
|
||||
123,
|
||||
-54,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1242,
|
||||
250,
|
||||
124,
|
||||
123,
|
||||
-54,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1367,
|
||||
250,
|
||||
124,
|
||||
123,
|
||||
-54,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1492,
|
||||
250,
|
||||
124,
|
||||
123,
|
||||
-54,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1617,
|
||||
250,
|
||||
124,
|
||||
123,
|
||||
-54,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1742,
|
||||
250,
|
||||
124,
|
||||
123,
|
||||
-54,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1867,
|
||||
250,
|
||||
124,
|
||||
123,
|
||||
-54,
|
||||
-58
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
374,
|
||||
127,
|
||||
125,
|
||||
-64,
|
||||
-54
|
||||
],
|
||||
[
|
||||
128,
|
||||
374,
|
||||
127,
|
||||
125,
|
||||
-64,
|
||||
-54
|
||||
],
|
||||
[
|
||||
256,
|
||||
374,
|
||||
127,
|
||||
125,
|
||||
-64,
|
||||
-54
|
||||
],
|
||||
[
|
||||
384,
|
||||
374,
|
||||
127,
|
||||
125,
|
||||
-64,
|
||||
-54
|
||||
],
|
||||
[
|
||||
512,
|
||||
374,
|
||||
127,
|
||||
125,
|
||||
-64,
|
||||
-54
|
||||
],
|
||||
[
|
||||
640,
|
||||
374,
|
||||
127,
|
||||
125,
|
||||
-64,
|
||||
-54
|
||||
],
|
||||
[
|
||||
768,
|
||||
374,
|
||||
127,
|
||||
125,
|
||||
-64,
|
||||
-54
|
||||
],
|
||||
[
|
||||
896,
|
||||
374,
|
||||
127,
|
||||
125,
|
||||
-64,
|
||||
-54
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
1024,
|
||||
374,
|
||||
125,
|
||||
123,
|
||||
-71,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1150,
|
||||
374,
|
||||
125,
|
||||
123,
|
||||
-71,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1276,
|
||||
374,
|
||||
125,
|
||||
123,
|
||||
-71,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1402,
|
||||
374,
|
||||
125,
|
||||
123,
|
||||
-71,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1528,
|
||||
374,
|
||||
125,
|
||||
123,
|
||||
-71,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1654,
|
||||
374,
|
||||
125,
|
||||
123,
|
||||
-71,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1780,
|
||||
374,
|
||||
125,
|
||||
123,
|
||||
-71,
|
||||
-58
|
||||
],
|
||||
[
|
||||
1906,
|
||||
374,
|
||||
125,
|
||||
123,
|
||||
-71,
|
||||
-58
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 298 KiB |
|
|
@ -0,0 +1,117 @@
|
|||
{
|
||||
"name": "bonespiritexplode",
|
||||
"celFile": "BoneSpiritExplode",
|
||||
"width": 512,
|
||||
"height": 569,
|
||||
"directions": 1,
|
||||
"framesPerDirection": 13,
|
||||
"animSpeed": 16,
|
||||
"groups": [
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
128,
|
||||
113,
|
||||
-62,
|
||||
-100
|
||||
],
|
||||
[
|
||||
129,
|
||||
0,
|
||||
128,
|
||||
113,
|
||||
-62,
|
||||
-100
|
||||
],
|
||||
[
|
||||
258,
|
||||
0,
|
||||
128,
|
||||
113,
|
||||
-62,
|
||||
-100
|
||||
],
|
||||
[
|
||||
0,
|
||||
114,
|
||||
128,
|
||||
113,
|
||||
-62,
|
||||
-100
|
||||
],
|
||||
[
|
||||
129,
|
||||
114,
|
||||
128,
|
||||
113,
|
||||
-62,
|
||||
-100
|
||||
],
|
||||
[
|
||||
258,
|
||||
114,
|
||||
128,
|
||||
113,
|
||||
-62,
|
||||
-100
|
||||
],
|
||||
[
|
||||
0,
|
||||
228,
|
||||
128,
|
||||
113,
|
||||
-62,
|
||||
-100
|
||||
],
|
||||
[
|
||||
129,
|
||||
228,
|
||||
128,
|
||||
113,
|
||||
-62,
|
||||
-100
|
||||
],
|
||||
[
|
||||
258,
|
||||
228,
|
||||
128,
|
||||
113,
|
||||
-62,
|
||||
-100
|
||||
],
|
||||
[
|
||||
0,
|
||||
342,
|
||||
128,
|
||||
113,
|
||||
-62,
|
||||
-100
|
||||
],
|
||||
[
|
||||
129,
|
||||
342,
|
||||
128,
|
||||
113,
|
||||
-62,
|
||||
-100
|
||||
],
|
||||
[
|
||||
258,
|
||||
342,
|
||||
128,
|
||||
113,
|
||||
-62,
|
||||
-100
|
||||
],
|
||||
[
|
||||
0,
|
||||
456,
|
||||
128,
|
||||
113,
|
||||
-62,
|
||||
-100
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 17 KiB |
|
|
@ -0,0 +1,133 @@
|
|||
{
|
||||
"name": "freezeexplode",
|
||||
"celFile": "FreezeExplodeCenter",
|
||||
"width": 1024,
|
||||
"height": 323,
|
||||
"directions": 1,
|
||||
"framesPerDirection": 15,
|
||||
"animSpeed": 16,
|
||||
"groups": [
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
144,
|
||||
0,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
288,
|
||||
0,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
432,
|
||||
0,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
576,
|
||||
0,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
720,
|
||||
0,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
864,
|
||||
0,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
0,
|
||||
108,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
144,
|
||||
108,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
288,
|
||||
108,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
432,
|
||||
108,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
576,
|
||||
108,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
720,
|
||||
108,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
864,
|
||||
108,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
],
|
||||
[
|
||||
0,
|
||||
216,
|
||||
143,
|
||||
107,
|
||||
-75,
|
||||
-67
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 56 KiB |
|
|
@ -0,0 +1,811 @@
|
|||
{
|
||||
"name": "glacialspike",
|
||||
"celFile": "GlacialSpike",
|
||||
"width": 1024,
|
||||
"height": 719,
|
||||
"directions": 16,
|
||||
"framesPerDirection": 6,
|
||||
"animSpeed": 16,
|
||||
"groups": [
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
100,
|
||||
61,
|
||||
-34,
|
||||
-88
|
||||
],
|
||||
[
|
||||
101,
|
||||
0,
|
||||
100,
|
||||
61,
|
||||
-34,
|
||||
-88
|
||||
],
|
||||
[
|
||||
202,
|
||||
0,
|
||||
100,
|
||||
61,
|
||||
-34,
|
||||
-88
|
||||
],
|
||||
[
|
||||
303,
|
||||
0,
|
||||
100,
|
||||
61,
|
||||
-34,
|
||||
-88
|
||||
],
|
||||
[
|
||||
404,
|
||||
0,
|
||||
100,
|
||||
61,
|
||||
-34,
|
||||
-88
|
||||
],
|
||||
[
|
||||
505,
|
||||
0,
|
||||
100,
|
||||
61,
|
||||
-34,
|
||||
-88
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
606,
|
||||
0,
|
||||
104,
|
||||
67,
|
||||
-31,
|
||||
-63
|
||||
],
|
||||
[
|
||||
711,
|
||||
0,
|
||||
104,
|
||||
67,
|
||||
-31,
|
||||
-63
|
||||
],
|
||||
[
|
||||
816,
|
||||
0,
|
||||
104,
|
||||
67,
|
||||
-31,
|
||||
-63
|
||||
],
|
||||
[
|
||||
0,
|
||||
68,
|
||||
104,
|
||||
67,
|
||||
-31,
|
||||
-63
|
||||
],
|
||||
[
|
||||
105,
|
||||
68,
|
||||
104,
|
||||
67,
|
||||
-31,
|
||||
-63
|
||||
],
|
||||
[
|
||||
210,
|
||||
68,
|
||||
104,
|
||||
67,
|
||||
-31,
|
||||
-63
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
315,
|
||||
68,
|
||||
110,
|
||||
68,
|
||||
-79,
|
||||
-62
|
||||
],
|
||||
[
|
||||
426,
|
||||
68,
|
||||
110,
|
||||
68,
|
||||
-79,
|
||||
-62
|
||||
],
|
||||
[
|
||||
537,
|
||||
68,
|
||||
110,
|
||||
68,
|
||||
-79,
|
||||
-62
|
||||
],
|
||||
[
|
||||
648,
|
||||
68,
|
||||
110,
|
||||
68,
|
||||
-79,
|
||||
-62
|
||||
],
|
||||
[
|
||||
759,
|
||||
68,
|
||||
110,
|
||||
68,
|
||||
-79,
|
||||
-62
|
||||
],
|
||||
[
|
||||
870,
|
||||
68,
|
||||
110,
|
||||
68,
|
||||
-79,
|
||||
-62
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
137,
|
||||
96,
|
||||
59,
|
||||
-62,
|
||||
-86
|
||||
],
|
||||
[
|
||||
97,
|
||||
137,
|
||||
96,
|
||||
59,
|
||||
-62,
|
||||
-86
|
||||
],
|
||||
[
|
||||
194,
|
||||
137,
|
||||
96,
|
||||
59,
|
||||
-62,
|
||||
-86
|
||||
],
|
||||
[
|
||||
291,
|
||||
137,
|
||||
96,
|
||||
59,
|
||||
-62,
|
||||
-86
|
||||
],
|
||||
[
|
||||
388,
|
||||
137,
|
||||
96,
|
||||
59,
|
||||
-62,
|
||||
-86
|
||||
],
|
||||
[
|
||||
485,
|
||||
137,
|
||||
96,
|
||||
59,
|
||||
-62,
|
||||
-86
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
582,
|
||||
137,
|
||||
37,
|
||||
73,
|
||||
-19,
|
||||
-94
|
||||
],
|
||||
[
|
||||
620,
|
||||
137,
|
||||
37,
|
||||
73,
|
||||
-19,
|
||||
-94
|
||||
],
|
||||
[
|
||||
658,
|
||||
137,
|
||||
37,
|
||||
73,
|
||||
-19,
|
||||
-94
|
||||
],
|
||||
[
|
||||
696,
|
||||
137,
|
||||
37,
|
||||
73,
|
||||
-19,
|
||||
-94
|
||||
],
|
||||
[
|
||||
734,
|
||||
137,
|
||||
37,
|
||||
73,
|
||||
-19,
|
||||
-94
|
||||
],
|
||||
[
|
||||
772,
|
||||
137,
|
||||
37,
|
||||
73,
|
||||
-19,
|
||||
-94
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
810,
|
||||
137,
|
||||
134,
|
||||
48,
|
||||
-44,
|
||||
-71
|
||||
],
|
||||
[
|
||||
0,
|
||||
211,
|
||||
134,
|
||||
48,
|
||||
-44,
|
||||
-71
|
||||
],
|
||||
[
|
||||
135,
|
||||
211,
|
||||
134,
|
||||
48,
|
||||
-44,
|
||||
-71
|
||||
],
|
||||
[
|
||||
270,
|
||||
211,
|
||||
134,
|
||||
48,
|
||||
-44,
|
||||
-71
|
||||
],
|
||||
[
|
||||
405,
|
||||
211,
|
||||
134,
|
||||
48,
|
||||
-44,
|
||||
-71
|
||||
],
|
||||
[
|
||||
540,
|
||||
211,
|
||||
134,
|
||||
48,
|
||||
-44,
|
||||
-71
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
675,
|
||||
211,
|
||||
45,
|
||||
83,
|
||||
-21,
|
||||
-67
|
||||
],
|
||||
[
|
||||
721,
|
||||
211,
|
||||
45,
|
||||
83,
|
||||
-21,
|
||||
-67
|
||||
],
|
||||
[
|
||||
767,
|
||||
211,
|
||||
45,
|
||||
83,
|
||||
-21,
|
||||
-67
|
||||
],
|
||||
[
|
||||
813,
|
||||
211,
|
||||
45,
|
||||
83,
|
||||
-21,
|
||||
-67
|
||||
],
|
||||
[
|
||||
859,
|
||||
211,
|
||||
45,
|
||||
83,
|
||||
-21,
|
||||
-67
|
||||
],
|
||||
[
|
||||
905,
|
||||
211,
|
||||
45,
|
||||
83,
|
||||
-21,
|
||||
-67
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
295,
|
||||
129,
|
||||
48,
|
||||
-86,
|
||||
-69
|
||||
],
|
||||
[
|
||||
130,
|
||||
295,
|
||||
129,
|
||||
48,
|
||||
-86,
|
||||
-69
|
||||
],
|
||||
[
|
||||
260,
|
||||
295,
|
||||
129,
|
||||
48,
|
||||
-86,
|
||||
-69
|
||||
],
|
||||
[
|
||||
390,
|
||||
295,
|
||||
129,
|
||||
48,
|
||||
-86,
|
||||
-69
|
||||
],
|
||||
[
|
||||
520,
|
||||
295,
|
||||
129,
|
||||
48,
|
||||
-86,
|
||||
-69
|
||||
],
|
||||
[
|
||||
650,
|
||||
295,
|
||||
129,
|
||||
48,
|
||||
-86,
|
||||
-69
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
780,
|
||||
295,
|
||||
65,
|
||||
70,
|
||||
-22,
|
||||
-93
|
||||
],
|
||||
[
|
||||
846,
|
||||
295,
|
||||
65,
|
||||
70,
|
||||
-22,
|
||||
-93
|
||||
],
|
||||
[
|
||||
912,
|
||||
295,
|
||||
65,
|
||||
70,
|
||||
-22,
|
||||
-93
|
||||
],
|
||||
[
|
||||
0,
|
||||
366,
|
||||
65,
|
||||
70,
|
||||
-22,
|
||||
-93
|
||||
],
|
||||
[
|
||||
66,
|
||||
366,
|
||||
65,
|
||||
70,
|
||||
-22,
|
||||
-93
|
||||
],
|
||||
[
|
||||
132,
|
||||
366,
|
||||
65,
|
||||
70,
|
||||
-22,
|
||||
-93
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
198,
|
||||
366,
|
||||
124,
|
||||
50,
|
||||
-41,
|
||||
-81
|
||||
],
|
||||
[
|
||||
323,
|
||||
366,
|
||||
124,
|
||||
50,
|
||||
-41,
|
||||
-81
|
||||
],
|
||||
[
|
||||
448,
|
||||
366,
|
||||
124,
|
||||
50,
|
||||
-41,
|
||||
-81
|
||||
],
|
||||
[
|
||||
573,
|
||||
366,
|
||||
124,
|
||||
50,
|
||||
-41,
|
||||
-81
|
||||
],
|
||||
[
|
||||
698,
|
||||
366,
|
||||
124,
|
||||
50,
|
||||
-41,
|
||||
-81
|
||||
],
|
||||
[
|
||||
823,
|
||||
366,
|
||||
124,
|
||||
50,
|
||||
-41,
|
||||
-81
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
437,
|
||||
124,
|
||||
51,
|
||||
-39,
|
||||
-60
|
||||
],
|
||||
[
|
||||
125,
|
||||
437,
|
||||
124,
|
||||
51,
|
||||
-39,
|
||||
-60
|
||||
],
|
||||
[
|
||||
250,
|
||||
437,
|
||||
124,
|
||||
51,
|
||||
-39,
|
||||
-60
|
||||
],
|
||||
[
|
||||
375,
|
||||
437,
|
||||
124,
|
||||
51,
|
||||
-39,
|
||||
-60
|
||||
],
|
||||
[
|
||||
500,
|
||||
437,
|
||||
124,
|
||||
51,
|
||||
-39,
|
||||
-60
|
||||
],
|
||||
[
|
||||
625,
|
||||
437,
|
||||
124,
|
||||
51,
|
||||
-39,
|
||||
-60
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
750,
|
||||
437,
|
||||
71,
|
||||
78,
|
||||
-20,
|
||||
-65
|
||||
],
|
||||
[
|
||||
822,
|
||||
437,
|
||||
71,
|
||||
78,
|
||||
-20,
|
||||
-65
|
||||
],
|
||||
[
|
||||
894,
|
||||
437,
|
||||
71,
|
||||
78,
|
||||
-20,
|
||||
-65
|
||||
],
|
||||
[
|
||||
0,
|
||||
516,
|
||||
71,
|
||||
78,
|
||||
-20,
|
||||
-65
|
||||
],
|
||||
[
|
||||
72,
|
||||
516,
|
||||
71,
|
||||
78,
|
||||
-20,
|
||||
-65
|
||||
],
|
||||
[
|
||||
144,
|
||||
516,
|
||||
71,
|
||||
78,
|
||||
-20,
|
||||
-65
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
216,
|
||||
516,
|
||||
74,
|
||||
79,
|
||||
-54,
|
||||
-65
|
||||
],
|
||||
[
|
||||
291,
|
||||
516,
|
||||
74,
|
||||
79,
|
||||
-54,
|
||||
-65
|
||||
],
|
||||
[
|
||||
366,
|
||||
516,
|
||||
74,
|
||||
79,
|
||||
-54,
|
||||
-65
|
||||
],
|
||||
[
|
||||
441,
|
||||
516,
|
||||
74,
|
||||
79,
|
||||
-54,
|
||||
-65
|
||||
],
|
||||
[
|
||||
516,
|
||||
516,
|
||||
74,
|
||||
79,
|
||||
-54,
|
||||
-65
|
||||
],
|
||||
[
|
||||
591,
|
||||
516,
|
||||
74,
|
||||
79,
|
||||
-54,
|
||||
-65
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
666,
|
||||
516,
|
||||
129,
|
||||
52,
|
||||
-90,
|
||||
-59
|
||||
],
|
||||
[
|
||||
796,
|
||||
516,
|
||||
129,
|
||||
52,
|
||||
-90,
|
||||
-59
|
||||
],
|
||||
[
|
||||
0,
|
||||
596,
|
||||
129,
|
||||
52,
|
||||
-90,
|
||||
-59
|
||||
],
|
||||
[
|
||||
130,
|
||||
596,
|
||||
129,
|
||||
52,
|
||||
-90,
|
||||
-59
|
||||
],
|
||||
[
|
||||
260,
|
||||
596,
|
||||
129,
|
||||
52,
|
||||
-90,
|
||||
-59
|
||||
],
|
||||
[
|
||||
390,
|
||||
596,
|
||||
129,
|
||||
52,
|
||||
-90,
|
||||
-59
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
520,
|
||||
596,
|
||||
120,
|
||||
46,
|
||||
-78,
|
||||
-78
|
||||
],
|
||||
[
|
||||
641,
|
||||
596,
|
||||
120,
|
||||
46,
|
||||
-78,
|
||||
-78
|
||||
],
|
||||
[
|
||||
762,
|
||||
596,
|
||||
120,
|
||||
46,
|
||||
-78,
|
||||
-78
|
||||
],
|
||||
[
|
||||
883,
|
||||
596,
|
||||
120,
|
||||
46,
|
||||
-78,
|
||||
-78
|
||||
],
|
||||
[
|
||||
0,
|
||||
649,
|
||||
120,
|
||||
46,
|
||||
-78,
|
||||
-78
|
||||
],
|
||||
[
|
||||
121,
|
||||
649,
|
||||
120,
|
||||
46,
|
||||
-78,
|
||||
-78
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
242,
|
||||
649,
|
||||
65,
|
||||
70,
|
||||
-43,
|
||||
-93
|
||||
],
|
||||
[
|
||||
308,
|
||||
649,
|
||||
65,
|
||||
70,
|
||||
-43,
|
||||
-93
|
||||
],
|
||||
[
|
||||
374,
|
||||
649,
|
||||
65,
|
||||
70,
|
||||
-43,
|
||||
-93
|
||||
],
|
||||
[
|
||||
440,
|
||||
649,
|
||||
65,
|
||||
70,
|
||||
-43,
|
||||
-93
|
||||
],
|
||||
[
|
||||
506,
|
||||
649,
|
||||
65,
|
||||
70,
|
||||
-43,
|
||||
-93
|
||||
],
|
||||
[
|
||||
572,
|
||||
649,
|
||||
65,
|
||||
70,
|
||||
-43,
|
||||
-93
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 277 KiB |
|
|
@ -0,0 +1,141 @@
|
|||
{
|
||||
"name": "holybolt",
|
||||
"celFile": "HolyBoltMissile",
|
||||
"width": 512,
|
||||
"height": 137,
|
||||
"directions": 1,
|
||||
"framesPerDirection": 16,
|
||||
"animSpeed": 16,
|
||||
"groups": [
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
80,
|
||||
0,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
160,
|
||||
0,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
240,
|
||||
0,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
320,
|
||||
0,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
400,
|
||||
0,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
0,
|
||||
46,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
80,
|
||||
46,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
160,
|
||||
46,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
240,
|
||||
46,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
320,
|
||||
46,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
400,
|
||||
46,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
0,
|
||||
92,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
80,
|
||||
92,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
160,
|
||||
92,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
],
|
||||
[
|
||||
240,
|
||||
92,
|
||||
79,
|
||||
45,
|
||||
-39,
|
||||
-59
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
|
@ -0,0 +1,683 @@
|
|||
{
|
||||
"name": "iceblast",
|
||||
"celFile": "IceBlast",
|
||||
"width": 1024,
|
||||
"height": 431,
|
||||
"directions": 16,
|
||||
"framesPerDirection": 5,
|
||||
"animSpeed": 16,
|
||||
"groups": [
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
74,
|
||||
54,
|
||||
-16,
|
||||
-71
|
||||
],
|
||||
[
|
||||
75,
|
||||
0,
|
||||
74,
|
||||
54,
|
||||
-16,
|
||||
-71
|
||||
],
|
||||
[
|
||||
150,
|
||||
0,
|
||||
74,
|
||||
54,
|
||||
-16,
|
||||
-71
|
||||
],
|
||||
[
|
||||
225,
|
||||
0,
|
||||
74,
|
||||
54,
|
||||
-16,
|
||||
-71
|
||||
],
|
||||
[
|
||||
300,
|
||||
0,
|
||||
74,
|
||||
54,
|
||||
-16,
|
||||
-71
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
375,
|
||||
0,
|
||||
85,
|
||||
56,
|
||||
-14,
|
||||
-51
|
||||
],
|
||||
[
|
||||
461,
|
||||
0,
|
||||
85,
|
||||
56,
|
||||
-14,
|
||||
-51
|
||||
],
|
||||
[
|
||||
547,
|
||||
0,
|
||||
85,
|
||||
56,
|
||||
-14,
|
||||
-51
|
||||
],
|
||||
[
|
||||
633,
|
||||
0,
|
||||
85,
|
||||
56,
|
||||
-14,
|
||||
-51
|
||||
],
|
||||
[
|
||||
719,
|
||||
0,
|
||||
85,
|
||||
56,
|
||||
-14,
|
||||
-51
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
805,
|
||||
0,
|
||||
86,
|
||||
51,
|
||||
-70,
|
||||
-51
|
||||
],
|
||||
[
|
||||
892,
|
||||
0,
|
||||
86,
|
||||
51,
|
||||
-70,
|
||||
-51
|
||||
],
|
||||
[
|
||||
0,
|
||||
57,
|
||||
86,
|
||||
51,
|
||||
-70,
|
||||
-51
|
||||
],
|
||||
[
|
||||
87,
|
||||
57,
|
||||
86,
|
||||
51,
|
||||
-70,
|
||||
-51
|
||||
],
|
||||
[
|
||||
174,
|
||||
57,
|
||||
86,
|
||||
51,
|
||||
-70,
|
||||
-51
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
261,
|
||||
57,
|
||||
72,
|
||||
54,
|
||||
-58,
|
||||
-72
|
||||
],
|
||||
[
|
||||
334,
|
||||
57,
|
||||
72,
|
||||
54,
|
||||
-58,
|
||||
-72
|
||||
],
|
||||
[
|
||||
407,
|
||||
57,
|
||||
72,
|
||||
54,
|
||||
-58,
|
||||
-72
|
||||
],
|
||||
[
|
||||
480,
|
||||
57,
|
||||
72,
|
||||
54,
|
||||
-58,
|
||||
-72
|
||||
],
|
||||
[
|
||||
553,
|
||||
57,
|
||||
72,
|
||||
54,
|
||||
-58,
|
||||
-72
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
626,
|
||||
57,
|
||||
45,
|
||||
59,
|
||||
-23,
|
||||
-78
|
||||
],
|
||||
[
|
||||
672,
|
||||
57,
|
||||
45,
|
||||
59,
|
||||
-23,
|
||||
-78
|
||||
],
|
||||
[
|
||||
718,
|
||||
57,
|
||||
45,
|
||||
59,
|
||||
-23,
|
||||
-78
|
||||
],
|
||||
[
|
||||
764,
|
||||
57,
|
||||
45,
|
||||
59,
|
||||
-23,
|
||||
-78
|
||||
],
|
||||
[
|
||||
810,
|
||||
57,
|
||||
45,
|
||||
59,
|
||||
-23,
|
||||
-78
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
856,
|
||||
57,
|
||||
100,
|
||||
41,
|
||||
-16,
|
||||
-54
|
||||
],
|
||||
[
|
||||
0,
|
||||
117,
|
||||
100,
|
||||
41,
|
||||
-16,
|
||||
-54
|
||||
],
|
||||
[
|
||||
101,
|
||||
117,
|
||||
100,
|
||||
41,
|
||||
-16,
|
||||
-54
|
||||
],
|
||||
[
|
||||
202,
|
||||
117,
|
||||
100,
|
||||
41,
|
||||
-16,
|
||||
-54
|
||||
],
|
||||
[
|
||||
303,
|
||||
117,
|
||||
100,
|
||||
41,
|
||||
-16,
|
||||
-54
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
404,
|
||||
117,
|
||||
51,
|
||||
67,
|
||||
-26,
|
||||
-51
|
||||
],
|
||||
[
|
||||
456,
|
||||
117,
|
||||
51,
|
||||
67,
|
||||
-26,
|
||||
-51
|
||||
],
|
||||
[
|
||||
508,
|
||||
117,
|
||||
51,
|
||||
67,
|
||||
-26,
|
||||
-51
|
||||
],
|
||||
[
|
||||
560,
|
||||
117,
|
||||
51,
|
||||
67,
|
||||
-26,
|
||||
-51
|
||||
],
|
||||
[
|
||||
612,
|
||||
117,
|
||||
51,
|
||||
67,
|
||||
-26,
|
||||
-51
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
664,
|
||||
117,
|
||||
101,
|
||||
45,
|
||||
-85,
|
||||
-55
|
||||
],
|
||||
[
|
||||
766,
|
||||
117,
|
||||
101,
|
||||
45,
|
||||
-85,
|
||||
-55
|
||||
],
|
||||
[
|
||||
868,
|
||||
117,
|
||||
101,
|
||||
45,
|
||||
-85,
|
||||
-55
|
||||
],
|
||||
[
|
||||
0,
|
||||
185,
|
||||
101,
|
||||
45,
|
||||
-85,
|
||||
-55
|
||||
],
|
||||
[
|
||||
102,
|
||||
185,
|
||||
101,
|
||||
45,
|
||||
-85,
|
||||
-55
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
204,
|
||||
185,
|
||||
53,
|
||||
57,
|
||||
-17,
|
||||
-76
|
||||
],
|
||||
[
|
||||
258,
|
||||
185,
|
||||
53,
|
||||
57,
|
||||
-17,
|
||||
-76
|
||||
],
|
||||
[
|
||||
312,
|
||||
185,
|
||||
53,
|
||||
57,
|
||||
-17,
|
||||
-76
|
||||
],
|
||||
[
|
||||
366,
|
||||
185,
|
||||
53,
|
||||
57,
|
||||
-17,
|
||||
-76
|
||||
],
|
||||
[
|
||||
420,
|
||||
185,
|
||||
53,
|
||||
57,
|
||||
-17,
|
||||
-76
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
474,
|
||||
185,
|
||||
91,
|
||||
48,
|
||||
-16,
|
||||
-63
|
||||
],
|
||||
[
|
||||
566,
|
||||
185,
|
||||
91,
|
||||
48,
|
||||
-16,
|
||||
-63
|
||||
],
|
||||
[
|
||||
658,
|
||||
185,
|
||||
91,
|
||||
48,
|
||||
-16,
|
||||
-63
|
||||
],
|
||||
[
|
||||
750,
|
||||
185,
|
||||
91,
|
||||
48,
|
||||
-16,
|
||||
-63
|
||||
],
|
||||
[
|
||||
842,
|
||||
185,
|
||||
91,
|
||||
48,
|
||||
-16,
|
||||
-63
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
243,
|
||||
99,
|
||||
42,
|
||||
-15,
|
||||
-50
|
||||
],
|
||||
[
|
||||
100,
|
||||
243,
|
||||
99,
|
||||
42,
|
||||
-15,
|
||||
-50
|
||||
],
|
||||
[
|
||||
200,
|
||||
243,
|
||||
99,
|
||||
42,
|
||||
-15,
|
||||
-50
|
||||
],
|
||||
[
|
||||
300,
|
||||
243,
|
||||
99,
|
||||
42,
|
||||
-15,
|
||||
-50
|
||||
],
|
||||
[
|
||||
400,
|
||||
243,
|
||||
99,
|
||||
42,
|
||||
-15,
|
||||
-50
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
500,
|
||||
243,
|
||||
63,
|
||||
65,
|
||||
-15,
|
||||
-51
|
||||
],
|
||||
[
|
||||
564,
|
||||
243,
|
||||
63,
|
||||
65,
|
||||
-15,
|
||||
-51
|
||||
],
|
||||
[
|
||||
628,
|
||||
243,
|
||||
63,
|
||||
65,
|
||||
-15,
|
||||
-51
|
||||
],
|
||||
[
|
||||
692,
|
||||
243,
|
||||
63,
|
||||
65,
|
||||
-15,
|
||||
-51
|
||||
],
|
||||
[
|
||||
756,
|
||||
243,
|
||||
63,
|
||||
65,
|
||||
-15,
|
||||
-51
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
820,
|
||||
243,
|
||||
64,
|
||||
62,
|
||||
-47,
|
||||
-51
|
||||
],
|
||||
[
|
||||
885,
|
||||
243,
|
||||
64,
|
||||
62,
|
||||
-47,
|
||||
-51
|
||||
],
|
||||
[
|
||||
950,
|
||||
243,
|
||||
64,
|
||||
62,
|
||||
-47,
|
||||
-51
|
||||
],
|
||||
[
|
||||
0,
|
||||
309,
|
||||
64,
|
||||
62,
|
||||
-47,
|
||||
-51
|
||||
],
|
||||
[
|
||||
65,
|
||||
309,
|
||||
64,
|
||||
62,
|
||||
-47,
|
||||
-51
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
130,
|
||||
309,
|
||||
99,
|
||||
45,
|
||||
-83,
|
||||
-50
|
||||
],
|
||||
[
|
||||
230,
|
||||
309,
|
||||
99,
|
||||
45,
|
||||
-83,
|
||||
-50
|
||||
],
|
||||
[
|
||||
330,
|
||||
309,
|
||||
99,
|
||||
45,
|
||||
-83,
|
||||
-50
|
||||
],
|
||||
[
|
||||
430,
|
||||
309,
|
||||
99,
|
||||
45,
|
||||
-83,
|
||||
-50
|
||||
],
|
||||
[
|
||||
530,
|
||||
309,
|
||||
99,
|
||||
45,
|
||||
-83,
|
||||
-50
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
630,
|
||||
309,
|
||||
90,
|
||||
49,
|
||||
-75,
|
||||
-65
|
||||
],
|
||||
[
|
||||
721,
|
||||
309,
|
||||
90,
|
||||
49,
|
||||
-75,
|
||||
-65
|
||||
],
|
||||
[
|
||||
812,
|
||||
309,
|
||||
90,
|
||||
49,
|
||||
-75,
|
||||
-65
|
||||
],
|
||||
[
|
||||
903,
|
||||
309,
|
||||
90,
|
||||
49,
|
||||
-75,
|
||||
-65
|
||||
],
|
||||
[
|
||||
0,
|
||||
372,
|
||||
90,
|
||||
49,
|
||||
-75,
|
||||
-65
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
91,
|
||||
372,
|
||||
52,
|
||||
59,
|
||||
-38,
|
||||
-77
|
||||
],
|
||||
[
|
||||
144,
|
||||
372,
|
||||
52,
|
||||
59,
|
||||
-38,
|
||||
-77
|
||||
],
|
||||
[
|
||||
197,
|
||||
372,
|
||||
52,
|
||||
59,
|
||||
-38,
|
||||
-77
|
||||
],
|
||||
[
|
||||
250,
|
||||
372,
|
||||
52,
|
||||
59,
|
||||
-38,
|
||||
-77
|
||||
],
|
||||
[
|
||||
303,
|
||||
372,
|
||||
52,
|
||||
59,
|
||||
-38,
|
||||
-77
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 107 KiB |
|
|
@ -0,0 +1,811 @@
|
|||
{
|
||||
"name": "icebolt",
|
||||
"celFile": "Icebolt",
|
||||
"width": 512,
|
||||
"height": 357,
|
||||
"directions": 16,
|
||||
"framesPerDirection": 6,
|
||||
"animSpeed": 16,
|
||||
"groups": [
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
51,
|
||||
27,
|
||||
-18,
|
||||
-65
|
||||
],
|
||||
[
|
||||
52,
|
||||
0,
|
||||
51,
|
||||
27,
|
||||
-18,
|
||||
-65
|
||||
],
|
||||
[
|
||||
104,
|
||||
0,
|
||||
51,
|
||||
27,
|
||||
-18,
|
||||
-65
|
||||
],
|
||||
[
|
||||
156,
|
||||
0,
|
||||
51,
|
||||
27,
|
||||
-18,
|
||||
-65
|
||||
],
|
||||
[
|
||||
208,
|
||||
0,
|
||||
51,
|
||||
27,
|
||||
-18,
|
||||
-65
|
||||
],
|
||||
[
|
||||
260,
|
||||
0,
|
||||
51,
|
||||
27,
|
||||
-18,
|
||||
-65
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
312,
|
||||
0,
|
||||
55,
|
||||
28,
|
||||
-17,
|
||||
-53
|
||||
],
|
||||
[
|
||||
368,
|
||||
0,
|
||||
55,
|
||||
28,
|
||||
-17,
|
||||
-53
|
||||
],
|
||||
[
|
||||
424,
|
||||
0,
|
||||
55,
|
||||
28,
|
||||
-17,
|
||||
-53
|
||||
],
|
||||
[
|
||||
0,
|
||||
29,
|
||||
55,
|
||||
28,
|
||||
-17,
|
||||
-53
|
||||
],
|
||||
[
|
||||
56,
|
||||
29,
|
||||
55,
|
||||
28,
|
||||
-17,
|
||||
-53
|
||||
],
|
||||
[
|
||||
112,
|
||||
29,
|
||||
55,
|
||||
28,
|
||||
-17,
|
||||
-53
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
168,
|
||||
29,
|
||||
54,
|
||||
34,
|
||||
-37,
|
||||
-53
|
||||
],
|
||||
[
|
||||
223,
|
||||
29,
|
||||
54,
|
||||
34,
|
||||
-37,
|
||||
-53
|
||||
],
|
||||
[
|
||||
278,
|
||||
29,
|
||||
54,
|
||||
34,
|
||||
-37,
|
||||
-53
|
||||
],
|
||||
[
|
||||
333,
|
||||
29,
|
||||
54,
|
||||
34,
|
||||
-37,
|
||||
-53
|
||||
],
|
||||
[
|
||||
388,
|
||||
29,
|
||||
54,
|
||||
34,
|
||||
-37,
|
||||
-53
|
||||
],
|
||||
[
|
||||
443,
|
||||
29,
|
||||
54,
|
||||
34,
|
||||
-37,
|
||||
-53
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
64,
|
||||
53,
|
||||
29,
|
||||
-35,
|
||||
-67
|
||||
],
|
||||
[
|
||||
54,
|
||||
64,
|
||||
53,
|
||||
29,
|
||||
-35,
|
||||
-67
|
||||
],
|
||||
[
|
||||
108,
|
||||
64,
|
||||
53,
|
||||
29,
|
||||
-35,
|
||||
-67
|
||||
],
|
||||
[
|
||||
162,
|
||||
64,
|
||||
53,
|
||||
29,
|
||||
-35,
|
||||
-67
|
||||
],
|
||||
[
|
||||
216,
|
||||
64,
|
||||
53,
|
||||
29,
|
||||
-35,
|
||||
-67
|
||||
],
|
||||
[
|
||||
270,
|
||||
64,
|
||||
53,
|
||||
29,
|
||||
-35,
|
||||
-67
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
324,
|
||||
64,
|
||||
16,
|
||||
37,
|
||||
-8,
|
||||
-71
|
||||
],
|
||||
[
|
||||
341,
|
||||
64,
|
||||
16,
|
||||
37,
|
||||
-8,
|
||||
-71
|
||||
],
|
||||
[
|
||||
358,
|
||||
64,
|
||||
16,
|
||||
37,
|
||||
-8,
|
||||
-71
|
||||
],
|
||||
[
|
||||
375,
|
||||
64,
|
||||
16,
|
||||
37,
|
||||
-8,
|
||||
-71
|
||||
],
|
||||
[
|
||||
392,
|
||||
64,
|
||||
16,
|
||||
37,
|
||||
-8,
|
||||
-71
|
||||
],
|
||||
[
|
||||
409,
|
||||
64,
|
||||
16,
|
||||
37,
|
||||
-8,
|
||||
-71
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
426,
|
||||
64,
|
||||
70,
|
||||
21,
|
||||
-24,
|
||||
-58
|
||||
],
|
||||
[
|
||||
0,
|
||||
102,
|
||||
70,
|
||||
21,
|
||||
-24,
|
||||
-58
|
||||
],
|
||||
[
|
||||
71,
|
||||
102,
|
||||
70,
|
||||
21,
|
||||
-24,
|
||||
-58
|
||||
],
|
||||
[
|
||||
142,
|
||||
102,
|
||||
70,
|
||||
21,
|
||||
-24,
|
||||
-58
|
||||
],
|
||||
[
|
||||
213,
|
||||
102,
|
||||
70,
|
||||
21,
|
||||
-24,
|
||||
-58
|
||||
],
|
||||
[
|
||||
284,
|
||||
102,
|
||||
70,
|
||||
21,
|
||||
-24,
|
||||
-58
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
355,
|
||||
102,
|
||||
18,
|
||||
39,
|
||||
-9,
|
||||
-55
|
||||
],
|
||||
[
|
||||
374,
|
||||
102,
|
||||
18,
|
||||
39,
|
||||
-9,
|
||||
-55
|
||||
],
|
||||
[
|
||||
393,
|
||||
102,
|
||||
18,
|
||||
39,
|
||||
-9,
|
||||
-55
|
||||
],
|
||||
[
|
||||
412,
|
||||
102,
|
||||
18,
|
||||
39,
|
||||
-9,
|
||||
-55
|
||||
],
|
||||
[
|
||||
431,
|
||||
102,
|
||||
18,
|
||||
39,
|
||||
-9,
|
||||
-55
|
||||
],
|
||||
[
|
||||
450,
|
||||
102,
|
||||
18,
|
||||
39,
|
||||
-9,
|
||||
-55
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
142,
|
||||
70,
|
||||
25,
|
||||
-46,
|
||||
-57
|
||||
],
|
||||
[
|
||||
71,
|
||||
142,
|
||||
70,
|
||||
25,
|
||||
-46,
|
||||
-57
|
||||
],
|
||||
[
|
||||
142,
|
||||
142,
|
||||
70,
|
||||
25,
|
||||
-46,
|
||||
-57
|
||||
],
|
||||
[
|
||||
213,
|
||||
142,
|
||||
70,
|
||||
25,
|
||||
-46,
|
||||
-57
|
||||
],
|
||||
[
|
||||
284,
|
||||
142,
|
||||
70,
|
||||
25,
|
||||
-46,
|
||||
-57
|
||||
],
|
||||
[
|
||||
355,
|
||||
142,
|
||||
70,
|
||||
25,
|
||||
-46,
|
||||
-57
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
426,
|
||||
142,
|
||||
29,
|
||||
36,
|
||||
-10,
|
||||
-71
|
||||
],
|
||||
[
|
||||
456,
|
||||
142,
|
||||
29,
|
||||
36,
|
||||
-10,
|
||||
-71
|
||||
],
|
||||
[
|
||||
0,
|
||||
179,
|
||||
29,
|
||||
36,
|
||||
-10,
|
||||
-71
|
||||
],
|
||||
[
|
||||
30,
|
||||
179,
|
||||
29,
|
||||
36,
|
||||
-10,
|
||||
-71
|
||||
],
|
||||
[
|
||||
60,
|
||||
179,
|
||||
29,
|
||||
36,
|
||||
-10,
|
||||
-71
|
||||
],
|
||||
[
|
||||
90,
|
||||
179,
|
||||
29,
|
||||
36,
|
||||
-10,
|
||||
-71
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
120,
|
||||
179,
|
||||
66,
|
||||
24,
|
||||
-23,
|
||||
-64
|
||||
],
|
||||
[
|
||||
187,
|
||||
179,
|
||||
66,
|
||||
24,
|
||||
-23,
|
||||
-64
|
||||
],
|
||||
[
|
||||
254,
|
||||
179,
|
||||
66,
|
||||
24,
|
||||
-23,
|
||||
-64
|
||||
],
|
||||
[
|
||||
321,
|
||||
179,
|
||||
66,
|
||||
24,
|
||||
-23,
|
||||
-64
|
||||
],
|
||||
[
|
||||
388,
|
||||
179,
|
||||
66,
|
||||
24,
|
||||
-23,
|
||||
-64
|
||||
],
|
||||
[
|
||||
0,
|
||||
216,
|
||||
66,
|
||||
24,
|
||||
-23,
|
||||
-64
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
67,
|
||||
216,
|
||||
66,
|
||||
20,
|
||||
-22,
|
||||
-51
|
||||
],
|
||||
[
|
||||
134,
|
||||
216,
|
||||
66,
|
||||
20,
|
||||
-22,
|
||||
-51
|
||||
],
|
||||
[
|
||||
201,
|
||||
216,
|
||||
66,
|
||||
20,
|
||||
-22,
|
||||
-51
|
||||
],
|
||||
[
|
||||
268,
|
||||
216,
|
||||
66,
|
||||
20,
|
||||
-22,
|
||||
-51
|
||||
],
|
||||
[
|
||||
335,
|
||||
216,
|
||||
66,
|
||||
20,
|
||||
-22,
|
||||
-51
|
||||
],
|
||||
[
|
||||
402,
|
||||
216,
|
||||
66,
|
||||
20,
|
||||
-22,
|
||||
-51
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
469,
|
||||
216,
|
||||
35,
|
||||
36,
|
||||
-9,
|
||||
-55
|
||||
],
|
||||
[
|
||||
0,
|
||||
253,
|
||||
35,
|
||||
36,
|
||||
-9,
|
||||
-55
|
||||
],
|
||||
[
|
||||
36,
|
||||
253,
|
||||
35,
|
||||
36,
|
||||
-9,
|
||||
-55
|
||||
],
|
||||
[
|
||||
72,
|
||||
253,
|
||||
35,
|
||||
36,
|
||||
-9,
|
||||
-55
|
||||
],
|
||||
[
|
||||
108,
|
||||
253,
|
||||
35,
|
||||
36,
|
||||
-9,
|
||||
-55
|
||||
],
|
||||
[
|
||||
144,
|
||||
253,
|
||||
35,
|
||||
36,
|
||||
-9,
|
||||
-55
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
180,
|
||||
253,
|
||||
31,
|
||||
39,
|
||||
-22,
|
||||
-55
|
||||
],
|
||||
[
|
||||
212,
|
||||
253,
|
||||
31,
|
||||
39,
|
||||
-22,
|
||||
-55
|
||||
],
|
||||
[
|
||||
244,
|
||||
253,
|
||||
31,
|
||||
39,
|
||||
-22,
|
||||
-55
|
||||
],
|
||||
[
|
||||
276,
|
||||
253,
|
||||
31,
|
||||
39,
|
||||
-22,
|
||||
-55
|
||||
],
|
||||
[
|
||||
308,
|
||||
253,
|
||||
31,
|
||||
39,
|
||||
-22,
|
||||
-55
|
||||
],
|
||||
[
|
||||
340,
|
||||
253,
|
||||
31,
|
||||
39,
|
||||
-22,
|
||||
-55
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
372,
|
||||
253,
|
||||
67,
|
||||
27,
|
||||
-45,
|
||||
-52
|
||||
],
|
||||
[
|
||||
440,
|
||||
253,
|
||||
67,
|
||||
27,
|
||||
-45,
|
||||
-52
|
||||
],
|
||||
[
|
||||
0,
|
||||
293,
|
||||
67,
|
||||
27,
|
||||
-45,
|
||||
-52
|
||||
],
|
||||
[
|
||||
68,
|
||||
293,
|
||||
67,
|
||||
27,
|
||||
-45,
|
||||
-52
|
||||
],
|
||||
[
|
||||
136,
|
||||
293,
|
||||
67,
|
||||
27,
|
||||
-45,
|
||||
-52
|
||||
],
|
||||
[
|
||||
204,
|
||||
293,
|
||||
67,
|
||||
27,
|
||||
-45,
|
||||
-52
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
272,
|
||||
293,
|
||||
66,
|
||||
23,
|
||||
-43,
|
||||
-62
|
||||
],
|
||||
[
|
||||
339,
|
||||
293,
|
||||
66,
|
||||
23,
|
||||
-43,
|
||||
-62
|
||||
],
|
||||
[
|
||||
406,
|
||||
293,
|
||||
66,
|
||||
23,
|
||||
-43,
|
||||
-62
|
||||
],
|
||||
[
|
||||
0,
|
||||
321,
|
||||
66,
|
||||
23,
|
||||
-43,
|
||||
-62
|
||||
],
|
||||
[
|
||||
67,
|
||||
321,
|
||||
66,
|
||||
23,
|
||||
-43,
|
||||
-62
|
||||
],
|
||||
[
|
||||
134,
|
||||
321,
|
||||
66,
|
||||
23,
|
||||
-43,
|
||||
-62
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
201,
|
||||
321,
|
||||
33,
|
||||
36,
|
||||
-23,
|
||||
-71
|
||||
],
|
||||
[
|
||||
235,
|
||||
321,
|
||||
33,
|
||||
36,
|
||||
-23,
|
||||
-71
|
||||
],
|
||||
[
|
||||
269,
|
||||
321,
|
||||
33,
|
||||
36,
|
||||
-23,
|
||||
-71
|
||||
],
|
||||
[
|
||||
303,
|
||||
321,
|
||||
33,
|
||||
36,
|
||||
-23,
|
||||
-71
|
||||
],
|
||||
[
|
||||
337,
|
||||
321,
|
||||
33,
|
||||
36,
|
||||
-23,
|
||||
-71
|
||||
],
|
||||
[
|
||||
371,
|
||||
321,
|
||||
33,
|
||||
36,
|
||||
-23,
|
||||
-71
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 42 KiB |
|
|
@ -0,0 +1,141 @@
|
|||
{
|
||||
"name": "iceexplode",
|
||||
"celFile": "IceArrowExplode",
|
||||
"width": 512,
|
||||
"height": 332,
|
||||
"directions": 1,
|
||||
"framesPerDirection": 16,
|
||||
"animSpeed": 16,
|
||||
"groups": [
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
84,
|
||||
0,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
168,
|
||||
0,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
252,
|
||||
0,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
336,
|
||||
0,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
420,
|
||||
0,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
0,
|
||||
111,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
84,
|
||||
111,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
168,
|
||||
111,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
252,
|
||||
111,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
336,
|
||||
111,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
420,
|
||||
111,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
0,
|
||||
222,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
84,
|
||||
222,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
168,
|
||||
222,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
],
|
||||
[
|
||||
252,
|
||||
222,
|
||||
83,
|
||||
110,
|
||||
-43,
|
||||
-90
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -0,0 +1,683 @@
|
|||
{
|
||||
"name": "lightningjavelin",
|
||||
"celFile": "LightningJavelin",
|
||||
"width": 1024,
|
||||
"height": 642,
|
||||
"directions": 16,
|
||||
"framesPerDirection": 5,
|
||||
"animSpeed": 16,
|
||||
"groups": [
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
100,
|
||||
63,
|
||||
-42,
|
||||
-83
|
||||
],
|
||||
[
|
||||
101,
|
||||
0,
|
||||
100,
|
||||
63,
|
||||
-42,
|
||||
-83
|
||||
],
|
||||
[
|
||||
202,
|
||||
0,
|
||||
100,
|
||||
63,
|
||||
-42,
|
||||
-83
|
||||
],
|
||||
[
|
||||
303,
|
||||
0,
|
||||
100,
|
||||
63,
|
||||
-42,
|
||||
-83
|
||||
],
|
||||
[
|
||||
404,
|
||||
0,
|
||||
100,
|
||||
63,
|
||||
-42,
|
||||
-83
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
505,
|
||||
0,
|
||||
111,
|
||||
67,
|
||||
-39,
|
||||
-89
|
||||
],
|
||||
[
|
||||
617,
|
||||
0,
|
||||
111,
|
||||
67,
|
||||
-39,
|
||||
-89
|
||||
],
|
||||
[
|
||||
729,
|
||||
0,
|
||||
111,
|
||||
67,
|
||||
-39,
|
||||
-89
|
||||
],
|
||||
[
|
||||
841,
|
||||
0,
|
||||
111,
|
||||
67,
|
||||
-39,
|
||||
-89
|
||||
],
|
||||
[
|
||||
0,
|
||||
68,
|
||||
111,
|
||||
67,
|
||||
-39,
|
||||
-89
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
112,
|
||||
68,
|
||||
108,
|
||||
67,
|
||||
-68,
|
||||
-89
|
||||
],
|
||||
[
|
||||
221,
|
||||
68,
|
||||
108,
|
||||
67,
|
||||
-68,
|
||||
-89
|
||||
],
|
||||
[
|
||||
330,
|
||||
68,
|
||||
108,
|
||||
67,
|
||||
-68,
|
||||
-89
|
||||
],
|
||||
[
|
||||
439,
|
||||
68,
|
||||
108,
|
||||
67,
|
||||
-68,
|
||||
-89
|
||||
],
|
||||
[
|
||||
548,
|
||||
68,
|
||||
108,
|
||||
67,
|
||||
-68,
|
||||
-89
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
657,
|
||||
68,
|
||||
101,
|
||||
62,
|
||||
-60,
|
||||
-82
|
||||
],
|
||||
[
|
||||
759,
|
||||
68,
|
||||
101,
|
||||
62,
|
||||
-60,
|
||||
-82
|
||||
],
|
||||
[
|
||||
861,
|
||||
68,
|
||||
101,
|
||||
62,
|
||||
-60,
|
||||
-82
|
||||
],
|
||||
[
|
||||
0,
|
||||
136,
|
||||
101,
|
||||
62,
|
||||
-60,
|
||||
-82
|
||||
],
|
||||
[
|
||||
102,
|
||||
136,
|
||||
101,
|
||||
62,
|
||||
-60,
|
||||
-82
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
204,
|
||||
136,
|
||||
60,
|
||||
71,
|
||||
-31,
|
||||
-89
|
||||
],
|
||||
[
|
||||
265,
|
||||
136,
|
||||
60,
|
||||
71,
|
||||
-31,
|
||||
-89
|
||||
],
|
||||
[
|
||||
326,
|
||||
136,
|
||||
60,
|
||||
71,
|
||||
-31,
|
||||
-89
|
||||
],
|
||||
[
|
||||
387,
|
||||
136,
|
||||
60,
|
||||
71,
|
||||
-31,
|
||||
-89
|
||||
],
|
||||
[
|
||||
448,
|
||||
136,
|
||||
60,
|
||||
71,
|
||||
-31,
|
||||
-89
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
509,
|
||||
136,
|
||||
133,
|
||||
60,
|
||||
-45,
|
||||
-86
|
||||
],
|
||||
[
|
||||
643,
|
||||
136,
|
||||
133,
|
||||
60,
|
||||
-45,
|
||||
-86
|
||||
],
|
||||
[
|
||||
777,
|
||||
136,
|
||||
133,
|
||||
60,
|
||||
-45,
|
||||
-86
|
||||
],
|
||||
[
|
||||
0,
|
||||
208,
|
||||
133,
|
||||
60,
|
||||
-45,
|
||||
-86
|
||||
],
|
||||
[
|
||||
134,
|
||||
208,
|
||||
133,
|
||||
60,
|
||||
-45,
|
||||
-86
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
268,
|
||||
208,
|
||||
56,
|
||||
79,
|
||||
-27,
|
||||
-90
|
||||
],
|
||||
[
|
||||
325,
|
||||
208,
|
||||
56,
|
||||
79,
|
||||
-27,
|
||||
-90
|
||||
],
|
||||
[
|
||||
382,
|
||||
208,
|
||||
56,
|
||||
79,
|
||||
-27,
|
||||
-90
|
||||
],
|
||||
[
|
||||
439,
|
||||
208,
|
||||
56,
|
||||
79,
|
||||
-27,
|
||||
-90
|
||||
],
|
||||
[
|
||||
496,
|
||||
208,
|
||||
56,
|
||||
79,
|
||||
-27,
|
||||
-90
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
553,
|
||||
208,
|
||||
131,
|
||||
60,
|
||||
-86,
|
||||
-85
|
||||
],
|
||||
[
|
||||
685,
|
||||
208,
|
||||
131,
|
||||
60,
|
||||
-86,
|
||||
-85
|
||||
],
|
||||
[
|
||||
817,
|
||||
208,
|
||||
131,
|
||||
60,
|
||||
-86,
|
||||
-85
|
||||
],
|
||||
[
|
||||
0,
|
||||
288,
|
||||
131,
|
||||
60,
|
||||
-86,
|
||||
-85
|
||||
],
|
||||
[
|
||||
132,
|
||||
288,
|
||||
131,
|
||||
60,
|
||||
-86,
|
||||
-85
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
264,
|
||||
288,
|
||||
71,
|
||||
69,
|
||||
-37,
|
||||
-88
|
||||
],
|
||||
[
|
||||
336,
|
||||
288,
|
||||
71,
|
||||
69,
|
||||
-37,
|
||||
-88
|
||||
],
|
||||
[
|
||||
408,
|
||||
288,
|
||||
71,
|
||||
69,
|
||||
-37,
|
||||
-88
|
||||
],
|
||||
[
|
||||
480,
|
||||
288,
|
||||
71,
|
||||
69,
|
||||
-37,
|
||||
-88
|
||||
],
|
||||
[
|
||||
552,
|
||||
288,
|
||||
71,
|
||||
69,
|
||||
-37,
|
||||
-88
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
624,
|
||||
288,
|
||||
123,
|
||||
61,
|
||||
-45,
|
||||
-84
|
||||
],
|
||||
[
|
||||
748,
|
||||
288,
|
||||
123,
|
||||
61,
|
||||
-45,
|
||||
-84
|
||||
],
|
||||
[
|
||||
872,
|
||||
288,
|
||||
123,
|
||||
61,
|
||||
-45,
|
||||
-84
|
||||
],
|
||||
[
|
||||
0,
|
||||
358,
|
||||
123,
|
||||
61,
|
||||
-45,
|
||||
-84
|
||||
],
|
||||
[
|
||||
124,
|
||||
358,
|
||||
123,
|
||||
61,
|
||||
-45,
|
||||
-84
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
248,
|
||||
358,
|
||||
129,
|
||||
60,
|
||||
-43,
|
||||
-88
|
||||
],
|
||||
[
|
||||
378,
|
||||
358,
|
||||
129,
|
||||
60,
|
||||
-43,
|
||||
-88
|
||||
],
|
||||
[
|
||||
508,
|
||||
358,
|
||||
129,
|
||||
60,
|
||||
-43,
|
||||
-88
|
||||
],
|
||||
[
|
||||
638,
|
||||
358,
|
||||
129,
|
||||
60,
|
||||
-43,
|
||||
-88
|
||||
],
|
||||
[
|
||||
768,
|
||||
358,
|
||||
129,
|
||||
60,
|
||||
-43,
|
||||
-88
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
898,
|
||||
358,
|
||||
78,
|
||||
76,
|
||||
-33,
|
||||
-90
|
||||
],
|
||||
[
|
||||
0,
|
||||
435,
|
||||
78,
|
||||
76,
|
||||
-33,
|
||||
-90
|
||||
],
|
||||
[
|
||||
79,
|
||||
435,
|
||||
78,
|
||||
76,
|
||||
-33,
|
||||
-90
|
||||
],
|
||||
[
|
||||
158,
|
||||
435,
|
||||
78,
|
||||
76,
|
||||
-33,
|
||||
-90
|
||||
],
|
||||
[
|
||||
237,
|
||||
435,
|
||||
78,
|
||||
76,
|
||||
-33,
|
||||
-90
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
316,
|
||||
435,
|
||||
76,
|
||||
76,
|
||||
-41,
|
||||
-90
|
||||
],
|
||||
[
|
||||
393,
|
||||
435,
|
||||
76,
|
||||
76,
|
||||
-41,
|
||||
-90
|
||||
],
|
||||
[
|
||||
470,
|
||||
435,
|
||||
76,
|
||||
76,
|
||||
-41,
|
||||
-90
|
||||
],
|
||||
[
|
||||
547,
|
||||
435,
|
||||
76,
|
||||
76,
|
||||
-41,
|
||||
-90
|
||||
],
|
||||
[
|
||||
624,
|
||||
435,
|
||||
76,
|
||||
76,
|
||||
-41,
|
||||
-90
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
701,
|
||||
435,
|
||||
127,
|
||||
59,
|
||||
-84,
|
||||
-87
|
||||
],
|
||||
[
|
||||
829,
|
||||
435,
|
||||
127,
|
||||
59,
|
||||
-84,
|
||||
-87
|
||||
],
|
||||
[
|
||||
0,
|
||||
512,
|
||||
127,
|
||||
59,
|
||||
-84,
|
||||
-87
|
||||
],
|
||||
[
|
||||
128,
|
||||
512,
|
||||
127,
|
||||
59,
|
||||
-84,
|
||||
-87
|
||||
],
|
||||
[
|
||||
256,
|
||||
512,
|
||||
127,
|
||||
59,
|
||||
-84,
|
||||
-87
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
384,
|
||||
512,
|
||||
121,
|
||||
61,
|
||||
-77,
|
||||
-83
|
||||
],
|
||||
[
|
||||
506,
|
||||
512,
|
||||
121,
|
||||
61,
|
||||
-77,
|
||||
-83
|
||||
],
|
||||
[
|
||||
628,
|
||||
512,
|
||||
121,
|
||||
61,
|
||||
-77,
|
||||
-83
|
||||
],
|
||||
[
|
||||
750,
|
||||
512,
|
||||
121,
|
||||
61,
|
||||
-77,
|
||||
-83
|
||||
],
|
||||
[
|
||||
872,
|
||||
512,
|
||||
121,
|
||||
61,
|
||||
-77,
|
||||
-83
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
0,
|
||||
574,
|
||||
71,
|
||||
68,
|
||||
-36,
|
||||
-86
|
||||
],
|
||||
[
|
||||
72,
|
||||
574,
|
||||
71,
|
||||
68,
|
||||
-36,
|
||||
-86
|
||||
],
|
||||
[
|
||||
144,
|
||||
574,
|
||||
71,
|
||||
68,
|
||||
-36,
|
||||
-86
|
||||
],
|
||||
[
|
||||
216,
|
||||
574,
|
||||
71,
|
||||
68,
|
||||
-36,
|
||||
-86
|
||||
],
|
||||
[
|
||||
288,
|
||||
574,
|
||||
71,
|
||||
68,
|
||||
-36,
|
||||
-86
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 650 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
|
@ -0,0 +1,117 @@
|
|||
{
|
||||
"name": "teethexplode",
|
||||
"celFile": "teethexplode",
|
||||
"width": 512,
|
||||
"height": 427,
|
||||
"directions": 1,
|
||||
"framesPerDirection": 13,
|
||||
"animSpeed": 16,
|
||||
"groups": [
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
105,
|
||||
106,
|
||||
-52,
|
||||
-100
|
||||
],
|
||||
[
|
||||
106,
|
||||
0,
|
||||
105,
|
||||
106,
|
||||
-52,
|
||||
-100
|
||||
],
|
||||
[
|
||||
212,
|
||||
0,
|
||||
105,
|
||||
106,
|
||||
-52,
|
||||
-100
|
||||
],
|
||||
[
|
||||
318,
|
||||
0,
|
||||
105,
|
||||
106,
|
||||
-52,
|
||||
-100
|
||||
],
|
||||
[
|
||||
0,
|
||||
107,
|
||||
105,
|
||||
106,
|
||||
-52,
|
||||
-100
|
||||
],
|
||||
[
|
||||
106,
|
||||
107,
|
||||
105,
|
||||
106,
|
||||
-52,
|
||||
-100
|
||||
],
|
||||
[
|
||||
212,
|
||||
107,
|
||||
105,
|
||||
106,
|
||||
-52,
|
||||
-100
|
||||
],
|
||||
[
|
||||
318,
|
||||
107,
|
||||
105,
|
||||
106,
|
||||
-52,
|
||||
-100
|
||||
],
|
||||
[
|
||||
0,
|
||||
214,
|
||||
105,
|
||||
106,
|
||||
-52,
|
||||
-100
|
||||
],
|
||||
[
|
||||
106,
|
||||
214,
|
||||
105,
|
||||
106,
|
||||
-52,
|
||||
-100
|
||||
],
|
||||
[
|
||||
212,
|
||||
214,
|
||||
105,
|
||||
106,
|
||||
-52,
|
||||
-100
|
||||
],
|
||||
[
|
||||
318,
|
||||
214,
|
||||
105,
|
||||
106,
|
||||
-52,
|
||||
-100
|
||||
],
|
||||
[
|
||||
0,
|
||||
321,
|
||||
105,
|
||||
106,
|
||||
-52,
|
||||
-100
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -1,10 +1,13 @@
|
|||
/**
|
||||
* 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)
|
||||
* Extracts authentic Diablo II v1.13c missile DCC sprites from MPQ archives:
|
||||
* - Fire: Firebolt, Fireball, FireArrowExplode2
|
||||
* - Cold: Icebolt, IceBlast, GlacialSpike, IceArrowExplode, FreezeExplodeCenter
|
||||
* - Lightning: ChargedBolt
|
||||
* - Necromancer: teethMissile, teethexplode, BoneSpear, BoneSpirit, BoneSpiritExplode, poisonNova
|
||||
* - Paladin: HolyBoltMissile, blessedhammer
|
||||
* - Amazon: SafeArrow, FireArrow, IceArrow, Arrow, LightningJavelin
|
||||
*
|
||||
* Bakes them into indexed-color PNG atlases and JSON metadata under:
|
||||
* - public/missiles/
|
||||
|
|
@ -20,7 +23,7 @@ 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 { encodeRgbaPng } from './png.ts'
|
||||
import type { SpriteFrame, SpriteSheet } from '../src/formats/sprite.ts'
|
||||
|
||||
export interface MissileFrameMeta {
|
||||
|
|
@ -53,18 +56,196 @@ export interface MissileMeta {
|
|||
}
|
||||
|
||||
const MISSILE_TARGETS = [
|
||||
// Fire spells & explosions
|
||||
{ name: 'firebolt', celFile: 'Firebolt', animSpeed: 16 },
|
||||
{ name: 'fireball', celFile: 'Fireball', animSpeed: 16 },
|
||||
{ name: 'fireexplode', celFile: 'FireArrowExplode2', animSpeed: 16 },
|
||||
|
||||
// Ice spells & explosions
|
||||
{ name: 'icebolt', celFile: 'Icebolt', animSpeed: 16 },
|
||||
{ name: 'iceblast', celFile: 'IceBlast', animSpeed: 16 },
|
||||
{ name: 'glacialspike', celFile: 'GlacialSpike', animSpeed: 16 },
|
||||
{ name: 'iceexplode', celFile: 'IceArrowExplode', animSpeed: 16 },
|
||||
{ name: 'freezeexplode', celFile: 'FreezeExplodeCenter', animSpeed: 16 },
|
||||
|
||||
// Lightning
|
||||
{ name: 'chargedbolt', celFile: 'ChargedBolt', animSpeed: 16 },
|
||||
|
||||
// Necromancer
|
||||
{ name: 'teeth', celFile: 'teethMissile', animSpeed: 16 },
|
||||
{ name: 'teethexplode', celFile: 'teethexplode', animSpeed: 16 },
|
||||
{ name: 'bonespear', celFile: 'BoneSpear', animSpeed: 16 },
|
||||
{ name: 'bonespirit', celFile: 'BoneSpirit', animSpeed: 16 },
|
||||
{ name: 'bonespiritexplode', celFile: 'BoneSpiritExplode', animSpeed: 16 },
|
||||
{ name: 'poisonnova', celFile: 'poisonNova', animSpeed: 16 },
|
||||
|
||||
// Paladin
|
||||
{ name: 'holybolt', celFile: 'HolyBoltMissile', animSpeed: 16 },
|
||||
{ name: 'blessedhammer', celFile: 'blessedhammer', animSpeed: 16 },
|
||||
|
||||
// Amazon
|
||||
{ name: 'magicarrow', celFile: 'SafeArrow', animSpeed: 16 },
|
||||
{ name: 'firearrow', celFile: 'FireArrow', animSpeed: 16 },
|
||||
{ name: 'icearrow', celFile: 'IceArrow', animSpeed: 16 },
|
||||
{ name: 'arrow', celFile: 'Arrow', animSpeed: 16, trans: 0 },
|
||||
{ name: 'lightningjavelin', celFile: 'LightningJavelin', 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 PAGE_SIZE = 4096
|
||||
|
||||
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))
|
||||
/**
|
||||
* Shelf-pack all frames of a missile SpriteSheet into a 32-bit RGBA PNG buffer.
|
||||
* Enforces Diablo II 1.13c Trans=1 transparency:
|
||||
* - Pure black / background antialiasing fringe pixels (max(r,g,b) <= 4 or idx 172) are strictly transparent (alpha=0).
|
||||
* - Semi-dark edge pixels ramp smoothly so no black borders or dark smudge boxes appear over the ground.
|
||||
* - Core pixels remain solid (alpha=255) for vibrant spell rendering.
|
||||
*/
|
||||
export function packMissileSheetToRgbaPng(
|
||||
sheet: SpriteSheet,
|
||||
palette: Uint8Array,
|
||||
trans: number = 1,
|
||||
): {
|
||||
png: Uint8Array
|
||||
width: number
|
||||
height: number
|
||||
groups: [number, number, number, number, number, number][][]
|
||||
} {
|
||||
const frames = sheet.groups.flatMap(g => g.frames)
|
||||
const widest = Math.max(1, ...frames.map(f => f.width))
|
||||
const area = frames.reduce((sum, f) => sum + (f.width + 1) * (f.height + 1), 0)
|
||||
let pageWidth = Math.min(PAGE_SIZE, Math.max(widest + 2, 512))
|
||||
while (pageWidth < PAGE_SIZE && pageWidth * pageWidth < area * 1.15) {
|
||||
pageWidth *= 2
|
||||
}
|
||||
pageWidth = Math.min(PAGE_SIZE, pageWidth)
|
||||
|
||||
let cursorX = 0
|
||||
let shelfY = 0
|
||||
let shelfHeight = 0
|
||||
const placements: { x: number; y: number; width: number; height: number }[] = []
|
||||
|
||||
for (const frame of frames) {
|
||||
const w = Math.max(1, Math.min(frame.width, pageWidth))
|
||||
const h = Math.max(1, Math.min(frame.height, PAGE_SIZE))
|
||||
if (cursorX + w > pageWidth) {
|
||||
shelfY += shelfHeight + 1
|
||||
shelfHeight = 0
|
||||
cursorX = 0
|
||||
}
|
||||
placements.push({ x: cursorX, y: shelfY, width: w, height: h })
|
||||
cursorX += w + 1
|
||||
shelfHeight = Math.max(shelfHeight, h)
|
||||
}
|
||||
|
||||
const pageHeight = Math.max(1, shelfY + shelfHeight)
|
||||
const rgba = new Uint8Array(pageWidth * pageHeight * 4)
|
||||
|
||||
let idx = 0
|
||||
for (const frame of frames) {
|
||||
const place = placements[idx]!
|
||||
idx += 1
|
||||
for (let row = 0; row < place.height; row += 1) {
|
||||
const fromRow = row * frame.width
|
||||
const toRow = (place.y + row) * pageWidth + place.x
|
||||
for (let col = 0; col < place.width; col += 1) {
|
||||
const srcAt = fromRow + col
|
||||
const dst = (toRow + col) * 4
|
||||
if (frame.mask[srcAt] === 0) {
|
||||
rgba[dst] = 0
|
||||
rgba[dst + 1] = 0
|
||||
rgba[dst + 2] = 0
|
||||
rgba[dst + 3] = 0
|
||||
} else {
|
||||
const val = frame.indices[srcAt]!
|
||||
if (val === 0) {
|
||||
rgba[dst] = 0
|
||||
rgba[dst + 1] = 0
|
||||
rgba[dst + 2] = 0
|
||||
rgba[dst + 3] = 0
|
||||
} else {
|
||||
const r = palette[val * 3]!
|
||||
const g = palette[val * 3 + 1]!
|
||||
const b = palette[val * 3 + 2]!
|
||||
if (trans === 1) {
|
||||
const m = Math.max(r, g, b)
|
||||
if (m <= 4) {
|
||||
rgba[dst] = 0
|
||||
rgba[dst + 1] = 0
|
||||
rgba[dst + 2] = 0
|
||||
rgba[dst + 3] = 0
|
||||
} else {
|
||||
const alpha = Math.min(255, Math.round(((m - 4) / (60 - 4)) * 255))
|
||||
rgba[dst] = r
|
||||
rgba[dst + 1] = g
|
||||
rgba[dst + 2] = b
|
||||
rgba[dst + 3] = alpha
|
||||
}
|
||||
} else {
|
||||
rgba[dst] = r
|
||||
rgba[dst + 1] = g
|
||||
rgba[dst + 2] = b
|
||||
rgba[dst + 3] = 255
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const groups: [number, number, number, number, number, number][][] = []
|
||||
let consumed = 0
|
||||
for (const group of sheet.groups) {
|
||||
const gList: [number, number, number, number, number, number][] = []
|
||||
for (let i = 0; i < group.frames.length; i += 1) {
|
||||
const p = placements[consumed]!
|
||||
const f = group.frames[i]!
|
||||
consumed += 1
|
||||
gList.push([p.x, p.y, p.width, p.height, f.anchorX ?? 0, f.anchorY ?? 0])
|
||||
}
|
||||
groups.push(gList)
|
||||
}
|
||||
|
||||
const png = encodeRgbaPng({ width: pageWidth, height: pageHeight, pixels: rgba })
|
||||
return { png, width: pageWidth, height: pageHeight, groups }
|
||||
}
|
||||
|
||||
function formatMetaTs(meta: MissileMeta): string {
|
||||
const groupsStr = meta.groups
|
||||
.map(
|
||||
group =>
|
||||
` [\n${group.map(f => ` [${f.join(', ')}]`).join(',\n')}\n ]`,
|
||||
)
|
||||
.join(',\n')
|
||||
|
||||
return `{\n name: ${JSON.stringify(meta.name)},\n celFile: ${JSON.stringify(meta.celFile)},\n width: ${meta.width},\n height: ${meta.height},\n directions: ${meta.directions},\n framesPerDirection: ${meta.framesPerDirection},\n animSpeed: ${meta.animSpeed},\n groups: [\n${groupsStr}\n ],\n}`
|
||||
}
|
||||
|
||||
export async function bakeMissiles(projectRoot: string = process.cwd()): Promise<Record<string, MissileMeta>> {
|
||||
const archivePaths = [
|
||||
join(projectRoot, 'samples', 'd2', 'Patch_D2.mpq'),
|
||||
join(projectRoot, 'samples', 'd2', 'd2exp.mpq'),
|
||||
join(projectRoot, 'samples', 'd2', 'd2data.mpq'),
|
||||
]
|
||||
const archives: MpqArchive[] = []
|
||||
for (const p of archivePaths) {
|
||||
try {
|
||||
archives.push(await MpqArchive.open(await fileSource(p)))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function readMpqFile(path: string): Promise<Uint8Array | null> {
|
||||
for (const a of archives) {
|
||||
const f = a.find(path)
|
||||
if (f) return a.read(f)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const palBytes = await readMpqFile('data/global/palette/ACT1/pal.pl2')
|
||||
if (!palBytes) throw new Error('data/global/palette/ACT1/pal.pl2 not found in MPQ archives')
|
||||
const pl2 = decodePl2(palBytes)
|
||||
|
||||
const publicOutDir = join(projectRoot, 'public', 'missiles')
|
||||
const packOutDir = join(projectRoot, 'samples', 'd2-packs', 'missiles')
|
||||
|
|
@ -74,10 +255,10 @@ export async function bakeMissiles(projectRoot: string = process.cwd()): Promise
|
|||
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 raw = await readMpqFile(`data/global/missiles/${target.celFile}.dcc`)
|
||||
if (!raw) throw new Error(`Missile DCC not found: data/global/missiles/${target.celFile}.dcc`)
|
||||
|
||||
const dcc = decodeDcc(await mpq.read(f))
|
||||
const dcc = decodeDcc(raw)
|
||||
const groups = dcc.directions.map(dir => ({
|
||||
frames: dir.frames.map(
|
||||
(frame): SpriteFrame => ({
|
||||
|
|
@ -92,7 +273,8 @@ export async function bakeMissiles(projectRoot: string = process.cwd()): Promise
|
|||
}))
|
||||
|
||||
const sheet: SpriteSheet = { groups, width: null }
|
||||
const packed = packSheetToPng(sheet, pl2.rgb)
|
||||
const trans = 'trans' in target ? (target as any).trans : 1
|
||||
const packed = packMissileSheetToRgbaPng(sheet, pl2.rgb, trans)
|
||||
|
||||
const meta: MissileMeta = {
|
||||
name: target.name,
|
||||
|
|
@ -118,6 +300,15 @@ export async function bakeMissiles(projectRoot: string = process.cwd()): Promise
|
|||
}
|
||||
|
||||
// Generate src/render/missiles-meta.ts
|
||||
const metaExports = MISSILE_TARGETS.map(t => {
|
||||
const upper = t.name.toUpperCase()
|
||||
return `export const ${upper}_META: MissileMeta = ${formatMetaTs(metaRecord[t.name]!)}`
|
||||
}).join('\n\n')
|
||||
|
||||
const mapEntries = MISSILE_TARGETS.map(t => {
|
||||
return ` '${t.name}': ${t.name.toUpperCase()}_META,`
|
||||
}).join('\n')
|
||||
|
||||
const metaTsContent = `/**
|
||||
* Authentic Diablo II v1.13c Missile Atlas Metadata.
|
||||
* Auto-generated by scripts/pack-missiles.ts.
|
||||
|
|
@ -157,12 +348,32 @@ export interface MissileMeta {
|
|||
* 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,
|
||||
]
|
||||
const DIR64_TO_DCC: Readonly<Record<number, readonly number[]>> = {
|
||||
4: [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
],
|
||||
8: [
|
||||
4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5,
|
||||
5, 5, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 6, 6,
|
||||
6, 6, 6, 6, 2, 2, 2, 2, 2, 2, 2, 2, 7, 7, 7, 7,
|
||||
7, 7, 7, 7, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4,
|
||||
],
|
||||
16: [
|
||||
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,
|
||||
],
|
||||
32: [
|
||||
4, 16, 16, 8, 8, 17, 17, 0, 0, 18, 18, 9, 9, 19, 19, 5,
|
||||
5, 20, 20, 10, 10, 21, 21, 1, 1, 22, 22, 11, 11, 23, 23, 6,
|
||||
6, 24, 24, 12, 12, 25, 25, 2, 2, 26, 26, 13, 13, 27, 27, 7,
|
||||
7, 28, 28, 14, 14, 29, 29, 3, 3, 30, 30, 15, 15, 31, 31, 4,
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate DCC direction index for a missile based on 2D velocity vector.
|
||||
|
|
@ -179,35 +390,36 @@ export function velocityToDccDirection(vx: number, vy: number, directions = 16):
|
|||
// 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
|
||||
const table = DIR64_TO_DCC[directions]
|
||||
if (table !== undefined) {
|
||||
return table[dir64] ?? 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
export const FIREBOLT_META: MissileMeta = ${JSON.stringify(metaRecord['firebolt'], null, 2)}
|
||||
${metaExports}
|
||||
|
||||
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,
|
||||
}
|
||||
export const MISSILE_METAS: Readonly<Record<string, MissileMeta>> = Object.freeze({
|
||||
${mapEntries}
|
||||
})
|
||||
`
|
||||
const metaTsPath = join(projectRoot, 'src', 'render', 'missiles-meta.ts')
|
||||
writeFileSync(metaTsPath, metaTsContent, 'utf-8')
|
||||
console.log(`[Missile Packer] Wrote ${metaTsPath}`)
|
||||
|
||||
const outTsPath = join(projectRoot, 'src', 'render', 'missiles-meta.ts')
|
||||
writeFileSync(outTsPath, metaTsContent, 'utf-8')
|
||||
console.log(`[Missile Packer] Wrote ${outTsPath}`)
|
||||
|
||||
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)
|
||||
})
|
||||
// Direct invocation via tsx
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
bakeMissiles()
|
||||
.then(() => {
|
||||
console.log('[Missile Packer] All missile assets baked successfully.')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[Missile Packer] Failed:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,3 +170,65 @@ export function encodeIndexedPng(image: IndexedImage): Uint8Array {
|
|||
for (const part of parts) { out.set(part, offset); offset += part.byteLength }
|
||||
return out
|
||||
}
|
||||
|
||||
/** An RGBA image ready to encode. */
|
||||
export interface RgbaImage {
|
||||
/** Width in pixels. */
|
||||
readonly width: number
|
||||
/** Height in pixels. */
|
||||
readonly height: number
|
||||
/** RGBA pixels, row-major, 4 bytes per pixel (`width * height * 4` bytes). */
|
||||
readonly pixels: Uint8Array
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a 32-bit RGBA image as a PNG (color type 6).
|
||||
*
|
||||
* @param image - the RGBA image.
|
||||
* @returns the PNG bytes.
|
||||
*/
|
||||
export function encodeRgbaPng(image: RgbaImage): Uint8Array {
|
||||
const { width, height, pixels } = image
|
||||
if (pixels.byteLength !== width * height * 4) {
|
||||
throw new Error(`pixel buffer is ${String(pixels.byteLength)} bytes for ${String(width)}x${String(height)} RGBA`)
|
||||
}
|
||||
|
||||
const ihdr = new Uint8Array(13)
|
||||
const ihdrView = new DataView(ihdr.buffer)
|
||||
ihdrView.setUint32(0, width)
|
||||
ihdrView.setUint32(4, height)
|
||||
ihdr[8] = 8 // bit depth
|
||||
ihdr[9] = 6 // colour type: RGBA
|
||||
ihdr[10] = 0 // deflate
|
||||
ihdr[11] = 0 // adaptive filtering
|
||||
ihdr[12] = 0 // no interlace
|
||||
|
||||
const stride = 4
|
||||
const raw = new Uint8Array((width * stride + 1) * height)
|
||||
const candidate = new Uint8Array(width * stride)
|
||||
let at = 0
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
const row = pixels.subarray(y * width * stride, (y + 1) * width * stride)
|
||||
const previous = y === 0 ? null : pixels.subarray((y - 1) * width * stride, y * width * stride)
|
||||
let bestType = 0
|
||||
let bestScore = Number.POSITIVE_INFINITY
|
||||
for (const type of [0, 1, 2]) {
|
||||
filterRow(type, row, previous, stride, candidate)
|
||||
const value = score(candidate)
|
||||
if (value < bestScore) { bestScore = value; bestType = type }
|
||||
}
|
||||
filterRow(bestType, row, previous, stride, candidate)
|
||||
raw[at] = bestType
|
||||
raw.set(candidate, at + 1)
|
||||
at += width * stride + 1
|
||||
}
|
||||
|
||||
const idat = new Uint8Array(deflateSync(raw, { level: 9 }))
|
||||
const parts = [SIGNATURE, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', new Uint8Array(0))]
|
||||
const size = parts.reduce((sum, part) => sum + part.byteLength, 0)
|
||||
const out = new Uint8Array(size)
|
||||
let offset = 0
|
||||
for (const part of parts) { out.set(part, offset); offset += part.byteLength }
|
||||
return out
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,15 @@
|
|||
import type { CharacterClassCode } from './classes.ts'
|
||||
import type { EquipSlotId, GridPlacement, UiInventoryItem } from '../ui/inventory.ts'
|
||||
import type { CharacterAttributes } from '../ui/character-sheet.ts'
|
||||
import type { HotkeySkillEntry } from '../ui/hotkeys.ts'
|
||||
import {
|
||||
type HotkeySkillEntry,
|
||||
DEFAULT_SORCERESS_SKILLS,
|
||||
isLeftUsableSkill,
|
||||
isPassiveSkill,
|
||||
isAuraSkill,
|
||||
} from '../ui/hotkeys.ts'
|
||||
import { SKILLS_BY_CLASS } from '../data/skills-catalog.ts'
|
||||
import { calculateManaCost } from './skill-calc-engine.ts'
|
||||
|
||||
export interface ClassProfileState {
|
||||
readonly classCode: CharacterClassCode
|
||||
|
|
@ -1648,124 +1656,46 @@ export const CLASS_STARTER_HARD_POINTS: Record<CharacterClassCode, Readonly<Reco
|
|||
// 6. DEFAULT SKILLS & HOTKEYS PER CLASS
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
function buildClassDefaultSkills(classCode: CharacterClassCode): readonly HotkeySkillEntry[] {
|
||||
if (classCode === 'sor') return DEFAULT_SORCERESS_SKILLS
|
||||
|
||||
const universal: HotkeySkillEntry[] = [
|
||||
{ skillId: 0, name: 'Attack', nameZh: '普通攻击', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 2, name: 'Throw', nameZh: '投掷', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 3, name: 'Unsummon', nameZh: '取消召唤', level: 1, manaCost: 0, leftUsable: false, rightUsable: true },
|
||||
]
|
||||
|
||||
const classSkills = SKILLS_BY_CLASS[classCode] ?? []
|
||||
const hardPoints = CLASS_STARTER_HARD_POINTS[classCode] ?? {}
|
||||
const entries: HotkeySkillEntry[] = []
|
||||
|
||||
for (const sk of classSkills) {
|
||||
if (isPassiveSkill(sk.id)) continue
|
||||
const hard = hardPoints[sk.id] ?? 1
|
||||
const mana = calculateManaCost(sk, hard)
|
||||
entries.push({
|
||||
skillId: sk.id,
|
||||
name: sk.name,
|
||||
nameZh: sk.nameZh,
|
||||
level: hard,
|
||||
manaCost: mana,
|
||||
leftUsable: isLeftUsableSkill(sk.id),
|
||||
rightUsable: true,
|
||||
isAura: isAuraSkill(sk.id),
|
||||
})
|
||||
}
|
||||
|
||||
return [...universal, ...entries]
|
||||
}
|
||||
|
||||
export const CLASS_DEFAULT_SKILLS: Record<CharacterClassCode, readonly HotkeySkillEntry[]> = {
|
||||
ama: [
|
||||
{ skillId: 0, name: 'Attack', nameZh: '普通攻击', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 2, name: 'Throw', nameZh: '投掷', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 3, name: 'Unsummon', nameZh: '取消召唤', level: 1, manaCost: 0, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 6, name: 'Magic Arrow', nameZh: '魔法箭', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 7, name: 'Fire Arrow', nameZh: '火箭', level: 1, manaCost: 3, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 11, name: 'Cold Arrow', nameZh: '冰箭', level: 1, manaCost: 3.5, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 12, name: 'Multiple Shot', nameZh: '多重箭', level: 10, manaCost: 13, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 22, name: 'Guided Arrow', nameZh: '导引箭', level: 20, manaCost: 8, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 26, name: 'Strafe', nameZh: '炮轰', level: 10, manaCost: 11, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 31, name: 'Freezing Arrow', nameZh: '冻结箭', level: 10, manaCost: 17, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 10, name: 'Jab', nameZh: '戳刺', level: 1, manaCost: 2, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 24, name: 'Charged Strike', nameZh: '充能一击', level: 5, manaCost: 5, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 35, name: 'Lightning Fury', nameZh: '闪电之怒', level: 10, manaCost: 14, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 8, name: 'Inner Sight', nameZh: '内视', level: 1, manaCost: 5, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 17, name: 'Slow Missiles', nameZh: '减速箭', level: 1, manaCost: 5, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 28, name: 'Decoy', nameZh: '诱饵', level: 1, manaCost: 19, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 32, name: 'Valkyrie', nameZh: '女武神', level: 10, manaCost: 34, leftUsable: false, rightUsable: true },
|
||||
],
|
||||
sor: [
|
||||
{ skillId: 0, name: 'Attack', nameZh: '普通攻击', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 2, name: 'Throw', nameZh: '投掷', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 3, name: 'Unsummon', nameZh: '取消召唤', level: 1, manaCost: 0, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 36, name: 'Fire Bolt', nameZh: '火弹', level: 5, manaCost: 2.5, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 47, name: 'Fire Ball', nameZh: '火球', level: 8, manaCost: 8.5, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 56, name: 'Meteor', nameZh: '陨石', level: 6, manaCost: 22, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 38, name: 'Charged Bolt', nameZh: '充能弹', level: 4, manaCost: 4, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 49, name: 'Lightning', nameZh: '闪电', level: 7, manaCost: 11, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 54, name: 'Teleport', nameZh: '传送', level: 4, manaCost: 21, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 44, name: 'Frost Nova', nameZh: '霜之新星', level: 3, manaCost: 11, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 59, name: 'Blizzard', nameZh: '暴风雪', level: 10, manaCost: 27, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 64, name: 'Frozen Orb', nameZh: '冰封球', level: 12, manaCost: 30, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 40, name: 'Frozen Armor', nameZh: '冰封装甲', level: 3, manaCost: 7, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 42, name: 'Static Field', nameZh: '静态力场', level: 5, manaCost: 9, leftUsable: false, rightUsable: true },
|
||||
],
|
||||
nec: [
|
||||
{ skillId: 0, name: 'Attack', nameZh: '普通攻击', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 2, name: 'Throw', nameZh: '投掷', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 3, name: 'Unsummon', nameZh: '取消召唤', level: 1, manaCost: 0, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 66, name: 'Amplify Damage', nameZh: '伤害加深', level: 1, manaCost: 4, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 67, name: 'Teeth', nameZh: '牙', level: 5, manaCost: 5, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 68, name: 'Bone Armor', nameZh: '白骨装甲', level: 1, manaCost: 11, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 70, name: 'Raise Skeleton', nameZh: '复生骷髅', level: 20, manaCost: 15, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 74, name: 'Corpse Explosion', nameZh: '尸体爆炸', level: 10, manaCost: 15, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 75, name: 'Clay Golem', nameZh: '粘土石魔', level: 1, manaCost: 15, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 80, name: 'Raise Skeletal Mage', nameZh: '复生骷髅法师', level: 5, manaCost: 12, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 84, name: 'Bone Spear', nameZh: '骨矛', level: 15, manaCost: 12, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 87, name: 'Decrepify', nameZh: '衰老', level: 1, manaCost: 11, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 92, name: 'Poison Nova', nameZh: '剧毒新星', level: 5, manaCost: 20, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 95, name: 'Revive', nameZh: '重生', level: 5, manaCost: 45, leftUsable: false, rightUsable: true },
|
||||
],
|
||||
pal: [
|
||||
{ skillId: 0, name: 'Attack', nameZh: '普通攻击', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 2, name: 'Throw', nameZh: '投掷', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 3, name: 'Unsummon', nameZh: '取消召唤', level: 1, manaCost: 0, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 96, name: 'Sacrifice', nameZh: '牺牲', level: 5, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 97, name: 'Smite', nameZh: '重击', level: 5, manaCost: 2, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 101, name: 'Holy Bolt', nameZh: '圣光弹', level: 5, manaCost: 6, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 106, name: 'Zeal', nameZh: '热忱', level: 5, manaCost: 2, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 107, name: 'Charge', nameZh: '突击', level: 1, manaCost: 9, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 112, name: 'Blessed Hammer', nameZh: '祝福之槌', level: 20, manaCost: 10, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 117, name: 'Holy Shield', nameZh: '圣盾', level: 15, manaCost: 35, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 98, name: 'Might', nameZh: '力量灵气', level: 1, manaCost: 0, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 113, name: 'Concentration', nameZh: '专注灵气', level: 20, manaCost: 0, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 115, name: 'Vigor', nameZh: '活力灵气', level: 15, manaCost: 0, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 122, name: 'Fanaticism', nameZh: '狂热灵气', level: 10, manaCost: 0, leftUsable: false, rightUsable: true },
|
||||
],
|
||||
bar: [
|
||||
{ skillId: 0, name: 'Attack', nameZh: '普通攻击', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 2, name: 'Throw', nameZh: '投掷', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 3, name: 'Unsummon', nameZh: '取消召唤', level: 1, manaCost: 0, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 126, name: 'Bash', nameZh: '重击', level: 5, manaCost: 2, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 130, name: 'Howl', nameZh: '狂嚎', level: 1, manaCost: 4, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 132, name: 'Leap', nameZh: '跳跃', level: 1, manaCost: 2, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 133, name: 'Double Swing', nameZh: '双手挥击', level: 5, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 138, name: 'Shout', nameZh: '大叫', level: 15, manaCost: 6, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 143, name: 'Leap Attack', nameZh: '跳跃攻击', level: 1, manaCost: 9, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 144, name: 'Concentrate', nameZh: '专心', level: 5, manaCost: 2, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 147, name: 'Frenzy', nameZh: '狂乱', level: 10, manaCost: 3, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 149, name: 'Battle Orders', nameZh: '战斗体制', level: 20, manaCost: 7, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 151, name: 'Whirlwind', nameZh: '旋风', level: 20, manaCost: 25, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 152, name: 'Berserk', nameZh: '狂战士', level: 1, manaCost: 4, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 155, name: 'Battle Command', nameZh: '战斗指挥', level: 1, manaCost: 11, leftUsable: false, rightUsable: true },
|
||||
],
|
||||
dru: [
|
||||
{ skillId: 0, name: 'Attack', nameZh: '普通攻击', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 2, name: 'Throw', nameZh: '投掷', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 3, name: 'Unsummon', nameZh: '取消召唤', level: 1, manaCost: 0, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 221, name: 'Arctic Blast', nameZh: '极地风暴', level: 1, manaCost: 4, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 223, name: 'Werewolf', nameZh: '狼人变化', level: 5, manaCost: 15, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 225, name: 'Firestorm', nameZh: '火风暴', level: 5, manaCost: 4, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 226, name: 'Oak Sage', nameZh: '橡木智者', level: 15, manaCost: 20, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 227, name: 'Cyclone Armor', nameZh: '飓风装甲', level: 15, manaCost: 15, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 229, name: 'Molten Boulder', nameZh: '熔火巨石', level: 5, manaCost: 10, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 235, name: 'Twister', nameZh: '龙卷风', level: 15, manaCost: 7, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 237, name: 'Summon Dire Wolf', nameZh: '召唤狂狼', level: 5, manaCost: 20, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 240, name: 'Tornado', nameZh: '暴风', level: 20, manaCost: 10, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 245, name: 'Hurricane', nameZh: '毁灭风暴', level: 20, manaCost: 30, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 247, name: 'Summon Grizzly', nameZh: '召唤灰熊', level: 5, manaCost: 40, leftUsable: false, rightUsable: true },
|
||||
],
|
||||
ass: [
|
||||
{ skillId: 0, name: 'Attack', nameZh: '普通攻击', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 2, name: 'Throw', nameZh: '投掷', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 3, name: 'Unsummon', nameZh: '取消召唤', level: 1, manaCost: 0, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 251, name: 'Fire Blast', nameZh: '火焰爆震', level: 15, manaCost: 3, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 254, name: 'Tiger Strike', nameZh: '虎击', level: 5, manaCost: 1, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 255, name: 'Dragon Talon', nameZh: '龙爪', level: 5, manaCost: 6, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 256, name: 'Shock Web', nameZh: '电能网', level: 15, manaCost: 6, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 258, name: 'Burst of Speed', nameZh: '速度爆发', level: 5, manaCost: 10, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 260, name: 'Dragon Claw', nameZh: '双龙爪', level: 5, manaCost: 2, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 261, name: 'Charged Bolt Sentry', nameZh: '电能守卫', level: 15, manaCost: 13, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 264, name: 'Cloak of Shadows', nameZh: '魔影斗篷', level: 1, manaCost: 13, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 267, name: 'Fade', nameZh: '能量消解', level: 5, manaCost: 10, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 271, name: 'Lightning Sentry', nameZh: '雷电守卫', level: 20, manaCost: 20, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 276, name: 'Death Sentry', nameZh: '亡者守卫', level: 20, manaCost: 20, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 279, name: 'Shadow Master', nameZh: '暗影大师', level: 5, manaCost: 35, leftUsable: false, rightUsable: true },
|
||||
],
|
||||
ama: buildClassDefaultSkills('ama'),
|
||||
sor: DEFAULT_SORCERESS_SKILLS,
|
||||
nec: buildClassDefaultSkills('nec'),
|
||||
pal: buildClassDefaultSkills('pal'),
|
||||
bar: buildClassDefaultSkills('bar'),
|
||||
dru: buildClassDefaultSkills('dru'),
|
||||
ass: buildClassDefaultSkills('ass'),
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -141,6 +141,12 @@ export interface Monster {
|
|||
corpseTicks: number
|
||||
/** Whether the monster is sleeping (inactive off-screen room). */
|
||||
sleeping?: boolean | undefined
|
||||
/** Ticks remaining frozen (cannot move or attack). */
|
||||
frozenTicks?: number | undefined
|
||||
/** Ticks remaining chilled (slowed movement). */
|
||||
chillTicks?: number | undefined
|
||||
/** Ticks remaining poisoned (damage over time). */
|
||||
poisonTicks?: number | undefined
|
||||
}
|
||||
|
||||
/** The player's combat-relevant state. */
|
||||
|
|
@ -840,6 +846,20 @@ function tickMonsters(world: CombatWorld, options: CombatOptions, terrain: Comba
|
|||
}
|
||||
if (monster.hitFlash > 0) monster.hitFlash -= 1
|
||||
if (monster.cooldown > 0) monster.cooldown -= 1
|
||||
if (monster.frozenTicks !== undefined && monster.frozenTicks > 0) {
|
||||
monster.frozenTicks -= 1
|
||||
monster.state = 'idle'
|
||||
continue
|
||||
}
|
||||
if (monster.chillTicks !== undefined && monster.chillTicks > 0) {
|
||||
monster.chillTicks -= 1
|
||||
}
|
||||
if (monster.poisonTicks !== undefined && monster.poisonTicks > 0) {
|
||||
monster.poisonTicks -= 1
|
||||
if (world.tick % 5 === 0 && monster.hp > 1) {
|
||||
monster.hp -= 1
|
||||
}
|
||||
}
|
||||
|
||||
if (options.disableMonsterAggro) {
|
||||
monster.state = 'idle'
|
||||
|
|
@ -876,7 +896,8 @@ function tickMonsters(world: CombatWorld, options: CombatOptions, terrain: Comba
|
|||
continue
|
||||
}
|
||||
monster.state = 'chase'
|
||||
const step = monster.stats.speed / 25
|
||||
const chillFactor = monster.chillTicks !== undefined && monster.chillTicks > 0 ? 0.5 : 1
|
||||
const step = (monster.stats.speed / 25) * chillFactor
|
||||
const movedMonster = moveWithCollision(monster, (dx / distance) * step, (dy / distance) * step, terrain)
|
||||
monster.x = movedMonster.x
|
||||
monster.y = movedMonster.y
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { tickCombat, createWorld, spawnMonsters, spawnMonsterPacks, damageMonste
|
|||
import type { CombatWorld, CombatOptions, MonsterPack, MonsterStats, SafeZone } from './combat.ts'
|
||||
import { Inventory, rollDrop, goldItem, totalStat } from './items.ts'
|
||||
import type { Item, ItemBase, Affix } from './items.ts'
|
||||
import { castSkill, tickProjectiles } from './skills.ts'
|
||||
import { castSkill, tickProjectiles, CANONICAL_113C_MISSILES, ISO_GROUND_ASPECT_RATIO } from './skills.ts'
|
||||
import type { SkillDef, Projectile } from './skills.ts'
|
||||
import { QuestLog, npcDialog } from './quests.ts'
|
||||
import type { NpcDef, QuestDef } from './quests.ts'
|
||||
|
|
@ -323,21 +323,77 @@ export class GameEngine {
|
|||
this.projectiles.length = 0
|
||||
this.projectiles.push(...outcome.alive)
|
||||
for (const hit of outcome.hits) {
|
||||
const targetMonster = this.world.monsters.find(m => m.index === hit.targetIndex)
|
||||
if (hit.isUndeadOnly) {
|
||||
const name = targetMonster?.stats?.name?.toLowerCase() ?? ''
|
||||
const id = targetMonster?.stats?.id?.toLowerCase() ?? ''
|
||||
const isUndead = (targetMonster as any)?.isUndead ||
|
||||
name.includes('skeleton') || name.includes('zombie') || name.includes('ghoul') || name.includes('ghost') || name.includes('wraith') ||
|
||||
id.includes('skeleton') || id.includes('zombie') || id.includes('ghoul') || id.includes('ghost') || id.includes('wraith')
|
||||
if (!isUndead) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
damageMonster(this.world, hit.targetIndex, hit.damage)
|
||||
this.metrics.castHits += 1
|
||||
|
||||
if (targetMonster) {
|
||||
if (hit.statusEffect === 'freeze') {
|
||||
targetMonster.frozenTicks = hit.statusDuration ?? 75
|
||||
} else if (hit.statusEffect === 'chill') {
|
||||
targetMonster.chillTicks = hit.statusDuration ?? 100
|
||||
} else if (hit.statusEffect === 'poison') {
|
||||
targetMonster.poisonTicks = hit.statusDuration ?? 100
|
||||
}
|
||||
}
|
||||
|
||||
// AoE splash damage
|
||||
if (hit.aoeRadius && hit.aoeRadius > 0) {
|
||||
for (const m of this.world.monsters) {
|
||||
if (m.index === hit.targetIndex || m.state === 'dead' || m.hp <= 0) continue
|
||||
if (hit.isUndeadOnly) {
|
||||
const name = m?.stats?.name?.toLowerCase() ?? ''
|
||||
const id = m?.stats?.id?.toLowerCase() ?? ''
|
||||
const isUndead = (m as any)?.isUndead ||
|
||||
name.includes('skeleton') || name.includes('zombie') || name.includes('ghoul') || name.includes('ghost') || name.includes('wraith') ||
|
||||
id.includes('skeleton') || id.includes('zombie') || id.includes('ghoul') || id.includes('ghost') || id.includes('wraith')
|
||||
if (!isUndead) continue
|
||||
}
|
||||
// Ground distance in 2:1 isometric projection:
|
||||
// dx^2 + (dy / ISO_GROUND_ASPECT_RATIO)^2 <= aoeRadius^2
|
||||
const dx = m.x - hit.x
|
||||
const dy = (m.y - hit.y) / ISO_GROUND_ASPECT_RATIO
|
||||
if (Math.hypot(dx, dy) <= hit.aoeRadius) {
|
||||
damageMonster(this.world, m.index, hit.damage)
|
||||
if (hit.statusEffect === 'freeze') {
|
||||
m.frozenTicks = hit.statusDuration ?? 75
|
||||
} else if (hit.statusEffect === 'chill') {
|
||||
m.chillTicks = hit.statusDuration ?? 100
|
||||
} else if (hit.statusEffect === 'poison') {
|
||||
m.poisonTicks = hit.statusDuration ?? 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const missileType = hit.missileType ?? (hit.skillId === '36' || hit.skillId === 'firebolt' ? 'firebolt' : undefined)
|
||||
if (missileType === 'firebolt' || hit.skillId === '36' || hit.skillId === 'firebolt') {
|
||||
const mslData = missileType ? CANONICAL_113C_MISSILES[missileType] : undefined
|
||||
const explType = mslData?.explosionMissile || (missileType === 'firebolt' ? 'fireexplode' : '')
|
||||
if (explType) {
|
||||
const explData = CANONICAL_113C_MISSILES[explType]
|
||||
const maxFrames = explData?.animLen || 12
|
||||
this.explosions.push({
|
||||
missileType: 'fireexplode',
|
||||
missileType: explType,
|
||||
x: hit.x,
|
||||
y: hit.y,
|
||||
frame: 0,
|
||||
maxFrames: 12,
|
||||
radius: 24,
|
||||
maxFrames,
|
||||
radius: hit.aoeRadius ?? 24,
|
||||
})
|
||||
this.world.events.push({
|
||||
kind: 'missileExplode',
|
||||
missileType: 'fireexplode',
|
||||
missileType: explType,
|
||||
skillId: hit.skillId,
|
||||
x: hit.x,
|
||||
y: hit.y,
|
||||
|
|
@ -345,21 +401,34 @@ export class GameEngine {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (outcome.wallHitEvents) {
|
||||
for (const wallHit of outcome.wallHitEvents) {
|
||||
if (wallHit.aoeRadius && wallHit.aoeRadius > 0) {
|
||||
for (const m of this.world.monsters) {
|
||||
if (m.state === 'dead' || m.hp <= 0) continue
|
||||
if (Math.hypot(m.x - wallHit.x, m.y - wallHit.y) <= wallHit.aoeRadius) {
|
||||
damageMonster(this.world, m.index, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
const missileType = wallHit.missileType ?? (wallHit.skillId === '36' || wallHit.skillId === 'firebolt' ? 'firebolt' : undefined)
|
||||
if (missileType === 'firebolt' || wallHit.skillId === '36' || wallHit.skillId === 'firebolt') {
|
||||
const mslData = missileType ? CANONICAL_113C_MISSILES[missileType] : undefined
|
||||
const explType = mslData?.explosionMissile || (missileType === 'firebolt' ? 'fireexplode' : '')
|
||||
if (explType) {
|
||||
const explData = CANONICAL_113C_MISSILES[explType]
|
||||
const maxFrames = explData?.animLen || 12
|
||||
this.explosions.push({
|
||||
missileType: 'fireexplode',
|
||||
missileType: explType,
|
||||
x: wallHit.x,
|
||||
y: wallHit.y,
|
||||
frame: 0,
|
||||
maxFrames: 12,
|
||||
radius: 24,
|
||||
maxFrames,
|
||||
radius: wallHit.aoeRadius ?? 24,
|
||||
})
|
||||
this.world.events.push({
|
||||
kind: 'missileExplode',
|
||||
missileType: 'fireexplode',
|
||||
missileType: explType,
|
||||
skillId: wallHit.skillId,
|
||||
x: wallHit.x,
|
||||
y: wallHit.y,
|
||||
|
|
@ -605,7 +674,7 @@ export class GameEngine {
|
|||
}
|
||||
const amount = entity.amount || 1
|
||||
this.groundItems.remove(id)
|
||||
const gIdx = this.ground.findIndex(g => Math.hypot(g.x - entity.x, g.y - entity.y) <= 16)
|
||||
const gIdx = this.ground.findIndex(g => Math.hypot(g.x - entity.x, g.y - entity.y) <= 32 || (g.item as any)?.isGold)
|
||||
if (gIdx !== -1) {
|
||||
this.ground.splice(gIdx, 1)
|
||||
}
|
||||
|
|
@ -639,7 +708,7 @@ export class GameEngine {
|
|||
}
|
||||
|
||||
this.groundItems.remove(id)
|
||||
const gIdx = this.ground.findIndex(g => Math.hypot(g.x - entity.x, g.y - entity.y) <= 16)
|
||||
const gIdx = this.ground.findIndex(g => Math.hypot(g.x - entity.x, g.y - entity.y) <= 32 || g.item === entity.item)
|
||||
if (gIdx !== -1) {
|
||||
this.ground.splice(gIdx, 1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -285,18 +285,19 @@ export class MissileEngine {
|
|||
startX: m.x,
|
||||
startY: m.y,
|
||||
targetX: m.x + Math.cos(subAngle) * 100,
|
||||
targetY: m.y + Math.sin(subAngle) * 100,
|
||||
targetY: m.y + Math.sin(subAngle) * (100 * 0.5),
|
||||
dmgPacket: m.dmgPacket,
|
||||
})
|
||||
subMissilesSpawned.push(subName)
|
||||
}
|
||||
|
||||
// Spiral flight for Blessed Hammer (`blessedhammer`, `skillId = 112`)
|
||||
// In 2:1 isometric ground perspective, vertical expansion is foreshortened by 0.5.
|
||||
if (m.sourceSkillId === 112 || m.name.toLowerCase() === 'blessedhammer') {
|
||||
m.angleRad += 0.24
|
||||
const radius = 14 + m.ageTicks * 4
|
||||
m.x += Math.cos(m.angleRad) * (radius * 0.25)
|
||||
m.y += Math.sin(m.angleRad) * (radius * 0.25)
|
||||
m.y += Math.sin(m.angleRad) * (radius * 0.25 * 0.5)
|
||||
} else {
|
||||
m.x += m.vx
|
||||
m.y += m.vy
|
||||
|
|
|
|||
|
|
@ -72,8 +72,43 @@ export interface Projectile {
|
|||
readonly fromPlayer: boolean
|
||||
/** Optional missile type / token (e.g. 'firebolt'). */
|
||||
readonly missileType?: string | undefined
|
||||
/** Pierces through enemies without expiring until range or wall hit. */
|
||||
readonly pierce?: boolean | undefined
|
||||
/** Set of monster indices already hit by this projectile (for pierce/nextHit). */
|
||||
readonly hitTargets?: Set<number> | undefined
|
||||
/** Area of effect splash radius in pixels upon impact. */
|
||||
readonly aoeRadius?: number | undefined
|
||||
/** Elemental status effect applied on hit: chill, freeze, poison. */
|
||||
readonly statusEffect?: 'chill' | 'freeze' | 'poison' | undefined
|
||||
/** Duration in ticks of the status effect. */
|
||||
readonly statusDuration?: number | undefined
|
||||
/** Homing missile: steers towards nearest alive monster. */
|
||||
readonly homing?: boolean | undefined
|
||||
/** Archimedean spiral trajectory (e.g. Blessed Hammer). */
|
||||
readonly spiral?: {
|
||||
readonly originX: number
|
||||
readonly originY: number
|
||||
readonly angle: number
|
||||
readonly radius: number
|
||||
} | undefined
|
||||
/** Random perpendicular trajectory wobble (e.g. Charged Bolt). */
|
||||
readonly jitter?: boolean | undefined
|
||||
/** Target restriction: only hits undead monsters. */
|
||||
readonly isUndeadOnly?: boolean | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* In Diablo II's 2:1 isometric projection (orthogonal diamond grid: 80x40 cells, 16x8 sub-tiles),
|
||||
* any horizontal circular curve on the map ground plane projects into scene-pixel coordinates
|
||||
* as an ellipse with a 2:1 aspect ratio (vertical scale / horizontal scale = 8 / 16 = 0.5).
|
||||
*
|
||||
* NOTE: The calculated coordinates (x, y) are physical MAP coordinates on the level plane,
|
||||
* NOT the rendered sprite icon offsets. Rendered icons are pre-rendered 2.5D DCC billboards
|
||||
* positioned via sprite frame anchors, while map coordinates govern physical trajectory and collision.
|
||||
*/
|
||||
export const ISO_GROUND_ASPECT_RATIO = 0.5
|
||||
|
||||
|
||||
/** What a cast produced. */
|
||||
export type CastResult =
|
||||
| { readonly kind: 'mana'; readonly cost: number }
|
||||
|
|
@ -144,6 +179,26 @@ export const CANONICAL_113C_MISSILES: Readonly<Record<string, MissileTxtData>> =
|
|||
speedPxPerSec: 500,
|
||||
distancePx: 1000,
|
||||
},
|
||||
fireball: {
|
||||
name: 'fireball',
|
||||
id: 62,
|
||||
vel: 20,
|
||||
maxVel: 20,
|
||||
range: 50,
|
||||
levRange: 0,
|
||||
celFile: 'Fireball',
|
||||
animLen: 5,
|
||||
animSpeed: 16,
|
||||
loopAnim: 1,
|
||||
numDirections: 16,
|
||||
explosionMissile: 'fireexplode',
|
||||
light: 7,
|
||||
red: 255,
|
||||
green: 178,
|
||||
blue: 64,
|
||||
speedPxPerSec: 500,
|
||||
distancePx: 1000,
|
||||
},
|
||||
fireexplode: {
|
||||
name: 'fireexplode',
|
||||
id: 29,
|
||||
|
|
@ -164,6 +219,466 @@ export const CANONICAL_113C_MISSILES: Readonly<Record<string, MissileTxtData>> =
|
|||
speedPxPerSec: 0,
|
||||
distancePx: 0,
|
||||
},
|
||||
explodingarrowexp: {
|
||||
name: 'explodingarrowexp',
|
||||
id: 47,
|
||||
vel: 0,
|
||||
maxVel: 0,
|
||||
range: 12,
|
||||
levRange: 0,
|
||||
celFile: 'FireArrowExplode2',
|
||||
animLen: 12,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 1,
|
||||
explosionMissile: '',
|
||||
light: 13,
|
||||
red: 255,
|
||||
green: 178,
|
||||
blue: 64,
|
||||
speedPxPerSec: 0,
|
||||
distancePx: 0,
|
||||
},
|
||||
icebolt: {
|
||||
name: 'icebolt',
|
||||
id: 59,
|
||||
vel: 12,
|
||||
maxVel: 12,
|
||||
range: 50,
|
||||
levRange: 0,
|
||||
celFile: 'Icebolt',
|
||||
animLen: 6,
|
||||
animSpeed: 16,
|
||||
loopAnim: 1,
|
||||
numDirections: 16,
|
||||
explosionMissile: 'iceexplode',
|
||||
light: 7,
|
||||
red: 81,
|
||||
green: 81,
|
||||
blue: 255,
|
||||
speedPxPerSec: 300,
|
||||
distancePx: 600,
|
||||
},
|
||||
iceblast: {
|
||||
name: 'iceblast',
|
||||
id: 91,
|
||||
vel: 12,
|
||||
maxVel: 12,
|
||||
range: 50,
|
||||
levRange: 0,
|
||||
celFile: 'IceBlast',
|
||||
animLen: 5,
|
||||
animSpeed: 16,
|
||||
loopAnim: 1,
|
||||
numDirections: 16,
|
||||
explosionMissile: 'freezeexplode',
|
||||
light: 7,
|
||||
red: 81,
|
||||
green: 81,
|
||||
blue: 255,
|
||||
speedPxPerSec: 300,
|
||||
distancePx: 600,
|
||||
},
|
||||
glacialspike: {
|
||||
name: 'glacialspike',
|
||||
id: 96,
|
||||
vel: 16,
|
||||
maxVel: 16,
|
||||
range: 40,
|
||||
levRange: 0,
|
||||
celFile: 'GlacialSpike',
|
||||
animLen: 6,
|
||||
animSpeed: 16,
|
||||
loopAnim: 1,
|
||||
numDirections: 16,
|
||||
explosionMissile: 'freezeexplode',
|
||||
light: 5,
|
||||
red: 81,
|
||||
green: 81,
|
||||
blue: 255,
|
||||
speedPxPerSec: 400,
|
||||
distancePx: 640,
|
||||
},
|
||||
iceexplode: {
|
||||
name: 'iceexplode',
|
||||
id: 30,
|
||||
vel: 0,
|
||||
maxVel: 0,
|
||||
range: 16,
|
||||
levRange: 0,
|
||||
celFile: 'IceArrowExplode',
|
||||
animLen: 16,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 1,
|
||||
explosionMissile: '',
|
||||
light: 11,
|
||||
red: 81,
|
||||
green: 81,
|
||||
blue: 255,
|
||||
speedPxPerSec: 0,
|
||||
distancePx: 0,
|
||||
},
|
||||
freezeexplode: {
|
||||
name: 'freezeexplode',
|
||||
id: 88,
|
||||
vel: 0,
|
||||
maxVel: 0,
|
||||
range: 15,
|
||||
levRange: 0,
|
||||
celFile: 'FreezeExplodeCenter',
|
||||
animLen: 15,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 1,
|
||||
explosionMissile: '',
|
||||
light: 11,
|
||||
red: 81,
|
||||
green: 81,
|
||||
blue: 255,
|
||||
speedPxPerSec: 0,
|
||||
distancePx: 0,
|
||||
},
|
||||
freezingarrowexp1: {
|
||||
name: 'freezingarrowexp1',
|
||||
id: 88,
|
||||
vel: 0,
|
||||
maxVel: 0,
|
||||
range: 15,
|
||||
levRange: 0,
|
||||
celFile: 'FreezeExplodeCenter',
|
||||
animLen: 15,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 1,
|
||||
explosionMissile: '',
|
||||
light: 11,
|
||||
red: 81,
|
||||
green: 81,
|
||||
blue: 255,
|
||||
speedPxPerSec: 0,
|
||||
distancePx: 0,
|
||||
},
|
||||
chargedbolt: {
|
||||
name: 'chargedbolt',
|
||||
id: 56,
|
||||
vel: 12,
|
||||
maxVel: 12,
|
||||
range: 98,
|
||||
levRange: 0,
|
||||
celFile: 'ChargedBolt',
|
||||
animLen: 10,
|
||||
animSpeed: 16,
|
||||
loopAnim: 1,
|
||||
numDirections: 16,
|
||||
explosionMissile: '',
|
||||
light: 3,
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255,
|
||||
speedPxPerSec: 300,
|
||||
distancePx: 1176,
|
||||
},
|
||||
teeth: {
|
||||
name: 'teeth',
|
||||
id: 114,
|
||||
vel: 16,
|
||||
maxVel: 16,
|
||||
range: 50,
|
||||
levRange: 0,
|
||||
celFile: 'teethMissile',
|
||||
animLen: 30,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 32,
|
||||
explosionMissile: 'teethexplode',
|
||||
light: 3,
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255,
|
||||
speedPxPerSec: 400,
|
||||
distancePx: 800,
|
||||
},
|
||||
teethexplode: {
|
||||
name: 'teethexplode',
|
||||
id: 204,
|
||||
vel: 0,
|
||||
maxVel: 0,
|
||||
range: 13,
|
||||
levRange: 0,
|
||||
celFile: 'teethexplode',
|
||||
animLen: 13,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 1,
|
||||
explosionMissile: '',
|
||||
light: 0,
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255,
|
||||
speedPxPerSec: 0,
|
||||
distancePx: 0,
|
||||
},
|
||||
bonespear: {
|
||||
name: 'bonespear',
|
||||
id: 192,
|
||||
vel: 24,
|
||||
maxVel: 24,
|
||||
range: 40,
|
||||
levRange: 0,
|
||||
celFile: 'BoneSpear',
|
||||
animLen: 6,
|
||||
animSpeed: 16,
|
||||
loopAnim: 1,
|
||||
numDirections: 16,
|
||||
explosionMissile: 'teethexplode',
|
||||
light: 3,
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255,
|
||||
speedPxPerSec: 600,
|
||||
distancePx: 960,
|
||||
},
|
||||
bonespearexplode: {
|
||||
name: 'bonespearexplode',
|
||||
id: 216,
|
||||
vel: 0,
|
||||
maxVel: 0,
|
||||
range: 13,
|
||||
levRange: 0,
|
||||
celFile: 'teethexplode',
|
||||
animLen: 13,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 1,
|
||||
explosionMissile: '',
|
||||
light: 0,
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255,
|
||||
speedPxPerSec: 0,
|
||||
distancePx: 0,
|
||||
},
|
||||
bonespirit: {
|
||||
name: 'bonespirit',
|
||||
id: 193,
|
||||
vel: 12,
|
||||
maxVel: 12,
|
||||
range: 128,
|
||||
levRange: 0,
|
||||
celFile: 'BoneSpirit',
|
||||
animLen: 8,
|
||||
animSpeed: 16,
|
||||
loopAnim: 1,
|
||||
numDirections: 8,
|
||||
explosionMissile: 'bonespiritexplode',
|
||||
light: 3,
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255,
|
||||
speedPxPerSec: 300,
|
||||
distancePx: 1536,
|
||||
},
|
||||
bonespiritexplode: {
|
||||
name: 'bonespiritexplode',
|
||||
id: 327,
|
||||
vel: 0,
|
||||
maxVel: 0,
|
||||
range: 13,
|
||||
levRange: 0,
|
||||
celFile: 'BoneSpiritExplode',
|
||||
animLen: 13,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 1,
|
||||
explosionMissile: '',
|
||||
light: 0,
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255,
|
||||
speedPxPerSec: 0,
|
||||
distancePx: 0,
|
||||
},
|
||||
poisonnova: {
|
||||
name: 'poisonnova',
|
||||
id: 118,
|
||||
vel: 12,
|
||||
maxVel: 12,
|
||||
range: 30,
|
||||
levRange: 0,
|
||||
celFile: 'poisonNova',
|
||||
animLen: 30,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 32,
|
||||
explosionMissile: '',
|
||||
light: 0,
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255,
|
||||
speedPxPerSec: 300,
|
||||
distancePx: 360,
|
||||
},
|
||||
holybolt: {
|
||||
name: 'holybolt',
|
||||
id: 55,
|
||||
vel: 20,
|
||||
maxVel: 20,
|
||||
range: 50,
|
||||
levRange: 0,
|
||||
celFile: 'HolyBoltMissile',
|
||||
animLen: 16,
|
||||
animSpeed: 16,
|
||||
loopAnim: 1,
|
||||
numDirections: 1,
|
||||
explosionMissile: '',
|
||||
light: 7,
|
||||
red: 222,
|
||||
green: 222,
|
||||
blue: 255,
|
||||
speedPxPerSec: 500,
|
||||
distancePx: 1000,
|
||||
},
|
||||
blessedhammer: {
|
||||
name: 'blessedhammer',
|
||||
id: 92,
|
||||
vel: 18,
|
||||
maxVel: 30,
|
||||
range: 120,
|
||||
levRange: 0,
|
||||
celFile: 'blessedhammer',
|
||||
animLen: 6,
|
||||
animSpeed: 16,
|
||||
loopAnim: 1,
|
||||
numDirections: 16,
|
||||
explosionMissile: '',
|
||||
light: 5,
|
||||
red: 222,
|
||||
green: 222,
|
||||
blue: 255,
|
||||
speedPxPerSec: 450,
|
||||
distancePx: 2160,
|
||||
},
|
||||
magicarrow: {
|
||||
name: 'magicarrow',
|
||||
id: 27,
|
||||
vel: 24,
|
||||
maxVel: 24,
|
||||
range: 40,
|
||||
levRange: 0,
|
||||
celFile: 'SafeArrow',
|
||||
animLen: 1,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 32,
|
||||
explosionMissile: 'teethexplode',
|
||||
light: 0,
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255,
|
||||
speedPxPerSec: 600,
|
||||
distancePx: 960,
|
||||
},
|
||||
firearrow: {
|
||||
name: 'firearrow',
|
||||
id: 12,
|
||||
vel: 24,
|
||||
maxVel: 24,
|
||||
range: 40,
|
||||
levRange: 0,
|
||||
celFile: 'FireArrow',
|
||||
animLen: 8,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 32,
|
||||
explosionMissile: 'fireexplode',
|
||||
light: 6,
|
||||
red: 255,
|
||||
green: 178,
|
||||
blue: 64,
|
||||
speedPxPerSec: 600,
|
||||
distancePx: 960,
|
||||
},
|
||||
icearrow: {
|
||||
name: 'icearrow',
|
||||
id: 28,
|
||||
vel: 24,
|
||||
maxVel: 24,
|
||||
range: 40,
|
||||
levRange: 0,
|
||||
celFile: 'IceArrow',
|
||||
animLen: 8,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 32,
|
||||
explosionMissile: 'iceexplode',
|
||||
light: 6,
|
||||
red: 81,
|
||||
green: 81,
|
||||
blue: 255,
|
||||
speedPxPerSec: 600,
|
||||
distancePx: 960,
|
||||
},
|
||||
arrow: {
|
||||
name: 'arrow',
|
||||
id: 0,
|
||||
vel: 24,
|
||||
maxVel: 24,
|
||||
range: 40,
|
||||
levRange: 0,
|
||||
celFile: 'Arrow',
|
||||
animLen: 1,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 32,
|
||||
explosionMissile: '',
|
||||
light: 0,
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255,
|
||||
speedPxPerSec: 600,
|
||||
distancePx: 960,
|
||||
},
|
||||
multipleshotarrow: {
|
||||
name: 'multipleshotarrow',
|
||||
id: 213,
|
||||
vel: 24,
|
||||
maxVel: 24,
|
||||
range: 50,
|
||||
levRange: 0,
|
||||
celFile: 'Arrow',
|
||||
animLen: 1,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
numDirections: 32,
|
||||
explosionMissile: '',
|
||||
light: 0,
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255,
|
||||
speedPxPerSec: 600,
|
||||
distancePx: 1200,
|
||||
},
|
||||
lightningjavelin: {
|
||||
name: 'lightningjavelin',
|
||||
id: 205,
|
||||
vel: 30,
|
||||
maxVel: 30,
|
||||
range: 25,
|
||||
levRange: 0,
|
||||
celFile: 'LightningJavelin',
|
||||
animLen: 5,
|
||||
animSpeed: 16,
|
||||
loopAnim: 1,
|
||||
numDirections: 16,
|
||||
explosionMissile: '',
|
||||
light: 6,
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255,
|
||||
speedPxPerSec: 750,
|
||||
distancePx: 750,
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -543,6 +1058,10 @@ export interface ProjectileOutcome {
|
|||
readonly y: number
|
||||
readonly skillId?: string | undefined
|
||||
readonly missileType?: string | undefined
|
||||
readonly aoeRadius?: number | undefined
|
||||
readonly statusEffect?: 'chill' | 'freeze' | 'poison' | undefined
|
||||
readonly statusDuration?: number | undefined
|
||||
readonly isUndeadOnly?: boolean | undefined
|
||||
}[]
|
||||
/** Projectiles that died on a wall or expired. */
|
||||
readonly expired: number
|
||||
|
|
@ -554,6 +1073,7 @@ export interface ProjectileOutcome {
|
|||
readonly y: number
|
||||
readonly skillId?: string | undefined
|
||||
readonly missileType?: string | undefined
|
||||
readonly aoeRadius?: number | undefined
|
||||
}[] | undefined
|
||||
}
|
||||
|
||||
|
|
@ -583,23 +1103,97 @@ export function tickProjectiles(
|
|||
y: number
|
||||
skillId?: string | undefined
|
||||
missileType?: string | undefined
|
||||
aoeRadius?: number | undefined
|
||||
statusEffect?: 'chill' | 'freeze' | 'poison' | undefined
|
||||
statusDuration?: number | undefined
|
||||
isUndeadOnly?: boolean | undefined
|
||||
}[] = []
|
||||
const wallHitEvents: {
|
||||
x: number
|
||||
y: number
|
||||
skillId?: string | undefined
|
||||
missileType?: string | undefined
|
||||
aoeRadius?: number | undefined
|
||||
}[] = []
|
||||
let expired = 0
|
||||
let wallHits = 0
|
||||
|
||||
for (const projectile of projectiles) {
|
||||
if (projectile.ttl <= 0) { expired += 1; continue }
|
||||
const x = projectile.x + projectile.vx
|
||||
const y = projectile.y + projectile.vy
|
||||
const moved: Projectile = { ...projectile, x, y, ttl: projectile.ttl - 1 }
|
||||
|
||||
let vx = projectile.vx
|
||||
let vy = projectile.vy
|
||||
let x = projectile.x + vx
|
||||
let y = projectile.y + vy
|
||||
let spiral = projectile.spiral
|
||||
|
||||
// 1. Archimedean spiral trajectory (e.g. Blessed Hammer)
|
||||
// The hammer traces an Archimedean spiral on the map ground plane. In oblique/isometric
|
||||
// perspective (2:1 projection), a circular ground expansion projects to an ellipse with a 2:1 aspect ratio.
|
||||
if (spiral !== undefined) {
|
||||
const nextAngle = spiral.angle + 0.22
|
||||
const nextRadius = spiral.radius + 3.2
|
||||
spiral = { ...spiral, angle: nextAngle, radius: nextRadius }
|
||||
x = spiral.originX + Math.cos(nextAngle) * nextRadius
|
||||
y = spiral.originY + Math.sin(nextAngle) * (nextRadius * ISO_GROUND_ASPECT_RATIO)
|
||||
vx = -Math.sin(nextAngle) * 18
|
||||
vy = Math.cos(nextAngle) * (18 * ISO_GROUND_ASPECT_RATIO)
|
||||
} else if (projectile.homing) {
|
||||
// 2. Homing trajectory (e.g. Bone Spirit) - smoothly steer towards closest living enemy
|
||||
let nearestDist = Number.POSITIVE_INFINITY
|
||||
let targetX = 0
|
||||
let targetY = 0
|
||||
let foundTarget = false
|
||||
for (const t of targets) {
|
||||
if (!t.alive) continue
|
||||
const d = Math.hypot(t.x - projectile.x, t.y - projectile.y)
|
||||
if (d < nearestDist && d > 4) {
|
||||
nearestDist = d
|
||||
targetX = t.x
|
||||
targetY = t.y
|
||||
foundTarget = true
|
||||
}
|
||||
}
|
||||
if (foundTarget) {
|
||||
const curAngle = Math.atan2(vy, vx)
|
||||
const targetAngle = Math.atan2(targetY - projectile.y, targetX - projectile.x)
|
||||
let diff = targetAngle - curAngle
|
||||
while (diff > Math.PI) diff -= 2 * Math.PI
|
||||
while (diff < -Math.PI) diff += 2 * Math.PI
|
||||
const maxTurn = 0.14
|
||||
const turn = Math.max(-maxTurn, Math.min(maxTurn, diff))
|
||||
const newAngle = curAngle + turn
|
||||
const speed = Math.hypot(vx, vy)
|
||||
vx = Math.cos(newAngle) * speed
|
||||
vy = Math.sin(newAngle) * speed
|
||||
x = projectile.x + vx
|
||||
y = projectile.y + vy
|
||||
}
|
||||
} else if (projectile.jitter) {
|
||||
// 3. Jitter trajectory (e.g. Charged Bolt) - sinusoidal perpendicular wiggle on the ground plane
|
||||
const perpX = -vy
|
||||
const perpY = vx
|
||||
const speed = Math.hypot(vx, vy)
|
||||
if (speed > 0) {
|
||||
const wobble = Math.sin(projectile.ttl * 0.8) * 1.5
|
||||
x += (perpX / speed) * wobble
|
||||
y += (perpY / speed) * (wobble * ISO_GROUND_ASPECT_RATIO)
|
||||
}
|
||||
}
|
||||
|
||||
const hitTargets = projectile.hitTargets ?? new Set<number>()
|
||||
const moved: Projectile = {
|
||||
...projectile,
|
||||
x,
|
||||
y,
|
||||
vx,
|
||||
vy,
|
||||
spiral,
|
||||
hitTargets,
|
||||
ttl: projectile.ttl - 1,
|
||||
}
|
||||
|
||||
// Walls first: a projectile dies against missile barriers (solid walls, closed doors)
|
||||
// while passing freely over ground-only barriers (rivers, lava, chasms) via COLLIDE_MASK_MISSILE.
|
||||
const blockedByTerrain =
|
||||
options.isMissileBlocked !== undefined
|
||||
? options.isMissileBlocked(x, y, projectile.x, projectile.y)
|
||||
|
|
@ -611,13 +1205,17 @@ export function tickProjectiles(
|
|||
y,
|
||||
skillId: projectile.skillId,
|
||||
missileType: projectile.missileType,
|
||||
aoeRadius: projectile.aoeRadius,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
let struck = false
|
||||
for (const target of targets) {
|
||||
if (!target.alive) continue
|
||||
if (hitTargets.has(target.index)) continue // already struck by piercing projectile
|
||||
if (Math.hypot(target.x - x, target.y - y) > target.radius + radius) continue
|
||||
|
||||
hits.push({
|
||||
targetIndex: target.index,
|
||||
damage: projectile.damage,
|
||||
|
|
@ -625,11 +1223,22 @@ export function tickProjectiles(
|
|||
y,
|
||||
skillId: projectile.skillId,
|
||||
missileType: projectile.missileType,
|
||||
aoeRadius: projectile.aoeRadius,
|
||||
statusEffect: projectile.statusEffect,
|
||||
statusDuration: projectile.statusDuration,
|
||||
isUndeadOnly: projectile.isUndeadOnly,
|
||||
})
|
||||
struck = true
|
||||
break
|
||||
|
||||
hitTargets.add(target.index)
|
||||
if (!projectile.pierce) {
|
||||
struck = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!struck) {
|
||||
alive.push(moved)
|
||||
}
|
||||
if (!struck) alive.push(moved)
|
||||
}
|
||||
return { alive, hits, expired, wallHits, wallHitEvents }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,12 @@ export interface DrawOptions {
|
|||
* - 4: Champion / Elite Blue Tint
|
||||
*/
|
||||
readonly paletteRow?: number
|
||||
/** Destination width in pixels (defaults to frame.width). */
|
||||
readonly width?: number
|
||||
/** Destination height in pixels (defaults to frame.height). */
|
||||
readonly height?: number
|
||||
/** Blend mode for rendering: 'normal' (alpha blend) or 'additive' (translucent spell glow). */
|
||||
readonly blendMode?: 'normal' | 'additive'
|
||||
}
|
||||
|
||||
/** Configuration options and event callbacks for SpriteRenderer. */
|
||||
|
|
@ -252,6 +258,8 @@ export class SpriteRenderer {
|
|||
private frameDrawCalls = 0
|
||||
/** Quads submitted since the last {@link begin}. */
|
||||
private frameQuads = 0
|
||||
/** Current active blend mode ('normal' or 'additive'). */
|
||||
private currentBlendMode: 'normal' | 'additive' = 'normal'
|
||||
|
||||
/** All textures allocated and managed by this renderer. */
|
||||
private readonly allocatedTextures = new Set<WebGLTexture>()
|
||||
|
|
@ -757,6 +765,10 @@ export class SpriteRenderer {
|
|||
this.batchTextureCount = 0
|
||||
this.frameDrawCalls = 0
|
||||
this.frameQuads = 0
|
||||
if (this.currentBlendMode !== 'normal') {
|
||||
this.currentBlendMode = 'normal'
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
}
|
||||
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||||
gl.clearColor(clear[0], clear[1], clear[2], 1)
|
||||
gl.clear(gl.COLOR_BUFFER_BIT)
|
||||
|
|
@ -772,6 +784,17 @@ export class SpriteRenderer {
|
|||
*/
|
||||
draw(frame: AtlasFrame, x: number, y: number, options: DrawOptions = {}): void {
|
||||
if (this.disposed) return
|
||||
const blendMode = options.blendMode ?? 'normal'
|
||||
if (blendMode !== this.currentBlendMode) {
|
||||
this.flush()
|
||||
this.currentBlendMode = blendMode
|
||||
const gl = this.gl
|
||||
if (blendMode === 'additive') {
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE)
|
||||
} else {
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
}
|
||||
}
|
||||
const page = options.atlas ?? this.defaultAtlas
|
||||
const unit = this.unitFor(page)
|
||||
const u0 = frame.x / page.width
|
||||
|
|
@ -781,8 +804,10 @@ export class SpriteRenderer {
|
|||
const tint = options.tint
|
||||
const flipped = options.flipX === true
|
||||
const paletteRow = page.indexed === true ? (options.paletteRow ?? 0) : -1
|
||||
const drawW = options.width ?? frame.width
|
||||
const drawH = options.height ?? frame.height
|
||||
this.quad(
|
||||
x, y, x + frame.width, y + frame.height,
|
||||
x, y, x + drawW, y + drawH,
|
||||
flipped ? u1 : u0, v0,
|
||||
flipped ? u0 : u1, v1,
|
||||
tint === undefined ? 1 : tint[0],
|
||||
|
|
@ -805,6 +830,11 @@ export class SpriteRenderer {
|
|||
*/
|
||||
drawSolid(x: number, y: number, width: number, height: number, color: readonly [number, number, number, number]): void {
|
||||
if (this.disposed) return
|
||||
if (this.currentBlendMode !== 'normal') {
|
||||
this.flush()
|
||||
this.currentBlendMode = 'normal'
|
||||
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA)
|
||||
}
|
||||
const unit = this.unitFor(this.whiteAtlas)
|
||||
this.quad(x, y, x + width, y + height, 0.5, 0.5, 0.5, 0.5, color[0], color[1], color[2], color[3], unit, -1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import { getEmbeddedDropTables } from '../game/embedded-drop-tables.ts'
|
|||
import { buildNpcDef, hasPackedSprite } from '../game/npc.ts'
|
||||
import { npcDialog } from '../game/quests.ts'
|
||||
import type { NpcDef } from '../game/quests.ts'
|
||||
import { BATCH1_SKILLS, getBatch1SkillDef, isBatch1Skill, getSkillManaCost, getMissileTxtData } from '../game/skills.ts'
|
||||
import { BATCH1_SKILLS, getBatch1SkillDef, isBatch1Skill, getSkillManaCost, getMissileTxtData, ISO_GROUND_ASPECT_RATIO } from '../game/skills.ts'
|
||||
import type { Projectile } from '../game/skills.ts'
|
||||
import { findSafeDropPosition, calculateBounceHeight, triggerFlippyBounce, type GroundItemEntity } from '../game/ground-items.ts'
|
||||
import {
|
||||
|
|
@ -53,6 +53,8 @@ import {
|
|||
computeInitialGroundLabelLayouts,
|
||||
resolveLadderCollisions,
|
||||
} from '../ui/ground-labels.ts'
|
||||
import { BAKED_UI_MANIFEST } from '../ui/baked-ui-meta.ts'
|
||||
import { resolveItemSpriteRect } from '../ui/inventory.ts'
|
||||
import type { AtlasFrame } from '../render/atlas.ts'
|
||||
import { SpriteRenderer } from '../render/renderer.ts'
|
||||
import type { AtlasHandle } from '../render/renderer.ts'
|
||||
|
|
@ -60,8 +62,7 @@ import { buildBounds, intersects, viewportRect } from '../render/cull.ts'
|
|||
import { RoofFadeController } from '../render/roofs.ts'
|
||||
import { facingToDirection } from '../game/character.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 { velocityToDccDirection, MISSILE_METAS, type MissileMeta, type MissileFrameTuple } from '../render/missiles-meta.ts'
|
||||
import { ActorAnimator, type AnimClipMeta } from '../game/actor-animator.ts'
|
||||
import { resolveMonsterArtSpec } from '../game/monster-mapping.ts'
|
||||
import { ACT_NAMES_ZH, LEVEL_NAMES_ZH, getLevelNames, sceneNameZh, variantLabelZh } from '../game/level-names-zh.ts'
|
||||
|
|
@ -997,8 +998,28 @@ export function getSkillRange(
|
|||
const info = getAttackWeaponInfo(weapon)
|
||||
return info.reach
|
||||
}
|
||||
if (skillId === 36) {
|
||||
const missile = getMissileTxtData('firebolt')
|
||||
const missileSkillMap: Record<number, string> = {
|
||||
36: 'firebolt',
|
||||
39: 'icebolt',
|
||||
45: 'iceblast',
|
||||
55: 'glacialspike',
|
||||
38: 'chargedbolt',
|
||||
67: 'teeth',
|
||||
84: 'bonespear',
|
||||
93: 'bonespirit',
|
||||
92: 'poisonnova',
|
||||
101: 'holybolt',
|
||||
112: 'blessedhammer',
|
||||
6: 'magicarrow',
|
||||
7: 'firearrow',
|
||||
11: 'icearrow',
|
||||
21: 'icearrow',
|
||||
12: 'arrow',
|
||||
16: 'firearrow',
|
||||
20: 'lightningjavelin',
|
||||
}
|
||||
if (skillId in missileSkillMap) {
|
||||
const missile = getMissileTxtData(missileSkillMap[skillId]!)
|
||||
return missile.distancePx || 1000
|
||||
}
|
||||
const def = getBatch1SkillDef(skillId)
|
||||
|
|
@ -1010,7 +1031,6 @@ export function getSkillRange(
|
|||
case 40: return 0
|
||||
case 42: return 160
|
||||
case 44: return 240
|
||||
case 47: return 450
|
||||
case 54: return 450
|
||||
case 56: return 450
|
||||
case 59: return 450
|
||||
|
|
@ -1029,6 +1049,15 @@ export function getSkillMana(skillId: number, hudManager?: HudManager | null): n
|
|||
return getSkillManaCost(skillId, 1)
|
||||
}
|
||||
switch (skillId) {
|
||||
case 45: return 6
|
||||
case 55: return 10
|
||||
case 84: return 7
|
||||
case 93: return 12
|
||||
case 92: return 20
|
||||
case 12: return 4
|
||||
case 16: return 5
|
||||
case 20: return 8
|
||||
case 21: return 4
|
||||
case 40: return 7
|
||||
case 42: return 9
|
||||
case 59: return 27
|
||||
|
|
@ -1264,38 +1293,208 @@ export function castSkill(
|
|||
let damage = 28
|
||||
let delay = 0
|
||||
let skillName = '技能'
|
||||
let missileType: string | undefined = undefined
|
||||
let pierce: boolean | undefined = undefined
|
||||
let aoeRadius: number | undefined = undefined
|
||||
let statusEffect: 'chill' | 'freeze' | 'poison' | undefined = undefined
|
||||
let statusDuration: number | undefined = undefined
|
||||
let homing: boolean | undefined = undefined
|
||||
let spiral: { originX: number; originY: number; angle: number; radius: number } | undefined = undefined
|
||||
let jitter: boolean | undefined = undefined
|
||||
let isUndeadOnly: boolean | undefined = undefined
|
||||
let multiCount = 1
|
||||
let spreadAngle = 0
|
||||
let isNova = false
|
||||
|
||||
if (skillId === 47) {
|
||||
speed = 450
|
||||
range = 450
|
||||
damage = 28
|
||||
if (skillId === 36) {
|
||||
const missile = getMissileTxtData('firebolt')
|
||||
speed = missile.speedPxPerSec || 500
|
||||
range = missile.distancePx || 1000
|
||||
damage = 16
|
||||
delay = 0
|
||||
missileType = 'firebolt'
|
||||
skillName = '火弹'
|
||||
} else if (skillId === 47) {
|
||||
const missile = getMissileTxtData('fireball')
|
||||
speed = missile.speedPxPerSec || 500
|
||||
range = missile.distancePx || 1000
|
||||
damage = 35
|
||||
delay = 0
|
||||
aoeRadius = 48
|
||||
missileType = 'fireball'
|
||||
skillName = '火球'
|
||||
} else if (skillId === 39) {
|
||||
const missile = getMissileTxtData('icebolt')
|
||||
speed = missile.speedPxPerSec || 300
|
||||
range = missile.distancePx || 600
|
||||
damage = 14
|
||||
delay = 0
|
||||
statusEffect = 'chill'
|
||||
statusDuration = 50
|
||||
missileType = 'icebolt'
|
||||
skillName = '冰弹'
|
||||
} else if (skillId === 45) {
|
||||
const missile = getMissileTxtData('iceblast')
|
||||
speed = missile.speedPxPerSec || 300
|
||||
range = missile.distancePx || 600
|
||||
damage = 24
|
||||
delay = 0
|
||||
statusEffect = 'freeze'
|
||||
statusDuration = 75
|
||||
missileType = 'iceblast'
|
||||
skillName = '冰风暴'
|
||||
} else if (skillId === 55) {
|
||||
const missile = getMissileTxtData('glacialspike')
|
||||
speed = missile.speedPxPerSec || 400
|
||||
range = missile.distancePx || 640
|
||||
damage = 32
|
||||
delay = 0
|
||||
aoeRadius = 64
|
||||
statusEffect = 'freeze'
|
||||
statusDuration = 50
|
||||
missileType = 'glacialspike'
|
||||
skillName = '冰尖柱'
|
||||
} else if (skillId === 38) {
|
||||
const missile = getMissileTxtData('chargedbolt')
|
||||
speed = missile.speedPxPerSec || 300
|
||||
range = missile.distancePx || 1176
|
||||
damage = 12
|
||||
delay = 0
|
||||
missileType = 'chargedbolt'
|
||||
jitter = true
|
||||
multiCount = 4
|
||||
spreadAngle = 0.45
|
||||
skillName = '充能弹'
|
||||
} else if (skillId === 67) {
|
||||
const missile = getMissileTxtData('teeth')
|
||||
speed = missile.speedPxPerSec || 400
|
||||
range = missile.distancePx || 800
|
||||
damage = 14
|
||||
delay = 0
|
||||
missileType = 'teeth'
|
||||
multiCount = 3
|
||||
spreadAngle = 0.40
|
||||
skillName = '牙'
|
||||
} else if (skillId === 84) {
|
||||
const missile = getMissileTxtData('bonespear')
|
||||
speed = missile.speedPxPerSec || 600
|
||||
range = missile.distancePx || 960
|
||||
damage = 30
|
||||
delay = 0
|
||||
pierce = true
|
||||
missileType = 'bonespear'
|
||||
skillName = '骨矛'
|
||||
} else if (skillId === 93) {
|
||||
const missile = getMissileTxtData('bonespirit')
|
||||
speed = missile.speedPxPerSec || 300
|
||||
range = missile.distancePx || 1536
|
||||
damage = 45
|
||||
delay = 0
|
||||
homing = true
|
||||
missileType = 'bonespirit'
|
||||
skillName = '骨魂'
|
||||
} else if (skillId === 92) {
|
||||
const missile = getMissileTxtData('poisonnova')
|
||||
speed = missile.speedPxPerSec || 300
|
||||
range = missile.distancePx || 360
|
||||
damage = 18
|
||||
delay = 0
|
||||
statusEffect = 'poison'
|
||||
statusDuration = 100
|
||||
missileType = 'poisonnova'
|
||||
isNova = true
|
||||
multiCount = 32
|
||||
skillName = '剧毒新星'
|
||||
} else if (skillId === 101) {
|
||||
const missile = getMissileTxtData('holybolt')
|
||||
speed = missile.speedPxPerSec || 500
|
||||
range = missile.distancePx || 1000
|
||||
damage = 35
|
||||
delay = 0
|
||||
isUndeadOnly = true
|
||||
missileType = 'holybolt'
|
||||
skillName = '圣光弹'
|
||||
} else if (skillId === 112) {
|
||||
const missile = getMissileTxtData('blessedhammer')
|
||||
speed = missile.speedPxPerSec || 450
|
||||
range = missile.distancePx || 2160
|
||||
damage = 40
|
||||
delay = 0
|
||||
pierce = true
|
||||
missileType = 'blessedhammer'
|
||||
const baseAngle = Math.atan2((targetY - player.y) / ISO_GROUND_ASPECT_RATIO, targetX - player.x)
|
||||
spiral = { originX: player.x, originY: player.y, angle: baseAngle, radius: 12 }
|
||||
skillName = '祝福之槌'
|
||||
} else if (skillId === 6) {
|
||||
const missile = getMissileTxtData('magicarrow')
|
||||
speed = missile.speedPxPerSec || 600
|
||||
range = missile.distancePx || 960
|
||||
damage = 18
|
||||
delay = 0
|
||||
missileType = 'magicarrow'
|
||||
skillName = '魔法箭'
|
||||
} else if (skillId === 7) {
|
||||
const missile = getMissileTxtData('firearrow')
|
||||
speed = missile.speedPxPerSec || 600
|
||||
range = missile.distancePx || 960
|
||||
damage = 20
|
||||
delay = 0
|
||||
missileType = 'firearrow'
|
||||
skillName = '火箭'
|
||||
} else if (skillId === 11) {
|
||||
const missile = getMissileTxtData('icearrow')
|
||||
speed = missile.speedPxPerSec || 600
|
||||
range = missile.distancePx || 960
|
||||
damage = 22
|
||||
delay = 0
|
||||
statusEffect = 'chill'
|
||||
statusDuration = 50
|
||||
missileType = 'icearrow'
|
||||
skillName = '冰箭'
|
||||
} else if (skillId === 21) {
|
||||
const missile = getMissileTxtData('icearrow')
|
||||
speed = missile.speedPxPerSec || 600
|
||||
range = missile.distancePx || 960
|
||||
damage = 26
|
||||
delay = 0
|
||||
statusEffect = 'freeze'
|
||||
statusDuration = 50
|
||||
missileType = 'icearrow'
|
||||
skillName = '急冻箭'
|
||||
} else if (skillId === 12) {
|
||||
const missile = getMissileTxtData('arrow')
|
||||
speed = missile.speedPxPerSec || 600
|
||||
range = missile.distancePx || 960
|
||||
damage = 16
|
||||
delay = 0
|
||||
missileType = 'arrow'
|
||||
multiCount = 5
|
||||
spreadAngle = 0.50
|
||||
skillName = '多重箭'
|
||||
} else if (skillId === 16) {
|
||||
const missile = getMissileTxtData('firearrow')
|
||||
speed = missile.speedPxPerSec || 600
|
||||
range = missile.distancePx || 960
|
||||
damage = 25
|
||||
delay = 0
|
||||
aoeRadius = 48
|
||||
missileType = 'firearrow'
|
||||
skillName = '爆裂箭'
|
||||
} else if (skillId === 20) {
|
||||
const missile = getMissileTxtData('lightningjavelin')
|
||||
speed = missile.speedPxPerSec || 750
|
||||
range = missile.distancePx || 750
|
||||
damage = 35
|
||||
delay = 0
|
||||
pierce = true
|
||||
missileType = 'lightningjavelin'
|
||||
skillName = '闪电之枪'
|
||||
} else if (skillId === 64) {
|
||||
speed = 400
|
||||
range = 480
|
||||
damage = 35
|
||||
delay = 25
|
||||
skillName = '冰封球'
|
||||
} else if (skillId === 36) {
|
||||
const missile = getMissileTxtData('firebolt')
|
||||
speed = missile.speedPxPerSec || 500
|
||||
range = missile.distancePx || 1000
|
||||
damage = 16
|
||||
delay = 0
|
||||
skillName = '火弹'
|
||||
} else if (skillId === 39) {
|
||||
speed = 450
|
||||
range = 400
|
||||
damage = 14
|
||||
delay = 0
|
||||
skillName = '冰弹'
|
||||
} else if (skillId === 38) {
|
||||
speed = 350
|
||||
range = 350
|
||||
damage = 12
|
||||
delay = 0
|
||||
skillName = '充能弹'
|
||||
} else if (skillId === 49) {
|
||||
speed = 600
|
||||
range = 500
|
||||
|
|
@ -1319,23 +1518,90 @@ export function castSkill(
|
|||
|
||||
const dx = targetX - player.x
|
||||
const dy = targetY - player.y
|
||||
const len = Math.hypot(dx, dy)
|
||||
const baseAngle = Math.atan2(dy, dx)
|
||||
const perTick = speed / 25
|
||||
const ttl = Math.max(1, Math.round(range / perTick))
|
||||
|
||||
const projectile: Projectile = {
|
||||
skillId: String(skillId),
|
||||
x: player.x,
|
||||
y: player.y,
|
||||
vx: len > 0 ? (dx / len) * perTick : perTick,
|
||||
vy: len > 0 ? (dy / len) * perTick : 0,
|
||||
damage,
|
||||
ttl,
|
||||
fromPlayer: true,
|
||||
missileType: skillId === 36 ? 'firebolt' : undefined,
|
||||
if (isNova) {
|
||||
for (let i = 0; i < multiCount; i++) {
|
||||
const angle = (i / multiCount) * 2 * Math.PI
|
||||
const pVx = Math.cos(angle) * perTick
|
||||
const pVy = Math.sin(angle) * (perTick * ISO_GROUND_ASPECT_RATIO)
|
||||
engine.projectiles.push({
|
||||
skillId: String(skillId),
|
||||
x: player.x,
|
||||
y: player.y,
|
||||
vx: pVx,
|
||||
vy: pVy,
|
||||
damage,
|
||||
ttl,
|
||||
fromPlayer: true,
|
||||
missileType,
|
||||
statusEffect,
|
||||
statusDuration,
|
||||
})
|
||||
}
|
||||
} else if (multiCount > 1) {
|
||||
const startAngle = baseAngle - spreadAngle / 2
|
||||
const step = spreadAngle / (multiCount - 1)
|
||||
for (let i = 0; i < multiCount; i++) {
|
||||
const angle = startAngle + i * step
|
||||
const pVx = Math.cos(angle) * perTick
|
||||
const pVy = Math.sin(angle) * perTick
|
||||
engine.projectiles.push({
|
||||
skillId: String(skillId),
|
||||
x: player.x,
|
||||
y: player.y,
|
||||
vx: pVx,
|
||||
vy: pVy,
|
||||
damage,
|
||||
ttl,
|
||||
fromPlayer: true,
|
||||
missileType,
|
||||
jitter,
|
||||
pierce,
|
||||
aoeRadius,
|
||||
statusEffect,
|
||||
statusDuration,
|
||||
isUndeadOnly,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let pVx: number
|
||||
let pVy: number
|
||||
let pX = player.x
|
||||
let pY = player.y
|
||||
if (spiral !== undefined) {
|
||||
pX = spiral.originX + Math.cos(spiral.angle) * spiral.radius
|
||||
pY = spiral.originY + Math.sin(spiral.angle) * (spiral.radius * ISO_GROUND_ASPECT_RATIO)
|
||||
pVx = -Math.sin(spiral.angle) * 18
|
||||
pVy = Math.cos(spiral.angle) * (18 * ISO_GROUND_ASPECT_RATIO)
|
||||
} else {
|
||||
const len = Math.hypot(dx, dy)
|
||||
pVx = len > 0 ? (dx / len) * perTick : perTick
|
||||
pVy = len > 0 ? (dy / len) * perTick : 0
|
||||
}
|
||||
engine.projectiles.push({
|
||||
skillId: String(skillId),
|
||||
x: pX,
|
||||
y: pY,
|
||||
vx: pVx,
|
||||
vy: pVy,
|
||||
damage,
|
||||
ttl,
|
||||
fromPlayer: true,
|
||||
missileType,
|
||||
pierce,
|
||||
aoeRadius,
|
||||
statusEffect,
|
||||
statusDuration,
|
||||
homing,
|
||||
spiral,
|
||||
jitter,
|
||||
isUndeadOnly,
|
||||
})
|
||||
}
|
||||
|
||||
engine.projectiles.push(projectile)
|
||||
player.cooldown = delay > 0 ? delay : 10
|
||||
engine.metrics.casts += 1
|
||||
status.textContent = `施展技能:${skillName}`
|
||||
|
|
@ -1461,6 +1727,10 @@ export class SceneMouseController {
|
|||
return
|
||||
}
|
||||
this.engine.groundItems.remove(item.id)
|
||||
const gIdx = this.engine.ground.findIndex(g => Math.hypot(g.x - item.x, g.y - item.y) <= 32 || g.item === item.item)
|
||||
if (gIdx !== -1) {
|
||||
this.engine.ground.splice(gIdx, 1)
|
||||
}
|
||||
this.engine.metrics.pickups += 1
|
||||
this.hudManager.syncPublishedState()
|
||||
this.status.textContent = `拾起物品:${item.nameZh || item.name}`
|
||||
|
|
@ -3249,7 +3519,7 @@ export async function loadMissileArtMap(
|
|||
): Promise<Map<string, LoadedMissileArt>> {
|
||||
const map = new Map<string, LoadedMissileArt>()
|
||||
if (typeof fetch === 'undefined') return map
|
||||
const targets = ['firebolt', 'fireball', 'fireexplode']
|
||||
const targets = Object.keys(MISSILE_METAS)
|
||||
const baseCandidates = [
|
||||
'/missiles',
|
||||
packBase ? `${packBase}/missiles` : '',
|
||||
|
|
@ -3274,8 +3544,8 @@ export async function loadMissileArtMap(
|
|||
} finally {
|
||||
bitmap.close()
|
||||
}
|
||||
const frames: AtlasFrame[][] = meta.groups.map(group =>
|
||||
group.map(([x, y, width, height, anchorX, anchorY]) => ({
|
||||
const frames: AtlasFrame[][] = meta.groups.map((group: readonly MissileFrameTuple[]) =>
|
||||
group.map(([x, y, width, height, anchorX, anchorY]: MissileFrameTuple) => ({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
|
|
@ -3297,11 +3567,11 @@ export async function loadMissileArtMap(
|
|||
}
|
||||
|
||||
/**
|
||||
* Render a Diablo II 1.13c style Fire Bolt projectile:
|
||||
* - Uses authentic baked Firebolt.dcc sprite atlas when available.
|
||||
* Render a Diablo II 1.13c missile projectile:
|
||||
* - Uses authentic baked DCC sprite atlas when available.
|
||||
* - Gracefully falls back to procedural glowing fireball & trailing particles in test/offline environments.
|
||||
*/
|
||||
export function drawFireboltProjectile(
|
||||
export function drawMissileProjectile(
|
||||
renderer: SpriteRenderer,
|
||||
shot: Projectile,
|
||||
missileArt?: LoadedMissileArt,
|
||||
|
|
@ -3309,13 +3579,17 @@ export function drawFireboltProjectile(
|
|||
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 frameIndex = Math.abs(Math.floor(((100 - 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 })
|
||||
const isAdditive = missileArt.meta.name !== 'arrow'
|
||||
renderer.draw(frame, drawX, drawY, {
|
||||
atlas: missileArt.handle,
|
||||
...(isAdditive ? { blendMode: 'additive' } : {}),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -3358,12 +3632,14 @@ export function drawFireboltProjectile(
|
|||
renderer.drawSolid(x - 2, y - 2, 4, 4, [1.0, 1.0, 0.88, 0.98])
|
||||
}
|
||||
|
||||
export const drawFireboltProjectile = drawMissileProjectile
|
||||
|
||||
/**
|
||||
* Render a Diablo II 1.13c style fire explosion (fireexplode):
|
||||
* - Uses authentic baked FireArrowExplode2.dcc sprite atlas when available.
|
||||
* Render a Diablo II 1.13c style explosion:
|
||||
* - Uses authentic baked DCC sprite atlas when available.
|
||||
* - Falls back to procedural explosion in test/offline environments.
|
||||
*/
|
||||
export function drawFireExplosion(
|
||||
export function drawExplosion(
|
||||
renderer: SpriteRenderer,
|
||||
exp: ActiveExplosion,
|
||||
explosionArt?: LoadedMissileArt,
|
||||
|
|
@ -3376,7 +3652,10 @@ export function drawFireExplosion(
|
|||
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 })
|
||||
renderer.draw(frame, drawX, drawY, {
|
||||
atlas: explosionArt.handle,
|
||||
blendMode: 'additive',
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -3414,6 +3693,8 @@ export function drawFireExplosion(
|
|||
}
|
||||
}
|
||||
|
||||
export const drawFireExplosion = drawExplosion
|
||||
|
||||
/** Quality colors for ground item highlights and glints. */
|
||||
export const GROUND_ITEM_QUALITY_COLORS: Record<string, readonly [number, number, number, number]> = {
|
||||
normal: [0.88, 0.88, 0.88, 1.0],
|
||||
|
|
@ -3428,16 +3709,55 @@ export const GROUND_ITEM_QUALITY_COLORS: Record<string, readonly [number, number
|
|||
gold: [1.0, 0.84, 0.2, 1.0],
|
||||
}
|
||||
|
||||
export let globalItemsAtlasHandle: AtlasHandle | undefined
|
||||
|
||||
/**
|
||||
* Load authentic 1.13c baked items atlas from /ui/items-atlas.png.
|
||||
*/
|
||||
export async function loadItemsAtlas(
|
||||
renderer: SpriteRenderer,
|
||||
packBase?: string,
|
||||
): Promise<AtlasHandle | undefined> {
|
||||
if (globalItemsAtlasHandle !== undefined) return globalItemsAtlasHandle
|
||||
if (typeof fetch === 'undefined' || typeof createImageBitmap === 'undefined') return undefined
|
||||
const candidates = [
|
||||
'/ui/items-atlas.png',
|
||||
packBase ? `${packBase}/ui/items-atlas.png` : '',
|
||||
'ui/items-atlas.png',
|
||||
'public/ui/items-atlas.png',
|
||||
].filter(Boolean)
|
||||
|
||||
for (const url of candidates) {
|
||||
try {
|
||||
const resp = await fetch(url)
|
||||
if (!resp.ok) continue
|
||||
const blob = await resp.blob()
|
||||
const bitmap = await createImageBitmap(blob)
|
||||
try {
|
||||
const handle = renderer.addAtlas(bitmap, BAKED_UI_MANIFEST.atlasWidth, BAKED_UI_MANIFEST.atlasHeight)
|
||||
globalItemsAtlasHandle = handle
|
||||
return handle
|
||||
} finally {
|
||||
bitmap.close()
|
||||
}
|
||||
} catch {
|
||||
// try next candidate
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a ground item entity with authentic Diablo II 1.13c appearance:
|
||||
* - Parabolic bounce height offset (when dropping or inventory full)
|
||||
* - Ground item icon / scaled silhouette
|
||||
* - Authentic item sprite icon / scaled silhouette lying on the ground
|
||||
* - Periodic glint/sparkle 4-point star animation
|
||||
*/
|
||||
export function drawGroundItem(
|
||||
renderer: SpriteRenderer,
|
||||
item: GroundItemEntity,
|
||||
now: number,
|
||||
itemsAtlas: AtlasHandle | undefined = globalItemsAtlasHandle,
|
||||
): void {
|
||||
const bounceH = item.bounceState ? calculateBounceHeight(item.bounceState, now) : 0
|
||||
const baseY = item.y - bounceH
|
||||
|
|
@ -3468,32 +3788,72 @@ export function drawGroundItem(
|
|||
renderer.drawSolid(item.x - w / 2 + 1, baseY - h + 1, w - 2, Math.max(2, Math.floor(h / 3)), [1.0, 0.92, 0.45, 1.0])
|
||||
} else {
|
||||
// 2. Equipment / Potion / Rune Item Rendering
|
||||
const w = item.invWidth > 1 ? 22 : 14
|
||||
const h = item.invHeight > 2 ? 34 : item.invHeight > 1 ? 24 : 14
|
||||
const sr = (itemsAtlas !== undefined && typeof (renderer as any).draw === 'function')
|
||||
? resolveItemSpriteRect(item.item || (item as any), BAKED_UI_MANIFEST.itemRects)
|
||||
: null
|
||||
|
||||
// Drop shadow: stays grounded on floor (item.y - 3), alpha fades slightly with height
|
||||
const shadowAlpha = bounceH > 0 ? Math.max(0.15, 0.38 * (1 - bounceH / 45)) : 0.38
|
||||
renderer.drawSolid(item.x - w / 2 - 2, item.y - 3, w + 4, 5, [0, 0, 0, shadowAlpha])
|
||||
if (sr !== null && itemsAtlas !== undefined) {
|
||||
// Authentic Diablo II ground item sprite icon:
|
||||
// Scales sprite to lie on ground naturally (~16-34px)
|
||||
const maxDim = Math.max(sr.w, sr.h)
|
||||
const scale = Math.min(0.75, 34 / maxDim)
|
||||
const baseW = Math.max(12, Math.round(sr.w * scale))
|
||||
const baseH = Math.max(12, Math.round(sr.h * scale))
|
||||
|
||||
// Authentic flippy tumble width modulation during vertical bounce
|
||||
let renderW = w
|
||||
if (bounceH > 0 && item.bounceState) {
|
||||
const elapsed = now - item.bounceState.startTime
|
||||
// Spin oscillation creating flipping card / tumbling item illusion
|
||||
const spinScale = Math.abs(Math.cos(elapsed * 0.018))
|
||||
renderW = Math.max(6, Math.round(w * (0.4 + 0.6 * spinScale)))
|
||||
// Drop shadow: stays grounded on floor (item.y - 2), alpha fades slightly with height
|
||||
const shadowAlpha = bounceH > 0 ? Math.max(0.12, 0.42 * (1 - bounceH / 45)) : 0.42
|
||||
const shadowW = Math.max(14, baseW + 6)
|
||||
renderer.drawSolid(item.x - shadowW / 2, item.y - 2, shadowW, 4, [0, 0, 0, shadowAlpha])
|
||||
|
||||
// Authentic flippy tumble width modulation during vertical bounce
|
||||
let renderW = baseW
|
||||
if (bounceH > 0 && item.bounceState) {
|
||||
const elapsed = now - item.bounceState.startTime
|
||||
// Spin oscillation creating flipping card / tumbling item illusion
|
||||
const spinScale = Math.abs(Math.cos(elapsed * 0.018))
|
||||
renderW = Math.max(4, Math.round(baseW * (0.35 + 0.65 * spinScale)))
|
||||
}
|
||||
|
||||
const frame: AtlasFrame = {
|
||||
x: sr.x,
|
||||
y: sr.y,
|
||||
width: sr.w,
|
||||
height: sr.h,
|
||||
}
|
||||
renderer.draw(frame, item.x - renderW / 2, baseY - baseH, {
|
||||
atlas: itemsAtlas,
|
||||
width: renderW,
|
||||
height: baseH,
|
||||
})
|
||||
} else {
|
||||
// Fallback for mock renderer or headless unit tests
|
||||
const w = item.invWidth > 1 ? 22 : 14
|
||||
const h = item.invHeight > 2 ? 34 : item.invHeight > 1 ? 24 : 14
|
||||
|
||||
// Drop shadow: stays grounded on floor (item.y - 3), alpha fades slightly with height
|
||||
const shadowAlpha = bounceH > 0 ? Math.max(0.15, 0.38 * (1 - bounceH / 45)) : 0.38
|
||||
renderer.drawSolid(item.x - w / 2 - 2, item.y - 3, w + 4, 5, [0, 0, 0, shadowAlpha])
|
||||
|
||||
// Authentic flippy tumble width modulation during vertical bounce
|
||||
let renderW = w
|
||||
if (bounceH > 0 && item.bounceState) {
|
||||
const elapsed = now - item.bounceState.startTime
|
||||
// Spin oscillation creating flipping card / tumbling item illusion
|
||||
const spinScale = Math.abs(Math.cos(elapsed * 0.018))
|
||||
renderW = Math.max(6, Math.round(w * (0.4 + 0.6 * spinScale)))
|
||||
}
|
||||
|
||||
// Body
|
||||
const qr = qColor[0]
|
||||
const qg = qColor[1]
|
||||
const qb = qColor[2]
|
||||
renderer.drawSolid(item.x - renderW / 2, baseY - h, renderW, h, [qr * 0.45, qg * 0.45, qb * 0.45, 0.9])
|
||||
// Border rim
|
||||
renderer.drawSolid(item.x - renderW / 2, baseY - h, renderW, 2, [qr, qg, qb, 0.95])
|
||||
renderer.drawSolid(item.x - renderW / 2, baseY - 2, renderW, 2, [qr, qg, qb, 0.95])
|
||||
renderer.drawSolid(item.x - renderW / 2, baseY - h, 2, h, [qr, qg, qb, 0.95])
|
||||
renderer.drawSolid(item.x + renderW / 2 - 2, baseY - h, 2, h, [qr, qg, qb, 0.95])
|
||||
}
|
||||
|
||||
// Body
|
||||
const qr = qColor[0]
|
||||
const qg = qColor[1]
|
||||
const qb = qColor[2]
|
||||
renderer.drawSolid(item.x - renderW / 2, baseY - h, renderW, h, [qr * 0.45, qg * 0.45, qb * 0.45, 0.9])
|
||||
// Border rim
|
||||
renderer.drawSolid(item.x - renderW / 2, baseY - h, renderW, 2, [qr, qg, qb, 0.95])
|
||||
renderer.drawSolid(item.x - renderW / 2, baseY - 2, renderW, 2, [qr, qg, qb, 0.95])
|
||||
renderer.drawSolid(item.x - renderW / 2, baseY - h, 2, h, [qr, qg, qb, 0.95])
|
||||
renderer.drawSolid(item.x + renderW / 2 - 2, baseY - h, 2, h, [qr, qg, qb, 0.95])
|
||||
}
|
||||
|
||||
// 3. Glint / Sparkle 4-Point Star Animation
|
||||
|
|
@ -3561,6 +3921,10 @@ async function runScene(initialRuntime: MapRuntime, renderer: SpriteRenderer, st
|
|||
void loadMissileArtMap(renderer, packEntityBase).then(map => {
|
||||
missileArtMap = map
|
||||
})
|
||||
let itemsAtlasHandle: AtlasHandle | undefined
|
||||
void loadItemsAtlas(renderer, packEntityBase).then(handle => {
|
||||
itemsAtlasHandle = handle
|
||||
})
|
||||
let monsterWalkFrame = 0
|
||||
const playerAnimator = new ActorAnimator()
|
||||
|
||||
|
|
@ -4612,10 +4976,10 @@ if (typeof window !== 'undefined') {
|
|||
|
||||
for (const shot of engine.projectiles) {
|
||||
pushEntity(shot.x, shot.y, () => {
|
||||
if (shot.skillId === '36' || shot.skillId === 'firebolt' || shot.missileType === 'firebolt') {
|
||||
drawFireboltProjectile(renderer, shot, missileArtMap.get('firebolt'))
|
||||
} else if (shot.skillId === '47' || shot.skillId === 'fireball' || shot.missileType === 'fireball') {
|
||||
drawFireboltProjectile(renderer, shot, missileArtMap.get('fireball'))
|
||||
const mType = shot.missileType ?? (shot.skillId === '36' ? 'firebolt' : shot.skillId === '47' ? 'fireball' : undefined)
|
||||
const art = mType !== undefined ? missileArtMap.get(mType) : undefined
|
||||
if (art !== undefined) {
|
||||
drawMissileProjectile(renderer, shot, art)
|
||||
} else {
|
||||
renderer.drawSolid(shot.x - 4, shot.y - 4, 8, 8, [0.9, 0.9, 0.2, 1])
|
||||
}
|
||||
|
|
@ -4624,25 +4988,17 @@ if (typeof window !== 'undefined') {
|
|||
|
||||
for (const exp of engine.explosions) {
|
||||
pushEntity(exp.x, exp.y, () => {
|
||||
drawFireExplosion(renderer, exp, missileArtMap.get('fireexplode'))
|
||||
const art = missileArtMap.get(exp.missileType) ?? missileArtMap.get('fireexplode')
|
||||
drawExplosion(renderer, exp, art)
|
||||
})
|
||||
}
|
||||
|
||||
for (const gItem of engine.groundItems.all) {
|
||||
pushEntity(gItem.x, gItem.y, () => {
|
||||
drawGroundItem(renderer, gItem, renderStarted)
|
||||
drawGroundItem(renderer, gItem, renderStarted, itemsAtlasHandle)
|
||||
})
|
||||
}
|
||||
|
||||
if (engine.groundItems.count === 0 && engine.ground.length > 0) {
|
||||
for (const entry of engine.ground) {
|
||||
pushEntity(entry.x, entry.y, () => {
|
||||
const size = 6
|
||||
renderer.drawSolid(entry.x - size / 2, entry.y - size, size, size, [0.8, 0.8, 0.8, 1])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
entities.sort((a, b) => a.depth - b.depth)
|
||||
|
||||
for (const entity of entities) {
|
||||
|
|
|
|||
|
|
@ -35,13 +35,13 @@ export interface HotkeySkillEntry {
|
|||
* left/right action buttons or appear in the Speedbar popup.
|
||||
*/
|
||||
export const PASSIVE_SKILL_IDS: ReadonlySet<number> = new Set<number>([
|
||||
// Amazon (Tab 1 Passives)
|
||||
// Amazon (Tab 1 Passives: Critical Strike 9, Dodge 13, Avoid 18, Penetrate 23, Evade 29, Pierce 33)
|
||||
9, // Critical Strike
|
||||
14, // Penetrate
|
||||
17, // Dodge
|
||||
22, // Avoid
|
||||
27, // Evade
|
||||
29, // Pierce
|
||||
13, // Dodge
|
||||
18, // Avoid
|
||||
23, // Penetrate
|
||||
29, // Evade
|
||||
33, // Pierce
|
||||
// Sorceress Passives
|
||||
37, // Warmth
|
||||
61, // Fire Mastery
|
||||
|
|
@ -74,6 +74,24 @@ export function isPassiveSkill(skillId: number): boolean {
|
|||
return PASSIVE_SKILL_IDS.has(skillId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Diablo II v1.13c Skills usable on the Left-Click action button (Skills.txt `leftskill = 1`).
|
||||
*/
|
||||
export const LEFT_USABLE_SKILL_IDS: ReadonlySet<number> = new Set<number>([
|
||||
0, 2, 6, 7, 10, 11, 12, 14, 15, 16, 19, 20, 21, 22, 24, 25, 26, 27, 30, 31, 34, 35,
|
||||
36, 38, 39, 41, 45, 47, 49, 53, 55, 56, 59, 64,
|
||||
67, 73, 84, 93,
|
||||
96, 97, 101, 106, 107, 111, 112, 116, 121,
|
||||
126, 132, 133, 139, 140, 143, 144, 147, 151, 152,
|
||||
225, 229, 230, 232, 238, 239, 240, 242, 243, 245, 248,
|
||||
251, 254, 255, 256, 257, 259, 260, 265, 266, 269, 270, 274, 275, 280,
|
||||
])
|
||||
|
||||
export function isLeftUsableSkill(skillId: number): boolean {
|
||||
if (isPassiveSkill(skillId)) return false
|
||||
return LEFT_USABLE_SKILL_IDS.has(skillId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Diablo II v1.13c Paladin Aura Skills (`Skills.txt` `aura = 1`, Tabs 1 & 2).
|
||||
* Paladin Auras are highlighted in yellow on the Speedbar and Quickbar slots,
|
||||
|
|
@ -190,20 +208,39 @@ export const LEFT_SKILL_BOUNDS = { x: 117, y: 551, width: 48, height: 48 } as co
|
|||
export const RIGHT_SKILL_BOUNDS = { x: 635, y: 551, width: 48, height: 48 } as const
|
||||
|
||||
export const DEFAULT_SORCERESS_SKILLS: readonly HotkeySkillEntry[] = [
|
||||
// Universal Skills
|
||||
{ skillId: 0, name: 'Attack', nameZh: '普通攻击', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 2, name: 'Throw', nameZh: '投掷', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 3, name: 'Unsummon', nameZh: '取消召唤', level: 1, manaCost: 0, leftUsable: false, rightUsable: true },
|
||||
// Fire Spells (8 active)
|
||||
{ skillId: 36, name: 'Fire Bolt', nameZh: '火弹', level: 5, manaCost: 2.5, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 41, name: 'Inferno', nameZh: '地狱之火', level: 1, manaCost: 0.6, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 46, name: 'Blaze', nameZh: '炽烈之径', level: 1, manaCost: 11, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 47, name: 'Fire Ball', nameZh: '火球', level: 8, manaCost: 8.5, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 51, name: 'Fire Wall', nameZh: '火墙', level: 1, manaCost: 22, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 52, name: 'Enchant', nameZh: '强化', level: 1, manaCost: 25, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 56, name: 'Meteor', nameZh: '陨石', level: 6, manaCost: 22, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 62, name: 'Hydra', nameZh: '九头海蛇', level: 1, manaCost: 20, leftUsable: false, rightUsable: true },
|
||||
// Lightning Spells (9 active)
|
||||
{ skillId: 38, name: 'Charged Bolt', nameZh: '充能弹', level: 4, manaCost: 4, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 49, name: 'Lightning', nameZh: '闪电', level: 7, manaCost: 11, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 54, name: 'Teleport', nameZh: '传送', level: 4, manaCost: 21, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 44, name: 'Frost Nova', nameZh: '霜之新星', level: 3, manaCost: 11, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 59, name: 'Blizzard', nameZh: '暴风雪', level: 10, manaCost: 27, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 64, name: 'Frozen Orb', nameZh: '冰封球', level: 12, manaCost: 30, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 40, name: 'Frozen Armor', nameZh: '冰封装甲', level: 3, manaCost: 7, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 42, name: 'Static Field', nameZh: '静态力场', level: 5, manaCost: 9, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 43, name: 'Telekinesis', nameZh: '心灵传动', level: 1, manaCost: 7, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 48, name: 'Nova', nameZh: '新星', level: 1, manaCost: 15, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 49, name: 'Lightning', nameZh: '闪电', level: 7, manaCost: 11, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 53, name: 'Chain Lightning', nameZh: '连锁闪电', level: 1, manaCost: 9, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 54, name: 'Teleport', nameZh: '传送', level: 4, manaCost: 21, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 57, name: 'Thunder Storm', nameZh: '雷云风暴', level: 1, manaCost: 19, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 58, name: 'Energy Shield', nameZh: '能量护盾', level: 1, manaCost: 5, leftUsable: false, rightUsable: true },
|
||||
// Cold Spells (9 active)
|
||||
{ skillId: 39, name: 'Ice Bolt', nameZh: '冰弹', level: 1, manaCost: 3, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 40, name: 'Frozen Armor', nameZh: '冰封装甲', level: 3, manaCost: 7, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 44, name: 'Frost Nova', nameZh: '霜之新星', level: 3, manaCost: 11, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 45, name: 'Ice Blast', nameZh: '冰风暴', level: 5, manaCost: 8, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 50, name: 'Shiver Armor', nameZh: '碎冰甲', level: 1, manaCost: 11, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 55, name: 'Glacial Spike', nameZh: '冰尖柱', level: 10, manaCost: 14.5, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 59, name: 'Blizzard', nameZh: '暴风雪', level: 10, manaCost: 27, leftUsable: true, rightUsable: true },
|
||||
{ skillId: 60, name: 'Chilling Armor', nameZh: '寒冰装甲', level: 1, manaCost: 17, leftUsable: false, rightUsable: true },
|
||||
{ skillId: 64, name: 'Frozen Orb', nameZh: '冰封球', level: 12, manaCost: 30, leftUsable: true, rightUsable: true },
|
||||
]
|
||||
|
||||
export class SkillHotkeysHud {
|
||||
|
|
@ -345,6 +382,17 @@ export class SkillHotkeysHud {
|
|||
this.preloadSkillIcons()
|
||||
}
|
||||
|
||||
addOrUpdateSkill(entry: HotkeySkillEntry): void {
|
||||
if (isPassiveSkill(entry.skillId) || entry.isPassive) return
|
||||
const idx = this.availableSkills.findIndex(s => s.skillId === entry.skillId)
|
||||
if (idx >= 0) {
|
||||
this.availableSkills[idx] = entry
|
||||
} else {
|
||||
this.availableSkills.push(entry)
|
||||
}
|
||||
this.preloadSkillIcons()
|
||||
}
|
||||
|
||||
getHotkeyLabelForSkill(side: 'left' | 'right', skillId: number): string | null {
|
||||
for (const [key, binding] of this.bindings.entries()) {
|
||||
if (binding.side === side && binding.skillId === skillId) return key
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@
|
|||
import { D2FontRenderer } from './font.ts'
|
||||
import { GlobesHud } from './globes.ts'
|
||||
import { BeltHud } from './belt.ts'
|
||||
import { SkillHotkeysHud } from './hotkeys.ts'
|
||||
import { SkillHotkeysHud, isPassiveSkill, isAuraSkill, isLeftUsableSkill } from './hotkeys.ts'
|
||||
import { ControlBarHud, type MiniPanelAction } from './control-bar.ts'
|
||||
import { SKILLS_BY_ID } from '../data/skills-catalog.ts'
|
||||
import { calculateManaCost } from '../game/skill-calc-engine.ts'
|
||||
import { InventoryPanel, type EquipSlotId, type UiInventoryItem, resolveItemSpriteRect, INV_GRID_ORIGIN } from './inventory.ts'
|
||||
import { CharacterSheetPanel, type BaseStatKey } from './character-sheet.ts'
|
||||
import { SkillTreePanel, SORCERESS_SKILL_TREE } from './skill-tree-panel.ts'
|
||||
|
|
@ -810,7 +812,7 @@ export class HudManager {
|
|||
this.syncPublishedState()
|
||||
return
|
||||
} else if (this.rightPanel === 'skill') {
|
||||
this.skillTree.handleClick(rightX, pt.y)
|
||||
this.skillTree.handleClick(rightX, pt.y, (skillId) => this.handleSkillAllocated(skillId))
|
||||
if (!this.skillTree.visible) this.rightPanel = 'none'
|
||||
this.syncPublishedState()
|
||||
return
|
||||
|
|
@ -819,6 +821,34 @@ export class HudManager {
|
|||
})
|
||||
}
|
||||
|
||||
private handleSkillAllocated(skillId: number): void {
|
||||
if (isPassiveSkill(skillId)) return
|
||||
const catalogEntry = SKILLS_BY_ID[skillId]
|
||||
if (!catalogEntry) return
|
||||
|
||||
const currentHard = this.skillTree.hardPoints.get(skillId) ?? 1
|
||||
const mana = calculateManaCost(catalogEntry, currentHard)
|
||||
const leftUsable = isLeftUsableSkill(skillId)
|
||||
|
||||
this.hotkeys.addOrUpdateSkill({
|
||||
skillId,
|
||||
name: catalogEntry.name,
|
||||
nameZh: catalogEntry.nameZh,
|
||||
level: currentHard,
|
||||
manaCost: mana,
|
||||
leftUsable,
|
||||
rightUsable: true,
|
||||
isAura: isAuraSkill(skillId),
|
||||
})
|
||||
|
||||
const profile = this.currentClass ? this.classProfiles.get(this.currentClass) : undefined
|
||||
if (profile) {
|
||||
profile.availableSkills = [...this.hotkeys.availableSkills]
|
||||
profile.hardPoints = new Map(this.skillTree.hardPoints)
|
||||
profile.unspentSkillPoints = this.skillTree.unspentSkillPoints
|
||||
}
|
||||
}
|
||||
|
||||
private buildControlBarState() {
|
||||
return {
|
||||
level: this.charSheet.attrs.level,
|
||||
|
|
|
|||
|
|
@ -289,6 +289,35 @@ export function resolveItemSpriteRect(
|
|||
return itemRects['invcrs'] ?? null
|
||||
}
|
||||
|
||||
// Tier 4.5: Name and semantic keyword match for common weapons, armors, and potions
|
||||
const nameStr = (
|
||||
(item.name || '') + ' ' +
|
||||
(item.id || '') + ' ' +
|
||||
((item as any).nameZh || '') + ' ' +
|
||||
((item as any).base?.name || '') + ' ' +
|
||||
((item as any).base?.nameZh || '')
|
||||
).toLowerCase()
|
||||
|
||||
if (nameStr.includes('sword') || nameStr.includes('剑')) return itemRects['invssd'] ?? itemRects['invbsd'] ?? itemRects['invcrs'] ?? null
|
||||
if (nameStr.includes('axe') || nameStr.includes('斧')) return itemRects['invhax'] ?? itemRects['invbax'] ?? null
|
||||
if (nameStr.includes('bow') || nameStr.includes('弓')) return itemRects['invsbw'] ?? itemRects['invhbw'] ?? null
|
||||
if (nameStr.includes('shield') || nameStr.includes('盾')) return itemRects['invsml'] ?? itemRects['invlrg'] ?? itemRects['invkit'] ?? null
|
||||
if (nameStr.includes('helm') || nameStr.includes('cap') || nameStr.includes('盔') || nameStr.includes('帽')) return itemRects['invcap'] ?? itemRects['invhlm'] ?? null
|
||||
if (nameStr.includes('armor') || nameStr.includes('plate') || nameStr.includes('甲')) return itemRects['invlea'] ?? itemRects['invqui'] ?? itemRects['invfld'] ?? null
|
||||
if (nameStr.includes('potion') || nameStr.includes('药')) {
|
||||
if (nameStr.includes('mana') || nameStr.includes('法力') || nameStr.includes('蓝')) return itemRects['invmp1'] ?? itemRects['invhp1'] ?? null
|
||||
if (nameStr.includes('rejuv') || nameStr.includes('活力') || nameStr.includes('紫')) return itemRects['invrvs'] ?? itemRects['invhp1'] ?? null
|
||||
return itemRects['invhp1'] ?? null
|
||||
}
|
||||
if (nameStr.includes('ring') || nameStr.includes('戒')) return itemRects['invrin1'] ?? null
|
||||
if (nameStr.includes('amulet') || nameStr.includes('项链')) return itemRects['invamu1'] ?? null
|
||||
if (nameStr.includes('boot') || nameStr.includes('靴') || nameStr.includes('鞋')) return itemRects['invlbt'] ?? itemRects['invhbt'] ?? null
|
||||
if (nameStr.includes('glove') || nameStr.includes('手套')) return itemRects['invlgl'] ?? itemRects['invhgl'] ?? null
|
||||
if (nameStr.includes('belt') || nameStr.includes('腰带') || nameStr.includes('带')) return itemRects['invlbl'] ?? itemRects['invhbl'] ?? null
|
||||
if (nameStr.includes('rune') || nameStr.includes('符文')) return itemRects['invr01'] ?? null
|
||||
if (nameStr.includes('scroll') || nameStr.includes('卷轴')) return itemRects['invtsc'] ?? itemRects['invisc'] ?? null
|
||||
if (nameStr.includes('gold') || nameStr.includes('金币')) return itemRects['invgld'] ?? null
|
||||
|
||||
// Tier 5: Dimension-based fallback guarantee (w x h in cells)
|
||||
const w = item.invWidth ?? (item as any).width ?? 1
|
||||
const h = item.invHeight ?? (item as any).height ?? 1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { SkillHotkeysHud, DEFAULT_SORCERESS_SKILLS, LEFT_SKILL_BOUNDS, RIGHT_SKILL_BOUNDS } from '../src/ui/hotkeys.ts'
|
||||
import { HudManager } from '../src/ui/hud-manager.ts'
|
||||
import { CLASS_DEFAULT_SKILLS, createStarterProfileForClass } from '../src/game/class-starter-profiles.ts'
|
||||
import { SKILLS_BY_ID } from '../src/data/skills-catalog.ts'
|
||||
|
||||
describe('Issue Fix: Glacial Spike (#55) and All Active Skills on Quickbar Speedbars', () => {
|
||||
let hud: SkillHotkeysHud
|
||||
|
||||
beforeEach(() => {
|
||||
hud = new SkillHotkeysHud()
|
||||
})
|
||||
|
||||
describe('1. Sorceress Right-Click & Left-Click Quickbar includes Glacial Spike', () => {
|
||||
it('has Glacial Spike (55) in DEFAULT_SORCERESS_SKILLS with level 10 and authentic mana', () => {
|
||||
const entry = DEFAULT_SORCERESS_SKILLS.find(s => s.skillId === 55)
|
||||
expect(entry).toBeDefined()
|
||||
expect(entry!.name).toBe('Glacial Spike')
|
||||
expect(entry!.nameZh).toBe('冰尖柱')
|
||||
expect(entry!.level).toBe(10)
|
||||
expect(entry!.manaCost).toBeCloseTo(14.5, 1)
|
||||
expect(entry!.leftUsable).toBe(true)
|
||||
expect(entry!.rightUsable).toBe(true)
|
||||
})
|
||||
|
||||
it('shows Glacial Spike in right-click speedbar popup cells', () => {
|
||||
const rightCells = hud.getSpeedbarSkills('right')
|
||||
const spikeCell = rightCells.find(c => c.skill.skillId === 55)
|
||||
expect(spikeCell).toBeDefined()
|
||||
expect(spikeCell!.skill.name).toBe('Glacial Spike')
|
||||
expect(spikeCell!.skill.nameZh).toBe('冰尖柱')
|
||||
})
|
||||
|
||||
it('shows Glacial Spike in left-click speedbar popup cells', () => {
|
||||
const leftCells = hud.getSpeedbarSkills('left')
|
||||
const spikeCell = leftCells.find(c => c.skill.skillId === 55)
|
||||
expect(spikeCell).toBeDefined()
|
||||
expect(spikeCell!.skill.name).toBe('Glacial Spike')
|
||||
})
|
||||
|
||||
it('assigns Glacial Spike to right-click slot when clicked in right speedbar', () => {
|
||||
hud.openSpeedbar('right')
|
||||
expect(hud.selectionOpen).toBe('right')
|
||||
|
||||
const rightCells = hud.getSpeedbarSkills('right')
|
||||
const spikeCell = rightCells.find(c => c.skill.skillId === 55)!
|
||||
expect(spikeCell).toBeDefined()
|
||||
|
||||
const handled = hud.handleClick(spikeCell.x + 10, spikeCell.y + 10)
|
||||
expect(handled).toBe(true)
|
||||
expect(hud.rightSkillId).toBe(55)
|
||||
expect(hud.selectionOpen).toBeNull()
|
||||
})
|
||||
|
||||
it('assigns Glacial Spike to left-click slot when clicked in left speedbar', () => {
|
||||
hud.openSpeedbar('left')
|
||||
expect(hud.selectionOpen).toBe('left')
|
||||
|
||||
const leftCells = hud.getSpeedbarSkills('left')
|
||||
const spikeCell = leftCells.find(c => c.skill.skillId === 55)!
|
||||
expect(spikeCell).toBeDefined()
|
||||
|
||||
const handled = hud.handleClick(spikeCell.x + 10, spikeCell.y + 10)
|
||||
expect(handled).toBe(true)
|
||||
expect(hud.leftSkillId).toBe(55)
|
||||
expect(hud.selectionOpen).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('2. All 7 Classes Starter Profiles include all Active Skills', () => {
|
||||
it('provides all 26 active skills on Sorceress right-click speedbar', () => {
|
||||
const sorProfile = createStarterProfileForClass('sor')
|
||||
const rightSkills = sorProfile.availableSkills.filter(s => s.rightUsable)
|
||||
// 3 universal (Attack 0, Throw 2, Unsummon 3) + 26 Sorceress active = 29
|
||||
expect(rightSkills.length).toBe(29)
|
||||
|
||||
// Glacial Spike is explicitly present
|
||||
expect(rightSkills.some(s => s.skillId === 55)).toBe(true)
|
||||
})
|
||||
|
||||
it('provides all active skills for all 7 classes without passives', () => {
|
||||
const classes = ['ama', 'sor', 'nec', 'pal', 'bar', 'dru', 'ass'] as const
|
||||
const expectedActiveCounts: Record<string, number> = {
|
||||
ama: 27, // 3 universal + 24 active
|
||||
sor: 29, // 3 universal + 26 active
|
||||
nec: 30, // 3 universal + 27 active
|
||||
pal: 33, // 3 universal + 30 active
|
||||
bar: 23, // 3 universal + 20 active
|
||||
dru: 32, // 3 universal + 29 active
|
||||
ass: 31, // 3 universal + 28 active
|
||||
}
|
||||
|
||||
for (const cls of classes) {
|
||||
const skills = CLASS_DEFAULT_SKILLS[cls]
|
||||
expect(skills.length).toBe(expectedActiveCounts[cls])
|
||||
// None should be passive
|
||||
for (const s of skills) {
|
||||
expect(s.isPassive).toBeFalsy()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('3. HudManager Skill Tree Allocation dynamically updates Available Skills', () => {
|
||||
const mockCanvas = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
style: { cursor: '' },
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
getContext: () => null,
|
||||
} as unknown as HTMLCanvasElement
|
||||
|
||||
function createMockHud() {
|
||||
const hud = new HudManager(mockCanvas, {
|
||||
onToggleAutomap: () => {},
|
||||
onWaypointTeleport: () => {},
|
||||
})
|
||||
hud.switchClass('sor')
|
||||
return hud
|
||||
}
|
||||
|
||||
it('initializes HudManager with Glacial Spike available in Sorceress profile', () => {
|
||||
const hudManager = createMockHud()
|
||||
expect(hudManager.hotkeys.availableSkills.some(s => s.skillId === 55)).toBe(true)
|
||||
})
|
||||
|
||||
it('allocates new skill in skill tree and adds it to hotkeys.availableSkills', () => {
|
||||
const hudManager = createMockHud()
|
||||
// Open Skill tree panel
|
||||
hudManager.rightPanel = 'skill'
|
||||
hudManager.skillTree.visible = true
|
||||
hudManager.skillTree.unspentSkillPoints = 5
|
||||
|
||||
// Select lightning tab (tab 1)
|
||||
hudManager.skillTree.activeTab = 1
|
||||
|
||||
// Find Nova (48) which requires level 12 and has reqskills: Static Field (42) which is already allocated in starter
|
||||
const nova = hudManager.hotkeys.availableSkills.find(s => s.skillId === 48)
|
||||
expect(nova).toBeDefined()
|
||||
|
||||
// Allocate point into Nova by clicking on it
|
||||
const node = hudManager.skillTree.getSkillTree().find(n => n.skillId === 48)!
|
||||
expect(node).toBeDefined()
|
||||
|
||||
const prevPoints = hudManager.skillTree.hardPoints.get(48) ?? 0
|
||||
const allocated = hudManager.skillTree.allocateSkill(48)
|
||||
expect(allocated).toBe(true)
|
||||
|
||||
// Test handleSkillAllocated
|
||||
const privateMethod = (hudManager as any).handleSkillAllocated.bind(hudManager)
|
||||
privateMethod(48)
|
||||
|
||||
const updatedNova = hudManager.hotkeys.availableSkills.find(s => s.skillId === 48)
|
||||
expect(updatedNova).toBeDefined()
|
||||
expect(updatedNova!.level).toBe(prevPoints + 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -230,5 +230,27 @@ describe('Ground Items Pickup & Pathfinding (Issue #391)', () => {
|
|||
expect(engine.groundItems.count).toBe(0)
|
||||
expect(status.textContent).toBe('拾起物品:戒指')
|
||||
})
|
||||
|
||||
it('cleans up both groundItems and engine.ground on pickup, leaving zero residual dots', () => {
|
||||
const engine = createTestEngine()
|
||||
engine.world.player.x = 500
|
||||
engine.world.player.y = 500
|
||||
|
||||
const entity = engine.dropItem(
|
||||
{ id: 'sword', name: 'Short Sword', nameZh: '短剑', invWidth: 1, invHeight: 2 },
|
||||
505,
|
||||
505,
|
||||
)
|
||||
|
||||
expect(engine.groundItems.count).toBe(1)
|
||||
expect(engine.ground.length).toBe(1)
|
||||
|
||||
const { controller } = createMockController(engine)
|
||||
controller.pickupGroundItem(entity)
|
||||
|
||||
// Verified: Both groundItems and engine.ground are cleanly cleared
|
||||
expect(engine.groundItems.count).toBe(0)
|
||||
expect(engine.ground.length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -270,4 +270,53 @@ describe('Issue #389 — Ground Item Rendering & Glint Sparkle Ground Truth', ()
|
|||
expect(drawnQuads.length).toBe(6)
|
||||
})
|
||||
})
|
||||
|
||||
describe('5. Authentic Ground Item Sprite Parity (Anti-Blue-Box & Clean Pickup)', () => {
|
||||
it('renders authentic item sprite using itemsAtlas instead of solid color box', () => {
|
||||
const drawnQuads: { x: number; y: number; w: number; h: number; color: readonly [number, number, number, number] }[] = []
|
||||
const drawnSprites: { frame: any; x: number; y: number; options: any }[] = []
|
||||
const renderer = {
|
||||
drawSolid(x: number, y: number, w: number, h: number, color: readonly [number, number, number, number]) {
|
||||
drawnQuads.push({ x, y, w, h, color })
|
||||
},
|
||||
draw(frame: any, x: number, y: number, options: any) {
|
||||
drawnSprites.push({ frame, x, y, options })
|
||||
},
|
||||
} as unknown as SpriteRenderer
|
||||
|
||||
const mockAtlas = { texture: {} as WebGLTexture, width: 1024, height: 1024 } as any
|
||||
const magicSword: GroundItemEntity = {
|
||||
id: 'magic_sword',
|
||||
item: { name: 'Short Sword', code: 'ssd' },
|
||||
name: 'Short Sword',
|
||||
nameZh: '短剑',
|
||||
quality: 'magic',
|
||||
isGold: false,
|
||||
amount: 1,
|
||||
invWidth: 1,
|
||||
invHeight: 3,
|
||||
dropTime: 0,
|
||||
x: 100,
|
||||
y: 200,
|
||||
cellX: 0,
|
||||
cellY: 0,
|
||||
sparklePhase: 0,
|
||||
}
|
||||
|
||||
drawGroundItem(renderer, magicSword, 1000, mockAtlas)
|
||||
|
||||
// Verified: Sprite is drawn using itemsAtlas
|
||||
expect(drawnSprites.length).toBe(1)
|
||||
const sprite = drawnSprites[0]!
|
||||
expect(sprite.options.atlas).toBe(mockAtlas)
|
||||
expect(sprite.options.width).toBeGreaterThan(0)
|
||||
expect(sprite.options.height).toBeGreaterThan(0)
|
||||
// Drop shadow is drawn on ground
|
||||
expect(drawnQuads.some(q => q.color[3] < 0.5 && q.y >= 198)).toBe(true)
|
||||
// Crucial: No opaque blue body box drawn in quads
|
||||
const blueBody = drawnQuads.find(q => q.color[2] > 0.8 && q.w > 10 && q.h > 10)
|
||||
expect(blueBody).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -33,14 +33,20 @@ describe('Diablo II v1.13c Quickbar & Speedbar Authentic Mechanics (Issue #382)'
|
|||
})
|
||||
|
||||
describe('1. Passive Skills Filtering (被动技能不进入快捷栏)', () => {
|
||||
it('identifies all 23 canonical Diablo II 1.13c passive skills', () => {
|
||||
// Amazon passives
|
||||
it('identifies all 26 canonical Diablo II 1.13c passive skills', () => {
|
||||
// Amazon passives (Critical Strike 9, Dodge 13, Avoid 18, Penetrate 23, Evade 29, Pierce 33)
|
||||
expect(isPassiveSkill(9)).toBe(true) // Critical Strike
|
||||
expect(isPassiveSkill(14)).toBe(true) // Penetrate
|
||||
expect(isPassiveSkill(17)).toBe(true) // Dodge
|
||||
expect(isPassiveSkill(22)).toBe(true) // Avoid
|
||||
expect(isPassiveSkill(27)).toBe(true) // Evade
|
||||
expect(isPassiveSkill(29)).toBe(true) // Pierce
|
||||
expect(isPassiveSkill(13)).toBe(true) // Dodge
|
||||
expect(isPassiveSkill(18)).toBe(true) // Avoid
|
||||
expect(isPassiveSkill(23)).toBe(true) // Penetrate
|
||||
expect(isPassiveSkill(29)).toBe(true) // Evade
|
||||
expect(isPassiveSkill(33)).toBe(true) // Pierce
|
||||
|
||||
// Amazon active skills that must not be marked as passive
|
||||
expect(isPassiveSkill(14)).toBe(false) // Power Strike
|
||||
expect(isPassiveSkill(17)).toBe(false) // Slow Missiles
|
||||
expect(isPassiveSkill(22)).toBe(false) // Guided Arrow
|
||||
expect(isPassiveSkill(27)).toBe(false) // Immolation Arrow
|
||||
|
||||
// Sorceress passives
|
||||
expect(isPassiveSkill(37)).toBe(true) // Warmth
|
||||
|
|
|
|||
|
|
@ -254,4 +254,93 @@ describe('SkillHotkeysHud & Dual Slot Mechanics', () => {
|
|||
hud.assignSkill('right', 3)
|
||||
expect(hud.rightSkillId).toBe(3)
|
||||
})
|
||||
|
||||
it('includes Glacial Spike (55) in right-click and left-click speedbars and allows assignment (Issue #385)', () => {
|
||||
// 1. Verify Glacial Spike is in availableSkills
|
||||
const glacialSpike = hud.availableSkills.find((s) => s.skillId === 55)
|
||||
expect(glacialSpike).toBeDefined()
|
||||
expect(glacialSpike!.name).toBe('Glacial Spike')
|
||||
expect(glacialSpike!.nameZh).toBe('冰尖柱')
|
||||
expect(glacialSpike!.level).toBe(10)
|
||||
expect(glacialSpike!.leftUsable).toBe(true)
|
||||
expect(glacialSpike!.rightUsable).toBe(true)
|
||||
|
||||
// 2. Right speedbar contains Glacial Spike
|
||||
const rightSpeedbar = hud.getSpeedbarSkills('right')
|
||||
const rightCell = rightSpeedbar.find((c) => c.skill.skillId === 55)
|
||||
expect(rightCell).toBeDefined()
|
||||
|
||||
// Click on Glacial Spike in right speedbar
|
||||
hud.openSpeedbar('right')
|
||||
const clickedRight = hud.handleClick(rightCell!.x + 10, rightCell!.y + 10)
|
||||
expect(clickedRight).toBe(true)
|
||||
expect(hud.rightSkillId).toBe(55)
|
||||
expect(hud.selectionOpen).toBeNull()
|
||||
|
||||
// 3. Left speedbar contains Glacial Spike
|
||||
const leftSpeedbar = hud.getSpeedbarSkills('left')
|
||||
const leftCell = leftSpeedbar.find((c) => c.skill.skillId === 55)
|
||||
expect(leftCell).toBeDefined()
|
||||
|
||||
// Click on Glacial Spike in left speedbar
|
||||
hud.openSpeedbar('left')
|
||||
const clickedLeft = hud.handleClick(leftCell!.x + 10, leftCell!.y + 10)
|
||||
expect(clickedLeft).toBe(true)
|
||||
expect(hud.leftSkillId).toBe(55)
|
||||
expect(hud.selectionOpen).toBeNull()
|
||||
|
||||
// 4. Icon resolution
|
||||
const icon = resolveSkillIcon(55)
|
||||
expect(icon.skillId).toBe(55)
|
||||
expect(icon.iconPath).toBe('/skills/icon_55.png')
|
||||
})
|
||||
|
||||
it('includes all 26 active Sorceress skills on right-click speedbar and excludes all 4 passives', () => {
|
||||
const rightSkills = hud.getSpeedbarSkills('right')
|
||||
// 3 universal (Attack 0, Throw 2, Unsummon 3) + 26 class active = 29 skills
|
||||
expect(rightSkills.length).toBe(29)
|
||||
|
||||
// Verify all 26 active Sorceress skills are present
|
||||
const activeIds = [
|
||||
36, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
|
||||
51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 62, 64,
|
||||
]
|
||||
for (const id of activeIds) {
|
||||
expect(rightSkills.some((c) => c.skill.skillId === id)).toBe(true)
|
||||
}
|
||||
|
||||
// Verify the 4 passives are NOT present
|
||||
const passiveIds = [37, 61, 63, 65]
|
||||
for (const id of passiveIds) {
|
||||
expect(rightSkills.some((c) => c.skill.skillId === id)).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('allows adding and updating skills dynamically via addOrUpdateSkill', () => {
|
||||
// Add custom skill
|
||||
hud.addOrUpdateSkill({
|
||||
skillId: 48, // Nova
|
||||
name: 'Nova',
|
||||
nameZh: '新星',
|
||||
level: 15,
|
||||
manaCost: 20,
|
||||
leftUsable: false,
|
||||
rightUsable: true,
|
||||
})
|
||||
const nova = hud.availableSkills.find((s) => s.skillId === 48)
|
||||
expect(nova).toBeDefined()
|
||||
expect(nova!.level).toBe(15)
|
||||
|
||||
// Cannot add passive skill
|
||||
hud.addOrUpdateSkill({
|
||||
skillId: 37, // Warmth
|
||||
name: 'Warmth',
|
||||
nameZh: '暖气',
|
||||
level: 5,
|
||||
manaCost: 0,
|
||||
leftUsable: false,
|
||||
rightUsable: true,
|
||||
})
|
||||
expect(hud.availableSkills.some((s) => s.skillId === 37)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||