update spirit
@@ -3,7 +3,11 @@
|
|||||||
> 本清单对应 [task-item.md](../../../.codebuddy/plan/kage_legend_mvp/task-item.md) 中的 **10.1 集成像素美术与音频资源**。
|
> 本清单对应 [task-item.md](../../../.codebuddy/plan/kage_legend_mvp/task-item.md) 中的 **10.1 集成像素美术与音频资源**。
|
||||||
> 真正的 PNG / WAV / MP3 二进制文件需由美术、音效组按下表规格制作并放入对应目录。
|
> 真正的 PNG / WAV / MP3 二进制文件需由美术、音效组按下表规格制作并放入对应目录。
|
||||||
|
|
||||||
> **当前状态**:所有条目均已用 1×1 纯色 PNG / 0.1s 静音 WAV / 空 MP3 帧**占位**,可通过 `node scripts/gen_placeholder_assets.js` 随时重新生成。占位资产仅保证工程可打开可联调,**不具有任何视觉/听觉效果**,必须由美术/音效替换为正式素材才能发布。
|
> **当前状态**:
|
||||||
|
> - **PNG 资源**:已用 Python 程序化绘制(`scripts/gen_pixel_art_assets.py`),按 `ASSETS.md` 规格生成像素美术占位,**可直接进游戏看到角色/敌人/场景/剧情插画**,但仅为过渡素材,正式版仍需美术替换。
|
||||||
|
> - **WAV / MP3 资源**:仍用 `scripts/gen_placeholder_assets.js` 生成的静音占位,便于工程联调。
|
||||||
|
>
|
||||||
|
> 两个脚本都是幂等的,可随时重跑。程序化 PNG 脚本会**覆盖**占位脚本的 1×1 纯色 PNG。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -93,51 +97,51 @@ MVP 仅启用青叶配色;红叶 / 雪原调色板预留接口但不纳入 MVP
|
|||||||
|
|
||||||
## 8. 资源交付跟踪表(Delivery Tracker)
|
## 8. 资源交付跟踪表(Delivery Tracker)
|
||||||
|
|
||||||
> 状态图例:⬜ 待制作 🟨 占位中(1×1 纯色 / 静音) 🟩 已交付正式版
|
> 状态图例:⬜ 待制作 🟨 静音/空包占位(WAV/MP3) 🟦 程序化像素美术占位(PNG) 🟩 已交付正式版
|
||||||
> 占位由 `scripts/gen_placeholder_assets.js` 生成,每次跑会刷新所有 🟨 条目。
|
> 🟨 由 `scripts/gen_placeholder_assets.js` 生成;🟦 由 `scripts/gen_pixel_art_assets.py` 生成。两个脚本都幂等可重跑。
|
||||||
|
|
||||||
### 8.1 主角 · 3 形态
|
### 8.1 主角 · 3 形态
|
||||||
|
|
||||||
| 资源 | 状态 | 负责人 | 计划交付 | 备注 |
|
| 资源 | 状态 | 负责人 | 计划交付 | 备注 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `textures/characters/kage_red.png` | 🟨 | TBD-美术 | TBD | 16×32 · 11 帧 |
|
| `textures/characters/kage_red.png` | 🟦 | TBD-美术 | TBD | 16×32 · 11 帧(程序化) |
|
||||||
| `textures/characters/kage_green.png` | 🟨 | TBD-美术 | TBD | 16×32 · 11 帧 |
|
| `textures/characters/kage_green.png` | 🟦 | TBD-美术 | TBD | 16×32 · 11 帧(程序化) |
|
||||||
| `textures/characters/kage_yellow.png` | 🟨 | TBD-美术 | TBD | 16×32 · 11 帧 |
|
| `textures/characters/kage_yellow.png` | 🟦 | TBD-美术 | TBD | 16×32 · 11 帧(程序化) |
|
||||||
|
|
||||||
### 8.2 敌人 + BOSS
|
### 8.2 敌人 + BOSS
|
||||||
|
|
||||||
| 资源 | 状态 | 负责人 | 计划交付 | 备注 |
|
| 资源 | 状态 | 负责人 | 计划交付 | 备注 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `textures/enemies/qing_ren.png` | 🟨 | TBD-美术 | TBD | 16×16 · 7 帧 |
|
| `textures/enemies/qing_ren.png` | 🟦 | TBD-美术 | TBD | 16×16 · 7 帧(程序化) |
|
||||||
| `textures/enemies/chi_ren.png` | 🟨 | TBD-美术 | TBD | 16×16 · 7 帧 |
|
| `textures/enemies/chi_ren.png` | 🟦 | TBD-美术 | TBD | 16×16 · 7 帧(程序化) |
|
||||||
| `textures/enemies/hei_ren.png` | 🟨 | TBD-美术 | TBD | 20×24 · 6 帧 |
|
| `textures/enemies/hei_ren.png` | 🟦 | TBD-美术 | TBD | 20×24 · 6 帧(程序化) |
|
||||||
| `textures/enemies/yao_fang.png` | 🟨 | TBD-美术 | TBD | 18×20 · 4 帧 |
|
| `textures/enemies/yao_fang.png` | 🟦 | TBD-美术 | TBD | 18×20 · 4 帧(程序化) |
|
||||||
| `textures/bosses/shuang_huan_fang.png` | 🟨 | TBD-美术 | TBD | 32×32 本体 + 96×32 双身 |
|
| `textures/bosses/shuang_huan_fang.png` | 🟦 | TBD-美术 | TBD | 32×32 × 9 帧(程序化) |
|
||||||
| `textures/bosses/butterfly.png` | 🟨 | TBD-美术 | TBD | 16×16 · 4 帧 |
|
| `textures/bosses/butterfly.png` | 🟦 | TBD-美术 | TBD | 16×16 · 4 帧(程序化) |
|
||||||
|
|
||||||
### 8.3 场景视差(3 主题 × 4 层 = 12 张)
|
### 8.3 场景视差(3 主题 × 4 层 = 12 张)
|
||||||
|
|
||||||
| 主题 | far | mid | near | fx | 负责人 | 计划交付 |
|
| 主题 | far | mid | near | fx | 负责人 | 计划交付 |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| 森林 `textures/scenes/forest/*` | 🟨 | 🟨 | 🟨 | 🟨 | TBD-美术 | TBD |
|
| 森林 `textures/scenes/forest/*` | 🟦 | 🟦 | 🟦 | 🟦 | TBD-美术 | TBD |
|
||||||
| 城墙 `textures/scenes/castle_wall/*` | 🟨 | 🟨 | 🟨 | 🟨 | TBD-美术 | TBD |
|
| 城墙 `textures/scenes/castle_wall/*` | 🟦 | 🟦 | 🟦 | 🟦 | TBD-美术 | TBD |
|
||||||
| 魔城 `textures/scenes/demon_castle/*` | 🟨 | 🟨 | 🟨 | 🟨 | TBD-美术 | TBD |
|
| 魔城 `textures/scenes/demon_castle/*` | 🟦 | 🟦 | 🟦 | 🟦 | TBD-美术 | TBD |
|
||||||
|
|
||||||
### 8.4 剧情背景插画(req 19.2)
|
### 8.4 剧情背景插画(req 19.2)
|
||||||
|
|
||||||
| 资源 | 状态 | 负责人 | 计划交付 | 备注 |
|
| 资源 | 状态 | 负责人 | 计划交付 | 备注 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `textures/story/ch1_page1_ninja.png` | 🟨 | TBD-美术 | TBD | 480×270 |
|
| `textures/story/ch1_page1_ninja.png` | 🟦 | TBD-美术 | TBD | 480×270(程序化) |
|
||||||
| `textures/story/ch1_page2_princess.png` | 🟨 | TBD-美术 | TBD | 480×270 |
|
| `textures/story/ch1_page2_princess.png` | 🟦 | TBD-美术 | TBD | 480×270(程序化) |
|
||||||
| `textures/story/ch1_page3_depart.png` | 🟨 | TBD-美术 | TBD | 480×270 |
|
| `textures/story/ch1_page3_depart.png` | 🟦 | TBD-美术 | TBD | 480×270(程序化) |
|
||||||
|
|
||||||
### 8.5 粒子特效贴图
|
### 8.5 粒子特效贴图
|
||||||
|
|
||||||
| 资源 | 状态 | 负责人 | 计划交付 | 备注 |
|
| 资源 | 状态 | 负责人 | 计划交付 | 备注 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `textures/fx/leaf_particle.png` | 🟨 | TBD-美术 | TBD | 透明底落叶 |
|
| `textures/fx/leaf_particle.png` | 🟦 | TBD-美术 | TBD | 透明底落叶(程序化) |
|
||||||
| `textures/fx/jump_dust.png` | 🟨 | TBD-美术 | TBD | 透明底尘土 |
|
| `textures/fx/jump_dust.png` | 🟦 | TBD-美术 | TBD | 透明底尘土(程序化) |
|
||||||
| `textures/fx/parry_spark.png` | 🟨 | TBD-美术 | TBD | 透明底火花 |
|
| `textures/fx/parry_spark.png` | 🟦 | TBD-美术 | TBD | 透明底火花(程序化) |
|
||||||
|
|
||||||
### 8.6 音效 WAV(每段 ≤ 0.5 s,req 16.1)
|
### 8.6 音效 WAV(每段 ≤ 0.5 s,req 16.1)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
{ "type": "yao_fang", "atPx": 2100 },
|
{ "type": "yao_fang", "atPx": 2100 },
|
||||||
{ "type": "chi_ren", "atPx": 2600, "count": 2 },
|
{ "type": "chi_ren", "atPx": 2600, "count": 2 },
|
||||||
{ "type": "yao_fang", "atPx": 3200 }
|
{ "type": "yao_fang", "atPx": 3200 }
|
||||||
|
],
|
||||||
|
"reinforcements": [
|
||||||
|
{ "type": "qing_ren", "intervalSec": 8, "count": 1, "maxTotal": 4, "edge": "right", "delaySec": 10 },
|
||||||
|
{ "type": "chi_ren", "intervalSec": 12, "count": 1, "maxTotal": 2, "edge": "both", "delaySec": 20 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -32,6 +36,10 @@
|
|||||||
{ "type": "chi_ren", "atPx": 1600, "count": 3 },
|
{ "type": "chi_ren", "atPx": 1600, "count": 3 },
|
||||||
{ "type": "qing_ren", "atPx": 2400, "count": 2 },
|
{ "type": "qing_ren", "atPx": 2400, "count": 2 },
|
||||||
{ "type": "yao_fang", "atPx": 3600 }
|
{ "type": "yao_fang", "atPx": 3600 }
|
||||||
|
],
|
||||||
|
"reinforcements": [
|
||||||
|
{ "type": "qing_ren", "intervalSec": 7, "count": 2, "maxTotal": 6, "edge": "both", "delaySec": 8 },
|
||||||
|
{ "type": "chi_ren", "intervalSec": 10, "count": 1, "maxTotal": 3, "edge": "right", "delaySec": 15 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -50,6 +58,10 @@
|
|||||||
{ "type": "chi_ren", "atPx": 2200, "count": 1 },
|
{ "type": "chi_ren", "atPx": 2200, "count": 1 },
|
||||||
{ "type": "qing_ren", "atPx": 3000, "count": 3 },
|
{ "type": "qing_ren", "atPx": 3000, "count": 3 },
|
||||||
{ "type": "qing_ren", "atPx": 4000, "count": 2 }
|
{ "type": "qing_ren", "atPx": 4000, "count": 2 }
|
||||||
|
],
|
||||||
|
"reinforcements": [
|
||||||
|
{ "type": "qing_ren", "intervalSec": 6, "count": 2, "maxTotal": 8, "edge": "both", "delaySec": 5 },
|
||||||
|
{ "type": "chi_ren", "intervalSec": 10, "count": 1, "maxTotal": 3, "edge": "right", "delaySec": 20 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -67,6 +79,10 @@
|
|||||||
{ "type": "qing_ren", "atPx": 1400, "count": 2 },
|
{ "type": "qing_ren", "atPx": 1400, "count": 2 },
|
||||||
{ "type": "hei_ren", "atPx": 2000 },
|
{ "type": "hei_ren", "atPx": 2000 },
|
||||||
{ "type": "chi_ren", "atPx": 2600, "count": 2 }
|
{ "type": "chi_ren", "atPx": 2600, "count": 2 }
|
||||||
|
],
|
||||||
|
"reinforcements": [
|
||||||
|
{ "type": "hei_ren", "intervalSec": 15, "count": 1, "maxTotal": 2, "edge": "both", "delaySec": 25 },
|
||||||
|
{ "type": "qing_ren", "intervalSec": 8, "count": 1, "maxTotal": 4, "edge": "both", "delaySec": 10 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -79,6 +95,9 @@
|
|||||||
"objective": { "kind": "defeat_boss", "bossId": "shuang_huan_fang" },
|
"objective": { "kind": "defeat_boss", "bossId": "shuang_huan_fang" },
|
||||||
"levelLengthPx": 1920,
|
"levelLengthPx": 1920,
|
||||||
"bgm": "bgm_boss",
|
"bgm": "bgm_boss",
|
||||||
"enemySpawns": []
|
"enemySpawns": [],
|
||||||
|
"reinforcements": [
|
||||||
|
{ "type": "qing_ren", "intervalSec": 10, "count": 1, "maxTotal": 3, "edge": "both", "delaySec": 15 }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,17 +6,17 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"index": 1,
|
"index": 1,
|
||||||
"illustration": "story/ch1_page1_ninja",
|
"illustration": "textures/story/ch1_page1_ninja",
|
||||||
"text": "在月影摇曳的古国,有一位代代相传的忍者——影。他身着赤红忍装,精通手里剑与忍者刀,守护着这片宁静的土地。"
|
"text": "在月影摇曳的古国,有一位代代相传的忍者——影。他身着赤红忍装,精通手里剑与忍者刀,守护着这片宁静的土地。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"index": 2,
|
"index": 2,
|
||||||
"illustration": "story/ch1_page2_princess",
|
"illustration": "textures/story/ch1_page2_princess",
|
||||||
"text": "然而在一个暴雨之夜,青忍的黑影撕开了宫殿的夜幕,公主被青忍的爪牙掳走,消失在魔城方向的天际。"
|
"text": "然而在一个暴雨之夜,青忍的黑影撕开了宫殿的夜幕,公主被青忍的爪牙掳走,消失在魔城方向的天际。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"index": 3,
|
"index": 3,
|
||||||
"illustration": "story/ch1_page3_depart",
|
"illustration": "textures/story/ch1_page3_depart",
|
||||||
"text": "为了将公主从邪恶的双幻坊手中救出,影踏上了穿越森林、洞穴、城壁、直入魔城天守阁的征程。"
|
"text": "为了将公主从邪恶的双幻坊手中救出,影踏上了穿越森林、洞穴、城壁、直入魔城天守阁的征程。"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 290 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "b8c5db2b-1d4f-4ade-9f4d-2c0da1d21f9e@f9941",
|
||||||
|
"displayName": "butterfly",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 3,
|
||||||
|
"width": 64,
|
||||||
|
"height": 10,
|
||||||
|
"rawWidth": 64,
|
||||||
|
"rawHeight": 16,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-32,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
32,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
-32,
|
||||||
|
5,
|
||||||
|
0,
|
||||||
|
32,
|
||||||
|
5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
13,
|
||||||
|
64,
|
||||||
|
13,
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
64,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0.1875,
|
||||||
|
1,
|
||||||
|
0.1875,
|
||||||
|
0,
|
||||||
|
0.8125,
|
||||||
|
1,
|
||||||
|
0.8125
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-32,
|
||||||
|
-5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
32,
|
||||||
|
5,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "b8c5db2b-1d4f-4ade-9f4d-2c0da1d21f9e@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "b8c5db2b-1d4f-4ade-9f4d-2c0da1d21f9e@6c48a"
|
"redirect": "b8c5db2b-1d4f-4ade-9f4d-2c0da1d21f9e@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 804 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "30ebfe50-e7e5-4c59-9d41-a23906d2406c@f9941",
|
||||||
|
"displayName": "shuang_huan_fang",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0.5,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 1,
|
||||||
|
"width": 288,
|
||||||
|
"height": 29,
|
||||||
|
"rawWidth": 288,
|
||||||
|
"rawHeight": 32,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-144,
|
||||||
|
-14.5,
|
||||||
|
0,
|
||||||
|
144,
|
||||||
|
-14.5,
|
||||||
|
0,
|
||||||
|
-144,
|
||||||
|
14.5,
|
||||||
|
0,
|
||||||
|
144,
|
||||||
|
14.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
31,
|
||||||
|
288,
|
||||||
|
31,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
288,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0.0625,
|
||||||
|
1,
|
||||||
|
0.0625,
|
||||||
|
0,
|
||||||
|
0.96875,
|
||||||
|
1,
|
||||||
|
0.96875
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-144,
|
||||||
|
-14.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
144,
|
||||||
|
14.5,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "30ebfe50-e7e5-4c59-9d41-a23906d2406c@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "30ebfe50-e7e5-4c59-9d41-a23906d2406c@6c48a"
|
"redirect": "30ebfe50-e7e5-4c59-9d41-a23906d2406c@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 958 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "de17d584-ed54-49ec-a1a5-de351ecb6e4d@f9941",
|
||||||
|
"displayName": "kage_green",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 1.5,
|
||||||
|
"offsetY": 1,
|
||||||
|
"trimX": 3,
|
||||||
|
"trimY": 0,
|
||||||
|
"width": 173,
|
||||||
|
"height": 30,
|
||||||
|
"rawWidth": 176,
|
||||||
|
"rawHeight": 32,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-86.5,
|
||||||
|
-15,
|
||||||
|
0,
|
||||||
|
86.5,
|
||||||
|
-15,
|
||||||
|
0,
|
||||||
|
-86.5,
|
||||||
|
15,
|
||||||
|
0,
|
||||||
|
86.5,
|
||||||
|
15,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
3,
|
||||||
|
32,
|
||||||
|
176,
|
||||||
|
32,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
176,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0.017045454545454544,
|
||||||
|
0.0625,
|
||||||
|
1,
|
||||||
|
0.0625,
|
||||||
|
0.017045454545454544,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-86.5,
|
||||||
|
-15,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
86.5,
|
||||||
|
15,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "de17d584-ed54-49ec-a1a5-de351ecb6e4d@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "de17d584-ed54-49ec-a1a5-de351ecb6e4d@6c48a"
|
"redirect": "de17d584-ed54-49ec-a1a5-de351ecb6e4d@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 983 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "1ea27d49-1512-48e8-b3d4-3636a93c07a3@f9941",
|
||||||
|
"displayName": "kage_red",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 1.5,
|
||||||
|
"offsetY": 1,
|
||||||
|
"trimX": 3,
|
||||||
|
"trimY": 0,
|
||||||
|
"width": 173,
|
||||||
|
"height": 30,
|
||||||
|
"rawWidth": 176,
|
||||||
|
"rawHeight": 32,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-86.5,
|
||||||
|
-15,
|
||||||
|
0,
|
||||||
|
86.5,
|
||||||
|
-15,
|
||||||
|
0,
|
||||||
|
-86.5,
|
||||||
|
15,
|
||||||
|
0,
|
||||||
|
86.5,
|
||||||
|
15,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
3,
|
||||||
|
32,
|
||||||
|
176,
|
||||||
|
32,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
176,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0.017045454545454544,
|
||||||
|
0.0625,
|
||||||
|
1,
|
||||||
|
0.0625,
|
||||||
|
0.017045454545454544,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-86.5,
|
||||||
|
-15,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
86.5,
|
||||||
|
15,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "1ea27d49-1512-48e8-b3d4-3636a93c07a3@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "1ea27d49-1512-48e8-b3d4-3636a93c07a3@6c48a"
|
"redirect": "1ea27d49-1512-48e8-b3d4-3636a93c07a3@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 993 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "cc33a404-518b-4d7a-9699-765d88256b1f@f9941",
|
||||||
|
"displayName": "kage_yellow",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 1.5,
|
||||||
|
"offsetY": 1,
|
||||||
|
"trimX": 3,
|
||||||
|
"trimY": 0,
|
||||||
|
"width": 173,
|
||||||
|
"height": 30,
|
||||||
|
"rawWidth": 176,
|
||||||
|
"rawHeight": 32,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-86.5,
|
||||||
|
-15,
|
||||||
|
0,
|
||||||
|
86.5,
|
||||||
|
-15,
|
||||||
|
0,
|
||||||
|
-86.5,
|
||||||
|
15,
|
||||||
|
0,
|
||||||
|
86.5,
|
||||||
|
15,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
3,
|
||||||
|
32,
|
||||||
|
176,
|
||||||
|
32,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
176,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0.017045454545454544,
|
||||||
|
0.0625,
|
||||||
|
1,
|
||||||
|
0.0625,
|
||||||
|
0.017045454545454544,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-86.5,
|
||||||
|
-15,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
86.5,
|
||||||
|
15,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "cc33a404-518b-4d7a-9699-765d88256b1f@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "cc33a404-518b-4d7a-9699-765d88256b1f@6c48a"
|
"redirect": "cc33a404-518b-4d7a-9699-765d88256b1f@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 258 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "36016f50-9022-4662-9b78-b9b29ba64510@f9941",
|
||||||
|
"displayName": "chi_ren",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 1,
|
||||||
|
"offsetY": -1,
|
||||||
|
"trimX": 2,
|
||||||
|
"trimY": 2,
|
||||||
|
"width": 110,
|
||||||
|
"height": 14,
|
||||||
|
"rawWidth": 112,
|
||||||
|
"rawHeight": 16,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-55,
|
||||||
|
-7,
|
||||||
|
0,
|
||||||
|
55,
|
||||||
|
-7,
|
||||||
|
0,
|
||||||
|
-55,
|
||||||
|
7,
|
||||||
|
0,
|
||||||
|
55,
|
||||||
|
7,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
2,
|
||||||
|
14,
|
||||||
|
112,
|
||||||
|
14,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
112,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0.017857142857142856,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0.017857142857142856,
|
||||||
|
0.875,
|
||||||
|
1,
|
||||||
|
0.875
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-55,
|
||||||
|
-7,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
55,
|
||||||
|
7,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "36016f50-9022-4662-9b78-b9b29ba64510@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "36016f50-9022-4662-9b78-b9b29ba64510@6c48a"
|
"redirect": "36016f50-9022-4662-9b78-b9b29ba64510@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 348 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "acfb19f8-ac1b-445f-8f81-fac6aa4071c8@f9941",
|
||||||
|
"displayName": "hei_ren",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 1.5,
|
||||||
|
"offsetY": 0,
|
||||||
|
"trimX": 4,
|
||||||
|
"trimY": 1,
|
||||||
|
"width": 115,
|
||||||
|
"height": 22,
|
||||||
|
"rawWidth": 120,
|
||||||
|
"rawHeight": 24,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-57.5,
|
||||||
|
-11,
|
||||||
|
0,
|
||||||
|
57.5,
|
||||||
|
-11,
|
||||||
|
0,
|
||||||
|
-57.5,
|
||||||
|
11,
|
||||||
|
0,
|
||||||
|
57.5,
|
||||||
|
11,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
4,
|
||||||
|
23,
|
||||||
|
119,
|
||||||
|
23,
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
119,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0.03333333333333333,
|
||||||
|
0.041666666666666664,
|
||||||
|
0.9916666666666667,
|
||||||
|
0.041666666666666664,
|
||||||
|
0.03333333333333333,
|
||||||
|
0.9583333333333334,
|
||||||
|
0.9916666666666667,
|
||||||
|
0.9583333333333334
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-57.5,
|
||||||
|
-11,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
57.5,
|
||||||
|
11,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "acfb19f8-ac1b-445f-8f81-fac6aa4071c8@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "acfb19f8-ac1b-445f-8f81-fac6aa4071c8@6c48a"
|
"redirect": "acfb19f8-ac1b-445f-8f81-fac6aa4071c8@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 262 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "c5261dd4-ea58-49eb-88ff-7c864104b499@f9941",
|
||||||
|
"displayName": "qing_ren",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 1,
|
||||||
|
"offsetY": -1,
|
||||||
|
"trimX": 2,
|
||||||
|
"trimY": 2,
|
||||||
|
"width": 110,
|
||||||
|
"height": 14,
|
||||||
|
"rawWidth": 112,
|
||||||
|
"rawHeight": 16,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-55,
|
||||||
|
-7,
|
||||||
|
0,
|
||||||
|
55,
|
||||||
|
-7,
|
||||||
|
0,
|
||||||
|
-55,
|
||||||
|
7,
|
||||||
|
0,
|
||||||
|
55,
|
||||||
|
7,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
2,
|
||||||
|
14,
|
||||||
|
112,
|
||||||
|
14,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
112,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0.017857142857142856,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0.017857142857142856,
|
||||||
|
0.875,
|
||||||
|
1,
|
||||||
|
0.875
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-55,
|
||||||
|
-7,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
55,
|
||||||
|
7,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "c5261dd4-ea58-49eb-88ff-7c864104b499@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "c5261dd4-ea58-49eb-88ff-7c864104b499@6c48a"
|
"redirect": "c5261dd4-ea58-49eb-88ff-7c864104b499@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 397 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "1c138e9b-5973-4b83-b632-dd13e00f5429@f9941",
|
||||||
|
"displayName": "yao_fang",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0.5,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 0,
|
||||||
|
"width": 72,
|
||||||
|
"height": 19,
|
||||||
|
"rawWidth": 72,
|
||||||
|
"rawHeight": 20,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-36,
|
||||||
|
-9.5,
|
||||||
|
0,
|
||||||
|
36,
|
||||||
|
-9.5,
|
||||||
|
0,
|
||||||
|
-36,
|
||||||
|
9.5,
|
||||||
|
0,
|
||||||
|
36,
|
||||||
|
9.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
20,
|
||||||
|
72,
|
||||||
|
20,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
72,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0.05,
|
||||||
|
1,
|
||||||
|
0.05,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-36,
|
||||||
|
-9.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
36,
|
||||||
|
9.5,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "1c138e9b-5973-4b83-b632-dd13e00f5429@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "1c138e9b-5973-4b83-b632-dd13e00f5429@6c48a"
|
"redirect": "1c138e9b-5973-4b83-b632-dd13e00f5429@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 202 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "9ca79ef0-d218-4ec6-9d27-0af065e6bcfd@f9941",
|
||||||
|
"displayName": "jump_dust",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0.5,
|
||||||
|
"offsetY": -3,
|
||||||
|
"trimX": 1,
|
||||||
|
"trimY": 6,
|
||||||
|
"width": 15,
|
||||||
|
"height": 10,
|
||||||
|
"rawWidth": 16,
|
||||||
|
"rawHeight": 16,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-7.5,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
7.5,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
-7.5,
|
||||||
|
5,
|
||||||
|
0,
|
||||||
|
7.5,
|
||||||
|
5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
1,
|
||||||
|
10,
|
||||||
|
16,
|
||||||
|
10,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
16,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0.0625,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0.0625,
|
||||||
|
0.625,
|
||||||
|
1,
|
||||||
|
0.625
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-7.5,
|
||||||
|
-5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
7.5,
|
||||||
|
5,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "9ca79ef0-d218-4ec6-9d27-0af065e6bcfd@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "9ca79ef0-d218-4ec6-9d27-0af065e6bcfd@6c48a"
|
"redirect": "9ca79ef0-d218-4ec6-9d27-0af065e6bcfd@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 170 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "6ff7aee5-e8fd-4a7a-89bc-95598bfbcbac@f9941",
|
||||||
|
"displayName": "leaf_particle",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 1,
|
||||||
|
"offsetY": 0.5,
|
||||||
|
"trimX": 4,
|
||||||
|
"trimY": 3,
|
||||||
|
"width": 10,
|
||||||
|
"height": 9,
|
||||||
|
"rawWidth": 16,
|
||||||
|
"rawHeight": 16,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-5,
|
||||||
|
-4.5,
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
-4.5,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
4.5,
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
4.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
4,
|
||||||
|
13,
|
||||||
|
14,
|
||||||
|
13,
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
14,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0.25,
|
||||||
|
0.25,
|
||||||
|
0.875,
|
||||||
|
0.25,
|
||||||
|
0.25,
|
||||||
|
0.8125,
|
||||||
|
0.875,
|
||||||
|
0.8125
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-5,
|
||||||
|
-4.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
5,
|
||||||
|
4.5,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "6ff7aee5-e8fd-4a7a-89bc-95598bfbcbac@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "6ff7aee5-e8fd-4a7a-89bc-95598bfbcbac@6c48a"
|
"redirect": "6ff7aee5-e8fd-4a7a-89bc-95598bfbcbac@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 143 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "adfcaead-eec0-4a25-b69c-571a34154af1@f9941",
|
||||||
|
"displayName": "parry_spark",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0.5,
|
||||||
|
"offsetY": -0.5,
|
||||||
|
"trimX": 1,
|
||||||
|
"trimY": 1,
|
||||||
|
"width": 15,
|
||||||
|
"height": 15,
|
||||||
|
"rawWidth": 16,
|
||||||
|
"rawHeight": 16,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-7.5,
|
||||||
|
-7.5,
|
||||||
|
0,
|
||||||
|
7.5,
|
||||||
|
-7.5,
|
||||||
|
0,
|
||||||
|
-7.5,
|
||||||
|
7.5,
|
||||||
|
0,
|
||||||
|
7.5,
|
||||||
|
7.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
1,
|
||||||
|
15,
|
||||||
|
16,
|
||||||
|
15,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
16,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0.0625,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0.0625,
|
||||||
|
0.9375,
|
||||||
|
1,
|
||||||
|
0.9375
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-7.5,
|
||||||
|
-7.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
7.5,
|
||||||
|
7.5,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "adfcaead-eec0-4a25-b69c-571a34154af1@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "adfcaead-eec0-4a25-b69c-571a34154af1@6c48a"
|
"redirect": "adfcaead-eec0-4a25-b69c-571a34154af1@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 1.3 KiB |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "58461a49-befa-4fa6-9a0c-3a4a2780a05e@f9941",
|
||||||
|
"displayName": "far",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 0,
|
||||||
|
"width": 480,
|
||||||
|
"height": 270,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
270,
|
||||||
|
480,
|
||||||
|
270,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
480,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "58461a49-befa-4fa6-9a0c-3a4a2780a05e@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "58461a49-befa-4fa6-9a0c-3a4a2780a05e@6c48a"
|
"redirect": "58461a49-befa-4fa6-9a0c-3a4a2780a05e@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 701 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "422d5dc6-7148-44f5-889c-f75a00567421@f9941",
|
||||||
|
"displayName": "fx",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": -95.5,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 191,
|
||||||
|
"width": 480,
|
||||||
|
"height": 79,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-39.5,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-39.5,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
39.5,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
39.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
79,
|
||||||
|
480,
|
||||||
|
79,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
480,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0.29259259259259257,
|
||||||
|
1,
|
||||||
|
0.29259259259259257
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-39.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
39.5,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "422d5dc6-7148-44f5-889c-f75a00567421@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "422d5dc6-7148-44f5-889c-f75a00567421@6c48a"
|
"redirect": "422d5dc6-7148-44f5-889c-f75a00567421@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 46 KiB |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "6db47cc3-72a3-4a9b-b100-48148f1cc934@f9941",
|
||||||
|
"displayName": "mid",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": -35,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 120,
|
||||||
|
"width": 480,
|
||||||
|
"height": 100,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-50,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-50,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
50,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
50,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
150,
|
||||||
|
480,
|
||||||
|
150,
|
||||||
|
0,
|
||||||
|
50,
|
||||||
|
480,
|
||||||
|
50
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0.18518518518518517,
|
||||||
|
1,
|
||||||
|
0.18518518518518517,
|
||||||
|
0,
|
||||||
|
0.5555555555555556,
|
||||||
|
1,
|
||||||
|
0.5555555555555556
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-50,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
50,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "6db47cc3-72a3-4a9b-b100-48148f1cc934@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "6db47cc3-72a3-4a9b-b100-48148f1cc934@6c48a"
|
"redirect": "6db47cc3-72a3-4a9b-b100-48148f1cc934@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 849 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "be8f61cb-4af2-466e-bc79-18e27d2a70f6@f9941",
|
||||||
|
"displayName": "near",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": -72.5,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 145,
|
||||||
|
"width": 480,
|
||||||
|
"height": 125,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-62.5,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-62.5,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
62.5,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
62.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
125,
|
||||||
|
480,
|
||||||
|
125,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
480,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0.46296296296296297,
|
||||||
|
1,
|
||||||
|
0.46296296296296297
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-62.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
62.5,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "be8f61cb-4af2-466e-bc79-18e27d2a70f6@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "be8f61cb-4af2-466e-bc79-18e27d2a70f6@6c48a"
|
"redirect": "be8f61cb-4af2-466e-bc79-18e27d2a70f6@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 1.4 KiB |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "23a1bcbc-d250-4d63-9a28-738127c71789@f9941",
|
||||||
|
"displayName": "far",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 0,
|
||||||
|
"width": 480,
|
||||||
|
"height": 270,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
270,
|
||||||
|
480,
|
||||||
|
270,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
480,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "23a1bcbc-d250-4d63-9a28-738127c71789@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "23a1bcbc-d250-4d63-9a28-738127c71789@6c48a"
|
"redirect": "23a1bcbc-d250-4d63-9a28-738127c71789@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 1.2 KiB |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "7ba84d2b-d60a-42c7-b7d9-4692b2283526@f9941",
|
||||||
|
"displayName": "fx",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": -0.5,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 1,
|
||||||
|
"width": 480,
|
||||||
|
"height": 269,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-134.5,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-134.5,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
134.5,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
134.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
269,
|
||||||
|
480,
|
||||||
|
269,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
480,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0.9962962962962963,
|
||||||
|
1,
|
||||||
|
0.9962962962962963
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-134.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
134.5,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "7ba84d2b-d60a-42c7-b7d9-4692b2283526@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "7ba84d2b-d60a-42c7-b7d9-4692b2283526@6c48a"
|
"redirect": "7ba84d2b-d60a-42c7-b7d9-4692b2283526@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 944 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "db901ada-5c31-4820-b0bd-1990341ac44f@f9941",
|
||||||
|
"displayName": "mid",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0.5,
|
||||||
|
"offsetY": 2,
|
||||||
|
"trimX": 101,
|
||||||
|
"trimY": 56,
|
||||||
|
"width": 279,
|
||||||
|
"height": 154,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-139.5,
|
||||||
|
-77,
|
||||||
|
0,
|
||||||
|
139.5,
|
||||||
|
-77,
|
||||||
|
0,
|
||||||
|
-139.5,
|
||||||
|
77,
|
||||||
|
0,
|
||||||
|
139.5,
|
||||||
|
77,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
101,
|
||||||
|
214,
|
||||||
|
380,
|
||||||
|
214,
|
||||||
|
101,
|
||||||
|
60,
|
||||||
|
380,
|
||||||
|
60
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0.21041666666666667,
|
||||||
|
0.2222222222222222,
|
||||||
|
0.7916666666666666,
|
||||||
|
0.2222222222222222,
|
||||||
|
0.21041666666666667,
|
||||||
|
0.7925925925925926,
|
||||||
|
0.7916666666666666,
|
||||||
|
0.7925925925925926
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-139.5,
|
||||||
|
-77,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
139.5,
|
||||||
|
77,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "db901ada-5c31-4820-b0bd-1990341ac44f@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "db901ada-5c31-4820-b0bd-1990341ac44f@6c48a"
|
"redirect": "db901ada-5c31-4820-b0bd-1990341ac44f@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 791 B |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "60456a45-0933-4a9f-b8bb-61cbf1761456@f9941",
|
||||||
|
"displayName": "near",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": -83,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 166,
|
||||||
|
"width": 480,
|
||||||
|
"height": 104,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-52,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-52,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
52,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
104,
|
||||||
|
480,
|
||||||
|
104,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
480,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0.3851851851851852,
|
||||||
|
1,
|
||||||
|
0.3851851851851852
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-52,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
52,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "60456a45-0933-4a9f-b8bb-61cbf1761456@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "60456a45-0933-4a9f-b8bb-61cbf1761456@6c48a"
|
"redirect": "60456a45-0933-4a9f-b8bb-61cbf1761456@6c48a"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.1 MiB |
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.0.27",
|
||||||
|
"importer": "image",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "68a7b8ce-cdd1-440b-94cb-00ebc277252b",
|
||||||
|
"files": [
|
||||||
|
".json",
|
||||||
|
".png"
|
||||||
|
],
|
||||||
|
"subMetas": {
|
||||||
|
"6c48a": {
|
||||||
|
"importer": "texture",
|
||||||
|
"uuid": "68a7b8ce-cdd1-440b-94cb-00ebc277252b@6c48a",
|
||||||
|
"displayName": "ChatGPT Image 2026年5月27日 07_52_37",
|
||||||
|
"id": "6c48a",
|
||||||
|
"name": "texture",
|
||||||
|
"userData": {
|
||||||
|
"wrapModeS": "repeat",
|
||||||
|
"wrapModeT": "repeat",
|
||||||
|
"minfilter": "linear",
|
||||||
|
"magfilter": "linear",
|
||||||
|
"mipfilter": "none",
|
||||||
|
"anisotropy": 0,
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "68a7b8ce-cdd1-440b-94cb-00ebc277252b",
|
||||||
|
"visible": false
|
||||||
|
},
|
||||||
|
"ver": "1.0.22",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"userData": {
|
||||||
|
"type": "texture",
|
||||||
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
|
"hasAlpha": false,
|
||||||
|
"redirect": "68a7b8ce-cdd1-440b-94cb-00ebc277252b@6c48a"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 1.8 KiB |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "a0897bc0-02aa-4dc7-a106-e3bb3ee845d6@f9941",
|
||||||
|
"displayName": "far",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 0,
|
||||||
|
"width": 480,
|
||||||
|
"height": 270,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
270,
|
||||||
|
480,
|
||||||
|
270,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
480,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "a0897bc0-02aa-4dc7-a106-e3bb3ee845d6@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "a0897bc0-02aa-4dc7-a106-e3bb3ee845d6@6c48a"
|
"redirect": "a0897bc0-02aa-4dc7-a106-e3bb3ee845d6@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 1.7 KiB |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "dd10a162-98e8-4eb0-b51f-f3e85db1dbf2@f9941",
|
||||||
|
"displayName": "fx",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 0,
|
||||||
|
"width": 480,
|
||||||
|
"height": 270,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
270,
|
||||||
|
480,
|
||||||
|
270,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
480,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "dd10a162-98e8-4eb0-b51f-f3e85db1dbf2@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "dd10a162-98e8-4eb0-b51f-f3e85db1dbf2@6c48a"
|
"redirect": "dd10a162-98e8-4eb0-b51f-f3e85db1dbf2@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 4.0 KiB |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "a77268f3-adc4-4859-a395-bb20202cccad@f9941",
|
||||||
|
"displayName": "mid",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 25,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 30,
|
||||||
|
"width": 480,
|
||||||
|
"height": 160,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-80,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-80,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
80,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
80,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
480,
|
||||||
|
240,
|
||||||
|
0,
|
||||||
|
80,
|
||||||
|
480,
|
||||||
|
80
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0.2962962962962963,
|
||||||
|
1,
|
||||||
|
0.2962962962962963,
|
||||||
|
0,
|
||||||
|
0.8888888888888888,
|
||||||
|
1,
|
||||||
|
0.8888888888888888
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-80,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
80,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "a77268f3-adc4-4859-a395-bb20202cccad@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "a77268f3-adc4-4859-a395-bb20202cccad@6c48a"
|
"redirect": "a77268f3-adc4-4859-a395-bb20202cccad@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 4.9 KiB |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "269788c5-eed0-4a3f-b8bf-be14bb4b20f3@f9941",
|
||||||
|
"displayName": "near",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": -15.5,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 31,
|
||||||
|
"width": 480,
|
||||||
|
"height": 239,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-119.5,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-119.5,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
119.5,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
119.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
239,
|
||||||
|
480,
|
||||||
|
239,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
480,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0.8851851851851852,
|
||||||
|
1,
|
||||||
|
0.8851851851851852
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-119.5,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
119.5,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "269788c5-eed0-4a3f-b8bf-be14bb4b20f3@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "269788c5-eed0-4a3f-b8bf-be14bb4b20f3@6c48a"
|
"redirect": "269788c5-eed0-4a3f-b8bf-be14bb4b20f3@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 5.8 KiB |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "647f87dd-0c4b-48c9-a6ab-b991424c683a@f9941",
|
||||||
|
"displayName": "ch1_page1_ninja",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 0,
|
||||||
|
"width": 480,
|
||||||
|
"height": 270,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
270,
|
||||||
|
480,
|
||||||
|
270,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
480,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "647f87dd-0c4b-48c9-a6ab-b991424c683a@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "647f87dd-0c4b-48c9-a6ab-b991424c683a@6c48a"
|
"redirect": "647f87dd-0c4b-48c9-a6ab-b991424c683a@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 6.3 KiB |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "9074a6bf-d546-4e11-bd01-54d848018750@f9941",
|
||||||
|
"displayName": "ch1_page2_princess",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 0,
|
||||||
|
"width": 480,
|
||||||
|
"height": 270,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
270,
|
||||||
|
480,
|
||||||
|
270,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
480,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "9074a6bf-d546-4e11-bd01-54d848018750@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "9074a6bf-d546-4e11-bd01-54d848018750@6c48a"
|
"redirect": "9074a6bf-d546-4e11-bd01-54d848018750@6c48a"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 6.8 KiB |
@@ -31,10 +31,102 @@
|
|||||||
".json"
|
".json"
|
||||||
],
|
],
|
||||||
"subMetas": {}
|
"subMetas": {}
|
||||||
|
},
|
||||||
|
"f9941": {
|
||||||
|
"importer": "sprite-frame",
|
||||||
|
"uuid": "7f90c5af-45b8-4576-9c13-f40a8eeb00a4@f9941",
|
||||||
|
"displayName": "ch1_page3_depart",
|
||||||
|
"id": "f9941",
|
||||||
|
"name": "spriteFrame",
|
||||||
|
"userData": {
|
||||||
|
"trimThreshold": 1,
|
||||||
|
"rotated": false,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"trimX": 0,
|
||||||
|
"trimY": 0,
|
||||||
|
"width": 480,
|
||||||
|
"height": 270,
|
||||||
|
"rawWidth": 480,
|
||||||
|
"rawHeight": 270,
|
||||||
|
"borderTop": 0,
|
||||||
|
"borderBottom": 0,
|
||||||
|
"borderLeft": 0,
|
||||||
|
"borderRight": 0,
|
||||||
|
"packable": true,
|
||||||
|
"pixelsToUnit": 100,
|
||||||
|
"pivotX": 0.5,
|
||||||
|
"pivotY": 0.5,
|
||||||
|
"meshType": 0,
|
||||||
|
"vertices": {
|
||||||
|
"rawPosition": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
-135,
|
||||||
|
0,
|
||||||
|
-240,
|
||||||
|
135,
|
||||||
|
0,
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"uv": [
|
||||||
|
0,
|
||||||
|
270,
|
||||||
|
480,
|
||||||
|
270,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
480,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"nuv": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"minPos": [
|
||||||
|
-240,
|
||||||
|
-135,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"maxPos": [
|
||||||
|
240,
|
||||||
|
135,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isUuid": true,
|
||||||
|
"imageUuidOrDatabaseUri": "7f90c5af-45b8-4576-9c13-f40a8eeb00a4@6c48a",
|
||||||
|
"atlasUuid": "",
|
||||||
|
"trimType": "auto"
|
||||||
|
},
|
||||||
|
"ver": "1.0.12",
|
||||||
|
"imported": true,
|
||||||
|
"files": [
|
||||||
|
".json"
|
||||||
|
],
|
||||||
|
"subMetas": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userData": {
|
"userData": {
|
||||||
"type": "texture",
|
"type": "sprite-frame",
|
||||||
"fixAlphaTransparencyArtifacts": false,
|
"fixAlphaTransparencyArtifacts": false,
|
||||||
"hasAlpha": true,
|
"hasAlpha": true,
|
||||||
"redirect": "7f90c5af-45b8-4576-9c13-f40a8eeb00a4@6c48a"
|
"redirect": "7f90c5af-45b8-4576-9c13-f40a8eeb00a4@6c48a"
|
||||||
|
|||||||
@@ -197,6 +197,19 @@ export class ConfigMgr {
|
|||||||
throw new Error(`ConfigMgr: level "${lv.id}" spawn references unknown enemy "${sp.type}"`);
|
throw new Error(`ConfigMgr: level "${lv.id}" spawn references unknown enemy "${sp.type}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (lv.reinforcements) {
|
||||||
|
for (const rule of lv.reinforcements) {
|
||||||
|
if (!enemyIds.has(rule.type)) {
|
||||||
|
throw new Error(`ConfigMgr: level "${lv.id}" reinforcement references unknown enemy "${rule.type}"`);
|
||||||
|
}
|
||||||
|
if (rule.intervalSec <= 0) {
|
||||||
|
throw new Error(`ConfigMgr: level "${lv.id}" reinforcement intervalSec must be positive`);
|
||||||
|
}
|
||||||
|
if (rule.count <= 0) {
|
||||||
|
throw new Error(`ConfigMgr: level "${lv.id}" reinforcement count must be positive`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,22 @@ export interface ILevelObjective {
|
|||||||
bossId?: string;
|
bossId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Reinforcement rule — enemies that jump in from screen edges during gameplay. */
|
||||||
|
export interface IReinforcementRule {
|
||||||
|
/** Which enemy type to spawn as reinforcement. */
|
||||||
|
type: EnemyType;
|
||||||
|
/** Minimum interval (seconds) between two reinforcement waves of this rule. */
|
||||||
|
intervalSec: number;
|
||||||
|
/** How many enemies to spawn per reinforcement wave. */
|
||||||
|
count: number;
|
||||||
|
/** Maximum total reinforcements of this rule per level (0 = unlimited). */
|
||||||
|
maxTotal?: number;
|
||||||
|
/** Which edge(s) the enemies appear from. */
|
||||||
|
edge: 'left' | 'right' | 'both';
|
||||||
|
/** Minimum elapsed seconds before this rule becomes active. */
|
||||||
|
delaySec?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ILevelConfig {
|
export interface ILevelConfig {
|
||||||
id: string; // e.g. '1-1'
|
id: string; // e.g. '1-1'
|
||||||
chapter: 1 | 2 | 3;
|
chapter: 1 | 2 | 3;
|
||||||
@@ -150,6 +166,8 @@ export interface ILevelConfig {
|
|||||||
bgm: string;
|
bgm: string;
|
||||||
/** Enemy spawn list evaluated by the LevelMgr. */
|
/** Enemy spawn list evaluated by the LevelMgr. */
|
||||||
enemySpawns: Array<{ type: EnemyType; atPx: number; count?: number }>;
|
enemySpawns: Array<{ type: EnemyType; atPx: number; count?: number }>;
|
||||||
|
/** Dynamic reinforcement rules — enemies jump in from screen edges. */
|
||||||
|
reinforcements?: IReinforcementRule[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -65,8 +65,12 @@ export class BossController {
|
|||||||
if (!this.butterflyRevealed) return [];
|
if (!this.butterflyRevealed) return [];
|
||||||
if (this.killed) return [];
|
if (this.killed) return [];
|
||||||
const out: BossOutcomeEvent[] = [];
|
const out: BossOutcomeEvent[] = [];
|
||||||
|
|
||||||
this.hp = Math.max(0, this.hp - 1);
|
this.hp = Math.max(0, this.hp - 1);
|
||||||
out.push(...this.checkPhaseTransition(), ...this.checkPrincessCutscene());
|
// Check phase transition AFTER decrementing HP so the ratio
|
||||||
|
// reflects the new HP (e.g. 3→2 = 2/3).
|
||||||
|
const hpRatio = this.hp / this.cfg.hp;
|
||||||
|
out.push(...this.checkPhaseTransitionAtRatio(hpRatio), ...this.checkPrincessCutscene());
|
||||||
if (this.hp === 0) {
|
if (this.hp === 0) {
|
||||||
this.killed = true;
|
this.killed = true;
|
||||||
out.push({ kind: 'boss_killed' });
|
out.push({ kind: 'boss_killed' });
|
||||||
@@ -76,8 +80,7 @@ export class BossController {
|
|||||||
|
|
||||||
// --------------------------------------------------------------------
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
private checkPhaseTransition(): BossOutcomeEvent[] {
|
private checkPhaseTransitionAtRatio(hpRatio: number): BossOutcomeEvent[] {
|
||||||
const hpRatio = this.hp / this.cfg.hp;
|
|
||||||
for (let i = this.phaseIndex + 1; i < this.cfg.phases.length; i++) {
|
for (let i = this.phaseIndex + 1; i < this.cfg.phases.length; i++) {
|
||||||
if (hpRatio <= this.cfg.phases[i].hpThreshold) {
|
if (hpRatio <= this.cfg.phases[i].hpThreshold) {
|
||||||
this.phaseIndex = i;
|
this.phaseIndex = i;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AttackType, EnemyType, IEnemyConfig } from '../data/Interfaces';
|
import { AttackType, EnemyType, IEnemyConfig, IReinforcementRule } from '../data/Interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enemy AI base class + four concrete subclasses (req 6.1-6.7).
|
* Enemy AI base class + four concrete subclasses (req 6.1-6.7).
|
||||||
@@ -47,6 +47,7 @@ export abstract class EnemyAIBase {
|
|||||||
public readonly type: EnemyType;
|
public readonly type: EnemyType;
|
||||||
public pos: { x: number; y: number };
|
public pos: { x: number; y: number };
|
||||||
public alive = true;
|
public alive = true;
|
||||||
|
public hp: number;
|
||||||
protected cooldownSec = 0;
|
protected cooldownSec = 0;
|
||||||
protected readonly cfg: IEnemyConfig;
|
protected readonly cfg: IEnemyConfig;
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ export abstract class EnemyAIBase {
|
|||||||
this.cfg = cfg;
|
this.cfg = cfg;
|
||||||
this.type = cfg.id;
|
this.type = cfg.id;
|
||||||
this.pos = { x: spawnX, y: spawnY };
|
this.pos = { x: spawnX, y: spawnY };
|
||||||
|
this.hp = cfg.hp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract update(ctx: IEnemyUpdateCtx): IEnemyAction[];
|
public abstract update(ctx: IEnemyUpdateCtx): IEnemyAction[];
|
||||||
@@ -197,15 +199,18 @@ export class EnemyManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update all live enemies that intersect `cull`. Returns the concatenated
|
* Update all live enemies that intersect `cull` (with optional margin).
|
||||||
* list of actions emitted so the caller (LevelMgr) can instantiate
|
* Returns the concatenated list of actions emitted so the caller
|
||||||
* projectiles, drops, etc.
|
* (LevelMgr) can instantiate projectiles, drops, etc.
|
||||||
|
*
|
||||||
|
* @param cullingMargin Extra pixels beyond `cull` to still activate enemies.
|
||||||
|
* Used for reinforcements that spawn just outside the screen.
|
||||||
*/
|
*/
|
||||||
public update(dtSec: number, nowMs: number, player: IPlayerSense, cull: ICullingRect): IEnemyAction[] {
|
public update(dtSec: number, nowMs: number, player: IPlayerSense, cull: ICullingRect, cullingMargin = 0): IEnemyAction[] {
|
||||||
const actions: IEnemyAction[] = [];
|
const actions: IEnemyAction[] = [];
|
||||||
for (const e of this.enemies) {
|
for (const e of this.enemies) {
|
||||||
if (!e.alive) continue;
|
if (!e.alive) continue;
|
||||||
if (!this.inside(e, cull)) continue;
|
if (!this.inside(e, cull, cullingMargin)) continue;
|
||||||
const ctx: IEnemyUpdateCtx = { dtSec, nowMs, player };
|
const ctx: IEnemyUpdateCtx = { dtSec, nowMs, player };
|
||||||
actions.push(...e.update(ctx));
|
actions.push(...e.update(ctx));
|
||||||
}
|
}
|
||||||
@@ -222,12 +227,145 @@ export class EnemyManager {
|
|||||||
this.enemies.length = 0;
|
this.enemies.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private inside(e: EnemyAIBase, cull: ICullingRect): boolean {
|
private inside(e: EnemyAIBase, cull: ICullingRect, margin = 0): boolean {
|
||||||
return (
|
return (
|
||||||
e.pos.x + e.aabb.w / 2 >= cull.leftX &&
|
e.pos.x + e.aabb.w / 2 >= cull.leftX - margin &&
|
||||||
e.pos.x - e.aabb.w / 2 <= cull.rightX &&
|
e.pos.x - e.aabb.w / 2 <= cull.rightX + margin &&
|
||||||
e.pos.y + e.aabb.h / 2 >= cull.bottomY &&
|
e.pos.y + e.aabb.h / 2 >= cull.bottomY - margin &&
|
||||||
e.pos.y - e.aabb.h / 2 <= cull.topY
|
e.pos.y - e.aabb.h / 2 <= cull.topY + margin
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Reinforcement Scheduler (dynamic edge spawns) -----------------
|
||||||
|
|
||||||
|
/** An enemy spawned by the reinforcement scheduler, with a jump-in arc. */
|
||||||
|
export interface IReinforcementSpawn {
|
||||||
|
enemy: EnemyAIBase;
|
||||||
|
/** The edge the enemy is jumping in from. */
|
||||||
|
edge: 'left' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages periodic reinforcement waves — enemies that jump in from screen
|
||||||
|
* edges even when the player is standing still. Each rule specifies an
|
||||||
|
* enemy type, interval, count, and which edge(s) they appear from.
|
||||||
|
*
|
||||||
|
* Usage: call `tick()` every frame. It returns newly-spawned enemies that
|
||||||
|
* the caller must register with `EnemyManager.spawn()` and create visual
|
||||||
|
* nodes for.
|
||||||
|
*/
|
||||||
|
export class ReinforcementScheduler {
|
||||||
|
private readonly timers = new Map<number, number>();
|
||||||
|
private readonly totals = new Map<number, number>();
|
||||||
|
|
||||||
|
constructor(private readonly rules: IReinforcementRule[]) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tick the scheduler. Returns enemies that should be spawned this frame.
|
||||||
|
* The caller is responsible for:
|
||||||
|
* 1. Passing the new enemies to `EnemyManager.spawn()`
|
||||||
|
* 2. Creating visual nodes for them
|
||||||
|
* 3. Calling `EnemyManager.update()` as usual
|
||||||
|
*
|
||||||
|
* @param dtSec Frame delta in seconds.
|
||||||
|
* @param elapsedSec Total elapsed seconds since level start.
|
||||||
|
* @param cull Current camera culling rect (used to determine edge positions).
|
||||||
|
* @param groundY Ground Y in physics coords (feet surface).
|
||||||
|
* @param enemyCfgFactory A function that returns the IEnemyConfig for a given EnemyType.
|
||||||
|
*/
|
||||||
|
public tick(
|
||||||
|
dtSec: number,
|
||||||
|
elapsedSec: number,
|
||||||
|
cull: ICullingRect,
|
||||||
|
groundY: number,
|
||||||
|
enemyCfgFactory: (type: EnemyType) => { moveSpeed: number; attackIntervalSec: number; size: { w: number; h: number } },
|
||||||
|
): IReinforcementSpawn[] {
|
||||||
|
const spawns: IReinforcementSpawn[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < this.rules.length; i++) {
|
||||||
|
const rule = this.rules[i];
|
||||||
|
|
||||||
|
// Check delay
|
||||||
|
if (rule.delaySec && elapsedSec < rule.delaySec) continue;
|
||||||
|
|
||||||
|
// Check max total
|
||||||
|
const total = this.totals.get(i) ?? 0;
|
||||||
|
if (rule.maxTotal && rule.maxTotal > 0 && total >= rule.maxTotal) continue;
|
||||||
|
|
||||||
|
// Accumulate timer
|
||||||
|
const timer = this.timers.get(i) ?? 0;
|
||||||
|
const newTimer = timer + dtSec;
|
||||||
|
this.timers.set(i, newTimer);
|
||||||
|
|
||||||
|
if (newTimer < rule.intervalSec) continue;
|
||||||
|
|
||||||
|
// Reset timer (keep remainder for smoother intervals)
|
||||||
|
this.timers.set(i, newTimer - rule.intervalSec);
|
||||||
|
|
||||||
|
// Determine edge(s)
|
||||||
|
const edges: Array<'left' | 'right'> = rule.edge === 'both'
|
||||||
|
? (Math.random() < 0.5 ? ['left'] : ['right'])
|
||||||
|
: [rule.edge];
|
||||||
|
|
||||||
|
for (const edge of edges) {
|
||||||
|
for (let j = 0; j < rule.count; j++) {
|
||||||
|
const cfg = enemyCfgFactory(rule.type);
|
||||||
|
// Spawn position: just outside the culling rect on the chosen edge.
|
||||||
|
// Moving enemies (moveSpeed > 0) start farther out and walk in;
|
||||||
|
// stationary enemies (YaoFang) start just inside the visible edge.
|
||||||
|
const margin = cfg.moveSpeed > 0 ? 60 : -30; // negative = inside screen
|
||||||
|
const spawnX = edge === 'left'
|
||||||
|
? cull.leftX - margin - j * 50
|
||||||
|
: cull.rightX + margin + j * 50;
|
||||||
|
const spawnY = groundY + cfg.size.h / 2;
|
||||||
|
|
||||||
|
const enemy = createReinforcementEnemy(rule.type, cfg, spawnX, spawnY, edge);
|
||||||
|
spawns.push({ enemy, edge });
|
||||||
|
this.totals.set(i, (this.totals.get(i) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return spawns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset all timers and counters (e.g. on level restart). */
|
||||||
|
public reset(): void {
|
||||||
|
this.timers.clear();
|
||||||
|
this.totals.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an enemy AI instance for a reinforcement spawn.
|
||||||
|
* Reuses the same AI classes as static spawns. The enemy spawns at ground
|
||||||
|
* level on the chosen edge; moving enemies (ChiRen) will walk into the
|
||||||
|
* screen automatically, while stationary enemies (YaoFang) are placed
|
||||||
|
* just inside the visible edge so they're immediately active.
|
||||||
|
*/
|
||||||
|
function createReinforcementEnemy(
|
||||||
|
type: EnemyType,
|
||||||
|
cfg: { moveSpeed: number; attackIntervalSec: number; size: { w: number; h: number } },
|
||||||
|
spawnX: number,
|
||||||
|
spawnY: number,
|
||||||
|
_edge: 'left' | 'right',
|
||||||
|
): EnemyAIBase {
|
||||||
|
const fullCfg = {
|
||||||
|
id: type,
|
||||||
|
displayName: type,
|
||||||
|
size: cfg.size,
|
||||||
|
moveSpeed: cfg.moveSpeed,
|
||||||
|
attackIntervalSec: cfg.attackIntervalSec,
|
||||||
|
attacks: [] as never[],
|
||||||
|
hp: type === EnemyType.HeiRen ? 2 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case EnemyType.ChiRen: return new ChiRenAI(fullCfg, spawnX, spawnY);
|
||||||
|
case EnemyType.HeiRen: return new HeiRenAI(fullCfg, spawnX, spawnY);
|
||||||
|
case EnemyType.YaoFang: return new YaoFangAI(fullCfg, spawnX, spawnY);
|
||||||
|
case EnemyType.QingRen:
|
||||||
|
default: return new QingRenAI(fullCfg, spawnX, spawnY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -80,13 +80,16 @@ export class JumpController {
|
|||||||
/** Called on `jumpPressed` UI event. */
|
/** Called on `jumpPressed` UI event. */
|
||||||
public pressJump(nowMs: number): IJumpDispatchResult {
|
public pressJump(nowMs: number): IJumpDispatchResult {
|
||||||
if (!this.motion.isGrounded) {
|
if (!this.motion.isGrounded) {
|
||||||
|
console.log('[JumpController] pressJump REJECTED — airborne, phase=', this.phase);
|
||||||
return { phase: this.phase, height: 0, horizontalImpulse: 0, reason: 'airborne' };
|
return { phase: this.phase, height: 0, horizontalImpulse: 0, reason: 'airborne' };
|
||||||
}
|
}
|
||||||
if (this.phase !== 'idle') {
|
if (this.phase !== 'idle') {
|
||||||
|
console.log('[JumpController] pressJump REJECTED — phase=', this.phase, ', not idle');
|
||||||
return { phase: this.phase, height: 0, horizontalImpulse: 0, reason: 'already_in_jump' };
|
return { phase: this.phase, height: 0, horizontalImpulse: 0, reason: 'already_in_jump' };
|
||||||
}
|
}
|
||||||
this.phase = 'charging';
|
this.phase = 'charging';
|
||||||
this.pressTs = nowMs;
|
this.pressTs = nowMs;
|
||||||
|
console.log('[JumpController] pressJump ACCEPTED — entering charging phase');
|
||||||
return { phase: this.phase, height: 0, horizontalImpulse: 0 };
|
return { phase: this.phase, height: 0, horizontalImpulse: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export interface IPlayerMotionOptions {
|
|||||||
platforms: IPlatform[];
|
platforms: IPlatform[];
|
||||||
/** Starting color state. */
|
/** Starting color state. */
|
||||||
initialColorState?: PlayerColorState;
|
initialColorState?: PlayerColorState;
|
||||||
|
/** Level horizontal extent (px). Used to clamp player X so they cannot leave the level. */
|
||||||
|
levelLengthPx: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_GRAVITY = 2500; // px/s² — tuned so a 250px jump peaks in ~0.45s
|
export const DEFAULT_GRAVITY = 2500; // px/s² — tuned so a 250px jump peaks in ~0.45s
|
||||||
@@ -63,12 +65,14 @@ export class PlayerMotionModel {
|
|||||||
private _aabb: IAxisAlignedBox;
|
private _aabb: IAxisAlignedBox;
|
||||||
private _platforms: IPlatform[];
|
private _platforms: IPlatform[];
|
||||||
private readonly gravity: number;
|
private readonly gravity: number;
|
||||||
|
private readonly _levelLengthPx: number;
|
||||||
|
|
||||||
constructor(options: IPlayerMotionOptions) {
|
constructor(options: IPlayerMotionOptions) {
|
||||||
this._aabb = { ...options.aabb };
|
this._aabb = { ...options.aabb };
|
||||||
this._platforms = options.platforms.slice();
|
this._platforms = options.platforms.slice();
|
||||||
this._colorState = options.initialColorState ?? PlayerColorState.Red;
|
this._colorState = options.initialColorState ?? PlayerColorState.Red;
|
||||||
this.gravity = options.gravity ?? DEFAULT_GRAVITY;
|
this.gravity = options.gravity ?? DEFAULT_GRAVITY;
|
||||||
|
this._levelLengthPx = options.levelLengthPx;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- accessors ----------------------------------------------------------
|
// -- accessors ----------------------------------------------------------
|
||||||
@@ -130,6 +134,9 @@ export class PlayerMotionModel {
|
|||||||
* ground; mid-air `_vx` is preserved (起跳定型).
|
* ground; mid-air `_vx` is preserved (起跳定型).
|
||||||
*/
|
*/
|
||||||
public update(dt: number): void {
|
public update(dt: number): void {
|
||||||
|
// Remember feet position before integration (for sweep test).
|
||||||
|
const prevFeetY = this._aabb.y - this._aabb.h / 2;
|
||||||
|
|
||||||
if (this._grounded) {
|
if (this._grounded) {
|
||||||
this._vx = this._horizontalInput * MOVE_SPEED[this._colorState];
|
this._vx = this._horizontalInput * MOVE_SPEED[this._colorState];
|
||||||
this._vy = 0;
|
this._vy = 0;
|
||||||
@@ -143,16 +150,34 @@ export class PlayerMotionModel {
|
|||||||
x: this._aabb.x + this._vx * dt,
|
x: this._aabb.x + this._vx * dt,
|
||||||
y: this._aabb.y + this._vy * dt,
|
y: this._aabb.y + this._vy * dt,
|
||||||
};
|
};
|
||||||
// Resolve against platforms (basic AABB vs. top-surface only).
|
|
||||||
|
const curFeetY = this._aabb.y - this._aabb.h / 2;
|
||||||
|
|
||||||
|
// Resolve against platforms using sweep test.
|
||||||
|
// If the feet crossed a platform surface this frame (prev above → cur below),
|
||||||
|
// snap the player onto that platform — prevents tunneling at high fall speeds.
|
||||||
this._grounded = false;
|
this._grounded = false;
|
||||||
for (const p of this._platforms) {
|
for (const p of this._platforms) {
|
||||||
if (this.isRestingOn(p)) {
|
const withinHorizontal = this._aabb.x >= p.leftX && this._aabb.x <= p.rightX;
|
||||||
|
if (!withinHorizontal) continue;
|
||||||
|
if (this._vy > 0) continue; // moving upward — cannot land
|
||||||
|
|
||||||
|
// Sweep test: feet were above topY last frame, now at or below topY.
|
||||||
|
const crossedSurface = prevFeetY >= p.topY - 0.5 && curFeetY <= p.topY + 0.5;
|
||||||
|
// Also catch the small-window case (slow fall, feet near surface).
|
||||||
|
const nearSurface = curFeetY <= p.topY + 0.5 && curFeetY >= p.topY - 6;
|
||||||
|
|
||||||
|
if (crossedSurface || nearSurface) {
|
||||||
this._grounded = true;
|
this._grounded = true;
|
||||||
this._aabb = { ...this._aabb, y: p.topY + this._aabb.h / 2 };
|
this._aabb = { ...this._aabb, y: p.topY + this._aabb.h / 2 };
|
||||||
if (this._vy < 0) this._vy = 0;
|
if (this._vy < 0) this._vy = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clamp AABB X within level boundaries so the player cannot leave the level.
|
||||||
|
const halfW = this._aabb.w / 2;
|
||||||
|
this._aabb.x = Math.max(halfW, Math.min(this._aabb.x, this._levelLengthPx - halfW));
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- helpers ------------------------------------------------------------
|
// -- helpers ------------------------------------------------------------
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export interface IPlayerState {
|
|||||||
export class PlayerStateMachine {
|
export class PlayerStateMachine {
|
||||||
private state: IPlayerState;
|
private state: IPlayerState;
|
||||||
|
|
||||||
constructor(initialLives = 3) {
|
constructor(initialLives = 1) {
|
||||||
this.state = {
|
this.state = {
|
||||||
color: PlayerColorState.Red,
|
color: PlayerColorState.Red,
|
||||||
lives: initialLives,
|
lives: initialLives,
|
||||||
@@ -135,9 +135,12 @@ export class PlayerStateMachine {
|
|||||||
this.startIFrames();
|
this.startIFrames();
|
||||||
if (this.state.lives === 0) {
|
if (this.state.lives === 0) {
|
||||||
this.state.isDead = true;
|
this.state.isDead = true;
|
||||||
}
|
|
||||||
return { kind: 'died', cause };
|
return { kind: 'died', cause };
|
||||||
}
|
}
|
||||||
|
// Lost a life but still alive — treat as a downgrade so callers
|
||||||
|
// can distinguish real death from a mere life-loss.
|
||||||
|
return { kind: 'downgraded', from: this.state.color, to: PlayerColorState.Red };
|
||||||
|
}
|
||||||
|
|
||||||
private startIFrames(): void {
|
private startIFrames(): void {
|
||||||
this.state.iframeSec = PLAYER_IFRAME_SECONDS;
|
this.state.iframeSec = PLAYER_IFRAME_SECONDS;
|
||||||
|
|||||||
@@ -1,21 +1,102 @@
|
|||||||
import { _decorator, Component, director, Color, Label, Node } from 'cc';
|
import { _decorator, Component, director, Color, Graphics, Label, Node, Sprite, UITransform, Vec3 } from 'cc';
|
||||||
import { ConfigMgr } from '../data/ConfigMgr';
|
import { ConfigMgr } from '../data/ConfigMgr';
|
||||||
import { CCJsonLoader } from './CCJsonLoader';
|
import { CCJsonLoader } from './CCJsonLoader';
|
||||||
import { LevelMgr } from '../logic/LevelMgr';
|
import { LevelMgr } from '../logic/LevelMgr';
|
||||||
import { ensureCanvasSize, createLabel } from './MainMenuEntry';
|
import {
|
||||||
import { DESIGN_HEIGHT } from '../common/Constants';
|
ensureCanvasSize,
|
||||||
|
createLabel,
|
||||||
|
createSprite,
|
||||||
|
createSpriteSheetFrame,
|
||||||
|
} from './MainMenuEntry';
|
||||||
|
import {
|
||||||
|
DESIGN_WIDTH,
|
||||||
|
DESIGN_HEIGHT,
|
||||||
|
PlayerColorState,
|
||||||
|
} from '../common/Constants';
|
||||||
|
import { globalEventBus, globalTimeMgr } from '../common/index';
|
||||||
|
import { PlayerMotionModel, HorizontalInput, IPlatform } from '../logic/PlayerMotionModel';
|
||||||
|
import { JumpController } from '../logic/JumpController';
|
||||||
|
import { PlayerStateMachine } from '../logic/PlayerStateMachine';
|
||||||
|
import { AttackController, IJumpStateProvider, IAttackDispatchEvent } from '../logic/AttackController';
|
||||||
|
import { EnemyAIBase, EnemyManager, QingRenAI, ChiRenAI, HeiRenAI, YaoFangAI, IEnemyAction, ICullingRect, ReinforcementScheduler, IReinforcementSpawn } from '../logic/EnemyAI';
|
||||||
|
import { CameraScroller, cameraFromLevel, PARALLAX_LAYERS, ParallaxLayer } from '../logic/CameraScroller';
|
||||||
|
import { DamageSystem } from '../logic/DamageSystem';
|
||||||
|
import { InputEvents } from '../ui/InputEvents';
|
||||||
|
import { JoystickAngleClass, DEFAULT_LAYOUT } from '../ui/InputModel';
|
||||||
|
import { FloatingControlLayer } from '../ui/FloatingControlLayer';
|
||||||
|
import { EnemyType, WeaponType, IReinforcementRule } from '../data/Interfaces';
|
||||||
|
|
||||||
const { ccclass, property } = _decorator;
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Coordinate conventions
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PlayerMotionModel uses +y-up (physics). Cocos Creator uses +y-down
|
||||||
|
// (render). The design resolution origin is at the centre of the 960×540
|
||||||
|
// canvas. We convert: cocosY = -physicsY (relative to canvas centre).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Ground platform Y in physics coords (+y up). */
|
||||||
|
const GROUND_Y = 110;
|
||||||
|
/** Hero spawn X in physics coords. */
|
||||||
|
const HERO_SPAWN_X = 120;
|
||||||
|
|
||||||
|
/** Visual scale applied to the hero sprite (16×32 → 48×96). */
|
||||||
|
const HERO_SCALE = 3;
|
||||||
|
/** Visual scale applied to enemy sprites (16×16 → 48×48). */
|
||||||
|
const ENEMY_SCALE = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map an EnemyType to its sprite-sheet resource path and frame dimensions.
|
||||||
|
*/
|
||||||
|
function enemySpriteInfo(type: EnemyType): { res: string; fw: number; fh: number } {
|
||||||
|
switch (type) {
|
||||||
|
case EnemyType.QingRen: return { res: 'textures/enemies/qing_ren', fw: 16, fh: 16 };
|
||||||
|
case EnemyType.ChiRen: return { res: 'textures/enemies/chi_ren', fw: 16, fh: 16 };
|
||||||
|
case EnemyType.HeiRen: return { res: 'textures/enemies/hei_ren', fw: 20, fh: 24 };
|
||||||
|
case EnemyType.YaoFang: return { res: 'textures/enemies/yao_fang', fw: 18, fh: 20 };
|
||||||
|
default: return { res: 'textures/enemies/qing_ren', fw: 16, fh: 16 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map an EnemyType enum to the concrete AI class. */
|
||||||
|
function createEnemyAI(type: EnemyType, cfg: { moveSpeed: number; attackIntervalSec: number; size: { w: number; h: number } }, spawnX: number, spawnY: number): EnemyAIBase {
|
||||||
|
const fullCfg = { id: type, displayName: type, size: cfg.size, moveSpeed: cfg.moveSpeed, attackIntervalSec: cfg.attackIntervalSec, attacks: [] as never[], hp: 1 };
|
||||||
|
switch (type) {
|
||||||
|
case EnemyType.ChiRen: return new ChiRenAI(fullCfg, spawnX, spawnY);
|
||||||
|
case EnemyType.HeiRen: return new HeiRenAI(fullCfg, spawnX, spawnY);
|
||||||
|
case EnemyType.YaoFang: return new YaoFangAI(fullCfg, spawnX, spawnY);
|
||||||
|
case EnemyType.QingRen:
|
||||||
|
default: return new QingRenAI(fullCfg, spawnX, spawnY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// JumpStateProvider — bridges JumpController ↔ PlayerMotionModel
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
class JumpStateProvider implements IJumpStateProvider {
|
||||||
|
private _lastJumpPressTs: number | undefined;
|
||||||
|
|
||||||
|
public setLastJumpPressTs(ts: number | undefined): void {
|
||||||
|
this._lastJumpPressTs = ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public lastJumpPressTs(): number | undefined {
|
||||||
|
return this._lastJumpPressTs;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private readonly motion: PlayerMotionModel) {}
|
||||||
|
|
||||||
|
public isGrounded(): boolean {
|
||||||
|
return this.motion.isGrounded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic Level scene entry (task 7.1-7.2 hookup).
|
* Generic Level scene entry (task 7.1-7.2 hookup).
|
||||||
*
|
*
|
||||||
* Attach to the root node of `Level_1_1` … `Level_1_5`. Configure the
|
* Now fully wired: input events → logic models → scene node positions,
|
||||||
* `levelId` property in the Inspector ("1-1", "1-2", ...).
|
* so both the player and enemies move in real-time.
|
||||||
*
|
|
||||||
* For MVP the scene has no real gameplay rendering yet; `autoBuildUI`
|
|
||||||
* simply draws a top-centered label with the current level id so you can
|
|
||||||
* verify scene transitions by eye.
|
|
||||||
*/
|
*/
|
||||||
@ccclass('LevelEntry')
|
@ccclass('LevelEntry')
|
||||||
export class LevelEntry extends Component {
|
export class LevelEntry extends Component {
|
||||||
@@ -28,27 +109,821 @@ export class LevelEntry extends Component {
|
|||||||
@property({ tooltip: '是否自动创建关卡 HUD (顶部显示 Level / 倒计时)' })
|
@property({ tooltip: '是否自动创建关卡 HUD (顶部显示 Level / 倒计时)' })
|
||||||
public autoBuildUI: boolean = true;
|
public autoBuildUI: boolean = true;
|
||||||
|
|
||||||
private mgr: LevelMgr | undefined;
|
// -- logic models -------------------------------------------------------
|
||||||
|
private mgr!: LevelMgr;
|
||||||
|
private cfg!: ConfigMgr;
|
||||||
|
private motion!: PlayerMotionModel;
|
||||||
|
private jumpCtrl!: JumpController;
|
||||||
|
private psm!: PlayerStateMachine;
|
||||||
|
private attackCtrl!: AttackController;
|
||||||
|
private jumpStateProvider!: JumpStateProvider;
|
||||||
|
private enemyMgr!: EnemyManager;
|
||||||
|
private camera!: CameraScroller;
|
||||||
|
private dmgSystem!: DamageSystem;
|
||||||
|
private reinforcementScheduler!: ReinforcementScheduler;
|
||||||
|
private levelElapsedSec = 0;
|
||||||
|
|
||||||
|
// -- view nodes ---------------------------------------------------------
|
||||||
private hudNode: Node | null = null;
|
private hudNode: Node | null = null;
|
||||||
|
private heroNode: Node | null = null;
|
||||||
|
/** Enemy AI → visual node mapping. */
|
||||||
|
private enemyNodes = new Map<EnemyAIBase, Node>();
|
||||||
|
/** Parallax background nodes keyed by layer name. */
|
||||||
|
private bgNodes = new Map<string, Node>();
|
||||||
|
/** Projectile visual nodes (shuriken, fireball, etc.). */
|
||||||
|
private projectileNodes: Node[] = [];
|
||||||
|
|
||||||
|
// -- input state --------------------------------------------------------
|
||||||
|
private currentJoystickKlass: JoystickAngleClass = 'none';
|
||||||
|
private currentHorizontalInput: HorizontalInput = 0;
|
||||||
|
|
||||||
|
// -- event handler refs (for cleanup) -----------------------------------
|
||||||
|
private boundHandlers: Array<[string, (payload: any) => void]> = [];
|
||||||
|
private _deathHandled = false;
|
||||||
|
/** Reference to the FloatingControlLayer container node — must remain the
|
||||||
|
* top-most sibling so dynamically-added sprites (projectiles, slashes,
|
||||||
|
* re-created hero) never occlude touch input. */
|
||||||
|
private ctrlLayerNode: Node | null = null;
|
||||||
|
|
||||||
protected async onLoad(): Promise<void> {
|
protected async onLoad(): Promise<void> {
|
||||||
|
// 1. Load configs
|
||||||
|
this.cfg = new ConfigMgr(new CCJsonLoader());
|
||||||
|
await this.cfg.load();
|
||||||
|
const levelCfg = this.cfg.level(this.levelId);
|
||||||
|
|
||||||
|
// 0. Reset global singletons that survive across scene loads.
|
||||||
|
// Placed AFTER await so update()-driven accumulation during the
|
||||||
|
// async gap does not negate the reset.
|
||||||
|
globalTimeMgr.reset();
|
||||||
|
|
||||||
|
this.mgr = new LevelMgr(levelCfg);
|
||||||
|
|
||||||
|
// 2. Build platforms (simple ground + level-specific platforms)
|
||||||
|
const platforms = this.buildPlatforms();
|
||||||
|
|
||||||
|
// 3. Create logic models
|
||||||
|
// AABB.y is the **centre** of the box; feet must align with the
|
||||||
|
// ground surface (topY = GROUND_Y), so centre = GROUND_Y + h/2.
|
||||||
|
const HERO_H = 32;
|
||||||
|
this.motion = new PlayerMotionModel({
|
||||||
|
aabb: { x: HERO_SPAWN_X, y: GROUND_Y + HERO_H / 2, w: 16, h: HERO_H },
|
||||||
|
platforms,
|
||||||
|
initialColorState: PlayerColorState.Red,
|
||||||
|
levelLengthPx: levelCfg.levelLengthPx,
|
||||||
|
});
|
||||||
|
this.psm = new PlayerStateMachine(1);
|
||||||
|
this.jumpStateProvider = new JumpStateProvider(this.motion);
|
||||||
|
this.jumpCtrl = new JumpController(this.motion);
|
||||||
|
this.attackCtrl = new AttackController(this.jumpStateProvider);
|
||||||
|
this.dmgSystem = new DamageSystem(this.psm);
|
||||||
|
this.camera = cameraFromLevel(levelCfg);
|
||||||
|
this.enemyMgr = new EnemyManager();
|
||||||
|
|
||||||
|
// 4. Spawn enemies from level config
|
||||||
|
for (const spawn of levelCfg.enemySpawns) {
|
||||||
|
const enemyCfg = this.cfg.enemy(spawn.type);
|
||||||
|
// Enemy pos.y = centre; feet must sit on ground (topY = GROUND_Y).
|
||||||
|
const spawnY = GROUND_Y + enemyCfg.size.h / 2;
|
||||||
|
const count = spawn.count ?? 1;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const ai = createEnemyAI(spawn.type, enemyCfg, spawn.atPx + i * 60, spawnY);
|
||||||
|
this.enemyMgr.spawn(ai);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4b. Initialize reinforcement scheduler from level config
|
||||||
|
this.reinforcementScheduler = new ReinforcementScheduler(levelCfg.reinforcements ?? []);
|
||||||
|
|
||||||
|
// 5. Build UI & sprites
|
||||||
if (this.autoBuildUI) this.buildDefaultUI();
|
if (this.autoBuildUI) this.buildDefaultUI();
|
||||||
const cfg = new ConfigMgr(new CCJsonLoader());
|
|
||||||
await cfg.load();
|
// 6. Add FloatingControlLayer
|
||||||
this.mgr = new LevelMgr(cfg.level(this.levelId));
|
this.addControlLayer();
|
||||||
|
|
||||||
|
// 7. Subscribe to input events
|
||||||
|
this.subscribeInputEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected update(dt: number): void {
|
protected update(dt: number): void {
|
||||||
if (!this.mgr) return;
|
// 1. Update time
|
||||||
const status = this.mgr.tick(dt);
|
globalTimeMgr.update(dt);
|
||||||
|
const scaledDt = globalTimeMgr.scaledDelta(dt);
|
||||||
|
if (scaledDt <= 0) return; // paused
|
||||||
|
|
||||||
|
// Guard: onLoad is async — models may not be ready yet.
|
||||||
|
if (!this.attackCtrl || !this.psm) return;
|
||||||
|
|
||||||
|
// 11. Early-out: if the player died in a previous frame, skip all
|
||||||
|
// gameplay logic (motion, attacks, enemies) and go straight to
|
||||||
|
// the death → scene-transition path.
|
||||||
|
if (this.psm.isDead) {
|
||||||
|
if (!this._deathHandled) {
|
||||||
|
const deathStatus = this.mgr.tick(scaledDt);
|
||||||
this.refreshHud();
|
this.refreshHud();
|
||||||
|
if (deathStatus === 'player_dead') {
|
||||||
|
this._deathHandled = true;
|
||||||
|
this.attackCtrl.reset();
|
||||||
|
this.unsubscribeInputEvents();
|
||||||
|
director.loadScene('Settlement');
|
||||||
|
}
|
||||||
|
// If deathStatus is still 'running', onPlayerDied() was just
|
||||||
|
// called in the previous frame's step-11 but LevelMgr hasn't
|
||||||
|
// propagated the state yet. _deathHandled stays false so we
|
||||||
|
// retry next frame.
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nowMs = globalTimeMgr.realTime * 1000;
|
||||||
|
|
||||||
|
// 2. Tick player motion FIRST so that isGrounded is up-to-date
|
||||||
|
// when jumpCtrl.tick reads it (fixes "cannot jump after landing"
|
||||||
|
// bug — jumpCtrl was reading stale isGrounded from previous frame).
|
||||||
|
this.motion.setHorizontalInput(this.currentHorizontalInput);
|
||||||
|
this.motion.update(scaledDt);
|
||||||
|
this.jumpCtrl.tick(nowMs);
|
||||||
|
|
||||||
|
// 3. Tick player state machine (i-frames)
|
||||||
|
this.psm.tick(scaledDt);
|
||||||
|
|
||||||
|
// 4. Tick attack controller
|
||||||
|
const attacks = this.attackCtrl.tick(nowMs, this.psm.color);
|
||||||
|
|
||||||
|
// 5. Tick enemies
|
||||||
|
const playerSense = {
|
||||||
|
x: this.motion.aabb.x,
|
||||||
|
y: this.motion.aabb.y,
|
||||||
|
isGrounded: this.motion.isGrounded,
|
||||||
|
};
|
||||||
|
const cull = this.camera.cullRect();
|
||||||
|
const CULLING_MARGIN = 100; // allow enemies just outside screen to still update AI (reinforcements)
|
||||||
|
const enemyActions = this.enemyMgr.update(scaledDt, nowMs, playerSense, cull as ICullingRect, CULLING_MARGIN);
|
||||||
|
|
||||||
|
// 6. Process enemy actions (spawn projectiles, etc.)
|
||||||
|
this.processEnemyActions(enemyActions);
|
||||||
|
|
||||||
|
// 6b. Tick reinforcement scheduler — spawn enemies from screen edges
|
||||||
|
this.levelElapsedSec += scaledDt;
|
||||||
|
const newReinforcements = this.reinforcementScheduler.tick(
|
||||||
|
scaledDt,
|
||||||
|
this.levelElapsedSec,
|
||||||
|
cull as ICullingRect,
|
||||||
|
GROUND_Y,
|
||||||
|
(type: EnemyType) => this.cfg.enemy(type),
|
||||||
|
);
|
||||||
|
for (const r of newReinforcements) {
|
||||||
|
this.enemyMgr.spawn(r.enemy);
|
||||||
|
// Create visual node for the reinforcement enemy
|
||||||
|
const info = enemySpriteInfo(r.enemy.type);
|
||||||
|
const ePos = this.physicsToCocos(r.enemy.pos.x, r.enemy.pos.y);
|
||||||
|
const node = createSpriteSheetFrame(
|
||||||
|
this.node,
|
||||||
|
info.res,
|
||||||
|
0,
|
||||||
|
info.fw, info.fh,
|
||||||
|
ePos.x, ePos.y,
|
||||||
|
info.fw * ENEMY_SCALE, info.fh * ENEMY_SCALE,
|
||||||
|
{ name: `Reinforce_${r.enemy.type}`, flipX: r.edge === 'right' },
|
||||||
|
);
|
||||||
|
this.enemyNodes.set(r.enemy, node);
|
||||||
|
this.promoteCtrlLayerToTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Process player attacks (spawn projectiles)
|
||||||
|
this.processPlayerAttacks(attacks);
|
||||||
|
|
||||||
|
// 8. Camera follow
|
||||||
|
this.camera.followPlayer(this.motion.aabb.x, this.motion.aabb.y);
|
||||||
|
|
||||||
|
// 9. Tick level manager
|
||||||
|
const status = this.mgr.tick(scaledDt);
|
||||||
|
|
||||||
|
// 10. Sync all visual nodes
|
||||||
|
this.syncHeroNode();
|
||||||
|
this.syncEnemyNodes();
|
||||||
|
this.syncParallax();
|
||||||
|
this.syncProjectiles(scaledDt);
|
||||||
|
this.refreshHud();
|
||||||
|
|
||||||
|
// Keep the FloatingControlLayer as the top-most sibling so touch
|
||||||
|
// events on attack/jump buttons are never shadowed by dynamically
|
||||||
|
// spawned sprites (projectiles / slash FX / re-created hero).
|
||||||
|
this.promoteCtrlLayerToTop();
|
||||||
|
|
||||||
|
// 11. Check game-over conditions
|
||||||
|
if (this.psm.isDead) {
|
||||||
|
this.mgr.onPlayerDied();
|
||||||
|
}
|
||||||
if (status === 'victory') {
|
if (status === 'victory') {
|
||||||
|
this.unsubscribeInputEvents();
|
||||||
director.loadScene(this.nextSceneName || this.deriveNextScene());
|
director.loadScene(this.nextSceneName || this.deriveNextScene());
|
||||||
} else if (status === 'timeout' || status === 'player_dead') {
|
} else if (status === 'timeout' || status === 'player_dead') {
|
||||||
|
this.unsubscribeInputEvents();
|
||||||
director.loadScene('Settlement');
|
director.loadScene('Settlement');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected onDestroy(): void {
|
||||||
|
this.attackCtrl?.reset();
|
||||||
|
this.unsubscribeInputEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// Platform construction
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
|
private buildPlatforms(): IPlatform[] {
|
||||||
|
const platforms: IPlatform[] = [];
|
||||||
|
// Ground platform — spans the entire level length
|
||||||
|
const levelCfg = this.cfg.level(this.levelId);
|
||||||
|
platforms.push({
|
||||||
|
topY: GROUND_Y,
|
||||||
|
leftX: 0,
|
||||||
|
rightX: levelCfg.levelLengthPx,
|
||||||
|
});
|
||||||
|
// TODO: Add elevated platforms per level layout when available.
|
||||||
|
return platforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// UI construction
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
|
private buildDefaultUI(): void {
|
||||||
|
ensureCanvasSize(this.node);
|
||||||
|
|
||||||
|
// Parallax background (4 layers).
|
||||||
|
const theme = this.pickThemeFolder();
|
||||||
|
for (const layer of PARALLAX_LAYERS) {
|
||||||
|
const node = createSprite(
|
||||||
|
this.node,
|
||||||
|
`textures/scenes/${theme}/${layer}`,
|
||||||
|
0, 0,
|
||||||
|
DESIGN_WIDTH, DESIGN_HEIGHT,
|
||||||
|
{ name: `BG_${layer}` },
|
||||||
|
);
|
||||||
|
this.bgNodes.set(layer, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hero sprite (first idle frame of kage_red, 16×32 upscaled ×3).
|
||||||
|
const heroPos = this.physicsToCocos(this.motion.aabb.x, this.motion.aabb.y);
|
||||||
|
this.heroNode = createSpriteSheetFrame(
|
||||||
|
this.node,
|
||||||
|
'textures/characters/kage_red',
|
||||||
|
0,
|
||||||
|
16, 32,
|
||||||
|
heroPos.x, heroPos.y,
|
||||||
|
16 * HERO_SCALE, 32 * HERO_SCALE,
|
||||||
|
{ name: 'Hero' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enemy sprites — one per spawned enemy.
|
||||||
|
for (const enemy of this.enemyMgr.all) {
|
||||||
|
const info = enemySpriteInfo(enemy.type);
|
||||||
|
const ePos = this.physicsToCocos(enemy.pos.x, enemy.pos.y);
|
||||||
|
const node = createSpriteSheetFrame(
|
||||||
|
this.node,
|
||||||
|
info.res,
|
||||||
|
0,
|
||||||
|
info.fw, info.fh,
|
||||||
|
ePos.x, ePos.y,
|
||||||
|
info.fw * ENEMY_SCALE, info.fh * ENEMY_SCALE,
|
||||||
|
{ name: `Enemy_${enemy.type}`, flipX: true },
|
||||||
|
);
|
||||||
|
this.enemyNodes.set(enemy, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HUD text.
|
||||||
|
createLabel(this.node, `Level ${this.levelId}`, 0, DESIGN_HEIGHT / 2 - 50, 28, Color.WHITE);
|
||||||
|
this.hudNode = createLabel(
|
||||||
|
this.node,
|
||||||
|
'Time: --',
|
||||||
|
0,
|
||||||
|
DESIGN_HEIGHT / 2 - 90,
|
||||||
|
22,
|
||||||
|
new Color(255, 220, 120, 255),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add the FloatingControlLayer component on a full-screen UI child node. */
|
||||||
|
private addControlLayer(): void {
|
||||||
|
console.log('[LevelEntry] addControlLayer start — this.node=', this.node?.name, 'isValid=', this.node?.isValid);
|
||||||
|
const ctrlNode = new Node('FloatingControlLayer');
|
||||||
|
ctrlNode.layer = this.node.layer;
|
||||||
|
this.node.addChild(ctrlNode);
|
||||||
|
this.ctrlLayerNode = ctrlNode;
|
||||||
|
const ut = ctrlNode.addComponent(UITransform);
|
||||||
|
ut.setContentSize(DESIGN_WIDTH, DESIGN_HEIGHT);
|
||||||
|
ctrlNode.setPosition(new Vec3(0, 0, 0));
|
||||||
|
|
||||||
|
// Create visual sub-nodes for joystick and buttons.
|
||||||
|
const fcl = ctrlNode.addComponent(FloatingControlLayer);
|
||||||
|
console.log('[LevelEntry] addControlLayer — FloatingControlLayer component added. fcl=', !!fcl, 'ctrlNode.isValid=', ctrlNode.isValid);
|
||||||
|
const layout = DEFAULT_LAYOUT;
|
||||||
|
|
||||||
|
// Joystick: background disc + handle.
|
||||||
|
fcl.joystickRoot = this.createJoystickVisual(ctrlNode, layout.joystick);
|
||||||
|
// Jump button.
|
||||||
|
fcl.jumpRoot = this.createButtonVisual(ctrlNode, layout.jump, 'J', new Color(80, 200, 80, 180));
|
||||||
|
// Shuriken button.
|
||||||
|
fcl.shurikenRoot = this.createButtonVisual(ctrlNode, layout.shuriken, 'S', new Color(80, 120, 220, 180));
|
||||||
|
// Ninja-sword button.
|
||||||
|
fcl.ninjaSwordRoot = this.createButtonVisual(ctrlNode, layout.ninjaSword, 'K', new Color(220, 80, 80, 180));
|
||||||
|
console.log('[LevelEntry] addControlLayer done — visuals built');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the FloatingControlLayer is always the top-most sibling of
|
||||||
|
* `this.node`. Dynamically-spawned sprites (projectiles / slash FX /
|
||||||
|
* re-created hero sprite) are appended to the end of the sibling list
|
||||||
|
* by default, pushing the ctrl layer down the Z-order. When that
|
||||||
|
* happens, touch events on attack/jump buttons can be intercepted by
|
||||||
|
* those sprites' UITransform bounding boxes — producing the
|
||||||
|
* "buttons occasionally unresponsive" symptom.
|
||||||
|
*
|
||||||
|
* Called in `update()` every frame as a cheap safety net.
|
||||||
|
*/
|
||||||
|
private promoteCtrlLayerToTop(): void {
|
||||||
|
const ctrl = this.ctrlLayerNode as any;
|
||||||
|
if (!ctrl || !ctrl.isValid) return;
|
||||||
|
const parent = ctrl.parent;
|
||||||
|
if (!parent) return;
|
||||||
|
const lastIndex = parent.children.length - 1;
|
||||||
|
if (ctrl.getSiblingIndex() !== lastIndex) {
|
||||||
|
ctrl.setSiblingIndex(lastIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a joystick visual (semi-transparent disc + smaller handle). */
|
||||||
|
private createJoystickVisual(parent: Node, rect: { cx: number; cy: number; w: number; h: number }): Node {
|
||||||
|
const root = new Node('JoystickRoot');
|
||||||
|
root.layer = parent.layer;
|
||||||
|
parent.addChild(root);
|
||||||
|
const rootUt = root.addComponent(UITransform);
|
||||||
|
rootUt.setContentSize(rect.w, rect.h);
|
||||||
|
root.setPosition(new Vec3(rect.cx - DESIGN_WIDTH / 2, rect.cy - DESIGN_HEIGHT / 2, 0));
|
||||||
|
|
||||||
|
// Background disc.
|
||||||
|
const bg = new Node('JoystickBg');
|
||||||
|
bg.layer = parent.layer;
|
||||||
|
root.addChild(bg);
|
||||||
|
const bgUt = bg.addComponent(UITransform);
|
||||||
|
const bgRadius = rect.w / 2;
|
||||||
|
bgUt.setContentSize(rect.w, rect.h);
|
||||||
|
const bgGfx = bg.addComponent(Graphics);
|
||||||
|
bgGfx.fillColor = new Color(255, 255, 255, 50);
|
||||||
|
bgGfx.strokeColor = new Color(255, 255, 255, 100);
|
||||||
|
bgGfx.circle(0, 0, bgRadius);
|
||||||
|
bgGfx.fill();
|
||||||
|
bgGfx.stroke();
|
||||||
|
|
||||||
|
// Handle (smaller circle, offset to show direction).
|
||||||
|
const handle = new Node('JoystickHandle');
|
||||||
|
handle.layer = parent.layer;
|
||||||
|
root.addChild(handle);
|
||||||
|
const handleRadius = bgRadius * 0.4;
|
||||||
|
const handleUt = handle.addComponent(UITransform);
|
||||||
|
handleUt.setContentSize(handleRadius * 2, handleRadius * 2);
|
||||||
|
const handleGfx = handle.addComponent(Graphics);
|
||||||
|
handleGfx.fillColor = new Color(255, 255, 255, 140);
|
||||||
|
handleGfx.strokeColor = new Color(255, 255, 255, 200);
|
||||||
|
handleGfx.circle(0, 0, handleRadius);
|
||||||
|
handleGfx.fill();
|
||||||
|
handleGfx.stroke();
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a simple circular button with a text label. */
|
||||||
|
private createButtonVisual(
|
||||||
|
parent: Node,
|
||||||
|
rect: { cx: number; cy: number; w: number; h: number },
|
||||||
|
label: string,
|
||||||
|
tint: Color,
|
||||||
|
): Node {
|
||||||
|
const root = new Node(`Btn_${label}`);
|
||||||
|
root.layer = parent.layer;
|
||||||
|
parent.addChild(root);
|
||||||
|
const radius = Math.min(rect.w, rect.h) / 2;
|
||||||
|
const rootUt = root.addComponent(UITransform);
|
||||||
|
rootUt.setContentSize(rect.w, rect.h);
|
||||||
|
root.setPosition(new Vec3(rect.cx - DESIGN_WIDTH / 2, rect.cy - DESIGN_HEIGHT / 2, 0));
|
||||||
|
|
||||||
|
// Circle background.
|
||||||
|
const bg = new Node('Bg');
|
||||||
|
bg.layer = parent.layer;
|
||||||
|
root.addChild(bg);
|
||||||
|
const bgUt = bg.addComponent(UITransform);
|
||||||
|
bgUt.setContentSize(rect.w, rect.h);
|
||||||
|
const bgGfx = bg.addComponent(Graphics);
|
||||||
|
bgGfx.fillColor = tint;
|
||||||
|
bgGfx.strokeColor = new Color(255, 255, 255, 120);
|
||||||
|
bgGfx.circle(0, 0, radius);
|
||||||
|
bgGfx.fill();
|
||||||
|
bgGfx.stroke();
|
||||||
|
|
||||||
|
// Text label.
|
||||||
|
const lblNode = new Node('Label');
|
||||||
|
lblNode.layer = parent.layer;
|
||||||
|
root.addChild(lblNode);
|
||||||
|
const lblUt = lblNode.addComponent(UITransform);
|
||||||
|
lblUt.setContentSize(rect.w, rect.h);
|
||||||
|
const lbl = lblNode.addComponent(Label);
|
||||||
|
lbl.string = label;
|
||||||
|
lbl.fontSize = 28;
|
||||||
|
lbl.lineHeight = 28;
|
||||||
|
lbl.color = new Color(255, 255, 255, 220);
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// Input event subscription
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
|
private subscribeInputEvents(): void {
|
||||||
|
// Defensive: clear any stale handlers left by a previous LevelEntry
|
||||||
|
// instance that was destroyed before unsubscribeInputEvents() ran.
|
||||||
|
// Only clear the events that LevelEntry registers — not ButtonVisualChanged
|
||||||
|
// or other events that might be registered by other components.
|
||||||
|
const eventsToClear = [
|
||||||
|
InputEvents.JoystickMove,
|
||||||
|
InputEvents.JumpPressed,
|
||||||
|
InputEvents.JumpReleased,
|
||||||
|
InputEvents.ShurikenPressed,
|
||||||
|
InputEvents.ShurikenReleased,
|
||||||
|
InputEvents.NinjaSwordPressed,
|
||||||
|
InputEvents.NinjaSwordReleased,
|
||||||
|
];
|
||||||
|
for (const event of eventsToClear) {
|
||||||
|
globalEventBus.off(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onJoystickMove = (payload: { dx: number; dy: number; klass: JoystickAngleClass }) => {
|
||||||
|
console.log('[LevelEntry] JoystickMove:', payload.klass);
|
||||||
|
this.currentJoystickKlass = payload.klass;
|
||||||
|
if (payload.klass === 'none') {
|
||||||
|
this.currentHorizontalInput = 0;
|
||||||
|
} else if (payload.dx >= 0) {
|
||||||
|
this.currentHorizontalInput = 1;
|
||||||
|
} else {
|
||||||
|
this.currentHorizontalInput = -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onJumpPressed = () => {
|
||||||
|
const nowMs = globalTimeMgr.realTime * 1000;
|
||||||
|
console.log('[LevelEntry] JumpPressed at nowMs=', nowMs, 'realTime=', globalTimeMgr.realTime);
|
||||||
|
this.jumpStateProvider.setLastJumpPressTs(nowMs);
|
||||||
|
this.jumpCtrl.pressJump(nowMs);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onJumpReleased = (payload: { holdMs: number }) => {
|
||||||
|
const nowMs = globalTimeMgr.realTime * 1000;
|
||||||
|
console.log('[LevelEntry] JumpReleased holdMs=', payload.holdMs);
|
||||||
|
this.jumpCtrl.releaseJump(nowMs, this.currentJoystickKlass, this.psm.color);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onShurikenPressed = () => {
|
||||||
|
const nowMs = globalTimeMgr.realTime * 1000;
|
||||||
|
console.log('[LevelEntry] ShurikenPressed at nowMs=', nowMs, 'active=', this.attackCtrl.getActive(), 'pressed=', this.attackCtrl.isPressed(WeaponType.Shuriken));
|
||||||
|
this.attackCtrl.press(WeaponType.Shuriken, nowMs);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onShurikenReleased = () => {
|
||||||
|
console.log('[LevelEntry] ShurikenReleased');
|
||||||
|
this.attackCtrl.release(WeaponType.Shuriken);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNinjaSwordPressed = () => {
|
||||||
|
const nowMs = globalTimeMgr.realTime * 1000;
|
||||||
|
console.log('[LevelEntry] NinjaSwordPressed at nowMs=', nowMs);
|
||||||
|
this.attackCtrl.press(WeaponType.NinjaSword, nowMs);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNinjaSwordReleased = () => {
|
||||||
|
console.log('[LevelEntry] NinjaSwordReleased');
|
||||||
|
this.attackCtrl.release(WeaponType.NinjaSword);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlers: Array<[string, (payload: any) => void]> = [
|
||||||
|
[InputEvents.JoystickMove, onJoystickMove],
|
||||||
|
[InputEvents.JumpPressed, onJumpPressed],
|
||||||
|
[InputEvents.JumpReleased, onJumpReleased],
|
||||||
|
[InputEvents.ShurikenPressed, onShurikenPressed],
|
||||||
|
[InputEvents.ShurikenReleased, onShurikenReleased],
|
||||||
|
[InputEvents.NinjaSwordPressed, onNinjaSwordPressed],
|
||||||
|
[InputEvents.NinjaSwordReleased, onNinjaSwordReleased],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [event, handler] of handlers) {
|
||||||
|
globalEventBus.on(event, handler);
|
||||||
|
this.boundHandlers.push([event, handler]);
|
||||||
|
}
|
||||||
|
console.log('[LevelEntry] subscribeInputEvents done — registered', handlers.length, 'handlers. Listener counts:',
|
||||||
|
Object.values(InputEvents).map(e => `${e}=${globalEventBus.listenerCount(e)}`).join(', '));
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsubscribeInputEvents(): void {
|
||||||
|
for (const [event, handler] of this.boundHandlers) {
|
||||||
|
globalEventBus.off(event, handler);
|
||||||
|
}
|
||||||
|
this.boundHandlers.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// Visual sync — physics coords → Cocos node positions
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
|
/** Convert physics world coords (+y up) to Cocos node position (centred at 0,0). */
|
||||||
|
private physicsToCocos(physX: number, physY: number): Vec3 {
|
||||||
|
return new Vec3(
|
||||||
|
physX - DESIGN_WIDTH / 2 - this.camera.offsetX,
|
||||||
|
physY - DESIGN_HEIGHT / 2 - this.camera.offsetY,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncHeroNode(): void {
|
||||||
|
if (!this.heroNode || !this.heroNode.isValid) return;
|
||||||
|
const pos = this.physicsToCocos(this.motion.aabb.x, this.motion.aabb.y);
|
||||||
|
this.heroNode.setPosition(pos);
|
||||||
|
|
||||||
|
// Flip sprite based on horizontal velocity.
|
||||||
|
if (this.motion.vx > 0) {
|
||||||
|
this.heroNode.setScale(Math.abs(this.heroNode.scale.x), this.heroNode.scale.y, this.heroNode.scale.z);
|
||||||
|
} else if (this.motion.vx < 0) {
|
||||||
|
this.heroNode.setScale(-Math.abs(this.heroNode.scale.x), this.heroNode.scale.y, this.heroNode.scale.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update color-state texture.
|
||||||
|
this.updateHeroTexture();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _lastColorState: PlayerColorState = PlayerColorState.Red;
|
||||||
|
private updateHeroTexture(): void {
|
||||||
|
if (this.psm.color === this._lastColorState) return;
|
||||||
|
this._lastColorState = this.psm.color;
|
||||||
|
if (!this.heroNode || !this.heroNode.isValid) return;
|
||||||
|
|
||||||
|
const texMap: Record<PlayerColorState, string> = {
|
||||||
|
[PlayerColorState.Red]: 'textures/characters/kage_red',
|
||||||
|
[PlayerColorState.Green]: 'textures/characters/kage_green',
|
||||||
|
[PlayerColorState.Yellow]: 'textures/characters/kage_yellow',
|
||||||
|
};
|
||||||
|
// Recreate the hero node with the new texture.
|
||||||
|
// Use the physics model position directly (avoids CC 3.x Node.position getter issues).
|
||||||
|
const cocosPos = this.physicsToCocos(this.motion.aabb.x, this.motion.aabb.y);
|
||||||
|
const facingRight = this.motion.vx >= 0;
|
||||||
|
this.heroNode.destroy();
|
||||||
|
this.heroNode = createSpriteSheetFrame(
|
||||||
|
this.node,
|
||||||
|
texMap[this.psm.color],
|
||||||
|
0,
|
||||||
|
16, 32,
|
||||||
|
cocosPos.x, cocosPos.y,
|
||||||
|
16 * HERO_SCALE, 32 * HERO_SCALE,
|
||||||
|
{ name: 'Hero' },
|
||||||
|
);
|
||||||
|
if (!facingRight) {
|
||||||
|
this.heroNode.setScale(-HERO_SCALE, HERO_SCALE, HERO_SCALE);
|
||||||
|
}
|
||||||
|
// Newly-added hero sprite would otherwise sit on top of the ctrl
|
||||||
|
// layer and shadow touch input — promote ctrl layer back to top.
|
||||||
|
this.promoteCtrlLayerToTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncEnemyNodes(): void {
|
||||||
|
for (const [enemy, node] of this.enemyNodes) {
|
||||||
|
if (!node.isValid || !enemy.alive) {
|
||||||
|
if (node.isValid) {
|
||||||
|
node.active = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const pos = this.physicsToCocos(enemy.pos.x, enemy.pos.y);
|
||||||
|
node.setPosition(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncParallax(): void {
|
||||||
|
for (const layer of PARALLAX_LAYERS) {
|
||||||
|
const node = this.bgNodes.get(layer);
|
||||||
|
if (!node || !node.isValid) continue;
|
||||||
|
const offset = this.camera.offsetForLayer(layer);
|
||||||
|
// Parallax scroll: camera moves right → bg shifts left; camera moves up → bg shifts down.
|
||||||
|
node.setPosition(new Vec3(-offset.x, -offset.y, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// Projectile handling (visual only — spawn & move)
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
|
private processEnemyActions(actions: IEnemyAction[]): void {
|
||||||
|
for (const action of actions) {
|
||||||
|
if (action.kind === 'fire_bullet' && action.originX !== undefined && action.originY !== undefined) {
|
||||||
|
this.spawnProjectile(
|
||||||
|
action.attackType ?? 'shuriken',
|
||||||
|
action.originX,
|
||||||
|
action.originY,
|
||||||
|
action.velX ?? 0,
|
||||||
|
action.velY ?? 0,
|
||||||
|
true, // enemy projectile → red tint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private processPlayerAttacks(attacks: IAttackDispatchEvent[]): void {
|
||||||
|
for (const atk of attacks) {
|
||||||
|
const heroX = this.motion.aabb.x;
|
||||||
|
const heroY = this.motion.aabb.y;
|
||||||
|
const direction = this.motion.vx >= 0 ? 1 : -1;
|
||||||
|
|
||||||
|
if (atk.weapon === WeaponType.Shuriken) {
|
||||||
|
this.spawnProjectile(
|
||||||
|
'shuriken',
|
||||||
|
heroX + direction * 20,
|
||||||
|
heroY + 10,
|
||||||
|
direction * 350,
|
||||||
|
0,
|
||||||
|
false, // player projectile → white tint
|
||||||
|
);
|
||||||
|
} else if (atk.weapon === WeaponType.NinjaSword) {
|
||||||
|
// Sword is melee — check hit against enemies directly.
|
||||||
|
this.processSwordHit(heroX, heroY, direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private processSwordHit(heroX: number, heroY: number, direction: number): void {
|
||||||
|
const swordRange = 50;
|
||||||
|
const weaponCfg = this.cfg.weapon(WeaponType.NinjaSword);
|
||||||
|
for (const enemy of this.enemyMgr.all) {
|
||||||
|
if (!enemy.alive) continue;
|
||||||
|
const dx = enemy.pos.x - heroX;
|
||||||
|
const dy = enemy.pos.y - heroY;
|
||||||
|
if (Math.abs(dx) <= swordRange && Math.abs(dy) <= 40 && (direction >= 0 ? dx >= 0 : dx <= 0)) {
|
||||||
|
const remainingHp = this.dmgSystem.applyToEnemy(enemy.hp, weaponCfg.damage);
|
||||||
|
enemy.hp = remainingHp;
|
||||||
|
if (remainingHp <= 0) {
|
||||||
|
this.enemyMgr.kill(enemy);
|
||||||
|
this.mgr.onEnemyKilled(enemy.type);
|
||||||
|
// Hide the visual node.
|
||||||
|
const node = this.enemyNodes.get(enemy);
|
||||||
|
if (node && node.isValid) node.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sword active parry window (req 3.7-3.8).
|
||||||
|
this.psm.setSwordActive(true);
|
||||||
|
// Deactivate after a short delay (handled by next frame reset).
|
||||||
|
setTimeout(() => this.psm.setSwordActive(false), 200);
|
||||||
|
|
||||||
|
// Visual feedback: brief slash arc in front of the hero.
|
||||||
|
this.showSwordSlash(heroX, heroY, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show a brief slash flash in front of the hero for visual feedback. */
|
||||||
|
private showSwordSlash(physX: number, physY: number, direction: number): void {
|
||||||
|
const pos = this.physicsToCocos(physX + direction * 35, physY);
|
||||||
|
const slashNode = new Node('SwordSlash');
|
||||||
|
slashNode.layer = this.node.layer;
|
||||||
|
this.node.addChild(slashNode);
|
||||||
|
const ut = slashNode.addComponent(UITransform);
|
||||||
|
ut.setContentSize(40, 50);
|
||||||
|
slashNode.setPosition(new Vec3(pos.x, pos.y, 0));
|
||||||
|
const sp = slashNode.addComponent(Sprite);
|
||||||
|
sp.sizeMode = Sprite.SizeMode.CUSTOM;
|
||||||
|
sp.color = new Color(255, 255, 220, 200);
|
||||||
|
// Restore ctrl-layer Z-order after appending the slash sprite.
|
||||||
|
this.promoteCtrlLayerToTop();
|
||||||
|
// Auto-remove after 120ms.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (slashNode.isValid) {
|
||||||
|
slashNode.removeFromParent();
|
||||||
|
slashNode.destroy();
|
||||||
|
}
|
||||||
|
}, 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
private spawnProjectile(
|
||||||
|
attackType: string,
|
||||||
|
x: number, y: number,
|
||||||
|
velX: number, velY: number,
|
||||||
|
isEnemy: boolean,
|
||||||
|
): void {
|
||||||
|
const resPath = attackType === 'fireball'
|
||||||
|
? 'textures/fx/parry_spark' // reuse spark as fireball placeholder
|
||||||
|
: attackType === 'smoke_bomb'
|
||||||
|
? 'textures/fx/jump_dust' // reuse dust as smoke placeholder
|
||||||
|
: isEnemy
|
||||||
|
? 'textures/enemies/qing_ren' // enemy shuriken uses enemy sheet
|
||||||
|
: 'textures/characters/kage_red'; // player shuriken uses hero sheet
|
||||||
|
const projSize = attackType === 'smoke_bomb' ? 24 : 20;
|
||||||
|
const frameSize = 16;
|
||||||
|
const node = createSpriteSheetFrame(
|
||||||
|
this.node,
|
||||||
|
resPath,
|
||||||
|
0,
|
||||||
|
frameSize, frameSize,
|
||||||
|
0, 0, // position set in syncProjectiles
|
||||||
|
projSize, projSize,
|
||||||
|
{ name: `Projectile_${attackType}`, flipX: isEnemy },
|
||||||
|
);
|
||||||
|
// Store physics state on the node's userData for movement tracking.
|
||||||
|
(node as any)._projState = { x, y, velX, velY, isEnemy, attackType, alive: true };
|
||||||
|
this.projectileNodes.push(node);
|
||||||
|
// Restore ctrl-layer Z-order after appending the projectile sprite.
|
||||||
|
this.promoteCtrlLayerToTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncProjectiles(dt: number): void {
|
||||||
|
const toRemove: number[] = [];
|
||||||
|
for (let i = 0; i < this.projectileNodes.length; i++) {
|
||||||
|
const node = this.projectileNodes[i];
|
||||||
|
const state = (node as any)._projState;
|
||||||
|
if (!state || !state.alive || !node.isValid) {
|
||||||
|
toRemove.push(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Move the projectile.
|
||||||
|
state.x += state.velX * dt;
|
||||||
|
state.y += state.velY * dt;
|
||||||
|
const pos = this.physicsToCocos(state.x, state.y);
|
||||||
|
node.setPosition(pos);
|
||||||
|
|
||||||
|
// Check bounds — remove if far outside the camera's culling rect.
|
||||||
|
const cull = this.camera.cullRect();
|
||||||
|
const margin = 200;
|
||||||
|
if (state.x < cull.leftX - margin || state.x > cull.rightX + margin ||
|
||||||
|
state.y < cull.bottomY - margin || state.y > cull.topY + margin) {
|
||||||
|
state.alive = false;
|
||||||
|
toRemove.push(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check collision.
|
||||||
|
if (state.isEnemy) {
|
||||||
|
// Enemy projectile → check against player.
|
||||||
|
const dx = state.x - this.motion.aabb.x;
|
||||||
|
const dy = state.y - this.motion.aabb.y;
|
||||||
|
if (Math.abs(dx) < 20 && Math.abs(dy) < 30) {
|
||||||
|
const outcome = this.dmgSystem.applyToPlayer({
|
||||||
|
attackType: state.attackType as any,
|
||||||
|
attackerX: state.x,
|
||||||
|
attackerY: state.y,
|
||||||
|
victimX: this.motion.aabb.x,
|
||||||
|
victimY: this.motion.aabb.y,
|
||||||
|
});
|
||||||
|
// Only destroy the projectile if it actually dealt damage
|
||||||
|
// (downgraded or died). On 'no_effect' (i-frames / parry),
|
||||||
|
// let the projectile fly through the player unharmed.
|
||||||
|
if (outcome && outcome.kind !== 'no_effect') {
|
||||||
|
state.alive = false;
|
||||||
|
toRemove.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Player projectile → check against enemies.
|
||||||
|
for (const enemy of this.enemyMgr.all) {
|
||||||
|
if (!enemy.alive) continue;
|
||||||
|
const dx = state.x - enemy.pos.x;
|
||||||
|
const dy = state.y - enemy.pos.y;
|
||||||
|
if (Math.abs(dx) < 20 && Math.abs(dy) < 20) {
|
||||||
|
const weaponCfg = this.cfg.weapon(WeaponType.Shuriken);
|
||||||
|
const remainingHp = this.dmgSystem.applyToEnemy(enemy.hp, weaponCfg.damage);
|
||||||
|
enemy.hp = remainingHp;
|
||||||
|
if (remainingHp <= 0) {
|
||||||
|
this.enemyMgr.kill(enemy);
|
||||||
|
this.mgr.onEnemyKilled(enemy.type);
|
||||||
|
const eNode = this.enemyNodes.get(enemy);
|
||||||
|
if (eNode && eNode.isValid) eNode.active = false;
|
||||||
|
}
|
||||||
|
state.alive = false;
|
||||||
|
toRemove.push(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove dead projectiles (iterate in reverse).
|
||||||
|
for (let i = toRemove.length - 1; i >= 0; i--) {
|
||||||
|
const idx = toRemove[i];
|
||||||
|
const node = this.projectileNodes[idx];
|
||||||
|
if (node && node.isValid) {
|
||||||
|
node.removeFromParent();
|
||||||
|
node.destroy();
|
||||||
|
}
|
||||||
|
this.projectileNodes.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// Scene flow
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
private deriveNextScene(): string {
|
private deriveNextScene(): string {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
'1-1': 'Level_1_2',
|
'1-1': 'Level_1_2',
|
||||||
@@ -60,17 +935,18 @@ export class LevelEntry extends Component {
|
|||||||
return map[this.levelId] ?? 'Settlement';
|
return map[this.levelId] ?? 'Settlement';
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildDefaultUI(): void {
|
private pickThemeFolder(): 'forest' | 'castle_wall' | 'demon_castle' {
|
||||||
ensureCanvasSize(this.node);
|
switch (this.levelId) {
|
||||||
createLabel(this.node, `Level ${this.levelId}`, 0, DESIGN_HEIGHT / 2 - 50, 28, Color.WHITE);
|
case '1-1':
|
||||||
this.hudNode = createLabel(
|
case '1-2':
|
||||||
this.node,
|
return 'forest';
|
||||||
'Time: --',
|
case '1-3':
|
||||||
0,
|
case '1-4':
|
||||||
DESIGN_HEIGHT / 2 - 90,
|
return 'castle_wall';
|
||||||
22,
|
case '1-5':
|
||||||
new Color(255, 220, 120, 255),
|
default:
|
||||||
);
|
return 'demon_castle';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshHud(): void {
|
private refreshHud(): void {
|
||||||
@@ -78,7 +954,8 @@ export class LevelEntry extends Component {
|
|||||||
const lb = this.hudNode.getComponent(Label);
|
const lb = this.hudNode.getComponent(Label);
|
||||||
if (lb) {
|
if (lb) {
|
||||||
const r = this.mgr.result();
|
const r = this.mgr.result();
|
||||||
lb.string = `Time: ${Math.max(0, Math.ceil(r.remainingSec))}s Kills: ${Object.values(r.kills).reduce((a, b) => a + b, 0)}`;
|
const colorStr = this.psm.color.charAt(0).toUpperCase() + this.psm.color.slice(1);
|
||||||
|
lb.string = `Time: ${Math.max(0, Math.ceil(r.remainingSec))}s Kills: ${Object.values(r.kills).reduce((a, b) => a + b, 0)} Lives: ${this.psm.lives} [${colorStr}]`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { _decorator, Component, director, Node, Label, Button, UITransform, Color, Vec3, Graphics } from 'cc';
|
import { _decorator, Component, director, Node, Label, Button, UITransform, Color, Vec3, Graphics, Sprite, SpriteFrame, Texture2D, resources, Rect, Size, ImageAsset } from 'cc';
|
||||||
import { UIFlowMgr, ISceneEnter } from '../ui/UIFlowMgr';
|
import { UIFlowMgr, ISceneEnter } from '../ui/UIFlowMgr';
|
||||||
import { DESIGN_WIDTH, DESIGN_HEIGHT } from '../common/Constants';
|
import { DESIGN_WIDTH, DESIGN_HEIGHT } from '../common/Constants';
|
||||||
|
|
||||||
@@ -28,6 +28,16 @@ const LEVEL_SCENE_MAP: Record<string, string> = {
|
|||||||
'1-5-boss': 'Boss_ShuangHuanFang',
|
'1-5-boss': 'Boss_ShuangHuanFang',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Level definitions for the level-select screen (chapter 1 MVP). */
|
||||||
|
const LEVEL_DEFS: ReadonlyArray<{ id: string; label: string }> = [
|
||||||
|
{ id: '1-1', label: '1-1 森林入口' },
|
||||||
|
{ id: '1-2', label: '1-2 竹林深处' },
|
||||||
|
{ id: '1-3', label: '1-3 城墙之下' },
|
||||||
|
{ id: '1-4', label: '1-4 密道暗行' },
|
||||||
|
{ id: '1-5', label: '1-5 魔城前庭' },
|
||||||
|
{ id: '1-5-boss', label: 'Boss 双幻坊' },
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MainMenu scene entry (task 9.2 hookup).
|
* MainMenu scene entry (task 9.2 hookup).
|
||||||
*
|
*
|
||||||
@@ -35,9 +45,13 @@ const LEVEL_SCENE_MAP: Record<string, string> = {
|
|||||||
* callback into a concrete `director.loadScene` call. Attach this component
|
* callback into a concrete `director.loadScene` call. Attach this component
|
||||||
* to the root node of `MainMenu.scene`.
|
* to the root node of `MainMenu.scene`.
|
||||||
*
|
*
|
||||||
* When `autoBuildUI` is enabled (default), two centered buttons (Start /
|
* Supports two display modes reusing the same scene:
|
||||||
* Settings) and a title label are created programmatically so the scene is
|
* - **main_menu**: title + Start / Settings buttons
|
||||||
* usable out of the box even before any art pass.
|
* - **level_select**: chapter heading + level buttons + Back button
|
||||||
|
*
|
||||||
|
* When `autoBuildUI` is enabled (default), all UI is created
|
||||||
|
* programmatically so the scene is usable out of the box even before
|
||||||
|
* any art pass.
|
||||||
*/
|
*/
|
||||||
@ccclass('MainMenuEntry')
|
@ccclass('MainMenuEntry')
|
||||||
export class MainMenuEntry extends Component {
|
export class MainMenuEntry extends Component {
|
||||||
@@ -45,12 +59,16 @@ export class MainMenuEntry extends Component {
|
|||||||
public autoBuildUI: boolean = true;
|
public autoBuildUI: boolean = true;
|
||||||
|
|
||||||
private flow: UIFlowMgr | undefined;
|
private flow: UIFlowMgr | undefined;
|
||||||
|
/** Nodes created in main-menu mode, so we can remove them when switching. */
|
||||||
|
private menuNodes: Node[] = [];
|
||||||
|
/** Nodes created in level-select mode, so we can remove them when switching. */
|
||||||
|
private levelSelectNodes: Node[] = [];
|
||||||
|
|
||||||
protected onLoad(): void {
|
protected onLoad(): void {
|
||||||
this.flow = new UIFlowMgr(undefined, {
|
this.flow = new UIFlowMgr(undefined, {
|
||||||
onSceneEnter: (ev) => this.handleSceneEnter(ev),
|
onSceneEnter: (ev) => this.handleSceneEnter(ev),
|
||||||
});
|
});
|
||||||
if (this.autoBuildUI) this.buildDefaultUI();
|
if (this.autoBuildUI) this.buildMainMenuUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Bind this to the "Start" button's click event in the Inspector. */
|
/** Bind this to the "Start" button's click event in the Inspector. */
|
||||||
@@ -63,8 +81,20 @@ export class MainMenuEntry extends Component {
|
|||||||
this.flow?.onOpenSettings();
|
this.flow?.onOpenSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Pick a level from the level-select screen. */
|
||||||
|
public onPressLevel(levelId: string): void {
|
||||||
|
this.flow?.onPickLevel(levelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Go back from level-select to main-menu. */
|
||||||
|
public onPressBackToMenu(): void {
|
||||||
|
this.showMainMenu();
|
||||||
|
}
|
||||||
|
|
||||||
private handleSceneEnter(ev: ISceneEnter): void {
|
private handleSceneEnter(ev: ISceneEnter): void {
|
||||||
const payload = ev.payload ?? {};
|
const payload = ev.payload ?? {};
|
||||||
|
|
||||||
|
// gameplay: always load a different scene.
|
||||||
if (ev.scene === 'gameplay' && typeof payload.levelId === 'string') {
|
if (ev.scene === 'gameplay' && typeof payload.levelId === 'string') {
|
||||||
const physical = LEVEL_SCENE_MAP[payload.levelId];
|
const physical = LEVEL_SCENE_MAP[payload.levelId];
|
||||||
if (physical) {
|
if (physical) {
|
||||||
@@ -72,28 +102,100 @@ export class MainMenuEntry extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// level_select: we reuse this scene — just swap UI panels.
|
||||||
|
if (ev.scene === 'level_select') {
|
||||||
|
this.showLevelSelect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// settings: for MVP, overlayed on MainMenu — just swap to menu mode.
|
||||||
|
if (ev.scene === 'settings') {
|
||||||
|
this.showMainMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For any other scene (boot / story_intro / settlement / main_menu),
|
||||||
|
// load the physical scene normally.
|
||||||
const physical = SCENE_MAP[ev.scene];
|
const physical = SCENE_MAP[ev.scene];
|
||||||
if (physical) director.loadScene(physical);
|
if (physical && ev.scene !== 'main_menu') {
|
||||||
|
director.loadScene(physical);
|
||||||
|
} else if (ev.scene === 'main_menu') {
|
||||||
|
this.showMainMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// UI mode switching
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
private showMainMenu(): void {
|
||||||
|
this.clearNodes(this.levelSelectNodes);
|
||||||
|
if (this.menuNodes.length === 0 && this.autoBuildUI) {
|
||||||
|
this.buildMainMenuUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private showLevelSelect(): void {
|
||||||
|
this.clearNodes(this.menuNodes);
|
||||||
|
if (this.levelSelectNodes.length === 0 && this.autoBuildUI) {
|
||||||
|
this.buildLevelSelectUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearNodes(arr: Node[]): void {
|
||||||
|
for (const n of arr) {
|
||||||
|
if (n.isValid) {
|
||||||
|
n.removeFromParent();
|
||||||
|
n.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arr.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Auto-built UI (development affordance; art pass will replace it)
|
// Auto-built UI (development affordance; art pass will replace it)
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
private buildDefaultUI(): void {
|
private buildMainMenuUI(): void {
|
||||||
// Ensure the host node has a UITransform matching the design resolution.
|
// Ensure the host node has a UITransform matching the design resolution.
|
||||||
ensureCanvasSize(this.node);
|
ensureCanvasSize(this.node);
|
||||||
|
|
||||||
// Title.
|
// Title.
|
||||||
createLabel(this.node, '影 之 传 说', 0, 140, 44, Color.WHITE);
|
this.menuNodes.push(createLabel(this.node, '影 之 传 说', 0, 140, 44, Color.WHITE));
|
||||||
|
|
||||||
// Start button (centered, 40 above origin).
|
// Start button (centered, 40 above origin).
|
||||||
createButton(this.node, 'Start', 0, 40, 220, 60, () => this.onPressStart());
|
this.menuNodes.push(createButton(this.node, 'Start', 0, 40, 220, 60, () => this.onPressStart()));
|
||||||
|
|
||||||
// Settings button (centered, 40 below origin).
|
// Settings button (centered, 40 below origin).
|
||||||
createButton(this.node, 'Settings', 0, -40, 220, 60, () => this.onPressSettings());
|
this.menuNodes.push(createButton(this.node, 'Settings', 0, -40, 220, 60, () => this.onPressSettings()));
|
||||||
|
|
||||||
// Hint line at the bottom.
|
// Hint line at the bottom.
|
||||||
createLabel(this.node, 'Chapter 1 · MVP', 0, -200, 20, new Color(180, 180, 180, 255));
|
this.menuNodes.push(createLabel(this.node, 'Chapter 1 · MVP', 0, -200, 20, new Color(180, 180, 180, 255)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildLevelSelectUI(): void {
|
||||||
|
ensureCanvasSize(this.node);
|
||||||
|
|
||||||
|
// Chapter heading.
|
||||||
|
this.levelSelectNodes.push(
|
||||||
|
createLabel(this.node, '第一章 青叶之刃', 0, 220, 36, Color.WHITE),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Level buttons — laid out vertically with even spacing.
|
||||||
|
const startY = 140;
|
||||||
|
const gapY = 55;
|
||||||
|
for (let i = 0; i < LEVEL_DEFS.length; i++) {
|
||||||
|
const def = LEVEL_DEFS[i];
|
||||||
|
const y = startY - i * gapY;
|
||||||
|
this.levelSelectNodes.push(
|
||||||
|
createButton(this.node, def.label, 0, y, 280, 44, () => this.onPressLevel(def.id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back button at the bottom.
|
||||||
|
this.levelSelectNodes.push(
|
||||||
|
createButton(this.node, 'Back', 0, -200, 160, 44, () => this.onPressBackToMenu()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,4 +281,150 @@ function createButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-export helpers so sibling Scene Entries can reuse them.
|
// Re-export helpers so sibling Scene Entries can reuse them.
|
||||||
export { ensureCanvasSize, createLabel, createButton };
|
export { ensureCanvasSize, createLabel, createButton, createSprite, createSpriteSheetFrame, createSolidRect };
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// Sprite helpers (task 10.1 hookup) — load a PNG from `resources/` and
|
||||||
|
// attach it to a fresh child Node. Returned synchronously; the Sprite's
|
||||||
|
// `spriteFrame` is assigned once `resources.load` resolves.
|
||||||
|
//
|
||||||
|
// Paths passed in are **relative to `assets/resources`** and **without
|
||||||
|
// extension**. Example: `textures/characters/kage_red` — the helper will
|
||||||
|
// append `/spriteFrame` automatically.
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// Internal: Load a PNG from `resources/` as a SpriteFrame.
|
||||||
|
//
|
||||||
|
// Strategy — try two paths so it works regardless of .meta type:
|
||||||
|
// 1. `resources.load(path, SpriteFrame)` — works when the .meta has
|
||||||
|
// `type: "sprite-frame"` (Creator auto-generates the sub-asset).
|
||||||
|
// 2. Fallback: `resources.load(path, ImageAsset)` → Texture2D →
|
||||||
|
// SpriteFrame — works when .meta has `type: "texture"`.
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
|
function _loadSpriteFrameFromImage(
|
||||||
|
resPath: string,
|
||||||
|
onReady: (sf: SpriteFrame) => void,
|
||||||
|
): void {
|
||||||
|
// Try the direct SpriteFrame path first (works with sprite-frame meta).
|
||||||
|
resources.load(resPath, SpriteFrame, (err, sf) => {
|
||||||
|
if (!err && sf) {
|
||||||
|
onReady(sf as SpriteFrame);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fallback: load raw image and build SpriteFrame manually.
|
||||||
|
resources.load(resPath, ImageAsset, (err2, imgAsset) => {
|
||||||
|
if (err2 || !imgAsset) {
|
||||||
|
console.warn(`[_loadSpriteFrameFromImage] Failed to load '${resPath}' (both paths):`, err, err2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tex = new Texture2D();
|
||||||
|
tex.image = imgAsset as ImageAsset;
|
||||||
|
const frame = new SpriteFrame();
|
||||||
|
frame.texture = tex;
|
||||||
|
frame.rect = new Rect(0, 0, tex.width, tex.height);
|
||||||
|
frame.originalSize = new Size(tex.width, tex.height);
|
||||||
|
onReady(frame);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a child Node with a Sprite that displays the entire PNG at
|
||||||
|
* `resPath`. If `w` / `h` are omitted the node will be resized to the
|
||||||
|
* native image size once the texture finishes loading.
|
||||||
|
*
|
||||||
|
* NOTE: Loads via `ImageAsset` → `Texture2D` → `SpriteFrame` pipeline
|
||||||
|
* instead of relying on a `/spriteFrame` sub-asset, because PNG .meta
|
||||||
|
* files may not have `type: "sprite-frame"`.
|
||||||
|
*/
|
||||||
|
function createSprite(
|
||||||
|
parent: Node,
|
||||||
|
resPath: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
w?: number,
|
||||||
|
h?: number,
|
||||||
|
opts?: { name?: string; color?: Color; flipX?: boolean }
|
||||||
|
): Node {
|
||||||
|
const n = new Node(opts?.name ?? `Sprite_${resPath.split('/').pop()}`);
|
||||||
|
n.layer = parent.layer;
|
||||||
|
parent.addChild(n);
|
||||||
|
const ut = n.addComponent(UITransform);
|
||||||
|
if (w != null && h != null) ut.setContentSize(w, h);
|
||||||
|
const sp = n.addComponent(Sprite);
|
||||||
|
sp.sizeMode = Sprite.SizeMode.CUSTOM;
|
||||||
|
if (opts?.color) sp.color = opts.color;
|
||||||
|
n.setPosition(new Vec3(x, y, 0));
|
||||||
|
if (opts?.flipX) n.setScale(-Math.abs(n.scale.x), n.scale.y, n.scale.z);
|
||||||
|
|
||||||
|
_loadSpriteFrameFromImage(resPath, (sf) => {
|
||||||
|
if (!n.isValid) return;
|
||||||
|
sp.spriteFrame = sf;
|
||||||
|
if (w == null || h == null) {
|
||||||
|
ut.setContentSize(sf.rect.width, sf.rect.height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a child Node that renders a single frame out of a horizontal
|
||||||
|
* sprite-sheet PNG. `frameIndex` is 0-based; `frameW`/`frameH` describe
|
||||||
|
* each frame's pixel size; the sheet is expected to lay frames left-to-
|
||||||
|
* right in one row (matches `gen_pixel_art_assets.py`).
|
||||||
|
*
|
||||||
|
* The returned Node is shown at `displayW`×`displayH` (defaults to the
|
||||||
|
* native frame size — i.e. 1 CSS px per source px).
|
||||||
|
*/
|
||||||
|
function createSpriteSheetFrame(
|
||||||
|
parent: Node,
|
||||||
|
resPath: string,
|
||||||
|
frameIndex: number,
|
||||||
|
frameW: number,
|
||||||
|
frameH: number,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
displayW?: number,
|
||||||
|
displayH?: number,
|
||||||
|
opts?: { name?: string; flipX?: boolean }
|
||||||
|
): Node {
|
||||||
|
const n = new Node(opts?.name ?? `Frame_${resPath.split('/').pop()}_${frameIndex}`);
|
||||||
|
n.layer = parent.layer;
|
||||||
|
parent.addChild(n);
|
||||||
|
const ut = n.addComponent(UITransform);
|
||||||
|
ut.setContentSize(displayW ?? frameW, displayH ?? frameH);
|
||||||
|
const sp = n.addComponent(Sprite);
|
||||||
|
sp.sizeMode = Sprite.SizeMode.CUSTOM;
|
||||||
|
n.setPosition(new Vec3(x, y, 0));
|
||||||
|
if (opts?.flipX) n.setScale(-Math.abs(n.scale.x), n.scale.y, n.scale.z);
|
||||||
|
|
||||||
|
_loadSpriteFrameFromImage(resPath, (sf) => {
|
||||||
|
if (!n.isValid) return;
|
||||||
|
// Slice a single frame out of the full sheet.
|
||||||
|
const sliced = new SpriteFrame();
|
||||||
|
sliced.texture = sf.texture;
|
||||||
|
sliced.rect = new Rect(frameIndex * frameW, 0, frameW, frameH);
|
||||||
|
sliced.originalSize = new Size(frameW, frameH);
|
||||||
|
sp.spriteFrame = sliced;
|
||||||
|
});
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for semi-transparent overlays (e.g. story-page text backdrop).
|
||||||
|
*/
|
||||||
|
function createSolidRect(parent: Node, x: number, y: number, w: number, h: number, color: Color): Node {
|
||||||
|
const n = new Node('SolidRect');
|
||||||
|
n.layer = parent.layer;
|
||||||
|
parent.addChild(n);
|
||||||
|
const ut = n.addComponent(UITransform);
|
||||||
|
ut.setContentSize(w, h);
|
||||||
|
const g = n.addComponent(Graphics);
|
||||||
|
g.fillColor = color;
|
||||||
|
g.rect(-w / 2, -h / 2, w, h);
|
||||||
|
g.fill();
|
||||||
|
n.setPosition(new Vec3(x, y, 0));
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { _decorator, Component, director, Label, Node, Color, EventTouch, UITransform } from 'cc';
|
import { _decorator, Component, director, Label, Node, Color, EventTouch, UITransform, Sprite, SpriteFrame, Texture2D, ImageAsset, Rect, Size, resources } from 'cc';
|
||||||
import { ConfigMgr } from '../data/ConfigMgr';
|
import { ConfigMgr } from '../data/ConfigMgr';
|
||||||
import { CCJsonLoader } from './CCJsonLoader';
|
import { CCJsonLoader } from './CCJsonLoader';
|
||||||
import { StorySceneCtrl } from '../ui/StorySceneCtrl';
|
import { StorySceneCtrl } from '../ui/StorySceneCtrl';
|
||||||
import { ensureCanvasSize, createLabel, createButton } from './MainMenuEntry';
|
import {
|
||||||
|
ensureCanvasSize,
|
||||||
|
createLabel,
|
||||||
|
createButton,
|
||||||
|
createSprite,
|
||||||
|
createSolidRect,
|
||||||
|
} from './MainMenuEntry';
|
||||||
import { DESIGN_WIDTH, DESIGN_HEIGHT } from '../common/Constants';
|
import { DESIGN_WIDTH, DESIGN_HEIGHT } from '../common/Constants';
|
||||||
|
import { IStoryPageConfig } from '../data/Interfaces';
|
||||||
|
|
||||||
const { ccclass, property } = _decorator;
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
@@ -30,8 +37,15 @@ export class StorySceneEntry extends Component {
|
|||||||
public autoBuildUI: boolean = true;
|
public autoBuildUI: boolean = true;
|
||||||
|
|
||||||
private ctrl: StorySceneCtrl | undefined;
|
private ctrl: StorySceneCtrl | undefined;
|
||||||
|
private bgNode: Node | null = null;
|
||||||
|
|
||||||
protected async onLoad(): Promise<void> {
|
protected async onLoad(): Promise<void> {
|
||||||
|
// Always ensure the illustration bgNode exists (even if the scene
|
||||||
|
// was hand-built in the editor and labelNode is already bound).
|
||||||
|
if (!this.bgNode) {
|
||||||
|
this.ensureBgNode();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.autoBuildUI && !this.labelNode) {
|
if (this.autoBuildUI && !this.labelNode) {
|
||||||
this.buildDefaultUI();
|
this.buildDefaultUI();
|
||||||
}
|
}
|
||||||
@@ -41,6 +55,7 @@ export class StorySceneEntry extends Component {
|
|||||||
const cfg = await this.loadStoryConfig();
|
const cfg = await this.loadStoryConfig();
|
||||||
this.ctrl = new StorySceneCtrl(cfg, undefined, {
|
this.ctrl = new StorySceneCtrl(cfg, undefined, {
|
||||||
onTextChanged: (text) => this.updateLabel(text),
|
onTextChanged: (text) => this.updateLabel(text),
|
||||||
|
onPageEntered: (page) => this.swapIllustration(page),
|
||||||
onFinished: () => director.loadScene('Level_1_1'),
|
onFinished: () => director.loadScene('Level_1_1'),
|
||||||
});
|
});
|
||||||
const outcome = this.ctrl.start();
|
const outcome = this.ctrl.start();
|
||||||
@@ -75,16 +90,75 @@ export class StorySceneEntry extends Component {
|
|||||||
return mgr.story(this.storyId);
|
return mgr.story(this.storyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildDefaultUI(): void {
|
/** Ensure the full-screen illustration layer exists. Called even when
|
||||||
|
* the scene was hand-built in the editor (labelNode already bound). */
|
||||||
|
private ensureBgNode(): void {
|
||||||
ensureCanvasSize(this.node);
|
ensureCanvasSize(this.node);
|
||||||
// Ensure the root node can receive touch (size = design resolution).
|
this.bgNode = new Node('StoryIllustration');
|
||||||
// Central typewriter label.
|
this.bgNode.layer = this.node.layer;
|
||||||
this.labelNode = createLabel(this.node, '', 0, 0, 28, Color.WHITE);
|
// Added before any scrim/label nodes so it renders behind them.
|
||||||
|
this.node.addChild(this.bgNode);
|
||||||
|
const bgUt = this.bgNode.addComponent(UITransform);
|
||||||
|
bgUt.setContentSize(DESIGN_WIDTH, DESIGN_HEIGHT);
|
||||||
|
const bgSp = this.bgNode.addComponent(Sprite);
|
||||||
|
bgSp.sizeMode = Sprite.SizeMode.CUSTOM;
|
||||||
|
this.bgNode.setPosition(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildDefaultUI(): void {
|
||||||
|
// bgNode is already created by ensureBgNode() — just build the
|
||||||
|
// text overlay, scrim and skip button.
|
||||||
|
|
||||||
|
// Dark scrim below the text area for readability.
|
||||||
|
createSolidRect(
|
||||||
|
this.node,
|
||||||
|
0,
|
||||||
|
-DESIGN_HEIGHT / 2 + 110,
|
||||||
|
DESIGN_WIDTH,
|
||||||
|
180,
|
||||||
|
new Color(0, 0, 0, 170),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Central typewriter label (sits on top of the scrim).
|
||||||
|
this.labelNode = createLabel(this.node, '', 0, -DESIGN_HEIGHT / 2 + 110, 26, Color.WHITE);
|
||||||
const ut = this.labelNode.getComponent(UITransform);
|
const ut = this.labelNode.getComponent(UITransform);
|
||||||
if (ut) ut.setContentSize(DESIGN_WIDTH - 80, DESIGN_HEIGHT - 120);
|
if (ut) ut.setContentSize(DESIGN_WIDTH - 80, 160);
|
||||||
|
const lb = this.labelNode.getComponent(Label);
|
||||||
|
if (lb) lb.enableWrapText = true;
|
||||||
|
|
||||||
// Skip button at bottom-right.
|
// Skip button at bottom-right.
|
||||||
const skipX = DESIGN_WIDTH / 2 - 90;
|
const skipX = DESIGN_WIDTH / 2 - 90;
|
||||||
const skipY = -DESIGN_HEIGHT / 2 + 50;
|
const skipY = -DESIGN_HEIGHT / 2 + 40;
|
||||||
createButton(this.node, 'Skip >>', skipX, skipY, 140, 50, () => this.onSkip());
|
createButton(this.node, 'Skip >>', skipX, skipY, 140, 50, () => this.onSkip());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Swap the full-screen illustration when the controller enters a new page. */
|
||||||
|
private swapIllustration(page: IStoryPageConfig): void {
|
||||||
|
if (!this.bgNode) return;
|
||||||
|
const sp = this.bgNode.getComponent(Sprite);
|
||||||
|
if (!sp) return;
|
||||||
|
const path = page.illustration;
|
||||||
|
// Strategy 1: direct SpriteFrame load (works with sprite-frame meta).
|
||||||
|
resources.load(path, SpriteFrame, (err, sf) => {
|
||||||
|
if (!err && sf) {
|
||||||
|
if (this.bgNode && this.bgNode.isValid) sp.spriteFrame = sf as SpriteFrame;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Strategy 2: ImageAsset → Texture2D → SpriteFrame fallback.
|
||||||
|
resources.load(path, ImageAsset, (err2, imgAsset) => {
|
||||||
|
if (err2 || !imgAsset) {
|
||||||
|
console.warn(`[StorySceneEntry] failed to load illustration '${path}':`, err, err2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.bgNode || !this.bgNode.isValid) return;
|
||||||
|
const tex = new Texture2D();
|
||||||
|
tex.image = imgAsset as ImageAsset;
|
||||||
|
const frame = new SpriteFrame();
|
||||||
|
frame.texture = tex;
|
||||||
|
frame.rect = new Rect(0, 0, tex.width, tex.height);
|
||||||
|
frame.originalSize = new Size(tex.width, tex.height);
|
||||||
|
sp.spriteFrame = frame;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { _decorator, Component, EventTouch, Node, Touch, UITransform, Vec2, Vec3, view } from 'cc';
|
import { _decorator, Component, EventTouch, Node, Touch, UITransform, Vec2, Vec3, view, input, Input } from 'cc';
|
||||||
import { globalEventBus, globalLogger } from '../common/index';
|
import { globalEventBus, globalLogger } from '../common/index';
|
||||||
import { PERF_TOUCH_RESPONSE_MAX_MS } from '../common/Constants';
|
import { DESIGN_WIDTH, DESIGN_HEIGHT, PERF_TOUCH_RESPONSE_MAX_MS } from '../common/Constants';
|
||||||
import {
|
import {
|
||||||
ControlId,
|
ControlId,
|
||||||
DEFAULT_LAYOUT,
|
DEFAULT_LAYOUT,
|
||||||
@@ -47,13 +47,43 @@ export class FloatingControlLayer extends Component {
|
|||||||
private layout: IFloatingLayout = DEFAULT_LAYOUT;
|
private layout: IFloatingLayout = DEFAULT_LAYOUT;
|
||||||
private router: MultiTouchRouter = new MultiTouchRouter(DEFAULT_LAYOUT);
|
private router: MultiTouchRouter = new MultiTouchRouter(DEFAULT_LAYOUT);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a touch point from the engine's UI coordinate space back into
|
||||||
|
* the 960×540 design-coordinate space used by `InputModel`.
|
||||||
|
*
|
||||||
|
* Under `FIT_HEIGHT`, the engine may widen the visible area beyond 960px
|
||||||
|
* on ultra-wide screens (19.5:9 etc.). `getUILocation()` returns values
|
||||||
|
* in that wider space, but our layout & hit-test are anchored to 960×540.
|
||||||
|
*
|
||||||
|
* The mapping is:
|
||||||
|
* designX = (uiX - extraLeft) * 960 / visibleWidth
|
||||||
|
* designY = uiY * 540 / visibleHeight (always 1:1 under FIT_HEIGHT)
|
||||||
|
*/
|
||||||
|
private uiToDesign(uiX: number, uiY: number): { x: number; y: number } {
|
||||||
|
const vs = view.getVisibleSize();
|
||||||
|
const scaleX = DESIGN_WIDTH / vs.width;
|
||||||
|
const scaleY = DESIGN_HEIGHT / vs.height;
|
||||||
|
// Under FIT_HEIGHT the design height always equals the visible height
|
||||||
|
// so scaleY ≈ 1, but we apply it anyway for robustness.
|
||||||
|
return {
|
||||||
|
x: uiX * scaleX,
|
||||||
|
y: uiY * scaleY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
protected onLoad(): void {
|
protected onLoad(): void {
|
||||||
|
console.log('[FloatingControlLayer] onLoad — node name=', this.node?.name, 'isValid=', this.node?.isValid);
|
||||||
this.applyInitialLayout();
|
this.applyInitialLayout();
|
||||||
this.bindTouchEvents();
|
this.bindTouchEvents();
|
||||||
|
console.log('[FloatingControlLayer] onLoad done — touch listeners bound');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onDestroy(): void {
|
protected onDestroy(): void {
|
||||||
|
console.log('[FloatingControlLayer] onDestroy — unbinding touch listeners');
|
||||||
this.unbindTouchEvents();
|
this.unbindTouchEvents();
|
||||||
|
this.router.clear();
|
||||||
|
this.processedPhases.clear();
|
||||||
|
this.lastStartTs.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Public API — called by `UIFlowMgr` when safe-area changes. */
|
/** Public API — called by `UIFlowMgr` when safe-area changes. */
|
||||||
@@ -98,6 +128,33 @@ export class FloatingControlLayer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bindTouchEvents(): void {
|
private bindTouchEvents(): void {
|
||||||
|
// CRITICAL: Cocos Creator 3.x has a known issue where
|
||||||
|
// `input.off(type, cb, target)` may fail to remove listeners whose
|
||||||
|
// target was a destroyed Component. This leaves "ghost" handlers on
|
||||||
|
// the global input dispatcher that silently break the touch event
|
||||||
|
// chain after scene transitions. We defensively clear ALL listeners
|
||||||
|
// for our touch events first, then re-register — guaranteeing a
|
||||||
|
// clean state.
|
||||||
|
try {
|
||||||
|
(input as any).off(Input.EventType.TOUCH_START);
|
||||||
|
(input as any).off(Input.EventType.TOUCH_MOVE);
|
||||||
|
(input as any).off(Input.EventType.TOUCH_END);
|
||||||
|
(input as any).off(Input.EventType.TOUCH_CANCEL);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[FloatingControlLayer] defensive input.off (no target) failed:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIMARY channel: global input API — receives raw touch events
|
||||||
|
// directly from the engine, bypassing node-tree dispatch entirely.
|
||||||
|
input.on(Input.EventType.TOUCH_START, this.onTouchStart, this);
|
||||||
|
input.on(Input.EventType.TOUCH_MOVE, this.onTouchMove, this);
|
||||||
|
input.on(Input.EventType.TOUCH_END, this.onTouchEnd, this);
|
||||||
|
input.on(Input.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
|
||||||
|
console.log('[FloatingControlLayer] bindTouchEvents — global input listeners registered');
|
||||||
|
|
||||||
|
// SECONDARY (back-compat): node-level listeners so that
|
||||||
|
// `ev.propagationStopped` keeps any underlying gameplay sprite from
|
||||||
|
// also reacting to the same touch.
|
||||||
this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
|
this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
|
||||||
this.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
|
this.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
|
||||||
this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this);
|
this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this);
|
||||||
@@ -105,17 +162,58 @@ export class FloatingControlLayer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private unbindTouchEvents(): void {
|
private unbindTouchEvents(): void {
|
||||||
|
input.off(Input.EventType.TOUCH_START, this.onTouchStart, this);
|
||||||
|
input.off(Input.EventType.TOUCH_MOVE, this.onTouchMove, this);
|
||||||
|
input.off(Input.EventType.TOUCH_END, this.onTouchEnd, this);
|
||||||
|
input.off(Input.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
|
||||||
|
|
||||||
this.node.off(Node.EventType.TOUCH_START, this.onTouchStart, this);
|
this.node.off(Node.EventType.TOUCH_START, this.onTouchStart, this);
|
||||||
this.node.off(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
|
this.node.off(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
|
||||||
this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this);
|
this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this);
|
||||||
this.node.off(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
|
this.node.off(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event de-dup. Because we listen on BOTH the global `input` API and the
|
||||||
|
* node-level `this.node.on` channel, the same touch may fire the same
|
||||||
|
* handler twice. This set records the `"${eventKind}:${touchId}"` string
|
||||||
|
* for touches already processed in this "phase"; the entry is cleared
|
||||||
|
* when the corresponding END/CANCEL arrives (or START starts a new
|
||||||
|
* phase for that id).
|
||||||
|
*/
|
||||||
|
private readonly processedPhases = new Set<string>();
|
||||||
|
|
||||||
private onTouchStart(ev: EventTouch): void {
|
private onTouchStart(ev: EventTouch): void {
|
||||||
const t = ev.getUILocation();
|
|
||||||
const start = FloatingControlLayer.now();
|
|
||||||
const touchId = this.touchId(ev);
|
const touchId = this.touchId(ev);
|
||||||
const hit = this.router.begin(touchId, t.x, t.y, start);
|
console.log('[FloatingControlLayer] onTouchStart — touchId=', touchId);
|
||||||
|
const key = `start:${touchId}`;
|
||||||
|
|
||||||
|
// Defensive: if a stale `start` marker exists for this touchId AND
|
||||||
|
// the router no longer has an active slot for it, the marker leaked
|
||||||
|
// from a previous touch cycle (e.g. onTouchEnd was not called on
|
||||||
|
// both channels, or a TOUCH_CANCEL race left the set dirty).
|
||||||
|
// Clear it so this new touch is not silently swallowed.
|
||||||
|
if (this.processedPhases.has(key) && !this.router.isPressedById(touchId)) {
|
||||||
|
console.log('[FloatingControlLayer] onTouchStart — clearing stale start marker for touchId=', touchId);
|
||||||
|
this.processedPhases.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.processedPhases.has(key)) {
|
||||||
|
console.log('[FloatingControlLayer] onTouchStart DEDUP-skip touchId=', touchId,
|
||||||
|
'processedPhases=', JSON.stringify([...this.processedPhases]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.processedPhases.add(key);
|
||||||
|
// A new TOUCH_START invalidates any stale END marker for the same id.
|
||||||
|
this.processedPhases.delete(`end:${touchId}`);
|
||||||
|
|
||||||
|
const t = ev.getUILocation();
|
||||||
|
const d = this.uiToDesign(t.x, t.y);
|
||||||
|
console.log('[FloatingControlLayer] onTouchStart uiLoc=(', t.x, ',', t.y, ') design=(', d.x.toFixed(1), ',', d.y.toFixed(1), ')');
|
||||||
|
const start = FloatingControlLayer.now();
|
||||||
|
const hit = this.router.begin(touchId, d.x, d.y, start);
|
||||||
|
console.log('[FloatingControlLayer] onTouchStart hit=', hit,
|
||||||
|
'router.activeTouchCount=', this.router.activeTouchCount);
|
||||||
this.recordLatency('input/touchStart', start);
|
this.recordLatency('input/touchStart', start);
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
// Let the touch fall through to the gameplay layer (req 1.3).
|
// Let the touch fall through to the gameplay layer (req 1.3).
|
||||||
@@ -133,7 +231,7 @@ export class FloatingControlLayer extends Component {
|
|||||||
globalEventBus.emit(InputEvents.NinjaSwordPressed, {});
|
globalEventBus.emit(InputEvents.NinjaSwordPressed, {});
|
||||||
break;
|
break;
|
||||||
case ControlId.Joystick:
|
case ControlId.Joystick:
|
||||||
this.broadcastJoystick(t.x, t.y);
|
this.broadcastJoystick(d.x, d.y);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -142,18 +240,36 @@ export class FloatingControlLayer extends Component {
|
|||||||
|
|
||||||
private onTouchMove(ev: EventTouch): void {
|
private onTouchMove(ev: EventTouch): void {
|
||||||
const t = ev.getUILocation();
|
const t = ev.getUILocation();
|
||||||
|
const d = this.uiToDesign(t.x, t.y);
|
||||||
const touchId = this.touchId(ev);
|
const touchId = this.touchId(ev);
|
||||||
const bound = this.router.move(touchId, t.x, t.y);
|
const bound = this.router.move(touchId, d.x, d.y);
|
||||||
if (bound === ControlId.Joystick) {
|
if (bound === ControlId.Joystick) {
|
||||||
this.broadcastJoystick(t.x, t.y);
|
this.broadcastJoystick(d.x, d.y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onTouchEnd(ev: EventTouch): void {
|
private onTouchEnd(ev: EventTouch): void {
|
||||||
const touchId = this.touchId(ev);
|
const touchId = this.touchId(ev);
|
||||||
|
const key = `end:${touchId}`;
|
||||||
|
if (this.processedPhases.has(key)) {
|
||||||
|
console.log('[FloatingControlLayer] onTouchEnd DEDUP-skip touchId=', touchId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.processedPhases.add(key);
|
||||||
|
// End of this phase — allow a future TOUCH_START with the same id.
|
||||||
|
this.processedPhases.delete(`start:${touchId}`);
|
||||||
|
|
||||||
const end = FloatingControlLayer.now();
|
const end = FloatingControlLayer.now();
|
||||||
const bound = this.router.end(touchId);
|
const bound = this.router.end(touchId);
|
||||||
if (!bound) return;
|
console.log('[FloatingControlLayer] onTouchEnd touchId=', touchId, 'bound=', bound,
|
||||||
|
'slots remaining=', this.router.activeTouchCount);
|
||||||
|
if (!bound) {
|
||||||
|
// Touch that missed every control — still clean up its timestamp
|
||||||
|
// record so the Map does not leak over long play sessions.
|
||||||
|
this.lastStartTs.delete(touchId);
|
||||||
|
this.processedPhases.delete(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (bound) {
|
switch (bound) {
|
||||||
case ControlId.Jump: {
|
case ControlId.Jump: {
|
||||||
const slotStart = this.lastStartTs.get(touchId);
|
const slotStart = this.lastStartTs.get(touchId);
|
||||||
@@ -174,6 +290,9 @@ export class FloatingControlLayer extends Component {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this.lastStartTs.delete(touchId);
|
this.lastStartTs.delete(touchId);
|
||||||
|
// Allow the same phase key to be re-used on a future touch id
|
||||||
|
// reassignment (some platforms recycle ids aggressively).
|
||||||
|
this.processedPhases.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
private broadcastJoystick(x: number, y: number): void {
|
private broadcastJoystick(x: number, y: number): void {
|
||||||
|
|||||||
@@ -109,21 +109,45 @@ export function clamp(v: number, min: number, max: number): number {
|
|||||||
return v < min ? min : v > max ? max : v;
|
return v < min ? min : v > max ? max : v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Hit-test tolerance (design px). Touches this far outside the visual
|
||||||
|
* bounding box still register as a hit. This compensates for finger
|
||||||
|
* imprecision on small touch targets (req 1.3, 20.3).
|
||||||
|
* Increased from 10→15 to better accommodate finger-pad size on mobile. */
|
||||||
|
export const HIT_TOLERANCE = 15;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if `(x, y)` lies inside `rect`. Origin is bottom-left.
|
* Returns true if `(x, y)` lies inside `rect`. Origin is bottom-left.
|
||||||
* Used by both `isInside` and the touch router.
|
* Used for the joystick which is rendered as a full rectangle.
|
||||||
*/
|
*/
|
||||||
export function isInsideRect(rect: IHitRect, x: number, y: number): boolean {
|
export function isInsideRect(rect: IHitRect, x: number, y: number): boolean {
|
||||||
const halfW = rect.w / 2;
|
const halfW = rect.w / 2;
|
||||||
const halfH = rect.h / 2;
|
const halfH = rect.h / 2;
|
||||||
return Math.abs(x - rect.cx) <= halfW && Math.abs(y - rect.cy) <= halfH;
|
return Math.abs(x - rect.cx) <= halfW + HIT_TOLERANCE && Math.abs(y - rect.cy) <= halfH + HIT_TOLERANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Which control (if any) is hit at `(x, y)`. Priority: attack > jump > joystick. */
|
/**
|
||||||
|
* Returns true if `(x, y)` lies inside the **circle** inscribed by `rect`.
|
||||||
|
* Buttons are rendered as circles via `Graphics.circle`; using a circular
|
||||||
|
* hit-test ensures the visual shape and the touch area match — no "dead
|
||||||
|
* zones" in the corners of a rectangular hit rect that visually lie outside
|
||||||
|
* the circle, and no missing the upper/lower arc of the circle.
|
||||||
|
*
|
||||||
|
* The effective radius is `min(w, h) / 2 + HIT_TOLERANCE`.
|
||||||
|
*/
|
||||||
|
export function isInsideCircle(rect: IHitRect, x: number, y: number): boolean {
|
||||||
|
const radius = Math.min(rect.w, rect.h) / 2;
|
||||||
|
const dx = x - rect.cx;
|
||||||
|
const dy = y - rect.cy;
|
||||||
|
return (dx * dx + dy * dy) <= (radius + HIT_TOLERANCE) * (radius + HIT_TOLERANCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Which control (if any) is hit at `(x, y)`. Priority: attack > jump > joystick.
|
||||||
|
* Buttons (jump, shuriken, ninjaSword) use circular hit-test to match their
|
||||||
|
* visual shape. The joystick retains rectangular hit-test for full-area coverage. */
|
||||||
export function hitTest(layout: IFloatingLayout, x: number, y: number): ControlId | null {
|
export function hitTest(layout: IFloatingLayout, x: number, y: number): ControlId | null {
|
||||||
if (isInsideRect(layout.ninjaSword, x, y)) return ControlId.NinjaSword;
|
if (isInsideCircle(layout.ninjaSword, x, y)) return ControlId.NinjaSword;
|
||||||
if (isInsideRect(layout.shuriken, x, y)) return ControlId.Shuriken;
|
if (isInsideCircle(layout.shuriken, x, y)) return ControlId.Shuriken;
|
||||||
if (isInsideRect(layout.jump, x, y)) return ControlId.Jump;
|
if (isInsideCircle(layout.jump, x, y)) return ControlId.Jump;
|
||||||
if (isInsideRect(layout.joystick, x, y)) return ControlId.Joystick;
|
if (isInsideRect(layout.joystick, x, y)) return ControlId.Joystick;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -268,6 +292,11 @@ export class MultiTouchRouter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Check whether a specific touchId still has an active slot in the router. */
|
||||||
|
public isPressedById(id: number): boolean {
|
||||||
|
return this.slots.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns how many simultaneous fingers are currently tracked. */
|
/** Returns how many simultaneous fingers are currently tracked. */
|
||||||
public get activeTouchCount(): number {
|
public get activeTouchCount(): number {
|
||||||
return this.slots.size;
|
return this.slots.size;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kage-legend-mvp",
|
"name": "kage-legend-mvp",
|
||||||
"version": "0.1.0",
|
"version": "3.8.8",
|
||||||
"description": "Shadow Legend: Ninja Rescue Princess - MVP (Chapter 1, Landscape, WeChat Mini Game)",
|
"description": "Shadow Legend: Ninja Rescue Princess - MVP (Chapter 1, Landscape, WeChat Mini Game)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": "KateLegend2 Team",
|
"author": "KateLegend2 Team",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"__version__": "3.0.9",
|
"__version__": "3.0.9",
|
||||||
"game": {
|
"game": {
|
||||||
"name": "UNKNOW GAME",
|
"name": "未知游戏",
|
||||||
"app_id": "UNKNOW",
|
"app_id": "UNKNOW",
|
||||||
"c_id": "0"
|
"c_id": "0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,19 +4,19 @@
|
|||||||
"customSplash": {
|
"customSplash": {
|
||||||
"id": "customSplash",
|
"id": "customSplash",
|
||||||
"label": "customSplash",
|
"label": "customSplash",
|
||||||
"enable": false,
|
"enable": true,
|
||||||
"customSplash": {
|
"customSplash": {
|
||||||
"complete": false,
|
"complete": false,
|
||||||
"form": "https://creator-api.cocos.com/api/form/show?"
|
"form": "https://creator-api.cocos.com/api/form/show?sid=8fef34a64f75f23c72b8c90e2d0833f0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"removeSplash": {
|
"removeSplash": {
|
||||||
"id": "removeSplash",
|
"id": "removeSplash",
|
||||||
"label": "removeSplash",
|
"label": "removeSplash",
|
||||||
"enable": false,
|
"enable": true,
|
||||||
"removeSplash": {
|
"removeSplash": {
|
||||||
"complete": false,
|
"complete": false,
|
||||||
"form": "https://creator-api.cocos.com/api/form/show?"
|
"form": "https://creator-api.cocos.com/api/form/show?sid=8fef34a64f75f23c72b8c90e2d0833f0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { PlayerMotionModel } from './assets/scripts/logic/PlayerMotionModel';
|
||||||
|
import { PlayerColorState, MOVE_SPEED } from './assets/scripts/common/Constants';
|
||||||
|
|
||||||
|
const m = new PlayerMotionModel({
|
||||||
|
aabb: { x: 0, y: 16, w: 16, h: 32 },
|
||||||
|
platforms: [{ topY: 0, leftX: -500, rightX: 500 }],
|
||||||
|
initialColorState: PlayerColorState.Red,
|
||||||
|
levelLengthPx: 2000,
|
||||||
|
});
|
||||||
|
console.log('after constructor: grounded=', m.isGrounded, 'aabb=', m.aabb, 'vx=', m.vx);
|
||||||
|
m.update(0.016);
|
||||||
|
console.log('after settle: grounded=', m.isGrounded, 'aabb=', m.aabb, 'vx=', m.vx, 'vy=', m.vy);
|
||||||
|
m.setHorizontalInput(1);
|
||||||
|
m.update(1);
|
||||||
|
console.log('after move: aabb.x=', m.aabb.x, 'vx=', m.vx);
|
||||||
@@ -54,17 +54,22 @@ export class Node {
|
|||||||
public name: string = '';
|
public name: string = '';
|
||||||
public active: boolean = true;
|
public active: boolean = true;
|
||||||
public layer: number = 0;
|
public layer: number = 0;
|
||||||
|
public isValid: boolean = true;
|
||||||
|
public scale: Vec3 = new Vec3(1, 1, 1);
|
||||||
|
|
||||||
constructor(name?: string) {
|
constructor(name?: string) {
|
||||||
this.name = name ?? '';
|
this.name = name ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public addChild(_child: Node): void {}
|
public addChild(_child: Node): void {}
|
||||||
|
public removeFromParent(): void {}
|
||||||
|
public destroy(): void { this.isValid = false; }
|
||||||
public on(_ev: string, _cb: (...args: unknown[]) => void, _target?: unknown): void {}
|
public on(_ev: string, _cb: (...args: unknown[]) => void, _target?: unknown): void {}
|
||||||
public off(_ev: string, _cb?: (...args: unknown[]) => void): void {}
|
public off(_ev: string, _cb?: (...args: unknown[]) => void): void {}
|
||||||
public getComponent<T>(_ctor: new (...args: unknown[]) => T): T | null { return null; }
|
public getComponent<T>(_ctor: new (...args: unknown[]) => T): T | null { return null; }
|
||||||
public addComponent<T>(_ctor: new (...args: unknown[]) => T): T { return {} as T; }
|
public addComponent<T>(_ctor: new (...args: unknown[]) => T): T { return {} as T; }
|
||||||
public setPosition(..._args: unknown[]): void {}
|
public setPosition(..._args: unknown[]): void {}
|
||||||
|
public setScale(..._args: unknown[]): void {}
|
||||||
|
|
||||||
public static EventType = {
|
public static EventType = {
|
||||||
TOUCH_START: 'touch-start',
|
TOUCH_START: 'touch-start',
|
||||||
@@ -158,6 +163,7 @@ export class Label {
|
|||||||
public horizontalAlign: number = 0;
|
public horizontalAlign: number = 0;
|
||||||
public verticalAlign: number = 0;
|
public verticalAlign: number = 0;
|
||||||
public useSystemFont: boolean = true;
|
public useSystemFont: boolean = true;
|
||||||
|
public enableWrapText: boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Button {
|
export class Button {
|
||||||
@@ -181,16 +187,39 @@ export class Sprite {
|
|||||||
public spriteFrame: unknown = null;
|
public spriteFrame: unknown = null;
|
||||||
public type: number = 0;
|
public type: number = 0;
|
||||||
public sizeMode: number = 0;
|
public sizeMode: number = 0;
|
||||||
|
public color: Color = Color.WHITE;
|
||||||
public static Type = { SIMPLE: 0, SLICED: 1, TILED: 2, FILLED: 3 };
|
public static Type = { SIMPLE: 0, SLICED: 1, TILED: 2, FILLED: 3 };
|
||||||
public static SizeMode = { CUSTOM: 0, TRIMMED: 1, RAW: 2 };
|
public static SizeMode = { CUSTOM: 0, TRIMMED: 1, RAW: 2 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Rect {
|
||||||
|
public x: number;
|
||||||
|
public y: number;
|
||||||
|
public width: number;
|
||||||
|
public height: number;
|
||||||
|
constructor(x: number = 0, y: number = 0, width: number = 0, height: number = 0) {
|
||||||
|
this.x = x; this.y = y; this.width = width; this.height = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Size {
|
||||||
|
public width: number;
|
||||||
|
public height: number;
|
||||||
|
constructor(width: number = 0, height: number = 0) {
|
||||||
|
this.width = width; this.height = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SpriteFrame {
|
export class SpriteFrame {
|
||||||
public texture: unknown = null;
|
public texture: unknown = null;
|
||||||
|
public rect: Rect = new Rect();
|
||||||
|
public originalSize: Size = new Size();
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Texture2D {
|
export class Texture2D {
|
||||||
public image: unknown = null;
|
public image: unknown = null;
|
||||||
|
public width: number = 0;
|
||||||
|
public height: number = 0;
|
||||||
public static PixelFormat = { RGBA8888: 35 };
|
public static PixelFormat = { RGBA8888: 35 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +234,21 @@ export class JsonAsset {
|
|||||||
export class Canvas {}
|
export class Canvas {}
|
||||||
export class EventTouch {}
|
export class EventTouch {}
|
||||||
|
|
||||||
|
/** Global input event source (Cocos Creator 3.8). */
|
||||||
|
export const input = {
|
||||||
|
on: (_ev: string, _cb: (...args: unknown[]) => void, _target?: unknown): void => {},
|
||||||
|
off: (_ev: string, _cb?: (...args: unknown[]) => void, _target?: unknown): void => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Input = {
|
||||||
|
EventType: {
|
||||||
|
TOUCH_START: 'touch-start',
|
||||||
|
TOUCH_MOVE: 'touch-move',
|
||||||
|
TOUCH_END: 'touch-end',
|
||||||
|
TOUCH_CANCEL: 'touch-cancel',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const resources = {
|
export const resources = {
|
||||||
load: (_path: string, _type: unknown, _cb: (err: Error | null, asset: unknown) => void): void => {},
|
load: (_path: string, _type: unknown, _cb: (err: Error | null, asset: unknown) => void): void => {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ const bossCfg: IBossConfig = {
|
|||||||
princessCutsceneAtHpRatio: 0.5,
|
princessCutsceneAtHpRatio: 0.5,
|
||||||
phases: [
|
phases: [
|
||||||
{ hpThreshold: 1.0, mode: 'pair_pincer', actionIntervalSec: 2.2 },
|
{ hpThreshold: 1.0, mode: 'pair_pincer', actionIntervalSec: 2.2 },
|
||||||
{ hpThreshold: 0.66, mode: 'fireball_spread', actionIntervalSec: 1.8 },
|
{ hpThreshold: 2 / 3, mode: 'fireball_spread', actionIntervalSec: 1.8 },
|
||||||
{ hpThreshold: 0.33, mode: 'clone_confuse', actionIntervalSec: 1.4 },
|
{ hpThreshold: 1 / 3, mode: 'clone_confuse', actionIntervalSec: 1.4 },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ function newPair(color: PlayerColorState = PlayerColorState.Red) {
|
|||||||
aabb: { x: 0, y: 16, w: 16, h: 32 },
|
aabb: { x: 0, y: 16, w: 16, h: 32 },
|
||||||
platforms: [{ topY: 0, leftX: -500, rightX: 500 }],
|
platforms: [{ topY: 0, leftX: -500, rightX: 500 }],
|
||||||
initialColorState: color,
|
initialColorState: color,
|
||||||
|
levelLengthPx: 2000,
|
||||||
});
|
});
|
||||||
motion.update(0.016); // settle on ground
|
motion.update(0.016); // settle on ground
|
||||||
const jump = new JumpController(motion);
|
const jump = new JumpController(motion);
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ function makeGroundPlatform(): IPlatform {
|
|||||||
|
|
||||||
function makeModel(color: PlayerColorState = PlayerColorState.Red) {
|
function makeModel(color: PlayerColorState = PlayerColorState.Red) {
|
||||||
return new PlayerMotionModel({
|
return new PlayerMotionModel({
|
||||||
aabb: { x: 0, y: 16, w: 16, h: 32 }, // character 16x32 resting on y=0 ground
|
aabb: { x: 100, y: 16, w: 16, h: 32 }, // character 16x32 resting on y=0 ground; x=100 avoids level-edge clamp
|
||||||
platforms: [makeGroundPlatform()],
|
platforms: [makeGroundPlatform()],
|
||||||
initialColorState: color,
|
initialColorState: color,
|
||||||
|
levelLengthPx: 2000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ describe('PlayerMotionModel — horizontal movement (req 2.1, 5.1-5.2)', () => {
|
|||||||
m.setHorizontalInput(1);
|
m.setHorizontalInput(1);
|
||||||
m.update(1);
|
m.update(1);
|
||||||
expect(m.vx).toBe(MOVE_SPEED[PlayerColorState.Red]);
|
expect(m.vx).toBe(MOVE_SPEED[PlayerColorState.Red]);
|
||||||
expect(m.aabb.x).toBeCloseTo(100, 1);
|
expect(m.aabb.x).toBeCloseTo(200, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('moves at 150 px/s in yellow state', () => {
|
it('moves at 150 px/s in yellow state', () => {
|
||||||
|
|||||||
@@ -44,21 +44,22 @@ describe('PlayerStateMachine — damage (req 5.3-5.6, 10.4-10.5)', () => {
|
|||||||
expect(sm.color).toBe(PlayerColorState.Red);
|
expect(sm.color).toBe(PlayerColorState.Red);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Red + shuriken → death, consumes one life', () => {
|
it('Red + shuriken → life loss (downgraded) when lives > 1', () => {
|
||||||
const sm = new PlayerStateMachine(2);
|
const sm = new PlayerStateMachine(2);
|
||||||
const out = sm.takeHit('shuriken');
|
const out = sm.takeHit('shuriken');
|
||||||
expect(out.kind).toBe('died');
|
expect(out.kind).toBe('downgraded');
|
||||||
expect(sm.lives).toBe(1);
|
expect(sm.lives).toBe(1);
|
||||||
expect(sm.isDead).toBe(false);
|
expect(sm.isDead).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fireball is always lethal regardless of color', () => {
|
it('fireball reduces one life regardless of color', () => {
|
||||||
const sm = new PlayerStateMachine(2);
|
const sm = new PlayerStateMachine(2);
|
||||||
sm.pickupCrystalJade();
|
sm.pickupCrystalJade();
|
||||||
sm.pickupCrystalJade();
|
sm.pickupCrystalJade();
|
||||||
const out = sm.takeHit('fireball');
|
const out = sm.takeHit('fireball');
|
||||||
expect(out).toEqual({ kind: 'died', cause: 'fireball' });
|
expect(out.kind).toBe('downgraded');
|
||||||
expect(sm.color).toBe(PlayerColorState.Red);
|
expect(sm.lives).toBe(1);
|
||||||
|
expect(sm.isDead).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('smoke bomb is always lethal', () => {
|
it('smoke bomb is always lethal', () => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
applySafeArea,
|
applySafeArea,
|
||||||
classifyDirection,
|
classifyDirection,
|
||||||
hitTest,
|
hitTest,
|
||||||
|
isInsideCircle,
|
||||||
isInsideRect,
|
isInsideRect,
|
||||||
joystickDirection,
|
joystickDirection,
|
||||||
ZERO_DIRECTION,
|
ZERO_DIRECTION,
|
||||||
@@ -21,8 +22,33 @@ describe('InputModel — layout geometry', () => {
|
|||||||
const r = { cx: 100, cy: 100, w: 40, h: 40 };
|
const r = { cx: 100, cy: 100, w: 40, h: 40 };
|
||||||
expect(isInsideRect(r, 100, 100)).toBe(true);
|
expect(isInsideRect(r, 100, 100)).toBe(true);
|
||||||
expect(isInsideRect(r, 120, 120)).toBe(true);
|
expect(isInsideRect(r, 120, 120)).toBe(true);
|
||||||
expect(isInsideRect(r, 121, 100)).toBe(false);
|
// With HIT_TOLERANCE=15, the effective boundary is ±35 (=halfW/halfH + tolerance).
|
||||||
expect(isInsideRect(r, 100, 79)).toBe(false);
|
// 121 is outside the visual box (120) but within tolerance (135).
|
||||||
|
expect(isInsideRect(r, 121, 100)).toBe(true);
|
||||||
|
// 79 is below the visual bottom (80) but within tolerance (65).
|
||||||
|
expect(isInsideRect(r, 100, 79)).toBe(true);
|
||||||
|
// Far outside even the tolerance range.
|
||||||
|
expect(isInsideRect(r, 136, 100)).toBe(false);
|
||||||
|
expect(isInsideRect(r, 100, 64)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isInsideCircle matches circular button shape', () => {
|
||||||
|
const r = { cx: 765, cy: 100, w: 90, h: 90 }; // shuriken button
|
||||||
|
// Centre — always a hit.
|
||||||
|
expect(isInsideCircle(r, 765, 100)).toBe(true);
|
||||||
|
// On the circle edge (radius = 45).
|
||||||
|
expect(isInsideCircle(r, 765 + 45, 100)).toBe(true); // right edge
|
||||||
|
expect(isInsideCircle(r, 765, 100 + 45)).toBe(true); // top edge (upper semicircle)
|
||||||
|
expect(isInsideCircle(r, 765 - 45, 100)).toBe(true); // left edge
|
||||||
|
expect(isInsideCircle(r, 765, 100 - 45)).toBe(true); // bottom edge
|
||||||
|
// Within HIT_TOLERANCE (15) outside the circle.
|
||||||
|
expect(isInsideCircle(r, 765 + 60, 100)).toBe(true); // right + tolerance
|
||||||
|
expect(isInsideCircle(r, 765, 100 + 60)).toBe(true); // top + tolerance (upper arc)
|
||||||
|
// Corner of the bounding rect but OUTSIDE the circle — should miss.
|
||||||
|
// Distance from centre to (765+45, 100+45) = sqrt(45²+45²) ≈ 63.6 > 45+15=60
|
||||||
|
expect(isInsideCircle(r, 765 + 45, 100 + 45)).toBe(false);
|
||||||
|
// Well outside.
|
||||||
|
expect(isInsideCircle(r, 765 + 70, 100)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||