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` 等)。
|
- 所有技能投射物(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 运行时直接高效索引与渲染。
|
- 必须通过离线解包烘焙脚本(如 `scripts/pack-missiles.ts`)结合章节调色板(`ACT1/pal.pl2`),将原版 DCC/DC6 的全部朝向(如 16 方向)与全帧序列预打包烘焙为 Web 原生 Sprite Atlas(图集)及对应元数据,供前端 WebGL 运行时直接高效索引与渲染。
|
||||||
- 音效接入解耦:现阶段音效系统(`Sounds.txt` / WAV 音频资源)允许暂缓接入,但视觉动画必须具备 100% 像素级原版 DCC 贴图资产与帧率对齐。
|
- 音效接入解耦:现阶段音效系统(`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. **严苛静态类型与全量单测**:
|
2. **严苛静态类型与全量单测**:
|
||||||
- `npm run typecheck` 必须保持 **0 errors**。
|
- `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` 等防退化单测)。
|
- `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.
|
* Offline Missile Asset & Data Baking Script.
|
||||||
*
|
*
|
||||||
* Extracts authentic Diablo II v1.13c missile DCC sprites from d2data.mpq:
|
* Extracts authentic Diablo II v1.13c missile DCC sprites from MPQ archives:
|
||||||
* - Firebolt.dcc (16 directions, 5 frames per direction)
|
* - Fire: Firebolt, Fireball, FireArrowExplode2
|
||||||
* - Fireball.dcc (16 directions, 5 frames per direction)
|
* - Cold: Icebolt, IceBlast, GlacialSpike, IceArrowExplode, FreezeExplodeCenter
|
||||||
* - FireArrowExplode2.dcc (1 direction, 12 frames)
|
* - 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:
|
* Bakes them into indexed-color PNG atlases and JSON metadata under:
|
||||||
* - public/missiles/
|
* - public/missiles/
|
||||||
|
|
@ -20,7 +23,7 @@ import { MpqArchive } from '../src/mpq/archive.ts'
|
||||||
import { fileSource } from '../src/mpq/file-source.ts'
|
import { fileSource } from '../src/mpq/file-source.ts'
|
||||||
import { decodeDcc } from '../src/formats/dcc.ts'
|
import { decodeDcc } from '../src/formats/dcc.ts'
|
||||||
import { decodePl2 } from '../src/formats/pl2.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'
|
import type { SpriteFrame, SpriteSheet } from '../src/formats/sprite.ts'
|
||||||
|
|
||||||
export interface MissileFrameMeta {
|
export interface MissileFrameMeta {
|
||||||
|
|
@ -53,18 +56,196 @@ export interface MissileMeta {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MISSILE_TARGETS = [
|
const MISSILE_TARGETS = [
|
||||||
|
// Fire spells & explosions
|
||||||
{ name: 'firebolt', celFile: 'Firebolt', animSpeed: 16 },
|
{ name: 'firebolt', celFile: 'Firebolt', animSpeed: 16 },
|
||||||
{ name: 'fireball', celFile: 'Fireball', animSpeed: 16 },
|
{ name: 'fireball', celFile: 'Fireball', animSpeed: 16 },
|
||||||
{ name: 'fireexplode', celFile: 'FireArrowExplode2', 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
|
] as const
|
||||||
|
|
||||||
export async function bakeMissiles(projectRoot: string = process.cwd()): Promise<Record<string, MissileMeta>> {
|
const PAGE_SIZE = 4096
|
||||||
const mpqPath = join(projectRoot, 'samples', 'd2', 'd2data.mpq')
|
|
||||||
const mpq = await MpqArchive.open(await fileSource(mpqPath))
|
|
||||||
|
|
||||||
const palFile = mpq.find('data/global/palette/ACT1/pal.pl2')
|
/**
|
||||||
if (!palFile) throw new Error('data/global/palette/ACT1/pal.pl2 not found in d2data.mpq')
|
* Shelf-pack all frames of a missile SpriteSheet into a 32-bit RGBA PNG buffer.
|
||||||
const pl2 = decodePl2(await mpq.read(palFile))
|
* 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 publicOutDir = join(projectRoot, 'public', 'missiles')
|
||||||
const packOutDir = join(projectRoot, 'samples', 'd2-packs', '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> = {}
|
const metaRecord: Record<string, MissileMeta> = {}
|
||||||
|
|
||||||
for (const target of MISSILE_TARGETS) {
|
for (const target of MISSILE_TARGETS) {
|
||||||
const f = mpq.find(`data/global/missiles/${target.celFile}.dcc`)
|
const raw = await readMpqFile(`data/global/missiles/${target.celFile}.dcc`)
|
||||||
if (!f) throw new Error(`Missile DCC not found: 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 => ({
|
const groups = dcc.directions.map(dir => ({
|
||||||
frames: dir.frames.map(
|
frames: dir.frames.map(
|
||||||
(frame): SpriteFrame => ({
|
(frame): SpriteFrame => ({
|
||||||
|
|
@ -92,7 +273,8 @@ export async function bakeMissiles(projectRoot: string = process.cwd()): Promise
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const sheet: SpriteSheet = { groups, width: null }
|
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 = {
|
const meta: MissileMeta = {
|
||||||
name: target.name,
|
name: target.name,
|
||||||
|
|
@ -118,6 +300,15 @@ export async function bakeMissiles(projectRoot: string = process.cwd()): Promise
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate src/render/missiles-meta.ts
|
// 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 = `/**
|
const metaTsContent = `/**
|
||||||
* Authentic Diablo II v1.13c Missile Atlas Metadata.
|
* Authentic Diablo II v1.13c Missile Atlas Metadata.
|
||||||
* Auto-generated by scripts/pack-missiles.ts.
|
* Auto-generated by scripts/pack-missiles.ts.
|
||||||
|
|
@ -157,12 +348,32 @@ export interface MissileMeta {
|
||||||
* The engine's 64-direction space -> DCC direction tables.
|
* The engine's 64-direction space -> DCC direction tables.
|
||||||
* Ported from OpenDiablo2 d2fileformats/d2dcc/dcc_dir_lookup.go (Dir64ToDcc).
|
* Ported from OpenDiablo2 d2fileformats/d2dcc/dcc_dir_lookup.go (Dir64ToDcc).
|
||||||
*/
|
*/
|
||||||
const DIR64_TO_DCC_16: readonly number[] = [
|
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,
|
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,
|
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,
|
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,
|
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.
|
* 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
|
// Map angle so that South (PI/2) is 0, turning clockwise
|
||||||
const fraction = (angle - Math.PI / 2) / (2 * Math.PI)
|
const fraction = (angle - Math.PI / 2) / (2 * Math.PI)
|
||||||
const dir64 = Math.round((((fraction % 1) + 1) % 1) * 64) % 64
|
const dir64 = Math.round((((fraction % 1) + 1) % 1) * 64) % 64
|
||||||
if (directions === 16) {
|
const table = DIR64_TO_DCC[directions]
|
||||||
return DIR64_TO_DCC_16[dir64] ?? 0
|
if (table !== undefined) {
|
||||||
|
return table[dir64] ?? 0
|
||||||
}
|
}
|
||||||
return 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 MISSILE_METAS: Readonly<Record<string, MissileMeta>> = Object.freeze({
|
||||||
|
${mapEntries}
|
||||||
export const FIREEXPLODE_META: MissileMeta = ${JSON.stringify(metaRecord['fireexplode'], null, 2)}
|
})
|
||||||
|
|
||||||
export const MISSILE_METAS: Readonly<Record<string, MissileMeta>> = {
|
|
||||||
firebolt: FIREBOLT_META,
|
|
||||||
fireball: FIREBALL_META,
|
|
||||||
fireexplode: FIREEXPLODE_META,
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
const metaTsPath = join(projectRoot, 'src', 'render', 'missiles-meta.ts')
|
|
||||||
writeFileSync(metaTsPath, metaTsContent, 'utf-8')
|
const outTsPath = join(projectRoot, 'src', 'render', 'missiles-meta.ts')
|
||||||
console.log(`[Missile Packer] Wrote ${metaTsPath}`)
|
writeFileSync(outTsPath, metaTsContent, 'utf-8')
|
||||||
|
console.log(`[Missile Packer] Wrote ${outTsPath}`)
|
||||||
|
|
||||||
return metaRecord
|
return metaRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute standalone if called from CLI
|
// Direct invocation via tsx
|
||||||
if (process.argv[1]?.endsWith('pack-missiles.ts')) {
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
bakeMissiles().catch((err: unknown) => {
|
bakeMissiles()
|
||||||
console.error('Bake missiles failed:', err)
|
.then(() => {
|
||||||
|
console.log('[Missile Packer] All missile assets baked successfully.')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('[Missile Packer] Failed:', err)
|
||||||
process.exit(1)
|
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 }
|
for (const part of parts) { out.set(part, offset); offset += part.byteLength }
|
||||||
return out
|
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 { CharacterClassCode } from './classes.ts'
|
||||||
import type { EquipSlotId, GridPlacement, UiInventoryItem } from '../ui/inventory.ts'
|
import type { EquipSlotId, GridPlacement, UiInventoryItem } from '../ui/inventory.ts'
|
||||||
import type { CharacterAttributes } from '../ui/character-sheet.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 {
|
export interface ClassProfileState {
|
||||||
readonly classCode: CharacterClassCode
|
readonly classCode: CharacterClassCode
|
||||||
|
|
@ -1648,124 +1656,46 @@ export const CLASS_STARTER_HARD_POINTS: Record<CharacterClassCode, Readonly<Reco
|
||||||
// 6. DEFAULT SKILLS & HOTKEYS PER CLASS
|
// 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[]> = {
|
export const CLASS_DEFAULT_SKILLS: Record<CharacterClassCode, readonly HotkeySkillEntry[]> = {
|
||||||
ama: [
|
ama: buildClassDefaultSkills('ama'),
|
||||||
{ skillId: 0, name: 'Attack', nameZh: '普通攻击', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
sor: DEFAULT_SORCERESS_SKILLS,
|
||||||
{ skillId: 2, name: 'Throw', nameZh: '投掷', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
nec: buildClassDefaultSkills('nec'),
|
||||||
{ skillId: 3, name: 'Unsummon', nameZh: '取消召唤', level: 1, manaCost: 0, leftUsable: false, rightUsable: true },
|
pal: buildClassDefaultSkills('pal'),
|
||||||
{ skillId: 6, name: 'Magic Arrow', nameZh: '魔法箭', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
bar: buildClassDefaultSkills('bar'),
|
||||||
{ skillId: 7, name: 'Fire Arrow', nameZh: '火箭', level: 1, manaCost: 3, leftUsable: true, rightUsable: true },
|
dru: buildClassDefaultSkills('dru'),
|
||||||
{ skillId: 11, name: 'Cold Arrow', nameZh: '冰箭', level: 1, manaCost: 3.5, leftUsable: true, rightUsable: true },
|
ass: buildClassDefaultSkills('ass'),
|
||||||
{ 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 },
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,12 @@ export interface Monster {
|
||||||
corpseTicks: number
|
corpseTicks: number
|
||||||
/** Whether the monster is sleeping (inactive off-screen room). */
|
/** Whether the monster is sleeping (inactive off-screen room). */
|
||||||
sleeping?: boolean | undefined
|
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. */
|
/** 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.hitFlash > 0) monster.hitFlash -= 1
|
||||||
if (monster.cooldown > 0) monster.cooldown -= 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) {
|
if (options.disableMonsterAggro) {
|
||||||
monster.state = 'idle'
|
monster.state = 'idle'
|
||||||
|
|
@ -876,7 +896,8 @@ function tickMonsters(world: CombatWorld, options: CombatOptions, terrain: Comba
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
monster.state = 'chase'
|
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)
|
const movedMonster = moveWithCollision(monster, (dx / distance) * step, (dy / distance) * step, terrain)
|
||||||
monster.x = movedMonster.x
|
monster.x = movedMonster.x
|
||||||
monster.y = movedMonster.y
|
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 type { CombatWorld, CombatOptions, MonsterPack, MonsterStats, SafeZone } from './combat.ts'
|
||||||
import { Inventory, rollDrop, goldItem, totalStat } from './items.ts'
|
import { Inventory, rollDrop, goldItem, totalStat } from './items.ts'
|
||||||
import type { Item, ItemBase, Affix } 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 type { SkillDef, Projectile } from './skills.ts'
|
||||||
import { QuestLog, npcDialog } from './quests.ts'
|
import { QuestLog, npcDialog } from './quests.ts'
|
||||||
import type { NpcDef, QuestDef } from './quests.ts'
|
import type { NpcDef, QuestDef } from './quests.ts'
|
||||||
|
|
@ -323,21 +323,77 @@ export class GameEngine {
|
||||||
this.projectiles.length = 0
|
this.projectiles.length = 0
|
||||||
this.projectiles.push(...outcome.alive)
|
this.projectiles.push(...outcome.alive)
|
||||||
for (const hit of outcome.hits) {
|
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)
|
damageMonster(this.world, hit.targetIndex, hit.damage)
|
||||||
this.metrics.castHits += 1
|
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)
|
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({
|
this.explosions.push({
|
||||||
missileType: 'fireexplode',
|
missileType: explType,
|
||||||
x: hit.x,
|
x: hit.x,
|
||||||
y: hit.y,
|
y: hit.y,
|
||||||
frame: 0,
|
frame: 0,
|
||||||
maxFrames: 12,
|
maxFrames,
|
||||||
radius: 24,
|
radius: hit.aoeRadius ?? 24,
|
||||||
})
|
})
|
||||||
this.world.events.push({
|
this.world.events.push({
|
||||||
kind: 'missileExplode',
|
kind: 'missileExplode',
|
||||||
missileType: 'fireexplode',
|
missileType: explType,
|
||||||
skillId: hit.skillId,
|
skillId: hit.skillId,
|
||||||
x: hit.x,
|
x: hit.x,
|
||||||
y: hit.y,
|
y: hit.y,
|
||||||
|
|
@ -345,21 +401,34 @@ export class GameEngine {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outcome.wallHitEvents) {
|
if (outcome.wallHitEvents) {
|
||||||
for (const wallHit of 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)
|
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({
|
this.explosions.push({
|
||||||
missileType: 'fireexplode',
|
missileType: explType,
|
||||||
x: wallHit.x,
|
x: wallHit.x,
|
||||||
y: wallHit.y,
|
y: wallHit.y,
|
||||||
frame: 0,
|
frame: 0,
|
||||||
maxFrames: 12,
|
maxFrames,
|
||||||
radius: 24,
|
radius: wallHit.aoeRadius ?? 24,
|
||||||
})
|
})
|
||||||
this.world.events.push({
|
this.world.events.push({
|
||||||
kind: 'missileExplode',
|
kind: 'missileExplode',
|
||||||
missileType: 'fireexplode',
|
missileType: explType,
|
||||||
skillId: wallHit.skillId,
|
skillId: wallHit.skillId,
|
||||||
x: wallHit.x,
|
x: wallHit.x,
|
||||||
y: wallHit.y,
|
y: wallHit.y,
|
||||||
|
|
@ -605,7 +674,7 @@ export class GameEngine {
|
||||||
}
|
}
|
||||||
const amount = entity.amount || 1
|
const amount = entity.amount || 1
|
||||||
this.groundItems.remove(id)
|
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) {
|
if (gIdx !== -1) {
|
||||||
this.ground.splice(gIdx, 1)
|
this.ground.splice(gIdx, 1)
|
||||||
}
|
}
|
||||||
|
|
@ -639,7 +708,7 @@ export class GameEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.groundItems.remove(id)
|
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) {
|
if (gIdx !== -1) {
|
||||||
this.ground.splice(gIdx, 1)
|
this.ground.splice(gIdx, 1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -285,18 +285,19 @@ export class MissileEngine {
|
||||||
startX: m.x,
|
startX: m.x,
|
||||||
startY: m.y,
|
startY: m.y,
|
||||||
targetX: m.x + Math.cos(subAngle) * 100,
|
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,
|
dmgPacket: m.dmgPacket,
|
||||||
})
|
})
|
||||||
subMissilesSpawned.push(subName)
|
subMissilesSpawned.push(subName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spiral flight for Blessed Hammer (`blessedhammer`, `skillId = 112`)
|
// 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') {
|
if (m.sourceSkillId === 112 || m.name.toLowerCase() === 'blessedhammer') {
|
||||||
m.angleRad += 0.24
|
m.angleRad += 0.24
|
||||||
const radius = 14 + m.ageTicks * 4
|
const radius = 14 + m.ageTicks * 4
|
||||||
m.x += Math.cos(m.angleRad) * (radius * 0.25)
|
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 {
|
} else {
|
||||||
m.x += m.vx
|
m.x += m.vx
|
||||||
m.y += m.vy
|
m.y += m.vy
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,43 @@ export interface Projectile {
|
||||||
readonly fromPlayer: boolean
|
readonly fromPlayer: boolean
|
||||||
/** Optional missile type / token (e.g. 'firebolt'). */
|
/** Optional missile type / token (e.g. 'firebolt'). */
|
||||||
readonly missileType?: string | undefined
|
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. */
|
/** What a cast produced. */
|
||||||
export type CastResult =
|
export type CastResult =
|
||||||
| { readonly kind: 'mana'; readonly cost: number }
|
| { readonly kind: 'mana'; readonly cost: number }
|
||||||
|
|
@ -144,6 +179,26 @@ export const CANONICAL_113C_MISSILES: Readonly<Record<string, MissileTxtData>> =
|
||||||
speedPxPerSec: 500,
|
speedPxPerSec: 500,
|
||||||
distancePx: 1000,
|
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: {
|
fireexplode: {
|
||||||
name: 'fireexplode',
|
name: 'fireexplode',
|
||||||
id: 29,
|
id: 29,
|
||||||
|
|
@ -164,6 +219,466 @@ export const CANONICAL_113C_MISSILES: Readonly<Record<string, MissileTxtData>> =
|
||||||
speedPxPerSec: 0,
|
speedPxPerSec: 0,
|
||||||
distancePx: 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 y: number
|
||||||
readonly skillId?: string | undefined
|
readonly skillId?: string | undefined
|
||||||
readonly missileType?: 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. */
|
/** Projectiles that died on a wall or expired. */
|
||||||
readonly expired: number
|
readonly expired: number
|
||||||
|
|
@ -554,6 +1073,7 @@ export interface ProjectileOutcome {
|
||||||
readonly y: number
|
readonly y: number
|
||||||
readonly skillId?: string | undefined
|
readonly skillId?: string | undefined
|
||||||
readonly missileType?: string | undefined
|
readonly missileType?: string | undefined
|
||||||
|
readonly aoeRadius?: number | undefined
|
||||||
}[] | undefined
|
}[] | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -583,23 +1103,97 @@ export function tickProjectiles(
|
||||||
y: number
|
y: number
|
||||||
skillId?: string | undefined
|
skillId?: string | undefined
|
||||||
missileType?: string | undefined
|
missileType?: string | undefined
|
||||||
|
aoeRadius?: number | undefined
|
||||||
|
statusEffect?: 'chill' | 'freeze' | 'poison' | undefined
|
||||||
|
statusDuration?: number | undefined
|
||||||
|
isUndeadOnly?: boolean | undefined
|
||||||
}[] = []
|
}[] = []
|
||||||
const wallHitEvents: {
|
const wallHitEvents: {
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
skillId?: string | undefined
|
skillId?: string | undefined
|
||||||
missileType?: string | undefined
|
missileType?: string | undefined
|
||||||
|
aoeRadius?: number | undefined
|
||||||
}[] = []
|
}[] = []
|
||||||
let expired = 0
|
let expired = 0
|
||||||
let wallHits = 0
|
let wallHits = 0
|
||||||
|
|
||||||
for (const projectile of projectiles) {
|
for (const projectile of projectiles) {
|
||||||
if (projectile.ttl <= 0) { expired += 1; continue }
|
if (projectile.ttl <= 0) { expired += 1; continue }
|
||||||
const x = projectile.x + projectile.vx
|
|
||||||
const y = projectile.y + projectile.vy
|
let vx = projectile.vx
|
||||||
const moved: Projectile = { ...projectile, x, y, ttl: projectile.ttl - 1 }
|
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)
|
// 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 =
|
const blockedByTerrain =
|
||||||
options.isMissileBlocked !== undefined
|
options.isMissileBlocked !== undefined
|
||||||
? options.isMissileBlocked(x, y, projectile.x, projectile.y)
|
? options.isMissileBlocked(x, y, projectile.x, projectile.y)
|
||||||
|
|
@ -611,13 +1205,17 @@ export function tickProjectiles(
|
||||||
y,
|
y,
|
||||||
skillId: projectile.skillId,
|
skillId: projectile.skillId,
|
||||||
missileType: projectile.missileType,
|
missileType: projectile.missileType,
|
||||||
|
aoeRadius: projectile.aoeRadius,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let struck = false
|
let struck = false
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
if (!target.alive) continue
|
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
|
if (Math.hypot(target.x - x, target.y - y) > target.radius + radius) continue
|
||||||
|
|
||||||
hits.push({
|
hits.push({
|
||||||
targetIndex: target.index,
|
targetIndex: target.index,
|
||||||
damage: projectile.damage,
|
damage: projectile.damage,
|
||||||
|
|
@ -625,11 +1223,22 @@ export function tickProjectiles(
|
||||||
y,
|
y,
|
||||||
skillId: projectile.skillId,
|
skillId: projectile.skillId,
|
||||||
missileType: projectile.missileType,
|
missileType: projectile.missileType,
|
||||||
|
aoeRadius: projectile.aoeRadius,
|
||||||
|
statusEffect: projectile.statusEffect,
|
||||||
|
statusDuration: projectile.statusDuration,
|
||||||
|
isUndeadOnly: projectile.isUndeadOnly,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
hitTargets.add(target.index)
|
||||||
|
if (!projectile.pierce) {
|
||||||
struck = true
|
struck = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (!struck) alive.push(moved)
|
}
|
||||||
|
|
||||||
|
if (!struck) {
|
||||||
|
alive.push(moved)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { alive, hits, expired, wallHits, wallHitEvents }
|
return { alive, hits, expired, wallHits, wallHitEvents }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,12 @@ export interface DrawOptions {
|
||||||
* - 4: Champion / Elite Blue Tint
|
* - 4: Champion / Elite Blue Tint
|
||||||
*/
|
*/
|
||||||
readonly paletteRow?: number
|
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. */
|
/** Configuration options and event callbacks for SpriteRenderer. */
|
||||||
|
|
@ -252,6 +258,8 @@ export class SpriteRenderer {
|
||||||
private frameDrawCalls = 0
|
private frameDrawCalls = 0
|
||||||
/** Quads submitted since the last {@link begin}. */
|
/** Quads submitted since the last {@link begin}. */
|
||||||
private frameQuads = 0
|
private frameQuads = 0
|
||||||
|
/** Current active blend mode ('normal' or 'additive'). */
|
||||||
|
private currentBlendMode: 'normal' | 'additive' = 'normal'
|
||||||
|
|
||||||
/** All textures allocated and managed by this renderer. */
|
/** All textures allocated and managed by this renderer. */
|
||||||
private readonly allocatedTextures = new Set<WebGLTexture>()
|
private readonly allocatedTextures = new Set<WebGLTexture>()
|
||||||
|
|
@ -757,6 +765,10 @@ export class SpriteRenderer {
|
||||||
this.batchTextureCount = 0
|
this.batchTextureCount = 0
|
||||||
this.frameDrawCalls = 0
|
this.frameDrawCalls = 0
|
||||||
this.frameQuads = 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.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||||||
gl.clearColor(clear[0], clear[1], clear[2], 1)
|
gl.clearColor(clear[0], clear[1], clear[2], 1)
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT)
|
gl.clear(gl.COLOR_BUFFER_BIT)
|
||||||
|
|
@ -772,6 +784,17 @@ export class SpriteRenderer {
|
||||||
*/
|
*/
|
||||||
draw(frame: AtlasFrame, x: number, y: number, options: DrawOptions = {}): void {
|
draw(frame: AtlasFrame, x: number, y: number, options: DrawOptions = {}): void {
|
||||||
if (this.disposed) return
|
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 page = options.atlas ?? this.defaultAtlas
|
||||||
const unit = this.unitFor(page)
|
const unit = this.unitFor(page)
|
||||||
const u0 = frame.x / page.width
|
const u0 = frame.x / page.width
|
||||||
|
|
@ -781,8 +804,10 @@ export class SpriteRenderer {
|
||||||
const tint = options.tint
|
const tint = options.tint
|
||||||
const flipped = options.flipX === true
|
const flipped = options.flipX === true
|
||||||
const paletteRow = page.indexed === true ? (options.paletteRow ?? 0) : -1
|
const paletteRow = page.indexed === true ? (options.paletteRow ?? 0) : -1
|
||||||
|
const drawW = options.width ?? frame.width
|
||||||
|
const drawH = options.height ?? frame.height
|
||||||
this.quad(
|
this.quad(
|
||||||
x, y, x + frame.width, y + frame.height,
|
x, y, x + drawW, y + drawH,
|
||||||
flipped ? u1 : u0, v0,
|
flipped ? u1 : u0, v0,
|
||||||
flipped ? u0 : u1, v1,
|
flipped ? u0 : u1, v1,
|
||||||
tint === undefined ? 1 : tint[0],
|
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 {
|
drawSolid(x: number, y: number, width: number, height: number, color: readonly [number, number, number, number]): void {
|
||||||
if (this.disposed) return
|
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)
|
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)
|
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 { buildNpcDef, hasPackedSprite } from '../game/npc.ts'
|
||||||
import { npcDialog } from '../game/quests.ts'
|
import { npcDialog } from '../game/quests.ts'
|
||||||
import type { NpcDef } 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 type { Projectile } from '../game/skills.ts'
|
||||||
import { findSafeDropPosition, calculateBounceHeight, triggerFlippyBounce, type GroundItemEntity } from '../game/ground-items.ts'
|
import { findSafeDropPosition, calculateBounceHeight, triggerFlippyBounce, type GroundItemEntity } from '../game/ground-items.ts'
|
||||||
import {
|
import {
|
||||||
|
|
@ -53,6 +53,8 @@ import {
|
||||||
computeInitialGroundLabelLayouts,
|
computeInitialGroundLabelLayouts,
|
||||||
resolveLadderCollisions,
|
resolveLadderCollisions,
|
||||||
} from '../ui/ground-labels.ts'
|
} 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 type { AtlasFrame } from '../render/atlas.ts'
|
||||||
import { SpriteRenderer } from '../render/renderer.ts'
|
import { SpriteRenderer } from '../render/renderer.ts'
|
||||||
import type { AtlasHandle } 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 { RoofFadeController } from '../render/roofs.ts'
|
||||||
import { facingToDirection } from '../game/character.ts'
|
import { facingToDirection } from '../game/character.ts'
|
||||||
import type { LoadedMonsterArt } from '../game/monster-art.ts'
|
import type { LoadedMonsterArt } from '../game/monster-art.ts'
|
||||||
import type { MissileMeta } from '../render/missiles-meta.ts'
|
import { velocityToDccDirection, MISSILE_METAS, type MissileMeta, type MissileFrameTuple } from '../render/missiles-meta.ts'
|
||||||
import { velocityToDccDirection } from '../render/missiles-meta.ts'
|
|
||||||
import { ActorAnimator, type AnimClipMeta } from '../game/actor-animator.ts'
|
import { ActorAnimator, type AnimClipMeta } from '../game/actor-animator.ts'
|
||||||
import { resolveMonsterArtSpec } from '../game/monster-mapping.ts'
|
import { resolveMonsterArtSpec } from '../game/monster-mapping.ts'
|
||||||
import { ACT_NAMES_ZH, LEVEL_NAMES_ZH, getLevelNames, sceneNameZh, variantLabelZh } from '../game/level-names-zh.ts'
|
import { ACT_NAMES_ZH, LEVEL_NAMES_ZH, getLevelNames, sceneNameZh, variantLabelZh } from '../game/level-names-zh.ts'
|
||||||
|
|
@ -997,8 +998,28 @@ export function getSkillRange(
|
||||||
const info = getAttackWeaponInfo(weapon)
|
const info = getAttackWeaponInfo(weapon)
|
||||||
return info.reach
|
return info.reach
|
||||||
}
|
}
|
||||||
if (skillId === 36) {
|
const missileSkillMap: Record<number, string> = {
|
||||||
const missile = getMissileTxtData('firebolt')
|
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
|
return missile.distancePx || 1000
|
||||||
}
|
}
|
||||||
const def = getBatch1SkillDef(skillId)
|
const def = getBatch1SkillDef(skillId)
|
||||||
|
|
@ -1010,7 +1031,6 @@ export function getSkillRange(
|
||||||
case 40: return 0
|
case 40: return 0
|
||||||
case 42: return 160
|
case 42: return 160
|
||||||
case 44: return 240
|
case 44: return 240
|
||||||
case 47: return 450
|
|
||||||
case 54: return 450
|
case 54: return 450
|
||||||
case 56: return 450
|
case 56: return 450
|
||||||
case 59: return 450
|
case 59: return 450
|
||||||
|
|
@ -1029,6 +1049,15 @@ export function getSkillMana(skillId: number, hudManager?: HudManager | null): n
|
||||||
return getSkillManaCost(skillId, 1)
|
return getSkillManaCost(skillId, 1)
|
||||||
}
|
}
|
||||||
switch (skillId) {
|
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 40: return 7
|
||||||
case 42: return 9
|
case 42: return 9
|
||||||
case 59: return 27
|
case 59: return 27
|
||||||
|
|
@ -1264,38 +1293,208 @@ export function castSkill(
|
||||||
let damage = 28
|
let damage = 28
|
||||||
let delay = 0
|
let delay = 0
|
||||||
let skillName = '技能'
|
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) {
|
if (skillId === 36) {
|
||||||
speed = 450
|
const missile = getMissileTxtData('firebolt')
|
||||||
range = 450
|
speed = missile.speedPxPerSec || 500
|
||||||
damage = 28
|
range = missile.distancePx || 1000
|
||||||
|
damage = 16
|
||||||
delay = 0
|
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 = '火球'
|
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) {
|
} else if (skillId === 64) {
|
||||||
speed = 400
|
speed = 400
|
||||||
range = 480
|
range = 480
|
||||||
damage = 35
|
damage = 35
|
||||||
delay = 25
|
delay = 25
|
||||||
skillName = '冰封球'
|
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) {
|
} else if (skillId === 49) {
|
||||||
speed = 600
|
speed = 600
|
||||||
range = 500
|
range = 500
|
||||||
|
|
@ -1319,23 +1518,90 @@ export function castSkill(
|
||||||
|
|
||||||
const dx = targetX - player.x
|
const dx = targetX - player.x
|
||||||
const dy = targetY - player.y
|
const dy = targetY - player.y
|
||||||
const len = Math.hypot(dx, dy)
|
const baseAngle = Math.atan2(dy, dx)
|
||||||
const perTick = speed / 25
|
const perTick = speed / 25
|
||||||
const ttl = Math.max(1, Math.round(range / perTick))
|
const ttl = Math.max(1, Math.round(range / perTick))
|
||||||
|
|
||||||
const projectile: Projectile = {
|
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),
|
skillId: String(skillId),
|
||||||
x: player.x,
|
x: player.x,
|
||||||
y: player.y,
|
y: player.y,
|
||||||
vx: len > 0 ? (dx / len) * perTick : perTick,
|
vx: pVx,
|
||||||
vy: len > 0 ? (dy / len) * perTick : 0,
|
vy: pVy,
|
||||||
damage,
|
damage,
|
||||||
ttl,
|
ttl,
|
||||||
fromPlayer: true,
|
fromPlayer: true,
|
||||||
missileType: skillId === 36 ? 'firebolt' : undefined,
|
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
|
player.cooldown = delay > 0 ? delay : 10
|
||||||
engine.metrics.casts += 1
|
engine.metrics.casts += 1
|
||||||
status.textContent = `施展技能:${skillName}`
|
status.textContent = `施展技能:${skillName}`
|
||||||
|
|
@ -1461,6 +1727,10 @@ export class SceneMouseController {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.engine.groundItems.remove(item.id)
|
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.engine.metrics.pickups += 1
|
||||||
this.hudManager.syncPublishedState()
|
this.hudManager.syncPublishedState()
|
||||||
this.status.textContent = `拾起物品:${item.nameZh || item.name}`
|
this.status.textContent = `拾起物品:${item.nameZh || item.name}`
|
||||||
|
|
@ -3249,7 +3519,7 @@ export async function loadMissileArtMap(
|
||||||
): Promise<Map<string, LoadedMissileArt>> {
|
): Promise<Map<string, LoadedMissileArt>> {
|
||||||
const map = new Map<string, LoadedMissileArt>()
|
const map = new Map<string, LoadedMissileArt>()
|
||||||
if (typeof fetch === 'undefined') return map
|
if (typeof fetch === 'undefined') return map
|
||||||
const targets = ['firebolt', 'fireball', 'fireexplode']
|
const targets = Object.keys(MISSILE_METAS)
|
||||||
const baseCandidates = [
|
const baseCandidates = [
|
||||||
'/missiles',
|
'/missiles',
|
||||||
packBase ? `${packBase}/missiles` : '',
|
packBase ? `${packBase}/missiles` : '',
|
||||||
|
|
@ -3274,8 +3544,8 @@ export async function loadMissileArtMap(
|
||||||
} finally {
|
} finally {
|
||||||
bitmap.close()
|
bitmap.close()
|
||||||
}
|
}
|
||||||
const frames: AtlasFrame[][] = meta.groups.map(group =>
|
const frames: AtlasFrame[][] = meta.groups.map((group: readonly MissileFrameTuple[]) =>
|
||||||
group.map(([x, y, width, height, anchorX, anchorY]) => ({
|
group.map(([x, y, width, height, anchorX, anchorY]: MissileFrameTuple) => ({
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width,
|
width,
|
||||||
|
|
@ -3297,11 +3567,11 @@ export async function loadMissileArtMap(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a Diablo II 1.13c style Fire Bolt projectile:
|
* Render a Diablo II 1.13c missile projectile:
|
||||||
* - Uses authentic baked Firebolt.dcc sprite atlas when available.
|
* - Uses authentic baked DCC sprite atlas when available.
|
||||||
* - Gracefully falls back to procedural glowing fireball & trailing particles in test/offline environments.
|
* - Gracefully falls back to procedural glowing fireball & trailing particles in test/offline environments.
|
||||||
*/
|
*/
|
||||||
export function drawFireboltProjectile(
|
export function drawMissileProjectile(
|
||||||
renderer: SpriteRenderer,
|
renderer: SpriteRenderer,
|
||||||
shot: Projectile,
|
shot: Projectile,
|
||||||
missileArt?: LoadedMissileArt,
|
missileArt?: LoadedMissileArt,
|
||||||
|
|
@ -3309,13 +3579,17 @@ export function drawFireboltProjectile(
|
||||||
if (missileArt !== undefined) {
|
if (missileArt !== undefined) {
|
||||||
const dccDir = velocityToDccDirection(shot.vx, shot.vy, missileArt.meta.directions)
|
const dccDir = velocityToDccDirection(shot.vx, shot.vy, missileArt.meta.directions)
|
||||||
const frameCount = Math.max(1, missileArt.meta.framesPerDirection)
|
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 group = missileArt.frames[dccDir] ?? missileArt.frames[0]
|
||||||
const frame = group?.[frameIndex] ?? group?.[0]
|
const frame = group?.[frameIndex] ?? group?.[0]
|
||||||
if (frame !== undefined) {
|
if (frame !== undefined) {
|
||||||
const drawX = frame.anchorX !== undefined ? shot.x + frame.anchorX : shot.x - frame.width / 2
|
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
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3358,12 +3632,14 @@ export function drawFireboltProjectile(
|
||||||
renderer.drawSolid(x - 2, y - 2, 4, 4, [1.0, 1.0, 0.88, 0.98])
|
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):
|
* Render a Diablo II 1.13c style explosion:
|
||||||
* - Uses authentic baked FireArrowExplode2.dcc sprite atlas when available.
|
* - Uses authentic baked DCC sprite atlas when available.
|
||||||
* - Falls back to procedural explosion in test/offline environments.
|
* - Falls back to procedural explosion in test/offline environments.
|
||||||
*/
|
*/
|
||||||
export function drawFireExplosion(
|
export function drawExplosion(
|
||||||
renderer: SpriteRenderer,
|
renderer: SpriteRenderer,
|
||||||
exp: ActiveExplosion,
|
exp: ActiveExplosion,
|
||||||
explosionArt?: LoadedMissileArt,
|
explosionArt?: LoadedMissileArt,
|
||||||
|
|
@ -3376,7 +3652,10 @@ export function drawFireExplosion(
|
||||||
if (frame !== undefined) {
|
if (frame !== undefined) {
|
||||||
const drawX = frame.anchorX !== undefined ? exp.x + frame.anchorX : exp.x - frame.width / 2
|
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
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3414,6 +3693,8 @@ export function drawFireExplosion(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const drawFireExplosion = drawExplosion
|
||||||
|
|
||||||
/** Quality colors for ground item highlights and glints. */
|
/** Quality colors for ground item highlights and glints. */
|
||||||
export const GROUND_ITEM_QUALITY_COLORS: Record<string, readonly [number, number, number, number]> = {
|
export const GROUND_ITEM_QUALITY_COLORS: Record<string, readonly [number, number, number, number]> = {
|
||||||
normal: [0.88, 0.88, 0.88, 1.0],
|
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],
|
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:
|
* Render a ground item entity with authentic Diablo II 1.13c appearance:
|
||||||
* - Parabolic bounce height offset (when dropping or inventory full)
|
* - 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
|
* - Periodic glint/sparkle 4-point star animation
|
||||||
*/
|
*/
|
||||||
export function drawGroundItem(
|
export function drawGroundItem(
|
||||||
renderer: SpriteRenderer,
|
renderer: SpriteRenderer,
|
||||||
item: GroundItemEntity,
|
item: GroundItemEntity,
|
||||||
now: number,
|
now: number,
|
||||||
|
itemsAtlas: AtlasHandle | undefined = globalItemsAtlasHandle,
|
||||||
): void {
|
): void {
|
||||||
const bounceH = item.bounceState ? calculateBounceHeight(item.bounceState, now) : 0
|
const bounceH = item.bounceState ? calculateBounceHeight(item.bounceState, now) : 0
|
||||||
const baseY = item.y - bounceH
|
const baseY = item.y - bounceH
|
||||||
|
|
@ -3468,6 +3788,45 @@ 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])
|
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 {
|
} else {
|
||||||
// 2. Equipment / Potion / Rune Item Rendering
|
// 2. Equipment / Potion / Rune Item Rendering
|
||||||
|
const sr = (itemsAtlas !== undefined && typeof (renderer as any).draw === 'function')
|
||||||
|
? resolveItemSpriteRect(item.item || (item as any), BAKED_UI_MANIFEST.itemRects)
|
||||||
|
: null
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
// 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 w = item.invWidth > 1 ? 22 : 14
|
||||||
const h = item.invHeight > 2 ? 34 : item.invHeight > 1 ? 24 : 14
|
const h = item.invHeight > 2 ? 34 : item.invHeight > 1 ? 24 : 14
|
||||||
|
|
||||||
|
|
@ -3495,6 +3854,7 @@ export function drawGroundItem(
|
||||||
renderer.drawSolid(item.x - renderW / 2, baseY - h, 2, h, [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])
|
renderer.drawSolid(item.x + renderW / 2 - 2, baseY - h, 2, h, [qr, qg, qb, 0.95])
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Glint / Sparkle 4-Point Star Animation
|
// 3. Glint / Sparkle 4-Point Star Animation
|
||||||
// Flash every ~2000ms with a pseudo-random phase offset based on coordinates
|
// Flash every ~2000ms with a pseudo-random phase offset based on coordinates
|
||||||
|
|
@ -3561,6 +3921,10 @@ async function runScene(initialRuntime: MapRuntime, renderer: SpriteRenderer, st
|
||||||
void loadMissileArtMap(renderer, packEntityBase).then(map => {
|
void loadMissileArtMap(renderer, packEntityBase).then(map => {
|
||||||
missileArtMap = map
|
missileArtMap = map
|
||||||
})
|
})
|
||||||
|
let itemsAtlasHandle: AtlasHandle | undefined
|
||||||
|
void loadItemsAtlas(renderer, packEntityBase).then(handle => {
|
||||||
|
itemsAtlasHandle = handle
|
||||||
|
})
|
||||||
let monsterWalkFrame = 0
|
let monsterWalkFrame = 0
|
||||||
const playerAnimator = new ActorAnimator()
|
const playerAnimator = new ActorAnimator()
|
||||||
|
|
||||||
|
|
@ -4612,10 +4976,10 @@ if (typeof window !== 'undefined') {
|
||||||
|
|
||||||
for (const shot of engine.projectiles) {
|
for (const shot of engine.projectiles) {
|
||||||
pushEntity(shot.x, shot.y, () => {
|
pushEntity(shot.x, shot.y, () => {
|
||||||
if (shot.skillId === '36' || shot.skillId === 'firebolt' || shot.missileType === 'firebolt') {
|
const mType = shot.missileType ?? (shot.skillId === '36' ? 'firebolt' : shot.skillId === '47' ? 'fireball' : undefined)
|
||||||
drawFireboltProjectile(renderer, shot, missileArtMap.get('firebolt'))
|
const art = mType !== undefined ? missileArtMap.get(mType) : undefined
|
||||||
} else if (shot.skillId === '47' || shot.skillId === 'fireball' || shot.missileType === 'fireball') {
|
if (art !== undefined) {
|
||||||
drawFireboltProjectile(renderer, shot, missileArtMap.get('fireball'))
|
drawMissileProjectile(renderer, shot, art)
|
||||||
} else {
|
} else {
|
||||||
renderer.drawSolid(shot.x - 4, shot.y - 4, 8, 8, [0.9, 0.9, 0.2, 1])
|
renderer.drawSolid(shot.x - 4, shot.y - 4, 8, 8, [0.9, 0.9, 0.2, 1])
|
||||||
}
|
}
|
||||||
|
|
@ -4624,25 +4988,17 @@ if (typeof window !== 'undefined') {
|
||||||
|
|
||||||
for (const exp of engine.explosions) {
|
for (const exp of engine.explosions) {
|
||||||
pushEntity(exp.x, exp.y, () => {
|
pushEntity(exp.x, exp.y, () => {
|
||||||
drawFireExplosion(renderer, exp, missileArtMap.get('fireexplode'))
|
const art = missileArtMap.get(exp.missileType) ?? missileArtMap.get('fireexplode')
|
||||||
|
drawExplosion(renderer, exp, art)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const gItem of engine.groundItems.all) {
|
for (const gItem of engine.groundItems.all) {
|
||||||
pushEntity(gItem.x, gItem.y, () => {
|
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)
|
entities.sort((a, b) => a.depth - b.depth)
|
||||||
|
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,13 @@ export interface HotkeySkillEntry {
|
||||||
* left/right action buttons or appear in the Speedbar popup.
|
* left/right action buttons or appear in the Speedbar popup.
|
||||||
*/
|
*/
|
||||||
export const PASSIVE_SKILL_IDS: ReadonlySet<number> = new Set<number>([
|
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
|
9, // Critical Strike
|
||||||
14, // Penetrate
|
13, // Dodge
|
||||||
17, // Dodge
|
18, // Avoid
|
||||||
22, // Avoid
|
23, // Penetrate
|
||||||
27, // Evade
|
29, // Evade
|
||||||
29, // Pierce
|
33, // Pierce
|
||||||
// Sorceress Passives
|
// Sorceress Passives
|
||||||
37, // Warmth
|
37, // Warmth
|
||||||
61, // Fire Mastery
|
61, // Fire Mastery
|
||||||
|
|
@ -74,6 +74,24 @@ export function isPassiveSkill(skillId: number): boolean {
|
||||||
return PASSIVE_SKILL_IDS.has(skillId)
|
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).
|
* 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,
|
* 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 RIGHT_SKILL_BOUNDS = { x: 635, y: 551, width: 48, height: 48 } as const
|
||||||
|
|
||||||
export const DEFAULT_SORCERESS_SKILLS: readonly HotkeySkillEntry[] = [
|
export const DEFAULT_SORCERESS_SKILLS: readonly HotkeySkillEntry[] = [
|
||||||
|
// Universal Skills
|
||||||
{ skillId: 0, name: 'Attack', nameZh: '普通攻击', level: 1, manaCost: 0, leftUsable: true, rightUsable: true },
|
{ 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: 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: 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: 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: 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: 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: 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: 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 {
|
export class SkillHotkeysHud {
|
||||||
|
|
@ -345,6 +382,17 @@ export class SkillHotkeysHud {
|
||||||
this.preloadSkillIcons()
|
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 {
|
getHotkeyLabelForSkill(side: 'left' | 'right', skillId: number): string | null {
|
||||||
for (const [key, binding] of this.bindings.entries()) {
|
for (const [key, binding] of this.bindings.entries()) {
|
||||||
if (binding.side === side && binding.skillId === skillId) return key
|
if (binding.side === side && binding.skillId === skillId) return key
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@
|
||||||
import { D2FontRenderer } from './font.ts'
|
import { D2FontRenderer } from './font.ts'
|
||||||
import { GlobesHud } from './globes.ts'
|
import { GlobesHud } from './globes.ts'
|
||||||
import { BeltHud } from './belt.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 { 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 { InventoryPanel, type EquipSlotId, type UiInventoryItem, resolveItemSpriteRect, INV_GRID_ORIGIN } from './inventory.ts'
|
||||||
import { CharacterSheetPanel, type BaseStatKey } from './character-sheet.ts'
|
import { CharacterSheetPanel, type BaseStatKey } from './character-sheet.ts'
|
||||||
import { SkillTreePanel, SORCERESS_SKILL_TREE } from './skill-tree-panel.ts'
|
import { SkillTreePanel, SORCERESS_SKILL_TREE } from './skill-tree-panel.ts'
|
||||||
|
|
@ -810,7 +812,7 @@ export class HudManager {
|
||||||
this.syncPublishedState()
|
this.syncPublishedState()
|
||||||
return
|
return
|
||||||
} else if (this.rightPanel === 'skill') {
|
} 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'
|
if (!this.skillTree.visible) this.rightPanel = 'none'
|
||||||
this.syncPublishedState()
|
this.syncPublishedState()
|
||||||
return
|
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() {
|
private buildControlBarState() {
|
||||||
return {
|
return {
|
||||||
level: this.charSheet.attrs.level,
|
level: this.charSheet.attrs.level,
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,35 @@ export function resolveItemSpriteRect(
|
||||||
return itemRects['invcrs'] ?? null
|
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)
|
// Tier 5: Dimension-based fallback guarantee (w x h in cells)
|
||||||
const w = item.invWidth ?? (item as any).width ?? 1
|
const w = item.invWidth ?? (item as any).width ?? 1
|
||||||
const h = item.invHeight ?? (item as any).height ?? 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(engine.groundItems.count).toBe(0)
|
||||||
expect(status.textContent).toBe('拾起物品:戒指')
|
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)
|
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 (被动技能不进入快捷栏)', () => {
|
describe('1. Passive Skills Filtering (被动技能不进入快捷栏)', () => {
|
||||||
it('identifies all 23 canonical Diablo II 1.13c passive skills', () => {
|
it('identifies all 26 canonical Diablo II 1.13c passive skills', () => {
|
||||||
// Amazon passives
|
// Amazon passives (Critical Strike 9, Dodge 13, Avoid 18, Penetrate 23, Evade 29, Pierce 33)
|
||||||
expect(isPassiveSkill(9)).toBe(true) // Critical Strike
|
expect(isPassiveSkill(9)).toBe(true) // Critical Strike
|
||||||
expect(isPassiveSkill(14)).toBe(true) // Penetrate
|
expect(isPassiveSkill(13)).toBe(true) // Dodge
|
||||||
expect(isPassiveSkill(17)).toBe(true) // Dodge
|
expect(isPassiveSkill(18)).toBe(true) // Avoid
|
||||||
expect(isPassiveSkill(22)).toBe(true) // Avoid
|
expect(isPassiveSkill(23)).toBe(true) // Penetrate
|
||||||
expect(isPassiveSkill(27)).toBe(true) // Evade
|
expect(isPassiveSkill(29)).toBe(true) // Evade
|
||||||
expect(isPassiveSkill(29)).toBe(true) // Pierce
|
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
|
// Sorceress passives
|
||||||
expect(isPassiveSkill(37)).toBe(true) // Warmth
|
expect(isPassiveSkill(37)).toBe(true) // Warmth
|
||||||
|
|
|
||||||
|
|
@ -254,4 +254,93 @@ describe('SkillHotkeysHud & Dual Slot Mechanics', () => {
|
||||||
hud.assignSkill('right', 3)
|
hud.assignSkill('right', 3)
|
||||||
expect(hud.rightSkillId).toBe(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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||