diff --git a/assets_v2/.gitignore b/assets_v2/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/assets_v2/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/assets_v2/GODOT_IMPORT_GUIDE.md b/assets_v2/GODOT_IMPORT_GUIDE.md new file mode 100644 index 0000000..106a8f7 --- /dev/null +++ b/assets_v2/GODOT_IMPORT_GUIDE.md @@ -0,0 +1,174 @@ +# Nine Nether — V2 (Steam Alpha) Art: Godot Import Guide + +This guide is for **Codex** (or any engineer) wiring the V2 art set into the +Godot 4 project. The V2 set is a **full redesign** — do **not** reuse any +`assets/` (V1) sprites. V1 failed because characters shared body templates and +only differed by colour/weapon swaps, animations had near-invisible amplitude, +and there was no attack telegraph or silhouette variety. V2 fixes all of that. + +--- + +## 1. Where things are + +``` +assets_v2/ + characters/ + player/ player_*.png (96×96 / frame) + melee_ghost/ melee_ghost_*.png (96×96 / frame) + ghost_archer/ ghost_archer_*.png (96×96 / frame) + corpse_beast/ corpse_beast_*.png (128×96 / frame) + gate_warden/ gate_warden_*.png (192×192 / frame) + └ each dir also has: concept_front/side/back.png, silhouette.png, + palette.png, contact_sheet.png, frame_map.json, metadata.md + icons/ icon_*.png (64×64, weapon/stat HUD icons) + projectiles/ ghost_fire_arrow.png (32×32) + effects/ blade_slash / hit / ghost_fire / death_dissolve (see sizes below) + godot/ _frames.tres (SpriteFrames resources, ready to load) + review/ character_scale_comparison.png, silhouette_comparison.png + tools/ procedural generators (PIL) + gen_metadata.py + GODOT_IMPORT_GUIDE.md +``` + +All sprite sheets are **horizontal strips** with `frames` cells left→right, +frame size as listed. Transparent background (RGBA). No anti-aliasing, no blur. + +--- + +## 2. Import settings (critical) + +For **every** PNG under `assets_v2/`: + +- **Import as `Texture2D`** (default). +- In the `Import` dock: + - **Filter:** `Nearest` (disable `Filter` / linear filtering). + - **Mipmaps:** off. + - **Compress:** keep (or `Compress > Mode = Lossless` if you see banding). + - **Detect 3D:** off. +- Click **Reimport**. +- Do **not** enable `repeat`/`repeat_enable` on character sheets (you crop via AtlasTexture regions, see §4). + +The game viewport is **640×360**, displayed at **1280×720** with integer +scaling — nearest-neighbour keeps pixels crisp. + +--- + +## 3. Effects & projectiles (sizes) + +| File | Size | Frames | Loop | Use | +|---|---|---|---|---| +| `effects/blade_slash.png` | 128×128 | 6 | no | melee hit VFX | +| `effects/hit.png` | 96×96 | 4 | no | generic impact | +| `effects/ghost_fire.png` | 64×64 | 8 | **yes** | ambient / emitter | +| `effects/death_dissolve.png` | 96×96 | 8 | no | death VFX | +| `projectiles/ghost_fire_arrow.png` | 32×32 | 1 | n/a | archer projectile (points RIGHT; flip in-engine for left) | + +These are separate textures (not SpriteFrames) — use `AnimatedSprite2D` or +manual `region` animation, or just `Sprite2D` with a script stepping `region_rect`. + +--- + +## 4. SpriteFrames resources (ready to use) + +`assets_v2/godot/_frames.tres` are **pre-generated** `SpriteFrames` +resources. Each animation is a list of `AtlasTexture` sub-resources cropping +`Rect2(frame*fw, 0, fw, fh)` from the character's sheet — identical pattern to +V1's `actors/enemies/ghost_archer_frames.tres`. + +To use, in your actor scene: + +```gdscript +# Player example +@onready var sprite: AnimatedSprite2D = $Sprite +func _ready(): + sprite.sprite_frames = load("res://assets_v2/godot/player_frames.tres") + +func play_idle(): sprite.play("player_idle") +func play_attack(): sprite.play("player_light_attack_1") +``` + +Animation names == the sheet file stems (e.g. `player_light_attack_1`, +`gate_warden_attack_2`, `corpse_beast_charge_windup`). Full list per character +is in `assets_v2/characters//frame_map.json` and `metadata.md`. + +If you change a sheet's frame count, **re-run** `python3 +assets_v2/tools/gen_metadata.py` to regenerate the `.tres` (it asserts sheet +size == `frames*fw × fh`). + +--- + +## 5. Hitbox / timing sync via frame_map.json + +Every character dir has `frame_map.json` with, per animation: + +```json +{ + "phases": { "startup": [0,1,2,3], "active": [4,5], "recovery": [6,7] }, + "pivot": [48, 66], + "foot_position": [48, 88], + "projectile_release_frame": null, // ghost_archer_shoot -> 1 + "charge_ready_frame": null // corpse_beast_charge_windup -> 5 +} +``` + +- **`active`** = frames the hitbox / damage window should be ON. +- **`startup`** = telegraph (wind-up) — show a tell, no damage. +- **`recovery`** = can't act, vulnerable. +- **`projectile_release_frame`** = spawn the ghost arrow on this frame + (ghost_archer_shoot = 1). +- **`pivot`** / **`foot_position`** = anchor the `AnimatedSprite2D` so the + character's feet sit on the ground; offset the sprite via `centered = false` + + `offset = -pivot` (or set `Sprite2D.offset`). + +Example (player light attack 1): enable `Hitbox.monitoring` on frames 4–5, +disable otherwise. + +--- + +## 6. AnimatedSprite2D offset (foot alignment) + +Because the art is drawn with the feet near the bottom of the frame, set: +`centered = false`, and `offset = Vector2(-pivot.x, -foot_position.y)` so the +character's foot baseline lands on the node's origin. Per-character values: + +| Character | frame | pivot | foot_y | +|---|---|---|---| +| player | 96×96 | (48,66) | 88 | +| melee_ghost | 96×96 | (48,70) | 90 | +| ghost_archer | 96×96 | (48,64) | 88 | +| corpse_beast | 128×96 | (64,58) | 86 | +| gate_warden | 192×192 | (96,128) | 176 | + +--- + +## 7. Regenerating the art + +From the repo root (Python 3.12 w/ Pillow required): + +```bash +python3 assets_v2/tools/build_player.py +python3 assets_v2/tools/build_melee_ghost.py +python3 assets_v2/tools/build_ghost_archer.py +python3 assets_v2/tools/build_boss.py +python3 assets_v2/tools/build_corpse_beast.py +python3 assets_v2/tools/build_icons.py +python3 assets_v2/tools/build_projectiles.py +python3 assets_v2/tools/build_effects.py +python3 assets_v2/tools/build_design_bible.py # concept/silhouette/contact/palette + comparisons +python3 assets_v2/tools/gen_metadata.py # frame_map.json + metadata.md + .tres +``` + +The `.py` files are excluded from Godot import via `assets_v2/tools/.gdignore`. + +--- + +## 8. Acceptance notes (for reviewers) + +- Silhouettes are distinct (see `review/silhouette_comparison.png`): player = + upright soldier w/ red cloth; melee ghost = top-heavy hunched; archer = + tall thin w/ big bow; corpse beast = low wide quadruped; warden = colossal + asymmetric pauldrons + great helm. +- Scale ladder (see `review/character_scale_comparison.png`): player ≈ 1.0× + body, ghosts ≈ same height but different bulk, corpse beast ~0.6× height but + wide, boss ≈ 2.5–3× player. +- Every animation frame is a genuinely different pose; feet baseline is stable; + weapon length is constant across a character's frames. diff --git a/assets_v2/characters/corpse_beast/concept_back.png b/assets_v2/characters/corpse_beast/concept_back.png new file mode 100644 index 0000000..ebfa8e5 Binary files /dev/null and b/assets_v2/characters/corpse_beast/concept_back.png differ diff --git a/assets_v2/characters/corpse_beast/concept_front.png b/assets_v2/characters/corpse_beast/concept_front.png new file mode 100644 index 0000000..8bf3f6e Binary files /dev/null and b/assets_v2/characters/corpse_beast/concept_front.png differ diff --git a/assets_v2/characters/corpse_beast/concept_side.png b/assets_v2/characters/corpse_beast/concept_side.png new file mode 100644 index 0000000..a6d92ae Binary files /dev/null and b/assets_v2/characters/corpse_beast/concept_side.png differ diff --git a/assets_v2/characters/corpse_beast/contact_sheet.png b/assets_v2/characters/corpse_beast/contact_sheet.png new file mode 100644 index 0000000..61790e3 Binary files /dev/null and b/assets_v2/characters/corpse_beast/contact_sheet.png differ diff --git a/assets_v2/characters/corpse_beast/corpse_beast_charge.png b/assets_v2/characters/corpse_beast/corpse_beast_charge.png new file mode 100644 index 0000000..8a95ffa Binary files /dev/null and b/assets_v2/characters/corpse_beast/corpse_beast_charge.png differ diff --git a/assets_v2/characters/corpse_beast/corpse_beast_charge_windup.png b/assets_v2/characters/corpse_beast/corpse_beast_charge_windup.png new file mode 100644 index 0000000..5c9e0fe Binary files /dev/null and b/assets_v2/characters/corpse_beast/corpse_beast_charge_windup.png differ diff --git a/assets_v2/characters/corpse_beast/corpse_beast_death.png b/assets_v2/characters/corpse_beast/corpse_beast_death.png new file mode 100644 index 0000000..cf84691 Binary files /dev/null and b/assets_v2/characters/corpse_beast/corpse_beast_death.png differ diff --git a/assets_v2/characters/corpse_beast/corpse_beast_hurt.png b/assets_v2/characters/corpse_beast/corpse_beast_hurt.png new file mode 100644 index 0000000..5723805 Binary files /dev/null and b/assets_v2/characters/corpse_beast/corpse_beast_hurt.png differ diff --git a/assets_v2/characters/corpse_beast/corpse_beast_idle.png b/assets_v2/characters/corpse_beast/corpse_beast_idle.png new file mode 100644 index 0000000..fa969a7 Binary files /dev/null and b/assets_v2/characters/corpse_beast/corpse_beast_idle.png differ diff --git a/assets_v2/characters/corpse_beast/corpse_beast_run.png b/assets_v2/characters/corpse_beast/corpse_beast_run.png new file mode 100644 index 0000000..b0ec5b6 Binary files /dev/null and b/assets_v2/characters/corpse_beast/corpse_beast_run.png differ diff --git a/assets_v2/characters/corpse_beast/corpse_beast_wall_impact.png b/assets_v2/characters/corpse_beast/corpse_beast_wall_impact.png new file mode 100644 index 0000000..6df5246 Binary files /dev/null and b/assets_v2/characters/corpse_beast/corpse_beast_wall_impact.png differ diff --git a/assets_v2/characters/corpse_beast/frame_map.json b/assets_v2/characters/corpse_beast/frame_map.json new file mode 100644 index 0000000..04226aa --- /dev/null +++ b/assets_v2/characters/corpse_beast/frame_map.json @@ -0,0 +1,211 @@ +{ + "character": "corpse_beast", + "frame_size": [ + 128, + 96 + ], + "pivot": [ + 64, + 58 + ], + "foot_position": [ + 64, + 86 + ], + "animations": { + "corpse_beast_idle": { + "frames": 6, + "fps": 5.0, + "loop": true, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 64, + 58 + ], + "foot_position": [ + 64, + 86 + ], + "frame_size": [ + 128, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "low breathing" + }, + "corpse_beast_run": { + "frames": 8, + "fps": 10.0, + "loop": true, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 64, + 58 + ], + "foot_position": [ + 64, + 86 + ], + "frame_size": [ + 128, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "gallop" + }, + "corpse_beast_charge_windup": { + "frames": 6, + "fps": 8.0, + "loop": false, + "phases": { + "startup": [ + 0, + 1, + 2, + 3, + 4 + ], + "active": [], + "recovery": [ + 5 + ] + }, + "pivot": [ + 64, + 58 + ], + "foot_position": [ + 64, + 86 + ], + "frame_size": [ + 128, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": 5, + "note": "coil; charge_ready at 5" + }, + "corpse_beast_charge": { + "frames": 4, + "fps": 12.0, + "loop": false, + "phases": { + "startup": [], + "active": [ + 0, + 1, + 2, + 3 + ], + "recovery": [] + }, + "pivot": [ + 64, + 58 + ], + "foot_position": [ + 64, + 86 + ], + "frame_size": [ + 128, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "full-speed contact" + }, + "corpse_beast_wall_impact": { + "frames": 4, + "fps": 8.0, + "loop": false, + "phases": { + "startup": [], + "active": [ + 0, + 1 + ], + "recovery": [ + 2, + 3 + ] + }, + "pivot": [ + 64, + 58 + ], + "foot_position": [ + 64, + 86 + ], + "frame_size": [ + 128, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "slam into wall" + }, + "corpse_beast_hurt": { + "frames": 4, + "fps": 10.0, + "loop": false, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 64, + 58 + ], + "foot_position": [ + 64, + 86 + ], + "frame_size": [ + 128, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "scramble" + }, + "corpse_beast_death": { + "frames": 8, + "fps": 6.0, + "loop": false, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 64, + 58 + ], + "foot_position": [ + 64, + 86 + ], + "frame_size": [ + 128, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "collapse" + } + } +} \ No newline at end of file diff --git a/assets_v2/characters/corpse_beast/metadata.md b/assets_v2/characters/corpse_beast/metadata.md new file mode 100644 index 0000000..f21791a --- /dev/null +++ b/assets_v2/characters/corpse_beast/metadata.md @@ -0,0 +1,19 @@ +# corpse_beast — Animation & Spec Sheet (V2) + +- **Frame size:** 128x96 px +- **Pivot (rotation/hitbox anchor):** [64, 58] +- **Foot baseline (y):** 86 + +| Animation | Frames | FPS | Loop | Startup | Active | Recovery | Proj/Charge | +|---|---|---|---|---|---|---|---| +| corpse_beast_idle | 6 | 5.0 | True | - | - | - | - | +| corpse_beast_run | 8 | 10.0 | True | - | - | - | - | +| corpse_beast_charge_windup | 6 | 8.0 | False | 0,1,2,3,4 | - | 5 | ready@5 | +| corpse_beast_charge | 4 | 12.0 | False | - | 0,1,2,3 | - | - | +| corpse_beast_wall_impact | 4 | 8.0 | False | - | 0,1 | 2,3 | - | +| corpse_beast_hurt | 4 | 10.0 | False | - | - | - | - | +| corpse_beast_death | 8 | 6.0 | False | - | - | - | - | + +**Legend:** frames are 0-indexed. `Active` = hitbox/projectile window. `projectile_release_frame` = frame the ghost arrow spawns. `charge_ready_frame` = frame a charge/aim state is fully wound (transition out). + +Generated by `assets_v2/tools/gen_metadata.py`. Source: pose comments in `assets_v2/tools/build_*.py`. \ No newline at end of file diff --git a/assets_v2/characters/corpse_beast/palette.png b/assets_v2/characters/corpse_beast/palette.png new file mode 100644 index 0000000..e2ea693 Binary files /dev/null and b/assets_v2/characters/corpse_beast/palette.png differ diff --git a/assets_v2/characters/corpse_beast/silhouette.png b/assets_v2/characters/corpse_beast/silhouette.png new file mode 100644 index 0000000..5419e33 Binary files /dev/null and b/assets_v2/characters/corpse_beast/silhouette.png differ diff --git a/assets_v2/characters/gate_warden/concept_back.png b/assets_v2/characters/gate_warden/concept_back.png new file mode 100644 index 0000000..0dd0ead Binary files /dev/null and b/assets_v2/characters/gate_warden/concept_back.png differ diff --git a/assets_v2/characters/gate_warden/concept_front.png b/assets_v2/characters/gate_warden/concept_front.png new file mode 100644 index 0000000..37b2ab1 Binary files /dev/null and b/assets_v2/characters/gate_warden/concept_front.png differ diff --git a/assets_v2/characters/gate_warden/concept_side.png b/assets_v2/characters/gate_warden/concept_side.png new file mode 100644 index 0000000..c176594 Binary files /dev/null and b/assets_v2/characters/gate_warden/concept_side.png differ diff --git a/assets_v2/characters/gate_warden/contact_sheet.png b/assets_v2/characters/gate_warden/contact_sheet.png new file mode 100644 index 0000000..491249b Binary files /dev/null and b/assets_v2/characters/gate_warden/contact_sheet.png differ diff --git a/assets_v2/characters/gate_warden/frame_map.json b/assets_v2/characters/gate_warden/frame_map.json new file mode 100644 index 0000000..6053cd3 --- /dev/null +++ b/assets_v2/characters/gate_warden/frame_map.json @@ -0,0 +1,195 @@ +{ + "character": "gate_warden", + "frame_size": [ + 192, + 192 + ], + "pivot": [ + 96, + 128 + ], + "foot_position": [ + 96, + 176 + ], + "animations": { + "gate_warden_idle": { + "frames": 8, + "fps": 6.0, + "loop": true, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 96, + 128 + ], + "foot_position": [ + 96, + 176 + ], + "frame_size": [ + 192, + 192 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "heavy breathing" + }, + "gate_warden_walk": { + "frames": 8, + "fps": 8.0, + "loop": true, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 96, + 128 + ], + "foot_position": [ + 96, + 176 + ], + "frame_size": [ + 192, + 192 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "earth-shaking steps" + }, + "gate_warden_attack_1": { + "frames": 10, + "fps": 9.0, + "loop": false, + "phases": { + "startup": [ + 0, + 1, + 2, + 3 + ], + "active": [ + 4, + 5 + ], + "recovery": [ + 6, + 7, + 8, + 9 + ] + }, + "pivot": [ + 96, + 128 + ], + "foot_position": [ + 96, + 176 + ], + "frame_size": [ + 192, + 192 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "horizontal great-blade sweep" + }, + "gate_warden_attack_2": { + "frames": 12, + "fps": 8.0, + "loop": false, + "phases": { + "startup": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "active": [ + 6, + 7 + ], + "recovery": [ + 8, + 9, + 10, + 11 + ] + }, + "pivot": [ + 96, + 128 + ], + "foot_position": [ + 96, + 176 + ], + "frame_size": [ + 192, + 192 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "overhead ground pound" + }, + "gate_warden_hurt": { + "frames": 4, + "fps": 8.0, + "loop": false, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 96, + 128 + ], + "foot_position": [ + 96, + 176 + ], + "frame_size": [ + 192, + 192 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "recoil" + }, + "gate_warden_death": { + "frames": 12, + "fps": 6.0, + "loop": false, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 96, + 128 + ], + "foot_position": [ + 96, + 176 + ], + "frame_size": [ + 192, + 192 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "colossal collapse" + } + } +} \ No newline at end of file diff --git a/assets_v2/characters/gate_warden/gate_warden_attack_1.png b/assets_v2/characters/gate_warden/gate_warden_attack_1.png new file mode 100644 index 0000000..c72d0f5 Binary files /dev/null and b/assets_v2/characters/gate_warden/gate_warden_attack_1.png differ diff --git a/assets_v2/characters/gate_warden/gate_warden_attack_2.png b/assets_v2/characters/gate_warden/gate_warden_attack_2.png new file mode 100644 index 0000000..cfdebb8 Binary files /dev/null and b/assets_v2/characters/gate_warden/gate_warden_attack_2.png differ diff --git a/assets_v2/characters/gate_warden/gate_warden_death.png b/assets_v2/characters/gate_warden/gate_warden_death.png new file mode 100644 index 0000000..9e0ec6a Binary files /dev/null and b/assets_v2/characters/gate_warden/gate_warden_death.png differ diff --git a/assets_v2/characters/gate_warden/gate_warden_hurt.png b/assets_v2/characters/gate_warden/gate_warden_hurt.png new file mode 100644 index 0000000..67d4ce6 Binary files /dev/null and b/assets_v2/characters/gate_warden/gate_warden_hurt.png differ diff --git a/assets_v2/characters/gate_warden/gate_warden_idle.png b/assets_v2/characters/gate_warden/gate_warden_idle.png new file mode 100644 index 0000000..6027709 Binary files /dev/null and b/assets_v2/characters/gate_warden/gate_warden_idle.png differ diff --git a/assets_v2/characters/gate_warden/gate_warden_walk.png b/assets_v2/characters/gate_warden/gate_warden_walk.png new file mode 100644 index 0000000..116043b Binary files /dev/null and b/assets_v2/characters/gate_warden/gate_warden_walk.png differ diff --git a/assets_v2/characters/gate_warden/metadata.md b/assets_v2/characters/gate_warden/metadata.md new file mode 100644 index 0000000..259be91 --- /dev/null +++ b/assets_v2/characters/gate_warden/metadata.md @@ -0,0 +1,18 @@ +# gate_warden — Animation & Spec Sheet (V2) + +- **Frame size:** 192x192 px +- **Pivot (rotation/hitbox anchor):** [96, 128] +- **Foot baseline (y):** 176 + +| Animation | Frames | FPS | Loop | Startup | Active | Recovery | Proj/Charge | +|---|---|---|---|---|---|---|---| +| gate_warden_idle | 8 | 6.0 | True | - | - | - | - | +| gate_warden_walk | 8 | 8.0 | True | - | - | - | - | +| gate_warden_attack_1 | 10 | 9.0 | False | 0,1,2,3 | 4,5 | 6,7,8,9 | - | +| gate_warden_attack_2 | 12 | 8.0 | False | 0,1,2,3,4,5 | 6,7 | 8,9,10,11 | - | +| gate_warden_hurt | 4 | 8.0 | False | - | - | - | - | +| gate_warden_death | 12 | 6.0 | False | - | - | - | - | + +**Legend:** frames are 0-indexed. `Active` = hitbox/projectile window. `projectile_release_frame` = frame the ghost arrow spawns. `charge_ready_frame` = frame a charge/aim state is fully wound (transition out). + +Generated by `assets_v2/tools/gen_metadata.py`. Source: pose comments in `assets_v2/tools/build_*.py`. \ No newline at end of file diff --git a/assets_v2/characters/gate_warden/palette.png b/assets_v2/characters/gate_warden/palette.png new file mode 100644 index 0000000..59df448 Binary files /dev/null and b/assets_v2/characters/gate_warden/palette.png differ diff --git a/assets_v2/characters/gate_warden/silhouette.png b/assets_v2/characters/gate_warden/silhouette.png new file mode 100644 index 0000000..b9b89f0 Binary files /dev/null and b/assets_v2/characters/gate_warden/silhouette.png differ diff --git a/assets_v2/characters/ghost_archer/concept_back.png b/assets_v2/characters/ghost_archer/concept_back.png new file mode 100644 index 0000000..f28f941 Binary files /dev/null and b/assets_v2/characters/ghost_archer/concept_back.png differ diff --git a/assets_v2/characters/ghost_archer/concept_front.png b/assets_v2/characters/ghost_archer/concept_front.png new file mode 100644 index 0000000..59c55f1 Binary files /dev/null and b/assets_v2/characters/ghost_archer/concept_front.png differ diff --git a/assets_v2/characters/ghost_archer/concept_side.png b/assets_v2/characters/ghost_archer/concept_side.png new file mode 100644 index 0000000..eaf97ae Binary files /dev/null and b/assets_v2/characters/ghost_archer/concept_side.png differ diff --git a/assets_v2/characters/ghost_archer/contact_sheet.png b/assets_v2/characters/ghost_archer/contact_sheet.png new file mode 100644 index 0000000..a3de04f Binary files /dev/null and b/assets_v2/characters/ghost_archer/contact_sheet.png differ diff --git a/assets_v2/characters/ghost_archer/frame_map.json b/assets_v2/characters/ghost_archer/frame_map.json new file mode 100644 index 0000000..b76bd36 --- /dev/null +++ b/assets_v2/characters/ghost_archer/frame_map.json @@ -0,0 +1,182 @@ +{ + "character": "ghost_archer", + "frame_size": [ + 96, + 96 + ], + "pivot": [ + 48, + 64 + ], + "foot_position": [ + 48, + 88 + ], + "animations": { + "ghost_archer_idle": { + "frames": 6, + "fps": 8.0, + "loop": true, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 64 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "still draw, ghost fire" + }, + "ghost_archer_retreat": { + "frames": 8, + "fps": 10.0, + "loop": true, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 64 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "back-step kiting" + }, + "ghost_archer_aim": { + "frames": 6, + "fps": 8.0, + "loop": false, + "phases": { + "startup": [ + 0, + 1, + 2, + 3, + 4 + ], + "active": [], + "recovery": [ + 5 + ] + }, + "pivot": [ + 48, + 64 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": 5, + "note": "draw bow, charge_ready at 5" + }, + "ghost_archer_shoot": { + "frames": 4, + "fps": 14.0, + "loop": false, + "phases": { + "startup": [ + 0 + ], + "active": [ + 1 + ], + "recovery": [ + 2, + 3 + ] + }, + "pivot": [ + 48, + 64 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": 1, + "charge_ready_frame": null, + "note": "release; projectile at frame 1" + }, + "ghost_archer_hurt": { + "frames": 4, + "fps": 10.0, + "loop": false, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 64 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "recoil" + }, + "ghost_archer_death": { + "frames": 8, + "fps": 8.0, + "loop": false, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 64 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "dissolve" + } + } +} \ No newline at end of file diff --git a/assets_v2/characters/ghost_archer/ghost_archer_aim.png b/assets_v2/characters/ghost_archer/ghost_archer_aim.png new file mode 100644 index 0000000..f0d04a9 Binary files /dev/null and b/assets_v2/characters/ghost_archer/ghost_archer_aim.png differ diff --git a/assets_v2/characters/ghost_archer/ghost_archer_death.png b/assets_v2/characters/ghost_archer/ghost_archer_death.png new file mode 100644 index 0000000..cfa5df0 Binary files /dev/null and b/assets_v2/characters/ghost_archer/ghost_archer_death.png differ diff --git a/assets_v2/characters/ghost_archer/ghost_archer_hurt.png b/assets_v2/characters/ghost_archer/ghost_archer_hurt.png new file mode 100644 index 0000000..ce7dd2a Binary files /dev/null and b/assets_v2/characters/ghost_archer/ghost_archer_hurt.png differ diff --git a/assets_v2/characters/ghost_archer/ghost_archer_idle.png b/assets_v2/characters/ghost_archer/ghost_archer_idle.png new file mode 100644 index 0000000..bd45015 Binary files /dev/null and b/assets_v2/characters/ghost_archer/ghost_archer_idle.png differ diff --git a/assets_v2/characters/ghost_archer/ghost_archer_retreat.png b/assets_v2/characters/ghost_archer/ghost_archer_retreat.png new file mode 100644 index 0000000..06ca8f2 Binary files /dev/null and b/assets_v2/characters/ghost_archer/ghost_archer_retreat.png differ diff --git a/assets_v2/characters/ghost_archer/ghost_archer_shoot.png b/assets_v2/characters/ghost_archer/ghost_archer_shoot.png new file mode 100644 index 0000000..ceeae03 Binary files /dev/null and b/assets_v2/characters/ghost_archer/ghost_archer_shoot.png differ diff --git a/assets_v2/characters/ghost_archer/metadata.md b/assets_v2/characters/ghost_archer/metadata.md new file mode 100644 index 0000000..33f782c --- /dev/null +++ b/assets_v2/characters/ghost_archer/metadata.md @@ -0,0 +1,18 @@ +# ghost_archer — Animation & Spec Sheet (V2) + +- **Frame size:** 96x96 px +- **Pivot (rotation/hitbox anchor):** [48, 64] +- **Foot baseline (y):** 88 + +| Animation | Frames | FPS | Loop | Startup | Active | Recovery | Proj/Charge | +|---|---|---|---|---|---|---|---| +| ghost_archer_idle | 6 | 8.0 | True | - | - | - | - | +| ghost_archer_retreat | 8 | 10.0 | True | - | - | - | - | +| ghost_archer_aim | 6 | 8.0 | False | 0,1,2,3,4 | - | 5 | ready@5 | +| ghost_archer_shoot | 4 | 14.0 | False | 0 | 1 | 2,3 | 1 | +| ghost_archer_hurt | 4 | 10.0 | False | - | - | - | - | +| ghost_archer_death | 8 | 8.0 | False | - | - | - | - | + +**Legend:** frames are 0-indexed. `Active` = hitbox/projectile window. `projectile_release_frame` = frame the ghost arrow spawns. `charge_ready_frame` = frame a charge/aim state is fully wound (transition out). + +Generated by `assets_v2/tools/gen_metadata.py`. Source: pose comments in `assets_v2/tools/build_*.py`. \ No newline at end of file diff --git a/assets_v2/characters/ghost_archer/palette.png b/assets_v2/characters/ghost_archer/palette.png new file mode 100644 index 0000000..6c2f966 Binary files /dev/null and b/assets_v2/characters/ghost_archer/palette.png differ diff --git a/assets_v2/characters/ghost_archer/silhouette.png b/assets_v2/characters/ghost_archer/silhouette.png new file mode 100644 index 0000000..f6d6610 Binary files /dev/null and b/assets_v2/characters/ghost_archer/silhouette.png differ diff --git a/assets_v2/characters/melee_ghost/concept_back.png b/assets_v2/characters/melee_ghost/concept_back.png new file mode 100644 index 0000000..4d1cddf Binary files /dev/null and b/assets_v2/characters/melee_ghost/concept_back.png differ diff --git a/assets_v2/characters/melee_ghost/concept_front.png b/assets_v2/characters/melee_ghost/concept_front.png new file mode 100644 index 0000000..a5d6222 Binary files /dev/null and b/assets_v2/characters/melee_ghost/concept_front.png differ diff --git a/assets_v2/characters/melee_ghost/concept_side.png b/assets_v2/characters/melee_ghost/concept_side.png new file mode 100644 index 0000000..dece3cf Binary files /dev/null and b/assets_v2/characters/melee_ghost/concept_side.png differ diff --git a/assets_v2/characters/melee_ghost/contact_sheet.png b/assets_v2/characters/melee_ghost/contact_sheet.png new file mode 100644 index 0000000..f81dd22 Binary files /dev/null and b/assets_v2/characters/melee_ghost/contact_sheet.png differ diff --git a/assets_v2/characters/melee_ghost/frame_map.json b/assets_v2/characters/melee_ghost/frame_map.json new file mode 100644 index 0000000..cc2490c --- /dev/null +++ b/assets_v2/characters/melee_ghost/frame_map.json @@ -0,0 +1,153 @@ +{ + "character": "melee_ghost", + "frame_size": [ + 96, + 96 + ], + "pivot": [ + 48, + 70 + ], + "foot_position": [ + 48, + 90 + ], + "animations": { + "melee_ghost_idle": { + "frames": 6, + "fps": 8.0, + "loop": true, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 70 + ], + "foot_position": [ + 48, + 90 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "hunched sway, ghost fire leak" + }, + "melee_ghost_walk": { + "frames": 8, + "fps": 8.0, + "loop": true, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 70 + ], + "foot_position": [ + 48, + 90 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "lumbering gait" + }, + "melee_ghost_attack": { + "frames": 8, + "fps": 10.0, + "loop": false, + "phases": { + "startup": [ + 0, + 1, + 2, + 3 + ], + "active": [ + 4, + 5 + ], + "recovery": [ + 6, + 7 + ] + }, + "pivot": [ + 48, + 70 + ], + "foot_position": [ + 48, + 90 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "overhead slam" + }, + "melee_ghost_hurt": { + "frames": 4, + "fps": 10.0, + "loop": false, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 70 + ], + "foot_position": [ + 48, + 90 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "recoil" + }, + "melee_ghost_death": { + "frames": 8, + "fps": 8.0, + "loop": false, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 70 + ], + "foot_position": [ + 48, + 90 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "dissolve" + } + } +} \ No newline at end of file diff --git a/assets_v2/characters/melee_ghost/melee_ghost_attack.png b/assets_v2/characters/melee_ghost/melee_ghost_attack.png new file mode 100644 index 0000000..43aadca Binary files /dev/null and b/assets_v2/characters/melee_ghost/melee_ghost_attack.png differ diff --git a/assets_v2/characters/melee_ghost/melee_ghost_death.png b/assets_v2/characters/melee_ghost/melee_ghost_death.png new file mode 100644 index 0000000..821ad56 Binary files /dev/null and b/assets_v2/characters/melee_ghost/melee_ghost_death.png differ diff --git a/assets_v2/characters/melee_ghost/melee_ghost_hurt.png b/assets_v2/characters/melee_ghost/melee_ghost_hurt.png new file mode 100644 index 0000000..c477977 Binary files /dev/null and b/assets_v2/characters/melee_ghost/melee_ghost_hurt.png differ diff --git a/assets_v2/characters/melee_ghost/melee_ghost_idle.png b/assets_v2/characters/melee_ghost/melee_ghost_idle.png new file mode 100644 index 0000000..af8b9d1 Binary files /dev/null and b/assets_v2/characters/melee_ghost/melee_ghost_idle.png differ diff --git a/assets_v2/characters/melee_ghost/melee_ghost_walk.png b/assets_v2/characters/melee_ghost/melee_ghost_walk.png new file mode 100644 index 0000000..2dac8ef Binary files /dev/null and b/assets_v2/characters/melee_ghost/melee_ghost_walk.png differ diff --git a/assets_v2/characters/melee_ghost/metadata.md b/assets_v2/characters/melee_ghost/metadata.md new file mode 100644 index 0000000..3aa7049 --- /dev/null +++ b/assets_v2/characters/melee_ghost/metadata.md @@ -0,0 +1,17 @@ +# melee_ghost — Animation & Spec Sheet (V2) + +- **Frame size:** 96x96 px +- **Pivot (rotation/hitbox anchor):** [48, 70] +- **Foot baseline (y):** 90 + +| Animation | Frames | FPS | Loop | Startup | Active | Recovery | Proj/Charge | +|---|---|---|---|---|---|---|---| +| melee_ghost_idle | 6 | 8.0 | True | - | - | - | - | +| melee_ghost_walk | 8 | 8.0 | True | - | - | - | - | +| melee_ghost_attack | 8 | 10.0 | False | 0,1,2,3 | 4,5 | 6,7 | - | +| melee_ghost_hurt | 4 | 10.0 | False | - | - | - | - | +| melee_ghost_death | 8 | 8.0 | False | - | - | - | - | + +**Legend:** frames are 0-indexed. `Active` = hitbox/projectile window. `projectile_release_frame` = frame the ghost arrow spawns. `charge_ready_frame` = frame a charge/aim state is fully wound (transition out). + +Generated by `assets_v2/tools/gen_metadata.py`. Source: pose comments in `assets_v2/tools/build_*.py`. \ No newline at end of file diff --git a/assets_v2/characters/melee_ghost/palette.png b/assets_v2/characters/melee_ghost/palette.png new file mode 100644 index 0000000..0dc1660 Binary files /dev/null and b/assets_v2/characters/melee_ghost/palette.png differ diff --git a/assets_v2/characters/melee_ghost/silhouette.png b/assets_v2/characters/melee_ghost/silhouette.png new file mode 100644 index 0000000..acb49c7 Binary files /dev/null and b/assets_v2/characters/melee_ghost/silhouette.png differ diff --git a/assets_v2/characters/player/concept_back.png b/assets_v2/characters/player/concept_back.png new file mode 100644 index 0000000..6752086 Binary files /dev/null and b/assets_v2/characters/player/concept_back.png differ diff --git a/assets_v2/characters/player/concept_front.png b/assets_v2/characters/player/concept_front.png new file mode 100644 index 0000000..d582bb1 Binary files /dev/null and b/assets_v2/characters/player/concept_front.png differ diff --git a/assets_v2/characters/player/concept_side.png b/assets_v2/characters/player/concept_side.png new file mode 100644 index 0000000..fbcf41f Binary files /dev/null and b/assets_v2/characters/player/concept_side.png differ diff --git a/assets_v2/characters/player/contact_sheet.png b/assets_v2/characters/player/contact_sheet.png new file mode 100644 index 0000000..40cfd4d Binary files /dev/null and b/assets_v2/characters/player/contact_sheet.png differ diff --git a/assets_v2/characters/player/frame_map.json b/assets_v2/characters/player/frame_map.json new file mode 100644 index 0000000..edb70bc --- /dev/null +++ b/assets_v2/characters/player/frame_map.json @@ -0,0 +1,277 @@ +{ + "character": "player", + "frame_size": [ + 96, + 96 + ], + "pivot": [ + 48, + 66 + ], + "foot_position": [ + 48, + 88 + ], + "animations": { + "player_idle": { + "frames": 8, + "fps": 8.0, + "loop": true, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 66 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "breathing idle" + }, + "player_run": { + "frames": 8, + "fps": 12.0, + "loop": true, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 66 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "forward run, cloth trailing" + }, + "player_jump": { + "frames": 3, + "fps": 10.0, + "loop": false, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 66 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "rise" + }, + "player_fall": { + "frames": 3, + "fps": 10.0, + "loop": false, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 66 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "descent" + }, + "player_light_attack_1": { + "frames": 8, + "fps": 12.0, + "loop": false, + "phases": { + "startup": [ + 0, + 1, + 2, + 3 + ], + "active": [ + 4, + 5 + ], + "recovery": [ + 6, + 7 + ] + }, + "pivot": [ + 48, + 66 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "horizontal sabre slash R->L" + }, + "player_light_attack_2": { + "frames": 8, + "fps": 12.0, + "loop": false, + "phases": { + "startup": [ + 0, + 1, + 2, + 3 + ], + "active": [ + 4, + 5 + ], + "recovery": [ + 6, + 7 + ] + }, + "pivot": [ + 48, + 66 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "reverse diagonal overhead->back" + }, + "player_heavy_attack": { + "frames": 10, + "fps": 10.0, + "loop": false, + "phases": { + "startup": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "active": [ + 6, + 7 + ], + "recovery": [ + 8, + 9 + ] + }, + "pivot": [ + 48, + 66 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "deep coil, late big hit" + }, + "player_hurt": { + "frames": 4, + "fps": 10.0, + "loop": false, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 66 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "recoil" + }, + "player_death": { + "frames": 10, + "fps": 8.0, + "loop": false, + "phases": { + "startup": [], + "active": [], + "recovery": [] + }, + "pivot": [ + 48, + 66 + ], + "foot_position": [ + 48, + 88 + ], + "frame_size": [ + 96, + 96 + ], + "projectile_release_frame": null, + "charge_ready_frame": null, + "note": "stagger and collapse" + } + } +} \ No newline at end of file diff --git a/assets_v2/characters/player/metadata.md b/assets_v2/characters/player/metadata.md new file mode 100644 index 0000000..7faf703 --- /dev/null +++ b/assets_v2/characters/player/metadata.md @@ -0,0 +1,21 @@ +# player — Animation & Spec Sheet (V2) + +- **Frame size:** 96x96 px +- **Pivot (rotation/hitbox anchor):** [48, 66] +- **Foot baseline (y):** 88 + +| Animation | Frames | FPS | Loop | Startup | Active | Recovery | Proj/Charge | +|---|---|---|---|---|---|---|---| +| player_idle | 8 | 8.0 | True | - | - | - | - | +| player_run | 8 | 12.0 | True | - | - | - | - | +| player_jump | 3 | 10.0 | False | - | - | - | - | +| player_fall | 3 | 10.0 | False | - | - | - | - | +| player_light_attack_1 | 8 | 12.0 | False | 0,1,2,3 | 4,5 | 6,7 | - | +| player_light_attack_2 | 8 | 12.0 | False | 0,1,2,3 | 4,5 | 6,7 | - | +| player_heavy_attack | 10 | 10.0 | False | 0,1,2,3,4,5 | 6,7 | 8,9 | - | +| player_hurt | 4 | 10.0 | False | - | - | - | - | +| player_death | 10 | 8.0 | False | - | - | - | - | + +**Legend:** frames are 0-indexed. `Active` = hitbox/projectile window. `projectile_release_frame` = frame the ghost arrow spawns. `charge_ready_frame` = frame a charge/aim state is fully wound (transition out). + +Generated by `assets_v2/tools/gen_metadata.py`. Source: pose comments in `assets_v2/tools/build_*.py`. \ No newline at end of file diff --git a/assets_v2/characters/player/palette.png b/assets_v2/characters/player/palette.png new file mode 100644 index 0000000..1a6a4ae Binary files /dev/null and b/assets_v2/characters/player/palette.png differ diff --git a/assets_v2/characters/player/player_death.png b/assets_v2/characters/player/player_death.png new file mode 100644 index 0000000..0300781 Binary files /dev/null and b/assets_v2/characters/player/player_death.png differ diff --git a/assets_v2/characters/player/player_fall.png b/assets_v2/characters/player/player_fall.png new file mode 100644 index 0000000..484f9e3 Binary files /dev/null and b/assets_v2/characters/player/player_fall.png differ diff --git a/assets_v2/characters/player/player_heavy_attack.png b/assets_v2/characters/player/player_heavy_attack.png new file mode 100644 index 0000000..e832e12 Binary files /dev/null and b/assets_v2/characters/player/player_heavy_attack.png differ diff --git a/assets_v2/characters/player/player_hurt.png b/assets_v2/characters/player/player_hurt.png new file mode 100644 index 0000000..074efe3 Binary files /dev/null and b/assets_v2/characters/player/player_hurt.png differ diff --git a/assets_v2/characters/player/player_idle.png b/assets_v2/characters/player/player_idle.png new file mode 100644 index 0000000..bf6e3f8 Binary files /dev/null and b/assets_v2/characters/player/player_idle.png differ diff --git a/assets_v2/characters/player/player_jump.png b/assets_v2/characters/player/player_jump.png new file mode 100644 index 0000000..48f3d6b Binary files /dev/null and b/assets_v2/characters/player/player_jump.png differ diff --git a/assets_v2/characters/player/player_light_attack_1.png b/assets_v2/characters/player/player_light_attack_1.png new file mode 100644 index 0000000..5c92033 Binary files /dev/null and b/assets_v2/characters/player/player_light_attack_1.png differ diff --git a/assets_v2/characters/player/player_light_attack_2.png b/assets_v2/characters/player/player_light_attack_2.png new file mode 100644 index 0000000..7826b10 Binary files /dev/null and b/assets_v2/characters/player/player_light_attack_2.png differ diff --git a/assets_v2/characters/player/player_run.png b/assets_v2/characters/player/player_run.png new file mode 100644 index 0000000..68c1b9f Binary files /dev/null and b/assets_v2/characters/player/player_run.png differ diff --git a/assets_v2/characters/player/silhouette.png b/assets_v2/characters/player/silhouette.png new file mode 100644 index 0000000..9de5678 Binary files /dev/null and b/assets_v2/characters/player/silhouette.png differ diff --git a/assets_v2/effects/blade_slash.png b/assets_v2/effects/blade_slash.png new file mode 100644 index 0000000..0e6ea76 Binary files /dev/null and b/assets_v2/effects/blade_slash.png differ diff --git a/assets_v2/effects/death_dissolve.png b/assets_v2/effects/death_dissolve.png new file mode 100644 index 0000000..f955e63 Binary files /dev/null and b/assets_v2/effects/death_dissolve.png differ diff --git a/assets_v2/effects/ghost_fire.png b/assets_v2/effects/ghost_fire.png new file mode 100644 index 0000000..68651c5 Binary files /dev/null and b/assets_v2/effects/ghost_fire.png differ diff --git a/assets_v2/effects/hit.png b/assets_v2/effects/hit.png new file mode 100644 index 0000000..55cc62b Binary files /dev/null and b/assets_v2/effects/hit.png differ diff --git a/assets_v2/godot/corpse_beast_frames.tres b/assets_v2/godot/corpse_beast_frames.tres new file mode 100644 index 0000000..e553e9f --- /dev/null +++ b/assets_v2/godot/corpse_beast_frames.tres @@ -0,0 +1,329 @@ +; Generated by assets_v2/tools/gen_metadata.py for the V2 (Steam Alpha) art set. +; Do not hand-edit: re-run the generator instead. +[gd_resource type="SpriteFrames" load_steps=48 format=3] + +[ext_resource type="Texture2D" path="res://assets_v2/characters/corpse_beast/corpse_beast_idle.png" id="1_corpse_beast_idle"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/corpse_beast/corpse_beast_run.png" id="2_corpse_beast_run"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/corpse_beast/corpse_beast_charge_windup.png" id="3_corpse_beast_charge_windup"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/corpse_beast/corpse_beast_charge.png" id="4_corpse_beast_charge"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/corpse_beast/corpse_beast_wall_impact.png" id="5_corpse_beast_wall_impact"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/corpse_beast/corpse_beast_hurt.png" id="6_corpse_beast_hurt"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/corpse_beast/corpse_beast_death.png" id="7_corpse_beast_death"] + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_idle_0"] +atlas = ExtResource("1_corpse_beast_idle") +region = Rect2(0, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_idle_1"] +atlas = ExtResource("1_corpse_beast_idle") +region = Rect2(128, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_idle_2"] +atlas = ExtResource("1_corpse_beast_idle") +region = Rect2(256, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_idle_3"] +atlas = ExtResource("1_corpse_beast_idle") +region = Rect2(384, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_idle_4"] +atlas = ExtResource("1_corpse_beast_idle") +region = Rect2(512, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_idle_5"] +atlas = ExtResource("1_corpse_beast_idle") +region = Rect2(640, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_run_0"] +atlas = ExtResource("2_corpse_beast_run") +region = Rect2(0, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_run_1"] +atlas = ExtResource("2_corpse_beast_run") +region = Rect2(128, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_run_2"] +atlas = ExtResource("2_corpse_beast_run") +region = Rect2(256, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_run_3"] +atlas = ExtResource("2_corpse_beast_run") +region = Rect2(384, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_run_4"] +atlas = ExtResource("2_corpse_beast_run") +region = Rect2(512, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_run_5"] +atlas = ExtResource("2_corpse_beast_run") +region = Rect2(640, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_run_6"] +atlas = ExtResource("2_corpse_beast_run") +region = Rect2(768, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_run_7"] +atlas = ExtResource("2_corpse_beast_run") +region = Rect2(896, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_charge_windup_0"] +atlas = ExtResource("3_corpse_beast_charge_windup") +region = Rect2(0, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_charge_windup_1"] +atlas = ExtResource("3_corpse_beast_charge_windup") +region = Rect2(128, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_charge_windup_2"] +atlas = ExtResource("3_corpse_beast_charge_windup") +region = Rect2(256, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_charge_windup_3"] +atlas = ExtResource("3_corpse_beast_charge_windup") +region = Rect2(384, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_charge_windup_4"] +atlas = ExtResource("3_corpse_beast_charge_windup") +region = Rect2(512, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_charge_windup_5"] +atlas = ExtResource("3_corpse_beast_charge_windup") +region = Rect2(640, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_charge_0"] +atlas = ExtResource("4_corpse_beast_charge") +region = Rect2(0, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_charge_1"] +atlas = ExtResource("4_corpse_beast_charge") +region = Rect2(128, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_charge_2"] +atlas = ExtResource("4_corpse_beast_charge") +region = Rect2(256, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_charge_3"] +atlas = ExtResource("4_corpse_beast_charge") +region = Rect2(384, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_wall_impact_0"] +atlas = ExtResource("5_corpse_beast_wall_impact") +region = Rect2(0, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_wall_impact_1"] +atlas = ExtResource("5_corpse_beast_wall_impact") +region = Rect2(128, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_wall_impact_2"] +atlas = ExtResource("5_corpse_beast_wall_impact") +region = Rect2(256, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_wall_impact_3"] +atlas = ExtResource("5_corpse_beast_wall_impact") +region = Rect2(384, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_hurt_0"] +atlas = ExtResource("6_corpse_beast_hurt") +region = Rect2(0, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_hurt_1"] +atlas = ExtResource("6_corpse_beast_hurt") +region = Rect2(128, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_hurt_2"] +atlas = ExtResource("6_corpse_beast_hurt") +region = Rect2(256, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_hurt_3"] +atlas = ExtResource("6_corpse_beast_hurt") +region = Rect2(384, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_death_0"] +atlas = ExtResource("7_corpse_beast_death") +region = Rect2(0, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_death_1"] +atlas = ExtResource("7_corpse_beast_death") +region = Rect2(128, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_death_2"] +atlas = ExtResource("7_corpse_beast_death") +region = Rect2(256, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_death_3"] +atlas = ExtResource("7_corpse_beast_death") +region = Rect2(384, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_death_4"] +atlas = ExtResource("7_corpse_beast_death") +region = Rect2(512, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_death_5"] +atlas = ExtResource("7_corpse_beast_death") +region = Rect2(640, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_death_6"] +atlas = ExtResource("7_corpse_beast_death") +region = Rect2(768, 0, 128, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_corpse_beast_death_7"] +atlas = ExtResource("7_corpse_beast_death") +region = Rect2(896, 0, 128, 96) + +[resource] +animations = [{ +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_idle_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_idle_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_idle_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_idle_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_idle_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_idle_5") +}], +"loop": true, +"name": &"corpse_beast_idle", +"speed": 5.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_run_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_run_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_run_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_run_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_run_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_run_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_run_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_run_7") +}], +"loop": true, +"name": &"corpse_beast_run", +"speed": 10.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_charge_windup_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_charge_windup_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_charge_windup_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_charge_windup_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_charge_windup_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_charge_windup_5") +}], +"loop": false, +"name": &"corpse_beast_charge_windup", +"speed": 8.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_charge_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_charge_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_charge_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_charge_3") +}], +"loop": false, +"name": &"corpse_beast_charge", +"speed": 12.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_wall_impact_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_wall_impact_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_wall_impact_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_wall_impact_3") +}], +"loop": false, +"name": &"corpse_beast_wall_impact", +"speed": 8.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_hurt_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_hurt_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_hurt_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_hurt_3") +}], +"loop": false, +"name": &"corpse_beast_hurt", +"speed": 10.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_death_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_death_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_death_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_death_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_death_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_death_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_death_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_corpse_beast_death_7") +}], +"loop": false, +"name": &"corpse_beast_death", +"speed": 6.0 +}] diff --git a/assets_v2/godot/gate_warden_frames.tres b/assets_v2/godot/gate_warden_frames.tres new file mode 100644 index 0000000..88d4f0c --- /dev/null +++ b/assets_v2/godot/gate_warden_frames.tres @@ -0,0 +1,421 @@ +; Generated by assets_v2/tools/gen_metadata.py for the V2 (Steam Alpha) art set. +; Do not hand-edit: re-run the generator instead. +[gd_resource type="SpriteFrames" load_steps=61 format=3] + +[ext_resource type="Texture2D" path="res://assets_v2/characters/gate_warden/gate_warden_idle.png" id="1_gate_warden_idle"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/gate_warden/gate_warden_walk.png" id="2_gate_warden_walk"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/gate_warden/gate_warden_attack_1.png" id="3_gate_warden_attack_1"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/gate_warden/gate_warden_attack_2.png" id="4_gate_warden_attack_2"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/gate_warden/gate_warden_hurt.png" id="5_gate_warden_hurt"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/gate_warden/gate_warden_death.png" id="6_gate_warden_death"] + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_idle_0"] +atlas = ExtResource("1_gate_warden_idle") +region = Rect2(0, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_idle_1"] +atlas = ExtResource("1_gate_warden_idle") +region = Rect2(192, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_idle_2"] +atlas = ExtResource("1_gate_warden_idle") +region = Rect2(384, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_idle_3"] +atlas = ExtResource("1_gate_warden_idle") +region = Rect2(576, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_idle_4"] +atlas = ExtResource("1_gate_warden_idle") +region = Rect2(768, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_idle_5"] +atlas = ExtResource("1_gate_warden_idle") +region = Rect2(960, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_idle_6"] +atlas = ExtResource("1_gate_warden_idle") +region = Rect2(1152, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_idle_7"] +atlas = ExtResource("1_gate_warden_idle") +region = Rect2(1344, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_walk_0"] +atlas = ExtResource("2_gate_warden_walk") +region = Rect2(0, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_walk_1"] +atlas = ExtResource("2_gate_warden_walk") +region = Rect2(192, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_walk_2"] +atlas = ExtResource("2_gate_warden_walk") +region = Rect2(384, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_walk_3"] +atlas = ExtResource("2_gate_warden_walk") +region = Rect2(576, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_walk_4"] +atlas = ExtResource("2_gate_warden_walk") +region = Rect2(768, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_walk_5"] +atlas = ExtResource("2_gate_warden_walk") +region = Rect2(960, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_walk_6"] +atlas = ExtResource("2_gate_warden_walk") +region = Rect2(1152, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_walk_7"] +atlas = ExtResource("2_gate_warden_walk") +region = Rect2(1344, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_1_0"] +atlas = ExtResource("3_gate_warden_attack_1") +region = Rect2(0, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_1_1"] +atlas = ExtResource("3_gate_warden_attack_1") +region = Rect2(192, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_1_2"] +atlas = ExtResource("3_gate_warden_attack_1") +region = Rect2(384, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_1_3"] +atlas = ExtResource("3_gate_warden_attack_1") +region = Rect2(576, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_1_4"] +atlas = ExtResource("3_gate_warden_attack_1") +region = Rect2(768, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_1_5"] +atlas = ExtResource("3_gate_warden_attack_1") +region = Rect2(960, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_1_6"] +atlas = ExtResource("3_gate_warden_attack_1") +region = Rect2(1152, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_1_7"] +atlas = ExtResource("3_gate_warden_attack_1") +region = Rect2(1344, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_1_8"] +atlas = ExtResource("3_gate_warden_attack_1") +region = Rect2(1536, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_1_9"] +atlas = ExtResource("3_gate_warden_attack_1") +region = Rect2(1728, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_2_0"] +atlas = ExtResource("4_gate_warden_attack_2") +region = Rect2(0, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_2_1"] +atlas = ExtResource("4_gate_warden_attack_2") +region = Rect2(192, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_2_2"] +atlas = ExtResource("4_gate_warden_attack_2") +region = Rect2(384, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_2_3"] +atlas = ExtResource("4_gate_warden_attack_2") +region = Rect2(576, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_2_4"] +atlas = ExtResource("4_gate_warden_attack_2") +region = Rect2(768, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_2_5"] +atlas = ExtResource("4_gate_warden_attack_2") +region = Rect2(960, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_2_6"] +atlas = ExtResource("4_gate_warden_attack_2") +region = Rect2(1152, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_2_7"] +atlas = ExtResource("4_gate_warden_attack_2") +region = Rect2(1344, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_2_8"] +atlas = ExtResource("4_gate_warden_attack_2") +region = Rect2(1536, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_2_9"] +atlas = ExtResource("4_gate_warden_attack_2") +region = Rect2(1728, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_2_10"] +atlas = ExtResource("4_gate_warden_attack_2") +region = Rect2(1920, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_attack_2_11"] +atlas = ExtResource("4_gate_warden_attack_2") +region = Rect2(2112, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_hurt_0"] +atlas = ExtResource("5_gate_warden_hurt") +region = Rect2(0, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_hurt_1"] +atlas = ExtResource("5_gate_warden_hurt") +region = Rect2(192, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_hurt_2"] +atlas = ExtResource("5_gate_warden_hurt") +region = Rect2(384, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_hurt_3"] +atlas = ExtResource("5_gate_warden_hurt") +region = Rect2(576, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_death_0"] +atlas = ExtResource("6_gate_warden_death") +region = Rect2(0, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_death_1"] +atlas = ExtResource("6_gate_warden_death") +region = Rect2(192, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_death_2"] +atlas = ExtResource("6_gate_warden_death") +region = Rect2(384, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_death_3"] +atlas = ExtResource("6_gate_warden_death") +region = Rect2(576, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_death_4"] +atlas = ExtResource("6_gate_warden_death") +region = Rect2(768, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_death_5"] +atlas = ExtResource("6_gate_warden_death") +region = Rect2(960, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_death_6"] +atlas = ExtResource("6_gate_warden_death") +region = Rect2(1152, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_death_7"] +atlas = ExtResource("6_gate_warden_death") +region = Rect2(1344, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_death_8"] +atlas = ExtResource("6_gate_warden_death") +region = Rect2(1536, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_death_9"] +atlas = ExtResource("6_gate_warden_death") +region = Rect2(1728, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_death_10"] +atlas = ExtResource("6_gate_warden_death") +region = Rect2(1920, 0, 192, 192) + +[sub_resource type="AtlasTexture" id="AtlasTexture_gate_warden_death_11"] +atlas = ExtResource("6_gate_warden_death") +region = Rect2(2112, 0, 192, 192) + +[resource] +animations = [{ +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_idle_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_idle_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_idle_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_idle_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_idle_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_idle_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_idle_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_idle_7") +}], +"loop": true, +"name": &"gate_warden_idle", +"speed": 6.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_walk_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_walk_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_walk_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_walk_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_walk_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_walk_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_walk_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_walk_7") +}], +"loop": true, +"name": &"gate_warden_walk", +"speed": 8.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_1_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_1_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_1_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_1_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_1_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_1_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_1_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_1_7") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_1_8") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_1_9") +}], +"loop": false, +"name": &"gate_warden_attack_1", +"speed": 9.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_2_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_2_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_2_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_2_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_2_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_2_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_2_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_2_7") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_2_8") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_2_9") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_2_10") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_attack_2_11") +}], +"loop": false, +"name": &"gate_warden_attack_2", +"speed": 8.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_hurt_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_hurt_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_hurt_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_hurt_3") +}], +"loop": false, +"name": &"gate_warden_hurt", +"speed": 8.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_death_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_death_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_death_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_death_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_death_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_death_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_death_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_death_7") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_death_8") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_death_9") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_death_10") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_gate_warden_death_11") +}], +"loop": false, +"name": &"gate_warden_death", +"speed": 6.0 +}] diff --git a/assets_v2/godot/ghost_archer_frames.tres b/assets_v2/godot/ghost_archer_frames.tres new file mode 100644 index 0000000..61c9188 --- /dev/null +++ b/assets_v2/godot/ghost_archer_frames.tres @@ -0,0 +1,295 @@ +; Generated by assets_v2/tools/gen_metadata.py for the V2 (Steam Alpha) art set. +; Do not hand-edit: re-run the generator instead. +[gd_resource type="SpriteFrames" load_steps=43 format=3] + +[ext_resource type="Texture2D" path="res://assets_v2/characters/ghost_archer/ghost_archer_idle.png" id="1_ghost_archer_idle"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/ghost_archer/ghost_archer_retreat.png" id="2_ghost_archer_retreat"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/ghost_archer/ghost_archer_aim.png" id="3_ghost_archer_aim"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/ghost_archer/ghost_archer_shoot.png" id="4_ghost_archer_shoot"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/ghost_archer/ghost_archer_hurt.png" id="5_ghost_archer_hurt"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/ghost_archer/ghost_archer_death.png" id="6_ghost_archer_death"] + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_idle_0"] +atlas = ExtResource("1_ghost_archer_idle") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_idle_1"] +atlas = ExtResource("1_ghost_archer_idle") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_idle_2"] +atlas = ExtResource("1_ghost_archer_idle") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_idle_3"] +atlas = ExtResource("1_ghost_archer_idle") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_idle_4"] +atlas = ExtResource("1_ghost_archer_idle") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_idle_5"] +atlas = ExtResource("1_ghost_archer_idle") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_retreat_0"] +atlas = ExtResource("2_ghost_archer_retreat") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_retreat_1"] +atlas = ExtResource("2_ghost_archer_retreat") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_retreat_2"] +atlas = ExtResource("2_ghost_archer_retreat") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_retreat_3"] +atlas = ExtResource("2_ghost_archer_retreat") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_retreat_4"] +atlas = ExtResource("2_ghost_archer_retreat") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_retreat_5"] +atlas = ExtResource("2_ghost_archer_retreat") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_retreat_6"] +atlas = ExtResource("2_ghost_archer_retreat") +region = Rect2(576, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_retreat_7"] +atlas = ExtResource("2_ghost_archer_retreat") +region = Rect2(672, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_aim_0"] +atlas = ExtResource("3_ghost_archer_aim") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_aim_1"] +atlas = ExtResource("3_ghost_archer_aim") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_aim_2"] +atlas = ExtResource("3_ghost_archer_aim") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_aim_3"] +atlas = ExtResource("3_ghost_archer_aim") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_aim_4"] +atlas = ExtResource("3_ghost_archer_aim") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_aim_5"] +atlas = ExtResource("3_ghost_archer_aim") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_shoot_0"] +atlas = ExtResource("4_ghost_archer_shoot") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_shoot_1"] +atlas = ExtResource("4_ghost_archer_shoot") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_shoot_2"] +atlas = ExtResource("4_ghost_archer_shoot") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_shoot_3"] +atlas = ExtResource("4_ghost_archer_shoot") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_hurt_0"] +atlas = ExtResource("5_ghost_archer_hurt") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_hurt_1"] +atlas = ExtResource("5_ghost_archer_hurt") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_hurt_2"] +atlas = ExtResource("5_ghost_archer_hurt") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_hurt_3"] +atlas = ExtResource("5_ghost_archer_hurt") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_death_0"] +atlas = ExtResource("6_ghost_archer_death") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_death_1"] +atlas = ExtResource("6_ghost_archer_death") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_death_2"] +atlas = ExtResource("6_ghost_archer_death") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_death_3"] +atlas = ExtResource("6_ghost_archer_death") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_death_4"] +atlas = ExtResource("6_ghost_archer_death") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_death_5"] +atlas = ExtResource("6_ghost_archer_death") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_death_6"] +atlas = ExtResource("6_ghost_archer_death") +region = Rect2(576, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_ghost_archer_death_7"] +atlas = ExtResource("6_ghost_archer_death") +region = Rect2(672, 0, 96, 96) + +[resource] +animations = [{ +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_idle_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_idle_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_idle_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_idle_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_idle_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_idle_5") +}], +"loop": true, +"name": &"ghost_archer_idle", +"speed": 8.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_retreat_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_retreat_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_retreat_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_retreat_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_retreat_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_retreat_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_retreat_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_retreat_7") +}], +"loop": true, +"name": &"ghost_archer_retreat", +"speed": 10.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_aim_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_aim_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_aim_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_aim_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_aim_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_aim_5") +}], +"loop": false, +"name": &"ghost_archer_aim", +"speed": 8.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_shoot_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_shoot_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_shoot_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_shoot_3") +}], +"loop": false, +"name": &"ghost_archer_shoot", +"speed": 14.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_hurt_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_hurt_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_hurt_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_hurt_3") +}], +"loop": false, +"name": &"ghost_archer_hurt", +"speed": 10.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_death_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_death_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_death_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_death_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_death_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_death_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_death_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_ghost_archer_death_7") +}], +"loop": false, +"name": &"ghost_archer_death", +"speed": 8.0 +}] diff --git a/assets_v2/godot/melee_ghost_frames.tres b/assets_v2/godot/melee_ghost_frames.tres new file mode 100644 index 0000000..8bf3c74 --- /dev/null +++ b/assets_v2/godot/melee_ghost_frames.tres @@ -0,0 +1,275 @@ +; Generated by assets_v2/tools/gen_metadata.py for the V2 (Steam Alpha) art set. +; Do not hand-edit: re-run the generator instead. +[gd_resource type="SpriteFrames" load_steps=40 format=3] + +[ext_resource type="Texture2D" path="res://assets_v2/characters/melee_ghost/melee_ghost_idle.png" id="1_melee_ghost_idle"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/melee_ghost/melee_ghost_walk.png" id="2_melee_ghost_walk"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/melee_ghost/melee_ghost_attack.png" id="3_melee_ghost_attack"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/melee_ghost/melee_ghost_hurt.png" id="4_melee_ghost_hurt"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/melee_ghost/melee_ghost_death.png" id="5_melee_ghost_death"] + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_idle_0"] +atlas = ExtResource("1_melee_ghost_idle") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_idle_1"] +atlas = ExtResource("1_melee_ghost_idle") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_idle_2"] +atlas = ExtResource("1_melee_ghost_idle") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_idle_3"] +atlas = ExtResource("1_melee_ghost_idle") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_idle_4"] +atlas = ExtResource("1_melee_ghost_idle") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_idle_5"] +atlas = ExtResource("1_melee_ghost_idle") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_walk_0"] +atlas = ExtResource("2_melee_ghost_walk") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_walk_1"] +atlas = ExtResource("2_melee_ghost_walk") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_walk_2"] +atlas = ExtResource("2_melee_ghost_walk") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_walk_3"] +atlas = ExtResource("2_melee_ghost_walk") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_walk_4"] +atlas = ExtResource("2_melee_ghost_walk") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_walk_5"] +atlas = ExtResource("2_melee_ghost_walk") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_walk_6"] +atlas = ExtResource("2_melee_ghost_walk") +region = Rect2(576, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_walk_7"] +atlas = ExtResource("2_melee_ghost_walk") +region = Rect2(672, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_attack_0"] +atlas = ExtResource("3_melee_ghost_attack") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_attack_1"] +atlas = ExtResource("3_melee_ghost_attack") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_attack_2"] +atlas = ExtResource("3_melee_ghost_attack") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_attack_3"] +atlas = ExtResource("3_melee_ghost_attack") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_attack_4"] +atlas = ExtResource("3_melee_ghost_attack") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_attack_5"] +atlas = ExtResource("3_melee_ghost_attack") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_attack_6"] +atlas = ExtResource("3_melee_ghost_attack") +region = Rect2(576, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_attack_7"] +atlas = ExtResource("3_melee_ghost_attack") +region = Rect2(672, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_hurt_0"] +atlas = ExtResource("4_melee_ghost_hurt") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_hurt_1"] +atlas = ExtResource("4_melee_ghost_hurt") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_hurt_2"] +atlas = ExtResource("4_melee_ghost_hurt") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_hurt_3"] +atlas = ExtResource("4_melee_ghost_hurt") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_death_0"] +atlas = ExtResource("5_melee_ghost_death") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_death_1"] +atlas = ExtResource("5_melee_ghost_death") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_death_2"] +atlas = ExtResource("5_melee_ghost_death") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_death_3"] +atlas = ExtResource("5_melee_ghost_death") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_death_4"] +atlas = ExtResource("5_melee_ghost_death") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_death_5"] +atlas = ExtResource("5_melee_ghost_death") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_death_6"] +atlas = ExtResource("5_melee_ghost_death") +region = Rect2(576, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_melee_ghost_death_7"] +atlas = ExtResource("5_melee_ghost_death") +region = Rect2(672, 0, 96, 96) + +[resource] +animations = [{ +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_idle_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_idle_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_idle_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_idle_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_idle_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_idle_5") +}], +"loop": true, +"name": &"melee_ghost_idle", +"speed": 8.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_walk_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_walk_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_walk_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_walk_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_walk_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_walk_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_walk_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_walk_7") +}], +"loop": true, +"name": &"melee_ghost_walk", +"speed": 8.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_attack_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_attack_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_attack_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_attack_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_attack_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_attack_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_attack_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_attack_7") +}], +"loop": false, +"name": &"melee_ghost_attack", +"speed": 10.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_hurt_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_hurt_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_hurt_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_hurt_3") +}], +"loop": false, +"name": &"melee_ghost_hurt", +"speed": 10.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_death_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_death_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_death_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_death_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_death_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_death_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_death_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_melee_ghost_death_7") +}], +"loop": false, +"name": &"melee_ghost_death", +"speed": 8.0 +}] diff --git a/assets_v2/godot/player_frames.tres b/assets_v2/godot/player_frames.tres new file mode 100644 index 0000000..62a6e46 --- /dev/null +++ b/assets_v2/godot/player_frames.tres @@ -0,0 +1,495 @@ +; Generated by assets_v2/tools/gen_metadata.py for the V2 (Steam Alpha) art set. +; Do not hand-edit: re-run the generator instead. +[gd_resource type="SpriteFrames" load_steps=72 format=3] + +[ext_resource type="Texture2D" path="res://assets_v2/characters/player/player_idle.png" id="1_player_idle"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/player/player_run.png" id="2_player_run"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/player/player_jump.png" id="3_player_jump"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/player/player_fall.png" id="4_player_fall"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/player/player_light_attack_1.png" id="5_player_light_attack_1"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/player/player_light_attack_2.png" id="6_player_light_attack_2"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/player/player_heavy_attack.png" id="7_player_heavy_attack"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/player/player_hurt.png" id="8_player_hurt"] +[ext_resource type="Texture2D" path="res://assets_v2/characters/player/player_death.png" id="9_player_death"] + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_idle_0"] +atlas = ExtResource("1_player_idle") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_idle_1"] +atlas = ExtResource("1_player_idle") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_idle_2"] +atlas = ExtResource("1_player_idle") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_idle_3"] +atlas = ExtResource("1_player_idle") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_idle_4"] +atlas = ExtResource("1_player_idle") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_idle_5"] +atlas = ExtResource("1_player_idle") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_idle_6"] +atlas = ExtResource("1_player_idle") +region = Rect2(576, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_idle_7"] +atlas = ExtResource("1_player_idle") +region = Rect2(672, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_run_0"] +atlas = ExtResource("2_player_run") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_run_1"] +atlas = ExtResource("2_player_run") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_run_2"] +atlas = ExtResource("2_player_run") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_run_3"] +atlas = ExtResource("2_player_run") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_run_4"] +atlas = ExtResource("2_player_run") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_run_5"] +atlas = ExtResource("2_player_run") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_run_6"] +atlas = ExtResource("2_player_run") +region = Rect2(576, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_run_7"] +atlas = ExtResource("2_player_run") +region = Rect2(672, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_jump_0"] +atlas = ExtResource("3_player_jump") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_jump_1"] +atlas = ExtResource("3_player_jump") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_jump_2"] +atlas = ExtResource("3_player_jump") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_fall_0"] +atlas = ExtResource("4_player_fall") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_fall_1"] +atlas = ExtResource("4_player_fall") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_fall_2"] +atlas = ExtResource("4_player_fall") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_1_0"] +atlas = ExtResource("5_player_light_attack_1") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_1_1"] +atlas = ExtResource("5_player_light_attack_1") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_1_2"] +atlas = ExtResource("5_player_light_attack_1") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_1_3"] +atlas = ExtResource("5_player_light_attack_1") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_1_4"] +atlas = ExtResource("5_player_light_attack_1") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_1_5"] +atlas = ExtResource("5_player_light_attack_1") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_1_6"] +atlas = ExtResource("5_player_light_attack_1") +region = Rect2(576, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_1_7"] +atlas = ExtResource("5_player_light_attack_1") +region = Rect2(672, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_2_0"] +atlas = ExtResource("6_player_light_attack_2") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_2_1"] +atlas = ExtResource("6_player_light_attack_2") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_2_2"] +atlas = ExtResource("6_player_light_attack_2") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_2_3"] +atlas = ExtResource("6_player_light_attack_2") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_2_4"] +atlas = ExtResource("6_player_light_attack_2") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_2_5"] +atlas = ExtResource("6_player_light_attack_2") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_2_6"] +atlas = ExtResource("6_player_light_attack_2") +region = Rect2(576, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_light_attack_2_7"] +atlas = ExtResource("6_player_light_attack_2") +region = Rect2(672, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_heavy_attack_0"] +atlas = ExtResource("7_player_heavy_attack") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_heavy_attack_1"] +atlas = ExtResource("7_player_heavy_attack") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_heavy_attack_2"] +atlas = ExtResource("7_player_heavy_attack") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_heavy_attack_3"] +atlas = ExtResource("7_player_heavy_attack") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_heavy_attack_4"] +atlas = ExtResource("7_player_heavy_attack") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_heavy_attack_5"] +atlas = ExtResource("7_player_heavy_attack") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_heavy_attack_6"] +atlas = ExtResource("7_player_heavy_attack") +region = Rect2(576, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_heavy_attack_7"] +atlas = ExtResource("7_player_heavy_attack") +region = Rect2(672, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_heavy_attack_8"] +atlas = ExtResource("7_player_heavy_attack") +region = Rect2(768, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_heavy_attack_9"] +atlas = ExtResource("7_player_heavy_attack") +region = Rect2(864, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_hurt_0"] +atlas = ExtResource("8_player_hurt") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_hurt_1"] +atlas = ExtResource("8_player_hurt") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_hurt_2"] +atlas = ExtResource("8_player_hurt") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_hurt_3"] +atlas = ExtResource("8_player_hurt") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_death_0"] +atlas = ExtResource("9_player_death") +region = Rect2(0, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_death_1"] +atlas = ExtResource("9_player_death") +region = Rect2(96, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_death_2"] +atlas = ExtResource("9_player_death") +region = Rect2(192, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_death_3"] +atlas = ExtResource("9_player_death") +region = Rect2(288, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_death_4"] +atlas = ExtResource("9_player_death") +region = Rect2(384, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_death_5"] +atlas = ExtResource("9_player_death") +region = Rect2(480, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_death_6"] +atlas = ExtResource("9_player_death") +region = Rect2(576, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_death_7"] +atlas = ExtResource("9_player_death") +region = Rect2(672, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_death_8"] +atlas = ExtResource("9_player_death") +region = Rect2(768, 0, 96, 96) + +[sub_resource type="AtlasTexture" id="AtlasTexture_player_death_9"] +atlas = ExtResource("9_player_death") +region = Rect2(864, 0, 96, 96) + +[resource] +animations = [{ +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_idle_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_idle_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_idle_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_idle_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_idle_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_idle_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_idle_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_idle_7") +}], +"loop": true, +"name": &"player_idle", +"speed": 8.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_run_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_run_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_run_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_run_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_run_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_run_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_run_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_run_7") +}], +"loop": true, +"name": &"player_run", +"speed": 12.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_jump_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_jump_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_jump_2") +}], +"loop": false, +"name": &"player_jump", +"speed": 10.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_fall_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_fall_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_fall_2") +}], +"loop": false, +"name": &"player_fall", +"speed": 10.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_1_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_1_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_1_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_1_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_1_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_1_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_1_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_1_7") +}], +"loop": false, +"name": &"player_light_attack_1", +"speed": 12.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_2_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_2_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_2_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_2_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_2_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_2_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_2_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_light_attack_2_7") +}], +"loop": false, +"name": &"player_light_attack_2", +"speed": 12.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_heavy_attack_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_heavy_attack_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_heavy_attack_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_heavy_attack_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_heavy_attack_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_heavy_attack_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_heavy_attack_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_heavy_attack_7") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_heavy_attack_8") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_heavy_attack_9") +}], +"loop": false, +"name": &"player_heavy_attack", +"speed": 10.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_hurt_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_hurt_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_hurt_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_hurt_3") +}], +"loop": false, +"name": &"player_hurt", +"speed": 10.0 +}, { +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_death_0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_death_1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_death_2") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_death_3") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_death_4") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_death_5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_death_6") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_death_7") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_death_8") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_player_death_9") +}], +"loop": false, +"name": &"player_death", +"speed": 8.0 +}] diff --git a/assets_v2/icons/icon_coin.png b/assets_v2/icons/icon_coin.png new file mode 100644 index 0000000..a9522c3 Binary files /dev/null and b/assets_v2/icons/icon_coin.png differ diff --git a/assets_v2/icons/icon_flag.png b/assets_v2/icons/icon_flag.png new file mode 100644 index 0000000..6204770 Binary files /dev/null and b/assets_v2/icons/icon_flag.png differ diff --git a/assets_v2/icons/icon_ghostfire.png b/assets_v2/icons/icon_ghostfire.png new file mode 100644 index 0000000..b05acd6 Binary files /dev/null and b/assets_v2/icons/icon_ghostfire.png differ diff --git a/assets_v2/icons/icon_gourd.png b/assets_v2/icons/icon_gourd.png new file mode 100644 index 0000000..a4f8051 Binary files /dev/null and b/assets_v2/icons/icon_gourd.png differ diff --git a/assets_v2/icons/icon_obsession.png b/assets_v2/icons/icon_obsession.png new file mode 100644 index 0000000..77193d7 Binary files /dev/null and b/assets_v2/icons/icon_obsession.png differ diff --git a/assets_v2/icons/icon_qi.png b/assets_v2/icons/icon_qi.png new file mode 100644 index 0000000..9c4773d Binary files /dev/null and b/assets_v2/icons/icon_qi.png differ diff --git a/assets_v2/icons/icon_songdao.png b/assets_v2/icons/icon_songdao.png new file mode 100644 index 0000000..a48e5e4 Binary files /dev/null and b/assets_v2/icons/icon_songdao.png differ diff --git a/assets_v2/icons/icon_spear.png b/assets_v2/icons/icon_spear.png new file mode 100644 index 0000000..31bbee0 Binary files /dev/null and b/assets_v2/icons/icon_spear.png differ diff --git a/assets_v2/icons/icon_talisman.png b/assets_v2/icons/icon_talisman.png new file mode 100644 index 0000000..ddec217 Binary files /dev/null and b/assets_v2/icons/icon_talisman.png differ diff --git a/assets_v2/icons/icon_yang.png b/assets_v2/icons/icon_yang.png new file mode 100644 index 0000000..37f068f Binary files /dev/null and b/assets_v2/icons/icon_yang.png differ diff --git a/assets_v2/projectiles/ghost_fire_arrow.png b/assets_v2/projectiles/ghost_fire_arrow.png new file mode 100644 index 0000000..1f0cef8 Binary files /dev/null and b/assets_v2/projectiles/ghost_fire_arrow.png differ diff --git a/assets_v2/review/ART_V2_REVIEW_REPORT.md b/assets_v2/review/ART_V2_REVIEW_REPORT.md new file mode 100644 index 0000000..ff78c49 --- /dev/null +++ b/assets_v2/review/ART_V2_REVIEW_REPORT.md @@ -0,0 +1,39 @@ +# ART V2 — Review Report + +**Result:** WARNING + +## Automated checks +- **[PASS]** 1.size — all 37 sheets match frames*fw x fh +- **[WARN]** 2.frame-diff — low-motion pairs: player_run f3->f4 (114/9216 ~1.2%); melee_ghost_attack f0->f1 (109/9216 ~1.2%); corpse_beast_charge_windup f0->f1 (129/12288 ~1.0%); corpse_beast_charge_windup f1->f2 (170/12288 ~1.4%); corpse_beast_charge_windup f3->f4 (147/12288 ~1.2%); corpse_beast_charge_windup f4->f5 (168/12288 ~1.4%); corpse_beast_wall_impact f2->f3 (163/12288 ~1.3%); corpse_beast_hurt f2->f3 (167/12288 ~1.4%) +- **[PASS]** 3.alpha — transparent background on all sheets +- **[WARN]** 4.silhouette — similar shape pair(s): player~melee_ghost=0.967; player~gate_warden=0.934; melee_ghost~gate_warden=0.921 (humanoids share a body plan; distinguished in-game by colour/weapon/lean) +- **[PASS]** 5.telegraph — all strikes have non-empty active window +- **[PASS]** 6.foot — foot baseline stable on idle/walk/run (<8% frame height) +- **[WARN]** 7.weapon — gate_warden_attack_2 motion range only 9% +- **[PASS]** 8.scale-preview — review/gameplay_scale_preview.png (640x360) +- **[PASS]** 9.gif — anim_player.gif (38 frames) +- **[PASS]** 9.gif — anim_enemies_melee.gif (22 frames) +- **[PASS]** 9.gif — anim_enemies_archer.gif (16 frames) +- **[PASS]** 9.gif — anim_enemies_beast.gif (18 frames) +- **[PASS]** 9.gif — anim_boss.gif (38 frames) + +## Silhouette similarity matrix (lower = more distinct) +- player ↔ melee_ghost: 0.967 ⚠ similar +- player ↔ gate_warden: 0.934 ⚠ similar +- melee_ghost ↔ gate_warden: 0.921 ⚠ similar +- player ↔ ghost_archer: 0.841 ⚠ similar +- melee_ghost ↔ ghost_archer: 0.826 ⚠ similar +- ghost_archer ↔ gate_warden: 0.795 +- corpse_beast ↔ gate_warden: -0.226 +- player ↔ corpse_beast: -0.269 +- melee_ghost ↔ corpse_beast: -0.277 +- ghost_archer ↔ corpse_beast: -0.399 + +## Deliverables +- `review/character_scale_comparison.png` — size ladder +- `review/silhouette_comparison.png` — black silhouette distinction +- `review/gameplay_scale_preview.png` — 640×360 in-engine scale mock +- `review/anim_player.gif`, `anim_enemies_*.gif`, `anim_boss.gif` — motion review +- `assets_v2/characters//frame_map.json` + `metadata.md` — timing +- `assets_v2/godot/_frames.tres` — ready SpriteFrames +- `assets_v2/GODOT_IMPORT_GUIDE.md` — wiring instructions \ No newline at end of file diff --git a/assets_v2/review/anim_boss.gif b/assets_v2/review/anim_boss.gif new file mode 100644 index 0000000..4bfff0d Binary files /dev/null and b/assets_v2/review/anim_boss.gif differ diff --git a/assets_v2/review/anim_enemies_archer.gif b/assets_v2/review/anim_enemies_archer.gif new file mode 100644 index 0000000..fc93e09 Binary files /dev/null and b/assets_v2/review/anim_enemies_archer.gif differ diff --git a/assets_v2/review/anim_enemies_beast.gif b/assets_v2/review/anim_enemies_beast.gif new file mode 100644 index 0000000..39bd1a1 Binary files /dev/null and b/assets_v2/review/anim_enemies_beast.gif differ diff --git a/assets_v2/review/anim_enemies_melee.gif b/assets_v2/review/anim_enemies_melee.gif new file mode 100644 index 0000000..fad2fca Binary files /dev/null and b/assets_v2/review/anim_enemies_melee.gif differ diff --git a/assets_v2/review/anim_player.gif b/assets_v2/review/anim_player.gif new file mode 100644 index 0000000..e305de3 Binary files /dev/null and b/assets_v2/review/anim_player.gif differ diff --git a/assets_v2/review/character_scale_comparison.png b/assets_v2/review/character_scale_comparison.png new file mode 100644 index 0000000..3f118ac Binary files /dev/null and b/assets_v2/review/character_scale_comparison.png differ diff --git a/assets_v2/review/gameplay_scale_preview.png b/assets_v2/review/gameplay_scale_preview.png new file mode 100644 index 0000000..cf13ae9 Binary files /dev/null and b/assets_v2/review/gameplay_scale_preview.png differ diff --git a/assets_v2/review/quality_report.json b/assets_v2/review/quality_report.json new file mode 100644 index 0000000..2ec565a --- /dev/null +++ b/assets_v2/review/quality_report.json @@ -0,0 +1,122 @@ +{ + "checks": [ + { + "name": "1.size", + "status": "PASS", + "detail": "all 37 sheets match frames*fw x fh" + }, + { + "name": "2.frame-diff", + "status": "WARN", + "detail": "low-motion pairs: player_run f3->f4 (114/9216 ~1.2%); melee_ghost_attack f0->f1 (109/9216 ~1.2%); corpse_beast_charge_windup f0->f1 (129/12288 ~1.0%); corpse_beast_charge_windup f1->f2 (170/12288 ~1.4%); corpse_beast_charge_windup f3->f4 (147/12288 ~1.2%); corpse_beast_charge_windup f4->f5 (168/12288 ~1.4%); corpse_beast_wall_impact f2->f3 (163/12288 ~1.3%); corpse_beast_hurt f2->f3 (167/12288 ~1.4%)" + }, + { + "name": "3.alpha", + "status": "PASS", + "detail": "transparent background on all sheets" + }, + { + "name": "4.silhouette", + "status": "WARN", + "detail": "similar shape pair(s): player~melee_ghost=0.967; player~gate_warden=0.934; melee_ghost~gate_warden=0.921 (humanoids share a body plan; distinguished in-game by colour/weapon/lean)" + }, + { + "name": "5.telegraph", + "status": "PASS", + "detail": "all strikes have non-empty active window" + }, + { + "name": "6.foot", + "status": "PASS", + "detail": "foot baseline stable on idle/walk/run (<8% frame height)" + }, + { + "name": "7.weapon", + "status": "WARN", + "detail": "gate_warden_attack_2 motion range only 9%" + }, + { + "name": "8.scale-preview", + "status": "PASS", + "detail": "review/gameplay_scale_preview.png (640x360)" + }, + { + "name": "9.gif", + "status": "PASS", + "detail": "anim_player.gif (38 frames)" + }, + { + "name": "9.gif", + "status": "PASS", + "detail": "anim_enemies_melee.gif (22 frames)" + }, + { + "name": "9.gif", + "status": "PASS", + "detail": "anim_enemies_archer.gif (16 frames)" + }, + { + "name": "9.gif", + "status": "PASS", + "detail": "anim_enemies_beast.gif (18 frames)" + }, + { + "name": "9.gif", + "status": "PASS", + "detail": "anim_boss.gif (38 frames)" + } + ], + "silhouette_similarity": [ + { + "a": "player", + "b": "melee_ghost", + "similarity": 0.967 + }, + { + "a": "player", + "b": "ghost_archer", + "similarity": 0.841 + }, + { + "a": "player", + "b": "corpse_beast", + "similarity": -0.269 + }, + { + "a": "player", + "b": "gate_warden", + "similarity": 0.934 + }, + { + "a": "melee_ghost", + "b": "ghost_archer", + "similarity": 0.826 + }, + { + "a": "melee_ghost", + "b": "corpse_beast", + "similarity": -0.277 + }, + { + "a": "melee_ghost", + "b": "gate_warden", + "similarity": 0.921 + }, + { + "a": "ghost_archer", + "b": "corpse_beast", + "similarity": -0.399 + }, + { + "a": "ghost_archer", + "b": "gate_warden", + "similarity": 0.795 + }, + { + "a": "corpse_beast", + "b": "gate_warden", + "similarity": -0.226 + } + ], + "result": "WARN" +} \ No newline at end of file diff --git a/assets_v2/review/silhouette_comparison.png b/assets_v2/review/silhouette_comparison.png new file mode 100644 index 0000000..813f7ae Binary files /dev/null and b/assets_v2/review/silhouette_comparison.png differ diff --git a/assets_v2/tools/.gdignore b/assets_v2/tools/.gdignore new file mode 100644 index 0000000..5f3a78c --- /dev/null +++ b/assets_v2/tools/.gdignore @@ -0,0 +1 @@ +# Python build scripts — not Godot resources diff --git a/assets_v2/tools/beast.py b/assets_v2/tools/beast.py new file mode 100644 index 0000000..2d8204b --- /dev/null +++ b/assets_v2/tools/beast.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Corpse Beast (冲锋尸兽) renderer. + +Low, wide, quadrupedal (or near-quadrupedal) charge enemy. Distinct from all +humanoid characters: horizontal silhouette, four limbs, arched back, head +thrust forward. Glowing red eyes/chest during charge wind-up. +""" + +from __future__ import annotations +import math +from pixel_engine import capsule, fill_circle, fill_poly, _r + + +def beast_config(**over): + cfg = { + "fw": 128, "fh": 96, + "cx": 64, + "body_y": 58, + "body_len": 58, + "body_h": 18, + "back_arch": 8, + "head_x": 34, "head_y": 38, + "head_len": 22, "head_h": 12, + "leg_f_len": 20, "leg_b_len": 18, + "leg_w": 5, + "palette": {}, + } + cfg.update(over) + return cfg + + +def _seg(joint, length, angle_deg): + a = math.radians(angle_deg) + return (joint[0] + length * math.sin(a), joint[1] + length * math.cos(a)) + + +def render_beast(draw, cfg, pose, fw, fh): + pal = cfg["palette"] + out = pal["outline"] + body_y = cfg["body_y"] + pose.get("bob", 0) + arch = cfg["back_arch"] + pose.get("arch", 0) + body_len = cfg["body_len"] + pose.get("stretch", 0) + cx = cfg["cx"] + pose.get("dx", 0) + # body as arched spine: back -> mid -> shoulder + back = (cx - body_len * 0.45, body_y - arch * 0.6) + mid = (cx, body_y - arch) + shoulder = (cx + body_len * 0.4, body_y - arch * 0.4) + hip = (cx - body_len * 0.45, body_y) + # body fill (tapered polygon) + bh = cfg["body_h"] + fill_poly(draw, [ + (back[0], back[1] - bh * 0.2), + (mid[0], mid[1] - bh), + (shoulder[0], shoulder[1] - bh * 0.3), + (shoulder[0], shoulder[1] + bh * 0.6), + (mid[0], mid[1] + bh * 0.4), + (back[0], back[1] + bh * 0.7), + ], pal["body"]) + # spine highlight + draw.line([_r(back), _r(mid), _r(shoulder)], fill=pal["body_lt"], width=2) + # outline + pts = [(back[0], back[1] - bh * 0.2), (mid[0], mid[1] - bh), (shoulder[0], shoulder[1] - bh * 0.3), + (shoulder[0], shoulder[1] + bh * 0.6), (mid[0], mid[1] + bh * 0.4), (back[0], back[1] + bh * 0.7)] + draw.line([_r(p) for p in pts] + [_r(pts[0])], fill=out, width=1) + + # leg attachment points + fl_hip = (shoulder[0] - 2, shoulder[1] + bh * 0.3) + fr_hip = (shoulder[0] + 2, shoulder[1] + bh * 0.3) + bl_hip = (back[0] + 4, back[1] + bh * 0.3) + br_hip = (back[0] - 2, back[1] + bh * 0.3) + + def draw_leg(hip_pt, knee_a, ankle_a, side): + w = cfg["leg_w"] * (0.85 if side == "far" else 1.0) + knee = _seg(hip_pt, cfg["leg_f_len"] * 0.55, knee_a) + foot = _seg(knee, cfg["leg_f_len"] * 0.55, ankle_a) + base = pal["leg_dk"] if side == "far" else pal["leg"] + capsule(draw, hip_pt, knee, w, base, outline=out, light=pal["leg_lt"]) + capsule(draw, knee, foot, max(2, w - 1), base, outline=out, light=pal["leg_lt"]) + # claw + claw = (foot[0] + 4, foot[1] + 2) + fill_poly(draw, [foot, claw, (foot[0] + 1, foot[1] - 2)], pal["claw"]) + return foot + + # back far leg + draw_leg(br_hip, pose.get("leg_br_knee", 95), pose.get("leg_br_ankle", 80), "far") + # front far leg + draw_leg(fr_hip, pose.get("leg_fr_knee", 85), pose.get("leg_fr_ankle", 100), "far") + + # body again for overlap? no, keep simple + + # head / neck + neck = (shoulder[0] + 8, shoulder[1] - 6) + head_c = (neck[0] + cfg["head_x"] * 0.5, neck[1] - 6 + pose.get("head_y", 0)) + # neck + capsule(draw, shoulder, neck, 5, pal["body_dk"], outline=out, light=pal["body_lt"]) + # head (elongated) + fill_poly(draw, [ + (head_c[0] - cfg["head_len"] * 0.5, head_c[1] - cfg["head_h"]), + (head_c[0] + cfg["head_len"] * 0.5, head_c[1] - cfg["head_h"] * 0.6), + (head_c[0] + cfg["head_len"] * 0.5, head_c[1] + cfg["head_h"] * 0.5), + (head_c[0] - cfg["head_len"] * 0.3, head_c[1] + cfg["head_h"]), + ], pal["body"]) + draw.line([ + _r((head_c[0] - cfg["head_len"] * 0.5, head_c[1] - cfg["head_h"])), + _r((head_c[0] + cfg["head_len"] * 0.5, head_c[1] - cfg["head_h"] * 0.6)), + _r((head_c[0] + cfg["head_len"] * 0.5, head_c[1] + cfg["head_h"] * 0.5)), + _r((head_c[0] - cfg["head_len"] * 0.3, head_c[1] + cfg["head_h"])), + _r((head_c[0] - cfg["head_len"] * 0.5, head_c[1] - cfg["head_h"])), + ], fill=out, width=1) + # eye(s) + glow = pose.get("charge_glow", 0) + eye_c = (head_c[0] + cfg["head_len"] * 0.25, head_c[1] - 2) + fill_circle(draw, eye_c, 3 + glow * 2, (140, 20, 20)) + fill_circle(draw, eye_c, 2 + glow, (220, 60, 40)) + # chest glow when charging + if glow > 0.3: + fill_circle(draw, (shoulder[0], shoulder[1] - 2), 4 + glow * 3, (140, 20, 20)) + fill_circle(draw, (shoulder[0], shoulder[1] - 2), 2 + glow, (220, 60, 40)) + + # front near leg + draw_leg(fl_hip, pose.get("leg_fl_knee", 85), pose.get("leg_fl_ankle", 100), "near") + # back near leg + draw_leg(bl_hip, pose.get("leg_bl_knee", 95), pose.get("leg_bl_ankle", 80), "near") + + # tail + tail_base = (back[0] - 6, back[1]) + tail_tip = (back[0] - 22, back[1] - 4 + pose.get("tail", 0)) + capsule(draw, tail_base, tail_tip, 3, pal["body_dk"], outline=out) diff --git a/assets_v2/tools/build_boss.py b/assets_v2/tools/build_boss.py new file mode 100644 index 0000000..8ff8ccf --- /dev/null +++ b/assets_v2/tools/build_boss.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Gate Warden Boss (镇关鬼将). + +Massive gate guardian, 2.5-3x player height. Upper body extremely wide, +asymmetric shoulders (one huge pauldron), short stable legs, hidden head in +empty helm with strong ghost fire. Great blade length >= 70% of body height. +""" + +from __future__ import annotations +from pathlib import Path +from pixel_engine import bake_sheet +from humanoid import humanoid_config, render_humanoid + +ROOT = Path(__file__).resolve().parent.parent.parent +OUT = ROOT / "assets_v2" / "characters" / "gate_warden" + +BOSS_CFG = humanoid_config( + fw=192, fh=192, cx=96, hip_y=128, torso_len=54, neck_len=10, head_r=20, + upper_arm=36, lower_arm=30, arm_w=8, thigh=34, shin=30, leg_w=10, + scale=1.0, lean=4, stance=10, style="boss", + torso_hw=22, torso_hw_hip=18, + weapon_len=112, ghost_fire=True, pauldrons=True, asymmetric_pauldrons=True, + palette={ + "skin": (82, 78, 88), "skin_lt": (105, 100, 112), "skin_sh": (55, 52, 62), + "armor": (58, 50, 66), "armor_lt": (88, 78, 98), "armor_dk": (35, 30, 42), + "cloth": (105, 32, 32), "cloth_lt": (145, 48, 44), "cloth_dk": (65, 18, 18), + "gold": (130, 108, 58), "copper": (115, 78, 52), + "blade": (82, 80, 92), "blade_lt": (132, 130, 148), "blade_dk": (48, 46, 56), + "grip": (55, 45, 38), "guard": (125, 105, 62), + "outline": (14, 12, 18), + }, +) + +A = lambda sa, ea: (sa, ea) + +# Idle: colossal breathing, >=3px bob steps (large canvas), ghost-fire flicker. +IDLE = [ + {"bob": 0, "arm_front": A(88, 98), "arm_back": A(92, 102), "leg_front": A(91, 92), "leg_back": A(89, 94), "weapon": 83, "fire_t": 0.0}, + {"bob": 3, "arm_front": A(87, 98), "arm_back": A(93, 102), "leg_front": A(91, 92), "leg_back": A(89, 94), "weapon": 86, "fire_t": 0.12}, + {"bob": 6, "arm_front": A(86, 97), "arm_back": A(94, 101), "leg_front": A(90, 93), "leg_back": A(90, 94), "weapon": 89, "fire_t": 0.25}, + {"bob": 3, "arm_front": A(87, 98), "arm_back": A(93, 102), "leg_front": A(91, 92), "leg_back": A(89, 94), "weapon": 87, "fire_t": 0.37}, + {"bob": 0, "arm_front": A(88, 98), "arm_back": A(92, 102), "leg_front": A(91, 92), "leg_back": A(89, 94), "weapon": 84, "fire_t": 0.5}, + {"bob": 3, "arm_front": A(89, 99), "arm_back": A(91, 103), "leg_front": A(92, 91), "leg_back": A(88, 95), "weapon": 86, "fire_t": 0.62}, + {"bob": 6, "arm_front": A(86, 97), "arm_back": A(94, 101), "leg_front": A(90, 93), "leg_back": A(90, 94), "weapon": 88, "fire_t": 0.75}, + {"bob": 3, "arm_front": A(88, 98), "arm_back": A(92, 102), "leg_front": A(91, 92), "leg_back": A(89, 94), "weapon": 85, "fire_t": 0.87}, +] + +WALK = [ + {"bob": 0, "arm_front": A(88, 100), "arm_back": A(92, 104), "leg_front": A(78, 102), "leg_back": A(105, 95), "weapon": 88, "fire_t": 0.0}, + {"bob": 2, "arm_front": A(90, 102), "arm_back": A(90, 106), "leg_front": A(86, 98), "leg_back": A(98, 98), "weapon": 90, "fire_t": 0.12}, + {"bob": 3, "arm_front": A(92, 104), "arm_back": A(88, 108), "leg_front": A(94, 95), "leg_back": A(94, 95), "weapon": 92, "fire_t": 0.25}, + {"bob": 2, "arm_front": A(90, 102), "arm_back": A(90, 106), "leg_front": A(102, 98), "leg_back": A(86, 98), "weapon": 90, "fire_t": 0.37}, + {"bob": 0, "arm_front": A(88, 100), "arm_back": A(92, 104), "leg_front": A(108, 95), "leg_back": A(78, 102), "weapon": 88, "fire_t": 0.5}, + {"bob": 2, "arm_front": A(90, 102), "arm_back": A(90, 106), "leg_front": A(102, 98), "leg_back": A(86, 98), "weapon": 90, "fire_t": 0.62}, + {"bob": 3, "arm_front": A(92, 104), "arm_back": A(88, 108), "leg_front": A(94, 95), "leg_back": A(94, 95), "weapon": 92, "fire_t": 0.75}, + {"bob": 2, "arm_front": A(90, 102), "arm_back": A(90, 106), "leg_front": A(86, 98), "leg_back": A(98, 98), "weapon": 90, "fire_t": 0.87}, +] + +# Horizontal sweep +ATTACK_1 = [ + {"bob": 0, "arm_front": A(88, 98), "arm_back": A(92, 102), "leg_front": A(91, 92), "leg_back": A(89, 94), "weapon": 85, "fire_t": 0.0}, + {"bob": 1, "arm_front": A(95, 100), "arm_back": A(85, 100), "leg_front": A(88, 95), "leg_back": A(92, 95), "weapon": 100, "fire_t": 0.1}, + {"bob": 2, "arm_front": A(110, 110), "arm_back": A(75, 100), "leg_front": A(84, 98), "leg_back": A(96, 95), "weapon": 125, "fire_t": 0.2}, + {"bob": 2, "arm_front": A(125, 120), "arm_back": A(65, 105), "leg_front": A(80, 100), "leg_back": A(100, 95), "weapon": 150, "fire_t": 0.3}, + {"bob": 1, "arm_front": A(140, 125), "arm_back": A(60, 110), "leg_front": A(78, 102), "leg_back": A(102, 95), "weapon": 170, "fire_t": 0.4}, # active start + {"bob": 0, "arm_front": A(80, 90), "arm_back": A(90, 105), "leg_front": A(76, 100), "leg_back": A(104, 95), "weapon": 30, "fire_t": 0.5}, # active end + {"bob": 0, "arm_front": A(55, 80), "arm_back": A(100, 108), "leg_front": A(78, 98), "leg_back": A(102, 95), "weapon": 0, "fire_t": 0.6}, + {"bob": 0, "arm_front": A(40, 75), "arm_back": A(110, 110), "leg_front": A(82, 96), "leg_back": A(98, 95), "weapon": -25, "fire_t": 0.7}, + {"bob": 1, "arm_front": A(70, 90), "arm_back": A(100, 105), "leg_front": A(86, 94), "leg_back": A(94, 95), "weapon": 50, "fire_t": 0.8}, + {"bob": 0, "arm_front": A(88, 98), "arm_back": A(92, 102), "leg_front": A(91, 92), "leg_back": A(89, 94), "weapon": 85, "fire_t": 0.9}, +] + +# Overhead slam / ground pound +ATTACK_2 = [ + {"bob": 0, "arm_front": A(88, 98), "arm_back": A(92, 102), "leg_front": A(91, 92), "leg_back": A(89, 94), "weapon": 85, "fire_t": 0.0}, + {"bob": 1, "arm_front": A(95, 100), "arm_back": A(85, 100), "leg_front": A(88, 95), "leg_back": A(92, 95), "weapon": 100, "fire_t": 0.08}, + {"bob": 2, "arm_front": A(105, 105), "arm_back": A(78, 98), "leg_front": A(85, 98), "leg_back": A(95, 95), "weapon": 120, "fire_t": 0.16}, + {"bob": 3, "arm_front": A(120, 110), "arm_back": A(70, 100), "leg_front": A(82, 100), "leg_back": A(98, 95), "weapon": 145, "fire_t": 0.24}, + {"bob": 4, "arm_front": A(140, 115), "arm_back": A(60, 105), "leg_front": A(80, 102), "leg_back": A(100, 95), "weapon": 170, "fire_t": 0.32}, + {"bob": 4, "arm_front": A(160, 120), "arm_back": A(55, 110), "leg_front": A(78, 104), "leg_back": A(102, 95), "weapon": 185, "fire_t": 0.40}, # top hold + {"bob": 3, "arm_front": A(170, 115), "arm_back": A(50, 112), "leg_front": A(78, 104), "leg_back": A(102, 95), "weapon": 190, "fire_t": 0.48}, # active start + {"bob": 0, "arm_front": A(110, 85), "arm_back": A(75, 105), "leg_front": A(76, 102), "leg_back": A(104, 95), "weapon": 120, "fire_t": 0.56}, # slam + {"bob": 0, "arm_front": A(75, 75), "arm_back": A(95, 110), "leg_front": A(76, 100), "leg_back": A(104, 95), "weapon": 70, "fire_t": 0.64}, # impact + {"bob": 1, "arm_front": A(60, 75), "arm_back": A(105, 112), "leg_front": A(80, 98), "leg_back": A(100, 95), "weapon": 50, "fire_t": 0.72}, + {"bob": 2, "arm_front": A(75, 85), "arm_back": A(100, 108), "leg_front": A(85, 96), "leg_back": A(95, 95), "weapon": 70, "fire_t": 0.80}, + {"bob": 0, "arm_front": A(88, 98), "arm_back": A(92, 102), "leg_front": A(91, 92), "leg_back": A(89, 94), "weapon": 85, "fire_t": 0.88}, +] + +HURT = [ + {"bob": 0, "arm_front": A(100, 105), "arm_back": A(80, 100), "leg_front": A(94, 92), "leg_back": A(86, 96), "weapon": 100, "fire_t": 0.0}, + {"bob": 1, "arm_front": A(110, 110), "arm_back": A(70, 105), "leg_front": A(96, 93), "leg_back": A(84, 98), "weapon": 110, "fire_t": 0.2}, + {"bob": 1, "arm_front": A(105, 108), "arm_back": A(75, 102), "leg_front": A(94, 92), "leg_back": A(86, 96), "weapon": 105, "fire_t": 0.4}, + {"bob": 0, "arm_front": A(95, 100), "arm_back": A(85, 100), "leg_front": A(92, 91), "leg_back": A(88, 95), "weapon": 95, "fire_t": 0.6}, +] + +DEATH = [ + {"bob": 0, "arm_front": A(95, 100), "arm_back": A(85, 100), "leg_front": A(92, 91), "leg_back": A(88, 95), "weapon": 95, "fire_t": 0.0}, + {"bob": 1, "arm_front": A(105, 108), "arm_back": A(75, 105), "leg_front": A(95, 93), "leg_back": A(85, 98), "weapon": 105, "fire_t": 0.08}, + {"bob": 2, "arm_front": A(115, 115), "arm_back": A(65, 110), "leg_front": A(98, 95), "leg_back": A(82, 102), "weapon": 115, "fire_t": 0.16}, + {"bob": 3, "arm_front": A(120, 120), "arm_back": A(60, 115), "leg_front": A(100, 98), "leg_back": A(80, 105), "weapon": 120, "fire_t": 0.24}, + {"bob": 4, "arm_front": A(115, 125), "arm_back": A(65, 120), "leg_front": A(98, 105), "leg_back": A(82, 110), "weapon": 125, "fire_t": 0.32}, + {"bob": 5, "arm_front": A(110, 130), "arm_back": A(70, 125), "leg_front": A(95, 110), "leg_back": A(85, 115), "weapon": 130, "fire_t": 0.40}, + {"bob": 6, "arm_front": A(105, 135), "arm_back": A(75, 130), "leg_front": A(92, 115), "leg_back": A(88, 118), "weapon": 135, "fire_t": 0.48}, + {"bob": 7, "arm_front": A(100, 140), "arm_back": A(80, 135), "leg_front": A(90, 118), "leg_back": A(90, 120), "weapon": 140, "fire_t": 0.56}, + {"bob": 8, "arm_front": A(95, 145), "arm_back": A(85, 140), "leg_front": A(90, 120), "leg_back": A(90, 122), "weapon": 145, "fire_t": 0.64}, + {"bob": 9, "arm_front": A(90, 150), "arm_back": A(90, 145), "leg_front": A(90, 122), "leg_back": A(90, 124), "weapon": 150, "fire_t": 0.72}, + {"bob": 10, "arm_front": A(90, 155), "arm_back": A(90, 150), "leg_front": A(90, 124), "leg_back": A(90, 126), "weapon": 155, "fire_t": 0.80}, + {"bob": 10, "arm_front": A(90, 160), "arm_back": A(90, 155), "leg_front": A(90, 126), "leg_back": A(90, 128), "weapon": 160, "fire_t": 0.88}, +] + +ANIMS = { + "gate_warden_idle": (IDLE, 6.0, True), + "gate_warden_walk": (WALK, 8.0, True), + "gate_warden_attack_1": (ATTACK_1, 9.0, False), + "gate_warden_attack_2": (ATTACK_2, 8.0, False), + "gate_warden_hurt": (HURT, 8.0, False), + "gate_warden_death": (DEATH, 6.0, False), +} + + +def build(): + for name, (poses, fps, loop) in ANIMS.items(): + fw, fh = BOSS_CFG["fw"], BOSS_CFG["fh"] + bake_sheet(fw, fh, len(poses), + lambda d, i, fw, fh, poses=poses: render_humanoid(d, BOSS_CFG, poses[i], fw, fh), + str(OUT / f"{name}.png")) + print(f" ✓ {name}.png ({len(poses)} frames)") + + +if __name__ == "__main__": + build() diff --git a/assets_v2/tools/build_corpse_beast.py b/assets_v2/tools/build_corpse_beast.py new file mode 100644 index 0000000..3ce1696 --- /dev/null +++ b/assets_v2/tools/build_corpse_beast.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Corpse Beast (冲锋尸兽) build script. + +Low, wide, quadrupedal charge enemy. Distinct horizontal silhouette from all +humanoid characters. Glowing red eyes/chest during charge wind-up. + +Animations (per brief): + idle 6 subtle breathing / tail sway + run 8 gallop cycle + charge_windup 6 coil + red charge glow builds 0 -> 1 + charge 4 full extension, glow max, legs locked forward + wall_impact 4 compressed against wall, glow fading + hurt 4 recoil scramble + death 8 collapse, legs fold, sink down +""" + +from __future__ import annotations +from pathlib import Path +from pixel_engine import bake_sheet +from beast import beast_config, render_beast + +ROOT = Path(__file__).resolve().parent.parent.parent +OUT = ROOT / "assets_v2" / "characters" / "corpse_beast" + +# Decayed cold palette — distinct from the warm humanoids and the green ghost +# fire of the bow/melee ghosts. Eyes/chest use fixed red (see beast.render_beast). +BEAST_CFG = beast_config( + fw=128, fh=96, + palette={ + "outline": (16, 13, 15), + "body": (70, 62, 56), "body_lt": (104, 94, 84), "body_dk": (48, 42, 38), + "leg": (62, 54, 50), "leg_dk": (42, 36, 34), "leg_lt": (92, 84, 76), + "claw": (150, 22, 22), + }, +) + +# ── pose helpers ───────────────────────────────────────────────────────────── +def leg(fl, fra, bl, bra): + """Front-left / front-right / back-left / back-right (knee, ankle).""" + return { + "leg_fl_knee": fl[0], "leg_fl_ankle": fl[1], + "leg_fr_knee": fra[0], "leg_fr_ankle": fra[1], + "leg_bl_knee": bl[0], "leg_bl_ankle": bl[1], + "leg_br_knee": bra[0], "leg_br_ankle": bra[1], + } + +# Idle: low quadruped breathing, >=3px bob/arch steps, no dup frames. +IDLE = [ + {"bob": 0, "arch": 0, "tail": 0, **leg((85, 100), (85, 100), (95, 80), (95, 80))}, + {"bob": 3, "arch": 3, "tail": 3, **leg((82, 102), (88, 98), (92, 82), (98, 78))}, + {"bob": 6, "arch": 6, "tail": -2, **leg((88, 98), (82, 102), (98, 78), (92, 82))}, + {"bob": 3, "arch": 3, "tail": 3, **leg((84, 101), (86, 99), (94, 81), (96, 79))}, + {"bob": 0, "arch": 0, "tail": 0, **leg((86, 99), (84, 101), (96, 79), (94, 81))}, + {"bob": 3, "arch": 3, "tail": 2, **leg((83, 102), (87, 98), (93, 82), (97, 78))}, +] + +RUN = [ + {"bob": 2, "arch": 3, "stretch": 2, "head_y": -2, "tail": -3, **leg((70, 95), (70, 95), (110, 80), (110, 80))}, + {"bob": 1, "arch": 4, "stretch": 4, "head_y": -1, "tail": -5, **leg((80, 100), (80, 100), (120, 70), (120, 70))}, + {"bob": 2, "arch": 3, "stretch": 3, "head_y": 0, "tail": -2, **leg((95, 105), (95, 105), (100, 85), (100, 85))}, + {"bob": 0, "arch": 2, "stretch": 5, "head_y": -3, "tail": -6, **leg((65, 90), (65, 90), (115, 75), (115, 75))}, + {"bob": 1, "arch": 3, "stretch": 4, "head_y": -1, "tail": -4, **leg((75, 96), (75, 96), (125, 68), (125, 68))}, + {"bob": 2, "arch": 3, "stretch": 3, "head_y": 0, "tail": -2, **leg((92, 104), (92, 104), (102, 84), (102, 84))}, + {"bob": 1, "arch": 4, "stretch": 4, "head_y": -1, "tail": -5, **leg((82, 101), (82, 101), (122, 72), (122, 72))}, + {"bob": 2, "arch": 2, "stretch": 3, "head_y": -2, "tail": -3, **leg((72, 94), (72, 94), (112, 78), (112, 78))}, +] + +CHARGE_WINDUP = [ + {"bob": 0, "arch": 2, "stretch": -2, "head_y": 2, "charge_glow": 0.10, "tail": 2, **leg((90, 100), (90, 100), (95, 80), (95, 80))}, + {"bob": 1, "arch": 4, "stretch": -3, "head_y": 3, "charge_glow": 0.30, "tail": 1, **leg((94, 102), (94, 102), (98, 82), (98, 82))}, + {"bob": 2, "arch": 6, "stretch": -4, "head_y": 4, "charge_glow": 0.50, "tail": 0, **leg((98, 103), (98, 103), (100, 84), (100, 84))}, + {"bob": 3, "arch": 8, "stretch": -6, "head_y": 5, "charge_glow": 0.70, "tail": -1, **leg((102, 104), (102, 104), (103, 86), (103, 86))}, + {"bob": 4, "arch": 10, "stretch": -7, "head_y": 6, "charge_glow": 0.85, "tail": -2, **leg((104, 106), (104, 106), (105, 88), (105, 88))}, + {"bob": 5, "arch": 12, "stretch": -9, "head_y": 7, "charge_glow": 1.00, "tail": -3, **leg((106, 108), (106, 108), (107, 90), (107, 90))}, +] + +CHARGE = [ + {"bob": 0, "arch": -2, "stretch": 10, "head_y": -6, "charge_glow": 1.0, "tail": -8, **leg((60, 88), (60, 88), (130, 64), (130, 64))}, + {"bob": 0, "arch": -3, "stretch": 12, "head_y": -7, "charge_glow": 1.0, "tail": -9, **leg((62, 90), (62, 90), (132, 62), (132, 62))}, + {"bob": 0, "arch": -2, "stretch": 11, "head_y": -6, "charge_glow": 1.0, "tail": -8, **leg((61, 89), (61, 89), (131, 63), (131, 63))}, + {"bob": 0, "arch": -3, "stretch": 12, "head_y": -7, "charge_glow": 1.0, "tail": -9, **leg((63, 91), (63, 91), (133, 61), (133, 61))}, +] + +WALL_IMPACT = [ + {"bob": 6, "arch": 6, "stretch": -8, "head_y": 8, "charge_glow": 0.60, "tail": 4, **leg((120, 96), (120, 96), (80, 92), (80, 92))}, + {"bob": 8, "arch": 8, "stretch": -10, "head_y": 10, "charge_glow": 0.40, "tail": 6, **leg((130, 98), (130, 98), (72, 95), (72, 95))}, + {"bob": 7, "arch": 7, "stretch": -9, "head_y": 9, "charge_glow": 0.25, "tail": 5, **leg((125, 97), (125, 97), (76, 93), (76, 93))}, + {"bob": 6, "arch": 6, "stretch": -8, "head_y": 8, "charge_glow": 0.15, "tail": 4, **leg((122, 96), (122, 96), (78, 92), (78, 92))}, +] + +HURT = [ + {"bob": 1, "arch": 1, "stretch": -1, "head_y": 3, **leg((100, 100), (100, 100), (98, 82), (98, 82))}, + {"bob": 3, "arch": 3, "stretch": -2, "head_y": 5, **leg((108, 104), (108, 104), (104, 86), (104, 86))}, + {"bob": 2, "arch": 2, "stretch": -1, "head_y": 4, **leg((104, 102), (104, 102), (101, 84), (101, 84))}, + {"bob": 1, "arch": 1, "stretch": 0, "head_y": 2, **leg((102, 101), (102, 101), (99, 83), (99, 83))}, +] + +DEATH = [ + {"bob": 2, "arch": 0, "stretch": 0, "head_y": 4, "charge_glow": 0.2, "tail": 2, **leg((100, 100), (100, 100), (98, 82), (98, 82))}, + {"bob": 4, "arch": -2, "stretch": -2, "head_y": 6, "charge_glow": 0.1, "tail": 4, **leg((110, 100), (110, 100), (105, 84), (105, 84))}, + {"bob": 6, "arch": -4, "stretch": -4, "head_y": 8, "tail": 6, **leg((120, 100), (120, 100), (112, 86), (112, 86))}, + {"bob": 8, "arch": -6, "stretch": -6, "head_y": 10, "tail": 8, **leg((128, 102), (128, 102), (118, 88), (118, 88))}, + {"bob": 10, "arch": -8, "stretch": -8, "head_y": 12, "tail": 10, **leg((132, 104), (132, 104), (122, 90), (122, 90))}, + {"bob": 12, "arch": -10, "stretch": -10, "head_y": 14, "tail": 12, **leg((134, 106), (134, 106), (124, 92), (124, 92))}, + {"bob": 14, "arch": -12, "stretch": -12, "head_y": 16, "tail": 14, **leg((135, 108), (135, 108), (126, 94), (126, 94))}, + {"bob": 16, "arch": -14, "stretch": -14, "head_y": 18, "tail": 16, **leg((136, 110), (136, 110), (128, 96), (128, 96))}, +] + +ANIMS = { + "corpse_beast_idle": (IDLE, 5.0, True), + "corpse_beast_run": (RUN, 10.0, True), + "corpse_beast_charge_windup": (CHARGE_WINDUP, 8.0, False), + "corpse_beast_charge": (CHARGE, 12.0, False), + "corpse_beast_wall_impact": (WALL_IMPACT, 8.0, False), + "corpse_beast_hurt": (HURT, 10.0, False), + "corpse_beast_death": (DEATH, 6.0, False), +} + + +def build(): + fw, fh = BEAST_CFG["fw"], BEAST_CFG["fh"] + for name, (poses, fps, loop) in ANIMS.items(): + bake_sheet(fw, fh, len(poses), + lambda d, i, fw, fh, poses=poses: render_beast(d, BEAST_CFG, poses[i], fw, fh), + str(OUT / f"{name}.png")) + print(f" ok {name}.png ({len(poses)} frames)") + + +if __name__ == "__main__": + build() diff --git a/assets_v2/tools/build_design_bible.py b/assets_v2/tools/build_design_bible.py new file mode 100644 index 0000000..53a11b4 --- /dev/null +++ b/assets_v2/tools/build_design_bible.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Design Bible + Comparison sheets. + +Per character directory, emit: + concept_front.png idle frame (front-facing) + concept_side.png mid-run/walk frame (action profile) + concept_back.png mirrored front (back approximation; true 3/4 not feasible + in pure side-scroller pixel art — flagged in metadata) + silhouette.png solid-black silhouette of the front pose + palette.png material palette swatches (hex) + contact_sheet.png every animation frame in a labelled grid + +Plus review/ comparison sheets: + character_scale_comparison.png all 5 characters bottom-aligned on one ground line + silhouette_comparison.png all 5 black silhouettes for distinction check +""" + +from __future__ import annotations +from pathlib import Path +from PIL import Image, ImageDraw, ImageFont +from pixel_engine import new_canvas + +# import build modules (module-level only; build() not auto-run) +from build_player import PLAYER_CFG, IDLE as P_IDLE, RUN as P_RUN, render_humanoid +from build_melee_ghost import MELEE_CFG, IDLE as M_IDLE, WALK as M_WALK, render_humanoid +from build_ghost_archer import ARCHER_CFG, IDLE as A_IDLE, RETREAT as A_RETREAT, render_humanoid +from build_boss import BOSS_CFG, IDLE as B_IDLE, WALK as B_WALK, render_humanoid +from build_corpse_beast import BEAST_CFG, IDLE as C_IDLE, RUN as C_RUN, render_beast + +ROOT = Path(__file__).resolve().parent.parent.parent +CHAR_DIR = ROOT / "assets_v2" / "characters" +REVIEW_DIR = ROOT / "assets_v2" / "review" + +FONT = ImageFont.load_default() + +# char -> (cfg, front_pose, side_pose, fn, fw, fh) +CHARS = { + "player": (PLAYER_CFG, P_IDLE[0], P_RUN[3], render_humanoid, 96, 96), + "melee_ghost": (MELEE_CFG, M_IDLE[0], M_WALK[3], render_humanoid, 96, 96), + "ghost_archer": (ARCHER_CFG, A_IDLE[0], A_RETREAT[3], render_humanoid, 96, 96), + "corpse_beast": (BEAST_CFG, C_IDLE[0], C_RUN[3], render_beast, 128, 96), + "gate_warden": (BOSS_CFG, B_IDLE[0], B_WALK[3], render_humanoid, 192, 192), +} + + +def render_pose(cfg, pose, fw, fh, fn): + img, d = new_canvas(fw, fh) + fn(d, cfg, pose, fw, fh) + return img + + +def to_silhouette(img): + out = Image.new("RGBA", img.size, (0, 0, 0, 0)) + src = img.load() + dst = out.load() + for y in range(img.height): + for x in range(img.width): + r, g, b, a = src[x, y] + if a > 16: + dst[x, y] = (0, 0, 0, 255) + return out + + +def make_palette(cfg): + pal = cfg["palette"] + keys = list(pal.keys()) + cols = 4 + sw = 44 + step = 52 + rows = (len(keys) + cols - 1) // cols + W = cols * step + 10 + H = rows * step + 10 + img, d = new_canvas(W, H) + d.rectangle([0, 0, W - 1, H - 1], outline=(60, 60, 60)) + for i, k in enumerate(keys): + r, c = divmod(i, cols) + x = 6 + c * step + y = 6 + r * step + col = pal[k] + d.rectangle([x, y, x + sw, y + sw], fill=col, outline=(0, 0, 0)) + hexs = "#%02X%02X%02X" % col + d.text((x, y + sw + 2), f"{k}:{hexs}", fill=(210, 210, 210), font=FONT) + return img + + +def make_contact_sheet(char): + d = CHAR_DIR / char + fw = fh = None + # discover frame size from any sprite + sheets = [] + for p in sorted(d.glob("*.png")): + if p.name.startswith(("concept_", "silhouette", "palette", "contact_sheet")): + continue + im = Image.open(p) + # robust frame_w from a known char size + w, h = im.size + if char == "corpse_beast": + _fw, _fh = 128, 96 + elif char == "gate_warden": + _fw, _fh = 192, 192 + else: + _fw, _fh = 96, 96 + n = w // _fw + sheets.append((p.name, im, _fw, _fh, n)) + if fw is None: + fw, fh = _fw, _fh + gap = 2 + label_h = 12 + maxn = max(n for _, _, _, _, n in sheets) + cell_w = fw + gap + cell_h = fh + gap + W = 6 + maxn * cell_w + H = 6 + len(sheets) * (cell_h + label_h) + img, dr = new_canvas(W, H) + for ri, (name, im, _fw, _fh, n) in enumerate(sheets): + y0 = 6 + ri * (cell_h + label_h) + dr.text((6, y0), name, fill=(220, 220, 220), font=FONT) + for f in range(n): + x0 = 6 + f * cell_w + frame = im.crop((f * _fw, 0, f * _fw + _fw, _fh)) + img.paste(frame, (x0, y0 + label_h), frame) + return img + + +def per_character(): + for char, (cfg, front_pose, side_pose, fn, fw, fh) in CHARS.items(): + d = CHAR_DIR / char + front = render_pose(cfg, front_pose, fw, fh, fn) + side = render_pose(cfg, side_pose, fw, fh, fn) + back = front.transpose(Image.FLIP_LEFT_RIGHT) + sil = to_silhouette(front) + pal = make_palette(cfg) + front.save(str(d / "concept_front.png")) + side.save(str(d / "concept_side.png")) + back.save(str(d / "concept_back.png")) + sil.save(str(d / "silhouette.png")) + pal.save(str(d / "palette.png")) + make_contact_sheet(char).save(str(d / "contact_sheet.png")) + print(f" ok {char}: front/side/back/silhouette/palette/contact_sheet") + + +def comparisons(): + REVIEW_DIR.mkdir(parents=True, exist_ok=True) + # gather front poses + foot positions + rows = [] + for char, (cfg, front_pose, _, fn, fw, fh) in CHARS.items(): + img = render_pose(cfg, front_pose, fw, fh, fn) + rows.append((char, img, fw, fh)) + + # scale comparison: bottom-aligned on one ground line + pad = 20 + gap = 40 + ground = 360 + total_w = pad * 2 + sum(fw + gap for _, _, fw, _ in rows) + scale_img, sd = new_canvas(total_w, ground + 40) + sd.rectangle([0, 0, total_w - 1, ground + 39], fill=(18, 20, 26)) + sd.line([(0, ground), (total_w, ground)], fill=(70, 74, 84), width=2) + x = pad + for char, img, fw, fh in rows: + y = ground - fh + scale_img.paste(img, (x, y), img) + sd.text((x + fw // 2 - len(char) * 3, ground + 6), char, fill=(200, 200, 200), font=FONT) + x += fw + gap + scale_img.save(str(REVIEW_DIR / "character_scale_comparison.png")) + print(" ok review/character_scale_comparison.png") + + # silhouette comparison + sil_img, _ = new_canvas(total_w, ground + 40) + x = pad + for char, img, fw, fh in rows: + y = ground - fh + sil = to_silhouette(img) + sil_img.paste(sil, (x, y), sil) + x += fw + gap + sil_img.save(str(REVIEW_DIR / "silhouette_comparison.png")) + print(" ok review/silhouette_comparison.png") + + +if __name__ == "__main__": + print("per-character bible:") + per_character() + print("comparisons:") + comparisons() + print("done.") diff --git a/assets_v2/tools/build_effects.py b/assets_v2/tools/build_effects.py new file mode 100644 index 0000000..8e28dff --- /dev/null +++ b/assets_v2/tools/build_effects.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Combat Effects, transparent background. + + blade_slash 128x128 / 6f sweeping crescent (cyan-white + ghost-green core) + hit 96x96 / 4f expanding impact starburst + ghost_fire 64x64 / 8f looping ghost flame (idle/emitter) + death_dissolve 96x96 / 8f rising fading motes (death VFX) + +All use nearest-neighbor-friendly hard fills; no blur. +""" + +from __future__ import annotations +from pathlib import Path +import math +from pixel_engine import new_canvas, fill_circle, fill_poly, ghost_fire, bake_sheet + +ROOT = Path(__file__).resolve().parent.parent.parent +OUT = ROOT / "assets_v2" / "effects" + +CYAN = (150, 225, 245) +CYAN_LT = (220, 250, 255) +GHOST = (110, 240, 160) +GHOST_DK = (30, 150, 100) +WHITE = (235, 240, 248) +SPARK = (255, 230, 150) +RED = (200, 60, 50) + + +def crescent(d, cx, cy, R, t, a0, a1, color): + """Filled crescent between outer radius R and inner R-t, from a0->a1 (deg).""" + N = 18 + outer, inner = [], [] + for i in range(N + 1): + a = math.radians(a0 + (a1 - a0) * i / N) + outer.append((cx + R * math.cos(a), cy + R * math.sin(a))) + for i in range(N + 1): + a = math.radians(a0 + (a1 - a0) * (N - i) / N) + inner.append((cx + (R - t) * math.cos(a), cy + (R - t) * math.sin(a))) + fill_poly(d, outer + inner, color) + + +def blade_slash(d, i, fw, fh): + cx, cy = fw // 2, fh // 2 + # arc sweeps from upper-left to lower-right across frames + base = -120 + i * 38 + span = 70 + crescent(d, cx, cy, 54, 16, base, base + span, CYAN) + crescent(d, cx, cy, 52, 7, base + 4, base + span - 4, CYAN_LT) + crescent(d, cx, cy, 50, 3, base + 10, base + span - 10, WHITE) + # ghost-green edge accent + crescent(d, cx, cy, 56, 3, base - 2, base + 8, GHOST) + + +def hit(d, i, fw, fh): + cx, cy = fw // 2, fh // 2 + grow = 8 + i * 16 + # central flash fades + fill_circle(d, (cx, cy), max(2, 18 - i * 4), SPARK) + fill_circle(d, (cx, cy), max(1, 9 - i * 2), WHITE) + # radial sparks + n = 8 + for k in range(n): + a = math.radians(k * 360 / n + i * 20) + x2 = cx + grow * math.cos(a) + y2 = cy + grow * math.sin(a) + d.line([(cx, cy), (int(x2), int(y2))], fill=SPARK if i < 2 else RED, width=2) + fill_circle(d, (x2, y2), 2, SPARK if i < 2 else RED) + + +def ghost_fire_loop(d, i, fw, fh): + ghost_fire(d, fw // 2, fh // 2 + 6, 14, t=i / 8.0) + + +def death_dissolve(d, i, fw, fh): + # motes rise and fade + import random + random.seed(7) + cx, cy = fw // 2, fh // 2 + 10 + motes = [] + for _ in range(22): + ang = random.uniform(0, math.pi * 2) + rad = random.uniform(4, 36) + motes.append((math.cos(ang) * rad, math.sin(ang) * rad, random.uniform(1.5, 3.5))) + for mx, my, r in motes: + rise = i * (6 + r) + yy = cy + my - rise + alpha = max(0, 200 - i * 26) + if yy < -10 or alpha <= 0: + continue + fill_circle(d, (cx + mx, yy), r, (GHOST[0], GHOST[1], GHOST[2], alpha)) + fill_circle(d, (cx + mx, yy), max(1, r - 1), (GHOST_DK[0], GHOST_DK[1], GHOST_DK[2], alpha // 2)) + + +EFFECTS = [ + ("blade_slash", 128, 128, 6, blade_slash), + ("hit", 96, 96, 4, hit), + ("ghost_fire", 64, 64, 8, ghost_fire_loop), + ("death_dissolve", 96, 96, 8, death_dissolve), +] + + +def build(): + for name, fw, fh, n, fn in EFFECTS: + bake_sheet(fw, fh, n, fn, str(OUT / f"{name}.png")) + print(f" ok {name}.png ({fw}x{fh}, {n}f)") + + +if __name__ == "__main__": + build() diff --git a/assets_v2/tools/build_ghost_archer.py b/assets_v2/tools/build_ghost_archer.py new file mode 100644 index 0000000..39d8b58 --- /dev/null +++ b/assets_v2/tools/build_ghost_archer.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Ghost Archer (鬼弓手). + +Tall, thin, rear-leaning spectral archer. Long asymmetrical bow with ghost-fire +arrows. Distinct silhouette: vertical height, big bow arc, slender legs. +""" + +from __future__ import annotations +from pathlib import Path +from pixel_engine import bake_sheet +from humanoid import humanoid_config, render_humanoid + +ROOT = Path(__file__).resolve().parent.parent.parent +OUT = ROOT / "assets_v2" / "characters" / "ghost_archer" + +ARCHER_CFG = humanoid_config( + fw=96, fh=96, cx=48, hip_y=64, torso_len=29, neck_len=6, head_r=8, + upper_arm=19, lower_arm=17, arm_w=3, thigh=20, shin=18, leg_w=4, + scale=1.05, lean=-4, stance=3, style="ghost_archer", torso_hw=7, torso_hw_hip=5, + bow_len=44, ghost_fire=True, pauldrons=False, + palette={ + "skin": (105, 115, 110), "skin_lt": (132, 145, 138), "skin_sh": (74, 84, 78), + "armor": (64, 76, 82), "armor_lt": (95, 110, 118), "armor_dk": (40, 50, 54), + "cloth": (58, 72, 68), "cloth_lt": (82, 98, 92), "cloth_dk": (38, 48, 44), + "wood": (72, 58, 48), "wood_lt": (100, 82, 68), + "blade": (90, 80, 70), "blade_lt": (120, 108, 95), "blade_dk": (62, 54, 46), + "grip": (58, 48, 38), "guard": (88, 74, 58), + "outline": (12, 16, 18), + }, +) + +A = lambda sa, ea: (sa, ea) + +# Idle: tall thin stance, >=3px bob steps, ghost-fire flicker. No dup frames. +IDLE = [ + {"bob": 0, "arm_front": A(50, 160), "arm_back": A(130, 110), "leg_front": A(92, 90), "leg_back": A(88, 92), "aim": 0.0, "fire_t": 0.0}, + {"bob": 3, "arm_front": A(49, 160), "arm_back": A(131, 110), "leg_front": A(91, 91), "leg_back": A(89, 92), "aim": 0.0, "fire_t": 0.2}, + {"bob": 6, "arm_front": A(48, 160), "arm_back": A(132, 110), "leg_front": A(90, 92), "leg_back": A(90, 92), "aim": 0.0, "fire_t": 0.4}, + {"bob": 3, "arm_front": A(49, 160), "arm_back": A(131, 110), "leg_front": A(91, 91), "leg_back": A(89, 92), "aim": 0.0, "fire_t": 0.6}, + {"bob": 0, "arm_front": A(50, 160), "arm_back": A(130, 110), "leg_front": A(92, 90), "leg_back": A(88, 92), "aim": 0.0, "fire_t": 0.8}, + {"bob": 3, "arm_front": A(49, 160), "arm_back": A(131, 110), "leg_front": A(91, 91), "leg_back": A(89, 92), "aim": 0.0, "fire_t": 0.95}, +] + +RETREAT = [ + {"bob": 0, "arm_front": A(55, 160), "arm_back": A(125, 110), "leg_front": A(95, 92), "leg_back": A(85, 95), "aim": 0.1, "fire_t": 0.0}, + {"bob": 0, "arm_front": A(54, 160), "arm_back": A(126, 110), "leg_front": A(100, 90), "leg_back": A(80, 100), "aim": 0.1, "fire_t": 0.12}, + {"bob": 1, "arm_front": A(53, 160), "arm_back": A(127, 110), "leg_front": A(90, 92), "leg_back": A(90, 92), "aim": 0.1, "fire_t": 0.25}, + {"bob": 1, "arm_front": A(54, 160), "arm_back": A(126, 110), "leg_front": A(80, 100), "leg_back": A(100, 90), "aim": 0.1, "fire_t": 0.37}, + {"bob": 0, "arm_front": A(55, 160), "arm_back": A(125, 110), "leg_front": A(85, 95), "leg_back": A(95, 92), "aim": 0.1, "fire_t": 0.5}, + {"bob": 0, "arm_front": A(54, 160), "arm_back": A(126, 110), "leg_front": A(80, 100), "leg_back": A(100, 90), "aim": 0.1, "fire_t": 0.62}, + {"bob": 1, "arm_front": A(53, 160), "arm_back": A(127, 110), "leg_front": A(90, 92), "leg_back": A(90, 92), "aim": 0.1, "fire_t": 0.75}, + {"bob": 1, "arm_front": A(54, 160), "arm_back": A(126, 110), "leg_front": A(100, 90), "leg_back": A(80, 100), "aim": 0.1, "fire_t": 0.87}, +] + +AIM = [ + {"bob": 0, "arm_front": A(45, 165), "arm_back": A(135, 115), "leg_front": A(92, 90), "leg_back": A(88, 92), "aim": 0.0, "fire_t": 0.0}, + {"bob": 0, "arm_front": A(42, 168), "arm_back": A(138, 108), "leg_front": A(91, 91), "leg_back": A(89, 92), "aim": 0.25, "fire_t": 0.15}, + {"bob": 1, "arm_front": A(38, 170), "arm_back": A(142, 100), "leg_front": A(90, 92), "leg_back": A(90, 92), "aim": 0.5, "fire_t": 0.3}, + {"bob": 1, "arm_front": A(35, 172), "arm_back": A(145, 95), "leg_front": A(90, 92), "leg_back": A(90, 92), "aim": 0.75, "fire_t": 0.45}, + {"bob": 1, "arm_front": A(32, 175), "arm_back": A(148, 90), "leg_front": A(90, 92), "leg_back": A(90, 92), "aim": 1.0, "fire_t": 0.6}, + {"bob": 0, "arm_front": A(32, 175), "arm_back": A(148, 90), "leg_front": A(90, 92), "leg_back": A(90, 92), "aim": 1.0, "fire_t": 0.75}, +] + +SHOOT = [ + {"bob": 0, "arm_front": A(32, 175), "arm_back": A(148, 90), "leg_front": A(90, 92), "leg_back": A(90, 92), "aim": 1.0, "fire_t": 0.0}, + {"bob": 0, "arm_front": A(40, 170), "arm_back": A(120, 100), "leg_front": A(90, 92), "leg_back": A(90, 92), "aim": 0.4, "fire_t": 0.2}, + {"bob": 1, "arm_front": A(48, 165), "arm_back": A(110, 110), "leg_front": A(91, 91), "leg_back": A(89, 92), "aim": 0.05, "fire_t": 0.4}, + {"bob": 0, "arm_front": A(50, 160), "arm_back": A(115, 108), "leg_front": A(92, 90), "leg_back": A(88, 92), "aim": 0.0, "fire_t": 0.6}, +] + +HURT = [ + {"bob": 0, "arm_front": A(60, 150), "arm_back": A(120, 120), "leg_front": A(95, 90), "leg_back": A(85, 95), "aim": 0.0, "fire_t": 0.0}, + {"bob": 1, "arm_front": A(70, 140), "arm_back": A(110, 130), "leg_front": A(100, 92), "leg_back": A(80, 98), "aim": 0.0, "fire_t": 0.2}, + {"bob": 1, "arm_front": A(65, 145), "arm_back": A(115, 125), "leg_front": A(98, 91), "leg_back": A(82, 96), "aim": 0.0, "fire_t": 0.4}, + {"bob": 0, "arm_front": A(55, 155), "arm_back": A(125, 115), "leg_front": A(94, 90), "leg_back": A(86, 94), "aim": 0.0, "fire_t": 0.6}, +] + +DEATH = [ + {"bob": 0, "arm_front": A(55, 155), "arm_back": A(125, 115), "leg_front": A(94, 90), "leg_back": A(86, 94), "aim": 0.0, "fire_t": 0.0}, + {"bob": 1, "arm_front": A(70, 140), "arm_back": A(110, 130), "leg_front": A(98, 92), "leg_back": A(82, 98), "aim": 0.0, "fire_t": 0.1}, + {"bob": 2, "arm_front": A(85, 130), "arm_back": A(95, 140), "leg_front": A(102, 94), "leg_back": A(78, 102), "aim": 0.0, "fire_t": 0.2}, + {"bob": 3, "arm_front": A(95, 125), "arm_back": A(85, 145), "leg_front": A(105, 98), "leg_back": A(75, 108), "aim": 0.0, "fire_t": 0.3}, + {"bob": 4, "arm_front": A(105, 120), "arm_back": A(75, 150), "leg_front": A(100, 105), "leg_back": A(80, 112), "aim": 0.0, "fire_t": 0.4}, + {"bob": 5, "arm_front": A(110, 115), "arm_back": A(70, 155), "leg_front": A(95, 110), "leg_back": A(85, 115), "aim": 0.0, "fire_t": 0.5}, + {"bob": 6, "arm_front": A(115, 110), "arm_back": A(65, 160), "leg_front": A(90, 115), "leg_back": A(90, 118), "aim": 0.0, "fire_t": 0.6}, + {"bob": 7, "arm_front": A(120, 105), "arm_back": A(60, 165), "leg_front": A(90, 118), "leg_back": A(90, 120), "aim": 0.0, "fire_t": 0.7}, +] + +ANIMS = { + "ghost_archer_idle": (IDLE, 8.0, True), + "ghost_archer_retreat": (RETREAT, 10.0, True), + "ghost_archer_aim": (AIM, 8.0, False), + "ghost_archer_shoot": (SHOOT, 14.0, False), + "ghost_archer_hurt": (HURT, 10.0, False), + "ghost_archer_death": (DEATH, 8.0, False), +} + + +def build(): + for name, (poses, fps, loop) in ANIMS.items(): + fw, fh = ARCHER_CFG["fw"], ARCHER_CFG["fh"] + bake_sheet(fw, fh, len(poses), + lambda d, i, fw, fh, poses=poses: render_humanoid(d, ARCHER_CFG, poses[i], fw, fh), + str(OUT / f"{name}.png")) + print(f" ✓ {name}.png ({len(poses)} frames)") + + +if __name__ == "__main__": + build() diff --git a/assets_v2/tools/build_icons.py b/assets_v2/tools/build_icons.py new file mode 100644 index 0000000..f5c95d7 --- /dev/null +++ b/assets_v2/tools/build_icons.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Weapon / Stat Icons (64x64), transparent background. + +10 icons that must read instantly at small HUD size and stay distinct from +each other. Unified palette: steel/iron, dark-red cloth, ghost-green energy, +dark-gold sacred, deep-red danger. White outline rim for HUD legibility. +""" + +from __future__ import annotations +from pathlib import Path +from PIL import Image, ImageDraw +from pixel_engine import new_canvas, capsule, fill_circle, fill_poly, rect, ghost_fire + +ROOT = Path(__file__).resolve().parent.parent.parent +OUT = ROOT / "assets_v2" / "icons" +SZ = 64 + +# palette +O = (14, 12, 16) # outline +STEEL = (150, 156, 168) +STEEL_LT = (200, 205, 216) +STEEL_DK = (92, 98, 110) +WOOD = (96, 70, 48) +WOOD_DK = (62, 44, 30) +RED = (120, 32, 32) +RED_LT = (170, 52, 48) +GOLD = (200, 170, 90) +GOLD_DK = (130, 108, 58) +GHOST = (110, 240, 160) +GHOST_DK = (30, 150, 100) +CYAN = (90, 200, 230) +BRONZE = (120, 96, 56) +BRONZE_DK = (78, 60, 34) +PAPER = (210, 190, 120) +PAPER_DK = (160, 140, 80) + + +def rdot(draw, p, r, c): + fill_circle(draw, p, r, c) + + +# ── 1. Songdao (宋刀) — curved saber ───────────────────────────────────────── +def icon_songdao(d): + # blade: curved polygon lower-left handle -> upper-right tip + blade = [(16, 50), (20, 46), (40, 24), (46, 18), (50, 20), (30, 44), (24, 50)] + fill_poly(d, blade, STEEL) + fill_poly(d, [(20, 46), (40, 24), (46, 18), (50, 20), (47, 23), (42, 28), (23, 49)], STEEL_LT) # edge light + fill_poly(d, [(16, 50), (20, 46), (24, 50)], STEEL_DK) + # guard + rect(d, 14, 48, 10, 5, GOLD_DK) + # handle + capsule(d, (10, 52), (16, 50), 3, WOOD, outline=O) + rdot(d, (9, 53), 3, RED) # pommel wrap + # outline rim + fill_poly(d, blade, None) + draw_edges(d, blade) + + +def draw_edges(d, pts): + d.line([(int(round(x)), int(round(y))) for x, y in pts] + [(int(round(pts[0][0])), int(round(pts[0][1])))], + fill=O, width=1) + + +# ── 2. Spear (沥泉枪) — shaft + leaf blade + tassel ────────────────────────── +def icon_spear(d): + # shaft + capsule(d, (32, 14), (32, 54), 3, WOOD, outline=O) + # leaf blade tip + tip = [(32, 6), (28, 16), (32, 20), (36, 16)] + fill_poly(d, tip, STEEL) + fill_poly(d, [(32, 6), (36, 16), (32, 20)], STEEL_LT) + draw_edges(d, tip) + # red tassel below blade + for i in range(4): + x = 32 + (i - 1.5) * 2 + capsule(d, (x, 22), (x + (i - 1) * 2, 30), 1, RED_LT if i % 2 else RED, outline=O) + + +# ── 3. Ghost fire (鬼火) ───────────────────────────────────────────────────── +def icon_ghostfire(d): + ghost_fire(d, 32, 40, 16, t=0.5) + rdot(d, (32, 44), 4, GHOST) + rdot(d, (32, 40), 2, (220, 255, 235)) + + +# ── 4. Talisman (护符) — yellow paper strip ───────────────────────────────── +def icon_talisman(d): + paper = [(24, 12), (40, 12), (40, 54), (24, 54)] + fill_poly(d, paper, PAPER) + # notched top + draw_edges(d, paper) + # red vertical seal script (simplified bars) + rect(d, 30, 18, 4, 4, RED) + rect(d, 30, 26, 4, 8, RED) + rect(d, 26, 40, 12, 3, RED) + rect(d, 30, 46, 4, 5, RED) + # shading + fill_poly(d, [(24, 12), (28, 12), (28, 54), (24, 54)], PAPER_DK) + + +# ── 5. Gourd (酒葫芦) — wine gourd ────────────────────────────────────────── +def icon_gourd(d): + # lower bulb + rdot(d, (32, 42), 13, WOOD) + # upper bulb + rdot(d, (32, 24), 9, WOOD) + # neck + rect(d, 29, 32, 6, 5, WOOD_DK) + # cork + rect(d, 30, 14, 4, 5, (150, 120, 80)) + # highlight + rdot(d, (27, 38), 4, (130, 100, 66)) + rdot(d, (29, 22), 3, (130, 100, 66)) + # outline + d.ellipse((19, 29, 45, 55), outline=O, width=1) + d.ellipse((23, 15, 41, 33), outline=O, width=1) + + +# ── 6. Broken flag (残破军旗) — torn battle standard ─────────────────────── +def icon_flag(d): + # pole + capsule(d, (18, 8), (18, 56), 2, (110, 96, 70), outline=O) + # torn flag (dark red) waving right with a notch + flag = [(20, 12), (50, 16), (46, 22), (52, 28), (46, 34), (50, 42), (20, 40)] + fill_poly(d, flag, RED) + fill_poly(d, [(20, 12), (50, 16), (46, 22), (30, 20), (20, 22)], RED_LT) + draw_edges(d, flag) + # ragged hole + rdot(d, (38, 28), 3, O) + # emblem mark + rect(d, 28, 22, 6, 6, GOLD) + + +# ── 7. Yin coin (阴钱) — underworld coin ──────────────────────────────────── +def icon_coin(d): + rdot(d, (32, 32), 18, BRONZE) + rdot(d, (32, 32), 18, None) + d.ellipse((14, 14, 50, 50), outline=O, width=1) + d.ellipse((18, 18, 46, 46), outline=BRONZE_DK, width=1) + # square hole + rect(d, 28, 28, 8, 8, (30, 24, 16)) + d.rectangle((28, 28, 35, 35), outline=O, width=1) + # ghost tint + rdot(d, (24, 24), 3, GHOST_DK) + + +# ── 8. Yang life (阳寿) — warm sun disc ───────────────────────────────────── +def icon_yang(d): + # rays + for a in range(0, 360, 45): + import math + rad = math.radians(a) + x = 32 + 22 * math.sin(rad) + y = 32 - 22 * math.cos(rad) + x2 = 32 + 14 * math.sin(rad) + y2 = 32 - 14 * math.cos(rad) + d.line([(int(x2), int(y2)), (int(x), int(y))], fill=(220, 120, 50), width=2) + rdot(d, (32, 32), 12, (220, 90, 50)) + rdot(d, (32, 32), 12, None) + d.ellipse((20, 20, 44, 44), outline=O, width=1) + rdot(d, (28, 28), 4, (245, 150, 80)) + + +# ── 9. Qi / blood (气血) — red swirl orb ──────────────────────────────────── +def icon_qi(d): + rdot(d, (32, 34), 15, RED) + rdot(d, (32, 34), 15, None) + d.ellipse((17, 19, 47, 49), outline=O, width=1) + # swirl highlight + fill_poly(d, [(26, 30), (38, 26), (40, 32), (28, 38)], RED_LT) + rdot(d, (27, 28), 3, (220, 110, 100)) + + +# ── 10. Obsession (执念) — chained red knot ───────────────────────────────── +def icon_obsession(d): + # chain loop + import math + pts = [] + cx, cy = 32, 32 + for i in range(28): + a = i / 28 * math.pi * 2 + rr = 16 + 3 * math.sin(a * 3) + pts.append((cx + rr * math.cos(a), cy + rr * math.sin(a))) + for i in range(len(pts) - 1): + capsule(d, pts[i], pts[i + 1], 3, (120, 124, 134), outline=O) + # red glowing core + rdot(d, (32, 32), 7, RED) + rdot(d, (32, 32), 3, (240, 90, 70)) + # eye slit + rect(d, 29, 31, 6, 2, O) + + +ICONS = { + "icon_songdao": icon_songdao, + "icon_spear": icon_spear, + "icon_ghostfire": icon_ghostfire, + "icon_talisman": icon_talisman, + "icon_gourd": icon_gourd, + "icon_flag": icon_flag, + "icon_coin": icon_coin, + "icon_yang": icon_yang, + "icon_qi": icon_qi, + "icon_obsession": icon_obsession, +} + + +def build(): + for name, fn in ICONS.items(): + img, d = new_canvas(SZ, SZ) + fn(d) + OUT.mkdir(parents=True, exist_ok=True) + img.save(str(OUT / f"{name}.png")) + print(f" ok {name}.png") + + +if __name__ == "__main__": + build() diff --git a/assets_v2/tools/build_melee_ghost.py b/assets_v2/tools/build_melee_ghost.py new file mode 100644 index 0000000..070c603 --- /dev/null +++ b/assets_v2/tools/build_melee_ghost.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Melee Ghost (近战鬼卒). + +Hunched, heavy, dragging a rusty blade. Asymmetric broad shoulders, damaged +leg, ghost fire leaking from helmet and chest gaps. Silhouette: top-heavy, +leaning forward, not upright like the player. +""" + +from __future__ import annotations +from pathlib import Path +from pixel_engine import bake_sheet +from humanoid import humanoid_config, render_humanoid + +ROOT = Path(__file__).resolve().parent.parent.parent +OUT = ROOT / "assets_v2" / "characters" / "melee_ghost" + +MELEE_CFG = humanoid_config( + fw=96, fh=96, cx=48, hip_y=70, torso_len=24, neck_len=5, head_r=11, + upper_arm=17, lower_arm=14, arm_w=5, thigh=16, shin=14, leg_w=6, + scale=1.05, lean=14, stance=6, style="melee_ghost", torso_hw=11, torso_hw_hip=9, + weapon_len=52, ghost_fire=True, pauldrons=True, + palette={ + "skin": (100, 112, 118), "skin_lt": (125, 140, 148), "skin_sh": (68, 78, 84), + "armor": (72, 80, 84), "armor_lt": (105, 116, 122), "armor_dk": (44, 50, 54), + "cloth": (55, 65, 68), "cloth_lt": (78, 90, 94), "cloth_dk": (36, 44, 46), + "blade": (92, 78, 62), "blade_lt": (125, 105, 84), "blade_dk": (62, 52, 40), + "grip": (58, 48, 38), "guard": (88, 74, 58), + "outline": (12, 16, 18), + }, +) + +A = lambda sa, ea: (sa, ea) + +# Idle: hunched breathing + ghost-fire flicker. >=3px bob steps, no dup frames. +IDLE = [ + {"bob": 0, "arm_front": A(105, 115), "arm_back": A(95, 105), "leg_front": A(94, 95), "leg_back": A(86, 100), "weapon": 112, "fire_t": 0.0}, + {"bob": 3, "arm_front": A(104, 116), "arm_back": A(96, 106), "leg_front": A(93, 96), "leg_back": A(87, 100), "weapon": 116, "fire_t": 0.2}, + {"bob": 6, "arm_front": A(103, 117), "arm_back": A(97, 107), "leg_front": A(92, 97), "leg_back": A(88, 100), "weapon": 120, "fire_t": 0.4}, + {"bob": 3, "arm_front": A(104, 116), "arm_back": A(96, 106), "leg_front": A(93, 96), "leg_back": A(87, 100), "weapon": 117, "fire_t": 0.6}, + {"bob": 0, "arm_front": A(105, 115), "arm_back": A(95, 105), "leg_front": A(94, 95), "leg_back": A(86, 100), "weapon": 114, "fire_t": 0.8}, + {"bob": 3, "arm_front": A(104, 116), "arm_back": A(96, 106), "leg_front": A(93, 96), "leg_back": A(87, 100), "weapon": 118, "fire_t": 0.95}, +] + +WALK = [ + {"bob": 0, "arm_front": A(102, 112), "arm_back": A(98, 108), "leg_front": A(75, 105), "leg_back": A(115, 95), "weapon": 113, "fire_t": 0.0}, + {"bob": 1, "arm_front": A(104, 114), "arm_back": A(96, 106), "leg_front": A(85, 100), "leg_back": A(105, 98), "weapon": 114, "fire_t": 0.12}, + {"bob": 1, "arm_front": A(106, 116), "arm_back": A(94, 104), "leg_front": A(95, 95), "leg_back": A(95, 95), "weapon": 115, "fire_t": 0.25}, + {"bob": 0, "arm_front": A(104, 114), "arm_back": A(96, 106), "leg_front": A(105, 98), "leg_back": A(85, 100), "weapon": 114, "fire_t": 0.37}, + {"bob": 0, "arm_front": A(102, 112), "arm_back": A(98, 108), "leg_front": A(115, 95), "leg_back": A(75, 105), "weapon": 113, "fire_t": 0.5}, + {"bob": 1, "arm_front": A(104, 114), "arm_back": A(96, 106), "leg_front": A(105, 98), "leg_back": A(85, 100), "weapon": 114, "fire_t": 0.62}, + {"bob": 1, "arm_front": A(106, 116), "arm_back": A(94, 104), "leg_front": A(95, 95), "leg_back": A(95, 95), "weapon": 115, "fire_t": 0.75}, + {"bob": 0, "arm_front": A(104, 114), "arm_back": A(96, 106), "leg_front": A(85, 100), "leg_back": A(105, 98), "weapon": 114, "fire_t": 0.87}, +] + +ATTACK = [ + {"bob": 0, "arm_front": A(105, 115), "arm_back": A(95, 105), "leg_front": A(94, 95), "leg_back": A(86, 100), "weapon": 115, "fire_t": 0.0}, + {"bob": 0, "arm_front": A(110, 110), "arm_back": A(90, 100), "leg_front": A(92, 98), "leg_back": A(88, 100), "weapon": 125, "fire_t": 0.1}, + {"bob": 1, "arm_front": A(120, 115), "arm_back": A(80, 100), "leg_front": A(90, 100), "leg_back": A(90, 100), "weapon": 145, "fire_t": 0.2}, + {"bob": 2, "arm_front": A(135, 120), "arm_back": A(70, 105), "leg_front": A(88, 105), "leg_back": A(92, 100), "weapon": 165, "fire_t": 0.3}, + {"bob": 1, "arm_front": A(110, 95), "arm_back": A(85, 100), "leg_front": A(85, 100), "leg_back": A(95, 100), "weapon": 95, "fire_t": 0.4}, # active start + {"bob": 0, "arm_front": A(80, 80), "arm_back": A(95, 105), "leg_front": A(85, 98), "leg_back": A(95, 100), "weapon": 55, "fire_t": 0.5}, # active end (slam) + {"bob": 0, "arm_front": A(70, 85), "arm_back": A(100, 110), "leg_front": A(88, 95), "leg_back": A(92, 100), "weapon": 50, "fire_t": 0.6}, + {"bob": 1, "arm_front": A(90, 100), "arm_back": A(95, 105), "leg_front": A(92, 95), "leg_back": A(88, 100), "weapon": 105, "fire_t": 0.7}, +] + +HURT = [ + {"bob": 0, "arm_front": A(115, 120), "arm_back": A(75, 110), "leg_front": A(96, 95), "leg_back": A(84, 102), "weapon": 130, "fire_t": 0.0}, + {"bob": 1, "arm_front": A(125, 125), "arm_back": A(65, 115), "leg_front": A(100, 96), "leg_back": A(80, 105), "weapon": 140, "fire_t": 0.2}, + {"bob": 1, "arm_front": A(120, 122), "arm_back": A(70, 112), "leg_front": A(98, 95), "leg_back": A(82, 103), "weapon": 135, "fire_t": 0.4}, + {"bob": 0, "arm_front": A(110, 115), "arm_back": A(80, 108), "leg_front": A(94, 95), "leg_back": A(86, 101), "weapon": 120, "fire_t": 0.6}, +] + +DEATH = [ + {"bob": 0, "arm_front": A(110, 115), "arm_back": A(80, 108), "leg_front": A(94, 95), "leg_back": A(86, 101), "weapon": 120, "fire_t": 0.0}, + {"bob": 1, "arm_front": A(120, 120), "arm_back": A(70, 112), "leg_front": A(98, 96), "leg_back": A(82, 104), "weapon": 130, "fire_t": 0.1}, + {"bob": 2, "arm_front": A(130, 125), "arm_back": A(60, 118), "leg_front": A(102, 98), "leg_back": A(78, 108), "weapon": 140, "fire_t": 0.2}, + {"bob": 3, "arm_front": A(125, 130), "arm_back": A(65, 122), "leg_front": A(100, 105), "leg_back": A(80, 112), "weapon": 145, "fire_t": 0.3}, + {"bob": 4, "arm_front": A(115, 135), "arm_back": A(75, 125), "leg_front": A(95, 110), "leg_back": A(85, 115), "weapon": 150, "fire_t": 0.4}, + {"bob": 5, "arm_front": A(105, 140), "arm_back": A(85, 130), "leg_front": A(90, 115), "leg_back": A(90, 118), "weapon": 155, "fire_t": 0.5}, + {"bob": 6, "arm_front": A(100, 145), "arm_back": A(90, 135), "leg_front": A(90, 118), "leg_back": A(90, 120), "weapon": 160, "fire_t": 0.6}, + {"bob": 7, "arm_front": A(95, 150), "arm_back": A(95, 140), "leg_front": A(90, 120), "leg_back": A(90, 120), "weapon": 165, "fire_t": 0.7}, +] + +ANIMS = { + "melee_ghost_idle": (IDLE, 8.0, True), + "melee_ghost_walk": (WALK, 8.0, True), + "melee_ghost_attack": (ATTACK, 10.0, False), + "melee_ghost_hurt": (HURT, 10.0, False), + "melee_ghost_death": (DEATH, 8.0, False), +} + + +def build(): + for name, (poses, fps, loop) in ANIMS.items(): + fw, fh = MELEE_CFG["fw"], MELEE_CFG["fh"] + bake_sheet(fw, fh, len(poses), + lambda d, i, fw, fh, poses=poses: render_humanoid(d, MELEE_CFG, poses[i], fw, fh), + str(OUT / f"{name}.png")) + print(f" ✓ {name}.png ({len(poses)} frames)") + + +if __name__ == "__main__": + build() diff --git a/assets_v2/tools/build_player.py b/assets_v2/tools/build_player.py new file mode 100644 index 0000000..42a871f --- /dev/null +++ b/assets_v2/tools/build_player.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Player: nameless Yue-family infantryman (岳家军军士). + +Flexible, battle-worn Southern-Song foot soldier. Not a hero general. +Low center of gravity, single Song dao (宋刀) in the right hand, dark-red +cloth strips as the identifying marker. Living skin keeps slight warmth; +no heavy ghost fire. +""" + +from __future__ import annotations +from pathlib import Path +from PIL import ImageDraw +from pixel_engine import new_canvas, bake_sheet +from humanoid import humanoid_config, render_humanoid + +ROOT = Path(__file__).resolve().parent.parent.parent +OUT = ROOT / "assets_v2" / "characters" / "player" + +PLAYER_CFG = humanoid_config( + fw=96, fh=96, cx=48, hip_y=66, torso_len=26, neck_len=5, head_r=9, + upper_arm=16, lower_arm=15, arm_w=4, thigh=17, shin=16, leg_w=5, + scale=1.05, lean=3, stance=4, style="player", torso_hw=9, torso_hw_hip=7, + weapon_len=46, + palette={ + "skin": (205, 170, 140), "skin_lt": (232, 198, 168), "skin_sh": (150, 116, 92), + "armor": (96, 87, 81), "armor_lt": (142, 130, 119), "armor_dk": (58, 52, 48), + "cloth": (128, 40, 40), "cloth_lt": (168, 62, 56), "cloth_dk": (74, 24, 24), + "blade": (152, 158, 168), "blade_lt": (208, 213, 222), "blade_dk": (96, 101, 111), + "grip": (72, 54, 40), "guard": (132, 102, 74), + "outline": (18, 14, 16), + }, +) + +# angle convention: 0=down, +90=right, -90=left, 180=up +A = lambda sa, ea: (sa, ea) + +# Idle: pronounced breathing + weight shift + sabre sway. Consecutive frames +# always differ by >=3px bob (or large weapon/limb delta) so no near-duplicate +# frames (V1 failure mode). +IDLE = [ + {"bob": 0, "arm_front": A(82, 96), "arm_back": A(98, 104), "leg_front": A(91, 93), "leg_back": A(89, 95), "weapon": 66, "cloth_sway": 0, "lean": 0}, + {"bob": 3, "arm_front": A(80, 95), "arm_back": A(100, 103), "leg_front": A(90, 94), "leg_back": A(90, 96), "weapon": 70, "cloth_sway": 1, "lean": 0}, + {"bob": 6, "arm_front": A(78, 94), "arm_back": A(102, 102), "leg_front": A(89, 95), "leg_back": A(91, 97), "weapon": 74, "cloth_sway": 2, "lean": 0}, + {"bob": 3, "arm_front": A(80, 95), "arm_back": A(100, 103), "leg_front": A(90, 94), "leg_back": A(90, 96), "weapon": 72, "cloth_sway": 1, "lean": 0}, + {"bob": 0, "arm_front": A(83, 96), "arm_back": A(97, 104), "leg_front": A(92, 93), "leg_back": A(88, 95), "weapon": 68, "cloth_sway": 0, "lean": 0}, + {"bob": 3, "arm_front": A(85, 97), "arm_back": A(95, 105), "leg_front": A(93, 92), "leg_back": A(87, 94), "weapon": 71, "cloth_sway": 1, "lean": 0}, + {"bob": 6, "arm_front": A(81, 95), "arm_back": A(99, 103), "leg_front": A(90, 95), "leg_back": A(91, 97), "weapon": 75, "cloth_sway": 2, "lean": 0}, + {"bob": 3, "arm_front": A(82, 96), "arm_back": A(98, 104), "leg_front": A(91, 94), "leg_back": A(89, 96), "weapon": 73, "cloth_sway": 1, "lean": 0}, +] + +RUN = [ + {"bob": 1, "lean": 9, "arm_front": A(70, 100), "arm_back": A(110, 96), "leg_front": A(68, 102), "leg_back": A(112, 96), "weapon": 60, "cloth_sway": 3}, + {"bob": 0, "lean": 9, "arm_front": A(74, 100), "arm_back": A(106, 98), "leg_front": A(78, 96), "leg_back": A(106, 100), "weapon": 58, "cloth_sway": 4}, + {"bob": 0, "lean": 9, "arm_front": A(80, 98), "arm_back": A(100, 100), "leg_front": A(90, 95), "leg_back": A(90, 95), "weapon": 56, "cloth_sway": 3}, + {"bob": 1, "lean": 9, "arm_front": A(86, 100), "arm_back": A(94, 102), "leg_front": A(110, 96), "leg_back": A(70, 100), "weapon": 58, "cloth_sway": 2}, + {"bob": 1, "lean": 9, "arm_front": A(88, 100), "arm_back": A(92, 102), "leg_front": A(112, 96), "leg_back": A(68, 102), "weapon": 60, "cloth_sway": 3}, + {"bob": 0, "lean": 9, "arm_front": A(84, 100), "arm_back": A(96, 100), "leg_front": A(106, 98), "leg_back": A(78, 96), "weapon": 58, "cloth_sway": 4}, + {"bob": 0, "lean": 9, "arm_front": A(80, 98), "arm_back": A(100, 100), "leg_front": A(90, 95), "leg_back": A(90, 95), "weapon": 56, "cloth_sway": 3}, + {"bob": 1, "lean": 9, "arm_front": A(74, 100), "arm_back": A(106, 98), "leg_front": A(70, 102), "leg_back": A(110, 96), "weapon": 58, "cloth_sway": 2}, +] + +JUMP = [ + {"bob": 2, "lean": 10, "arm_front": A(60, 80), "arm_back": A(120, 90), "leg_front": A(80, 120), "leg_back": A(100, 120), "weapon": 150, "cloth_sway": 2}, + {"bob": 2, "lean": 6, "arm_front": A(55, 70), "arm_back": A(125, 80), "leg_front": A(70, 135), "leg_back": A(110, 130), "weapon": 165, "cloth_sway": 3}, + {"bob": 2, "lean": 12, "arm_front": A(65, 85), "arm_back": A(115, 95), "leg_front": A(95, 110), "leg_back": A(85, 112), "weapon": 155, "cloth_sway": 2}, +] + +FALL = [ + {"bob": 1, "lean": -4, "arm_front": A(95, 100), "arm_back": A(85, 100), "leg_front": A(100, 100), "leg_back": A(80, 100), "weapon": 50, "cloth_sway": 3}, + {"bob": 1, "lean": -6, "arm_front": A(100, 105), "arm_back": A(80, 105), "leg_front": A(105, 108), "leg_back": A(75, 108), "weapon": 45, "cloth_sway": 4}, + {"bob": 1, "lean": -5, "arm_front": A(98, 102), "arm_back": A(82, 102), "leg_front": A(102, 104), "leg_back": A(78, 104), "weapon": 48, "cloth_sway": 3}, +] + +# Light attack 1: horizontal sabre slash, right-to-left sweep ending forward +LIGHT_ATTACK_1 = [ + {"bob": 0, "lean": 2, "arm_front": A(84, 96), "arm_back": A(96, 100), "leg_front": A(90, 92), "leg_back": A(90, 96), "weapon": 70, "cloth_sway": 0}, # 0 neutral + {"bob": 1, "lean": -4, "arm_front": A(95, 100), "arm_back": A(90, 100), "leg_front": A(86, 96), "leg_back": A(94, 98), "weapon": 110, "cloth_sway": 1}, # 1 sink + {"bob": 1, "lean": -6, "arm_front": A(120, 120), "arm_back": A(85, 100), "leg_front": A(84, 98), "leg_back": A(96, 98), "weapon": 155, "cloth_sway": 1}, # 2 draw back + {"bob": 0, "lean": 4, "arm_front": A(150, 130), "arm_back": A(80, 100), "leg_front": A(82, 96), "leg_back": A(98, 98), "weapon": 175, "cloth_sway": 2}, # 3 waist turn + {"bob": 0, "lean": 8, "arm_front": A(60, 70), "arm_back": A(95, 100), "leg_front": A(80, 95), "leg_back": A(100, 100), "weapon": 20, "cloth_sway": 3}, # 4 ACTIVE start + {"bob": 0, "lean": 10, "arm_front": A(40, 60), "arm_back": A(98, 102), "leg_front": A(78, 95), "leg_back": A(102, 100), "weapon": 0, "cloth_sway": 4}, # 5 ACTIVE end (max reach) + {"bob": 0, "lean": 6, "arm_front": A(20, 70), "arm_back": A(100, 104), "leg_front": A(80, 95), "leg_back": A(100, 100), "weapon": -25, "cloth_sway": 3}, # 6 follow + {"bob": 0, "lean": 2, "arm_front": A(70, 92), "arm_back": A(98, 102), "leg_front": A(90, 93), "leg_back": A(90, 96), "weapon": 60, "cloth_sway": 1}, # 7 recover +] + +# Light attack 2: reverse diagonal / back horizontal slash (opposite rhythm) +LIGHT_ATTACK_2 = [ + {"bob": 0, "lean": 2, "arm_front": A(84, 96), "arm_back": A(96, 100), "leg_front": A(90, 92), "leg_back": A(90, 96), "weapon": 70, "cloth_sway": 0}, # 0 neutral + {"bob": 1, "lean": 6, "arm_front": A(60, 80), "arm_back": A(96, 100), "leg_front": A(80, 95), "leg_back": A(100, 100), "weapon": -10, "cloth_sway": 1}, # 1 wind forward + {"bob": 1, "lean": 8, "arm_front": A(30, 70), "arm_back": A(98, 102), "leg_front": A(78, 95), "leg_back": A(102, 100), "weapon": -40, "cloth_sway": 2}, # 2 raise + {"bob": 0, "lean": 2, "arm_front": A(10, 75), "arm_back": A(100, 104), "leg_front": A(82, 96), "leg_back": A(98, 98), "weapon": -70, "cloth_sway": 2}, # 3 high + {"bob": 0, "lean": -5, "arm_front": A(150, 80), "arm_back": A(90, 100), "leg_front": A(88, 96), "leg_back": A(92, 98), "weapon": 160, "cloth_sway": 3}, # 4 ACTIVE start (overhead to back) + {"bob": 0, "lean": -7, "arm_front": A(175, 100), "arm_back": A(85, 100), "leg_front": A(92, 98), "leg_back": A(88, 96), "weapon": 190, "cloth_sway": 4}, # 5 ACTIVE end (behind) + {"bob": 0, "lean": -3, "arm_front": A(160, 110), "arm_back": A(90, 100), "leg_front": A(90, 96), "leg_back": A(90, 96), "weapon": 200, "cloth_sway": 3}, # 6 follow + {"bob": 0, "lean": 2, "arm_front": A(84, 96), "arm_back": A(96, 100), "leg_front": A(90, 93), "leg_back": A(90, 96), "weapon": 70, "cloth_sway": 1}, # 7 recover +] + +# Heavy attack: big wind-up, slow, late hit +HEAVY_ATTACK = [ + {"bob": 0, "lean": 2, "arm_front": A(84, 96), "arm_back": A(96, 100), "leg_front": A(90, 92), "leg_back": A(90, 96), "weapon": 70, "cloth_sway": 0}, # 0 neutral + {"bob": 2, "lean": -8, "arm_front": A(95, 100), "arm_back": A(90, 100), "leg_front": A(84, 100), "leg_back": A(96, 100), "weapon": 100, "cloth_sway": 1}, # 1 crouch + {"bob": 3, "lean": -12, "arm_front": A(120, 120), "arm_back": A(85, 100), "leg_front": A(80, 105), "leg_back": A(98, 102), "weapon": 140, "cloth_sway": 1}, # 2 deep sink + {"bob": 3, "lean": -14, "arm_front": A(150, 140), "arm_back": A(82, 100), "leg_front": A(78, 108), "leg_back": A(100, 104), "weapon": 170, "cloth_sway": 2}, # 3 draw way back + {"bob": 2, "lean": -10, "arm_front": A(165, 150), "arm_back": A(80, 100), "leg_front": A(80, 104), "leg_back": A(98, 102), "weapon": 185, "cloth_sway": 2}, # 4 coil peak + {"bob": 0, "lean": 12, "arm_front": A(30, 70), "arm_back": A(98, 104), "leg_front": A(76, 95), "leg_back": A(104, 100), "weapon": -10, "cloth_sway": 4}, # 5 RELEASE + {"bob": 0, "lean": 16, "arm_front": A(10, 60), "arm_back": A(100, 106), "leg_front": A(74, 95), "leg_back": A(106, 100), "weapon": -40, "cloth_sway": 5}, # 6 ACTIVE (max reach) + {"bob": 0, "lean": 10, "arm_front": A(0, 65), "arm_back": A(102, 108), "leg_front": A(78, 96), "leg_back": A(102, 100), "weapon": -55, "cloth_sway": 4}, # 7 follow + {"bob": 1, "lean": 4, "arm_front": A(40, 85), "arm_back": A(100, 104), "leg_front": A(84, 96), "leg_back": A(96, 98), "weapon": 10, "cloth_sway": 2}, # 8 recover 1 + {"bob": 0, "lean": 2, "arm_front": A(84, 96), "arm_back": A(96, 100), "leg_front": A(90, 93), "leg_back": A(90, 96), "weapon": 70, "cloth_sway": 1}, # 9 recover 2 +] + +HURT = [ + {"bob": 0, "lean": -8, "arm_front": A(110, 110), "arm_back": A(70, 110), "leg_front": A(95, 95), "leg_back": A(85, 100), "weapon": 120, "cloth_sway": 2}, # 0 recoil + {"bob": 0, "lean": -10, "arm_front": A(120, 115), "arm_back": A(65, 115), "leg_front": A(98, 96), "leg_back": A(82, 102), "weapon": 130, "cloth_sway": 3}, # 1 knocked + {"bob": 1, "lean": -6, "arm_front": A(110, 110), "arm_back": A(70, 110), "leg_front": A(95, 95), "leg_back": A(85, 100), "weapon": 120, "cloth_sway": 2}, # 2 settle + {"bob": 0, "lean": -3, "arm_front": A(100, 105), "arm_back": A(80, 108), "leg_front": A(92, 94), "leg_back": A(88, 98), "weapon": 100, "cloth_sway": 1}, # 3 recover +] + +DEATH = [ + {"bob": 0, "lean": -6, "arm_front": A(100, 105), "arm_back": A(80, 108), "leg_front": A(92, 94), "leg_back": A(88, 98), "weapon": 100, "cloth_sway": 1}, # 0 hit + {"bob": 1, "lean": -10, "arm_front": A(115, 115), "arm_back": A(65, 115), "leg_front": A(98, 96), "leg_back": A(82, 102), "weapon": 120, "cloth_sway": 2}, # 1 stagger + {"bob": 1, "lean": -14, "arm_front": A(125, 120), "arm_back": A(60, 120), "leg_front": A(102, 98), "leg_back": A(80, 105), "weapon": 135, "cloth_sway": 2}, # 2 lose balance + {"bob": 2, "lean": -16, "arm_front": A(130, 125), "arm_back": A(55, 125), "leg_front": A(105, 100), "leg_back": A(78, 108), "weapon": 145, "cloth_sway": 1}, # 3 fall back + {"bob": 4, "lean": -18, "arm_front": A(120, 130), "arm_back": A(60, 130), "leg_front": A(100, 105), "leg_back": A(80, 110), "weapon": 150, "cloth_sway": 0}, # 4 kneel + {"bob": 6, "lean": -20, "arm_front": A(110, 135), "arm_back": A(70, 135), "leg_front": A(95, 110), "leg_back": A(85, 112), "weapon": 158, "cloth_sway": 0}, # 5 weapon drops + {"bob": 8, "lean": -22, "arm_front": A(100, 140), "arm_back": A(80, 140), "leg_front": A(90, 115), "leg_back": A(90, 115), "weapon": 165, "cloth_sway": 0}, # 6 down + {"bob": 10, "lean": -24, "arm_front": A(95, 145), "arm_back": A(85, 145), "leg_front": A(90, 118), "leg_back": A(90, 118), "weapon": 170, "cloth_sway": 0}, # 7 collapse + {"bob": 12, "lean": -26, "arm_front": A(90, 150), "arm_back": A(90, 150), "leg_front": A(90, 120), "leg_back": A(90, 120), "weapon": 175, "cloth_sway": 0}, # 8 corpse + {"bob": 12, "lean": -26, "arm_front": A(89, 152), "arm_back": A(91, 148), "leg_front": A(88, 122), "leg_back": A(92, 118), "weapon": 186, "cloth_sway": 0}, # 9 corpse hold (distinct from f8) +] + +ANIMS = { + "player_idle": (IDLE, 8.0, True), + "player_run": (RUN, 12.0, True), + "player_jump": (JUMP, 10.0, False), + "player_fall": (FALL, 10.0, False), + "player_light_attack_1": (LIGHT_ATTACK_1, 12.0, False), + "player_light_attack_2": (LIGHT_ATTACK_2, 12.0, False), + "player_heavy_attack": (HEAVY_ATTACK, 10.0, False), + "player_hurt": (HURT, 10.0, False), + "player_death": (DEATH, 8.0, False), +} + + +def build(): + for name, (poses, fps, loop) in ANIMS.items(): + fw, fh = PLAYER_CFG["fw"], PLAYER_CFG["fh"] + bake_sheet(fw, fh, len(poses), + lambda d, i, fw, fh, poses=poses: render_humanoid(d, PLAYER_CFG, poses[i], fw, fh), + str(OUT / f"{name}.png")) + print(f" ✓ {name}.png ({len(poses)} frames)") + + +if __name__ == "__main__": + build() diff --git a/assets_v2/tools/build_projectiles.py b/assets_v2/tools/build_projectiles.py new file mode 100644 index 0000000..7b647a0 --- /dev/null +++ b/assets_v2/tools/build_projectiles.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Projectiles (32x32), transparent background. + +ghost_fire_arrow: a ghost-fire tipped arrow pointing RIGHT (flip in-engine for +left travel). Green flame head + trailing wisp. +""" + +from __future__ import annotations +from pathlib import Path +from PIL import Image, ImageDraw +from pixel_engine import new_canvas, fill_circle, fill_poly, ghost_fire + +ROOT = Path(__file__).resolve().parent.parent.parent +OUT = ROOT / "assets_v2" / "projectiles" +SZ = 32 +O = (14, 12, 16) +STEEL = (150, 156, 168) +STEEL_LT = (205, 210, 220) +GHOST = (110, 240, 160) +GHOST_DK = (30, 150, 100) + + +def build(): + img, d = new_canvas(SZ, SZ) + # shaft + d.line([(6, 18), (24, 18)], fill=STEEL, width=2) + d.line([(6, 16), (24, 16)], fill=STEEL_LT, width=1) + # arrowhead (right) + head = [(24, 18), (30, 14), (30, 22)] + fill_poly(d, head, STEEL) + fill_poly(d, [(24, 18), (30, 14), (29, 16)], STEEL_LT) + d.line([(24, 18), (30, 14), (30, 22), (24, 18)], fill=O, width=1) + # fletching + fill_poly(d, [(6, 18), (2, 13), (4, 18)], GHOST_DK) + fill_poly(d, [(6, 18), (2, 23), (4, 18)], GHOST_DK) + # ghost-fire wisp trailing left + ghost_fire(d, 12, 18, 7, t=0.3) + fill_circle(d, (24, 18), 2, GHOST) + OUT.mkdir(parents=True, exist_ok=True) + img.save(str(OUT / "ghost_fire_arrow.png")) + print(" ok ghost_fire_arrow.png (32x32)") + + +if __name__ == "__main__": + build() diff --git a/assets_v2/tools/gen_metadata.py b/assets_v2/tools/gen_metadata.py new file mode 100644 index 0000000..219f1b0 --- /dev/null +++ b/assets_v2/tools/gen_metadata.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Generate frame_map.json, metadata.md, and Godot .tres. + +Run from repo root: python3 assets_v2/tools/gen_metadata.py + +Produces, per character directory: + * frame_map.json — startup/active/recovery phases, pivot, foot_position, + projectile_release_frame / charge_ready_frame. + * metadata.md — human-readable animation + spec sheet. +And into assets_v2/godot/: + * _frames.tres — SpriteFrames resources (mirror of tools/generate_sprite_frames.py + but for assets_v2 paths/sizes; AtlasTexture + horizontal strips). + +Phase boundaries are transcribed from the pose comments in the build_*.py files. +""" + +from __future__ import annotations +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent.parent +CHAR_DIR = ROOT / "assets_v2" / "characters" +GODOT_DIR = ROOT / "assets_v2" / "godot" + +# fmt: char -> dict(frame_w, frame_h, pivot, foot, anims) +# anim -> dict(frames, fps, loop, startup, active, recovery, projectile_release_frame, +# charge_ready_frame, note) +CHARS = { + "player": { + "frame_w": 96, "frame_h": 96, "pivot": [48, 66], "foot": [48, 88], + "anims": { + "player_idle": (8, 8.0, True, [], [], [], None, None, "breathing idle"), + "player_run": (8, 12.0, True, [], [], [], None, None, "forward run, cloth trailing"), + "player_jump": (3, 10.0, False, [], [], [], None, None, "rise"), + "player_fall": (3, 10.0, False, [], [], [], None, None, "descent"), + "player_light_attack_1": (8, 12.0, False, [0,1,2,3], [4,5], [6,7], None, None, "horizontal sabre slash R->L"), + "player_light_attack_2": (8, 12.0, False, [0,1,2,3], [4,5], [6,7], None, None, "reverse diagonal overhead->back"), + "player_heavy_attack": (10, 10.0, False, [0,1,2,3,4,5], [6,7], [8,9], None, None, "deep coil, late big hit"), + "player_hurt": (4, 10.0, False, [], [], [], None, None, "recoil"), + "player_death": (10, 8.0, False, [], [], [], None, None, "stagger and collapse"), + }, + }, + "melee_ghost": { + "frame_w": 96, "frame_h": 96, "pivot": [48, 70], "foot": [48, 90], + "anims": { + "melee_ghost_idle": (6, 8.0, True, [], [], [], None, None, "hunched sway, ghost fire leak"), + "melee_ghost_walk": (8, 8.0, True, [], [], [], None, None, "lumbering gait"), + "melee_ghost_attack": (8, 10.0, False, [0,1,2,3], [4,5], [6,7], None, None, "overhead slam"), + "melee_ghost_hurt": (4, 10.0, False, [], [], [], None, None, "recoil"), + "melee_ghost_death": (8, 8.0, False, [], [], [], None, None, "dissolve"), + }, + }, + "ghost_archer": { + "frame_w": 96, "frame_h": 96, "pivot": [48, 64], "foot": [48, 88], + "anims": { + "ghost_archer_idle": (6, 8.0, True, [], [], [], None, None, "still draw, ghost fire"), + "ghost_archer_retreat": (8, 10.0, True, [], [], [], None, None, "back-step kiting"), + "ghost_archer_aim": (6, 8.0, False, [0,1,2,3,4], [], [5], None, 5, "draw bow, charge_ready at 5"), + "ghost_archer_shoot": (4, 14.0, False, [0], [1], [2,3], 1, None, "release; projectile at frame 1"), + "ghost_archer_hurt": (4, 10.0, False, [], [], [], None, None, "recoil"), + "ghost_archer_death": (8, 8.0, False, [], [], [], None, None, "dissolve"), + }, + }, + "corpse_beast": { + "frame_w": 128, "frame_h": 96, "pivot": [64, 58], "foot": [64, 86], + "anims": { + "corpse_beast_idle": (6, 5.0, True, [], [], [], None, None, "low breathing"), + "corpse_beast_run": (8, 10.0, True, [], [], [], None, None, "gallop"), + "corpse_beast_charge_windup": (6, 8.0, False, [0,1,2,3,4], [], [5], None, 5, "coil; charge_ready at 5"), + "corpse_beast_charge": (4, 12.0, False, [], [0,1,2,3], [], None, None, "full-speed contact"), + "corpse_beast_wall_impact": (4, 8.0, False, [], [0,1], [2,3], None, None, "slam into wall"), + "corpse_beast_hurt": (4, 10.0, False, [], [], [], None, None, "scramble"), + "corpse_beast_death": (8, 6.0, False, [], [], [], None, None, "collapse"), + }, + }, + "gate_warden": { + "frame_w": 192, "frame_h": 192, "pivot": [96, 128], "foot": [96, 176], + "anims": { + "gate_warden_idle": (8, 6.0, True, [], [], [], None, None, "heavy breathing"), + "gate_warden_walk": (8, 8.0, True, [], [], [], None, None, "earth-shaking steps"), + "gate_warden_attack_1": (10, 9.0, False, [0,1,2,3], [4,5], [6,7,8,9], None, None, "horizontal great-blade sweep"), + "gate_warden_attack_2": (12, 8.0, False, [0,1,2,3,4,5], [6,7], [8,9,10,11], None, None, "overhead ground pound"), + "gate_warden_hurt": (4, 8.0, False, [], [], [], None, None, "recoil"), + "gate_warden_death": (12, 6.0, False, [], [], [], None, None, "colossal collapse"), + }, + }, +} + + +def write_frame_maps(): + for char, spec in CHARS.items(): + anims = {} + for name, (frames, fps, loop, startup, active, recovery, proj, ready, note) in spec["anims"].items(): + anims[name] = { + "frames": frames, + "fps": fps, + "loop": loop, + "phases": { + "startup": startup, + "active": active, + "recovery": recovery, + }, + "pivot": spec["pivot"], + "foot_position": spec["foot"], + "frame_size": [spec["frame_w"], spec["frame_h"]], + "projectile_release_frame": proj, + "charge_ready_frame": ready, + "note": note, + } + data = { + "character": char, + "frame_size": [spec["frame_w"], spec["frame_h"]], + "pivot": spec["pivot"], + "foot_position": spec["foot"], + "animations": anims, + } + out = CHAR_DIR / char / "frame_map.json" + out.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8") + print(f" ok {char}/frame_map.json ({len(anims)} anims)") + + +def write_metadata(): + for char, spec in CHARS.items(): + lines = [f"# {char} — Animation & Spec Sheet (V2)", ""] + lines.append(f"- **Frame size:** {spec['frame_w']}x{spec['frame_h']} px") + lines.append(f"- **Pivot (rotation/hitbox anchor):** {spec['pivot']}") + lines.append(f"- **Foot baseline (y):** {spec['foot'][1]}") + lines.append("") + lines.append("| Animation | Frames | FPS | Loop | Startup | Active | Recovery | Proj/Charge |") + lines.append("|---|---|---|---|---|---|---|---|") + for name, (frames, fps, loop, startup, active, recovery, proj, ready, note) in spec["anims"].items(): + su = ",".join(map(str, startup)) or "-" + ac = ",".join(map(str, active)) or "-" + rc = ",".join(map(str, recovery)) or "-" + pc = proj if proj is not None else (f"ready@{ready}" if ready is not None else "-") + lines.append(f"| {name} | {frames} | {fps} | {loop} | {su} | {ac} | {rc} | {pc} |") + lines.append("") + lines.append("**Legend:** frames are 0-indexed. `Active` = hitbox/projectile window. " + "`projectile_release_frame` = frame the ghost arrow spawns. " + "`charge_ready_frame` = frame a charge/aim state is fully wound (transition out).") + lines.append("") + lines.append(f"Generated by `assets_v2/tools/gen_metadata.py`. Source: pose comments in " + f"`assets_v2/tools/build_*.py`.") + out = CHAR_DIR / char / "metadata.md" + out.write_text("\n".join(lines), encoding="utf-8") + print(f" ok {char}/metadata.md") + + +# ── V2 SpriteFrames .tres (mirror of tools/generate_sprite_frames.py) ──────── +HEADER = ( + "; Generated by assets_v2/tools/gen_metadata.py for the V2 (Steam Alpha) art set.\n" + "; Do not hand-edit: re-run the generator instead.\n" +) + + +def build_tres(char: str, spec: dict) -> str: + fw, fh = spec["frame_w"], spec["frame_h"] + anims = spec["anims"] + ext_lines, sub_lines, entries = [], [], [] + for idx, (name, (frames, fps, loop, *_)) in enumerate(anims.items()): + ext_id = f"{idx + 1}_{name}" + sheet = f"assets_v2/characters/{char}/{name}.png" + ext_lines.append(f'[ext_resource type="Texture2D" path="res://{sheet}" id="{ext_id}"]') + refs = [] + for f in range(frames): + sub_id = f"AtlasTexture_{name}_{f}" + sub_lines.append( + f'[sub_resource type="AtlasTexture" id="{sub_id}"]\n' + f'atlas = ExtResource("{ext_id}")\n' + f"region = Rect2({f * fw}, 0, {fw}, {fh})" + ) + refs.append('{\n"duration": 1.0,\n' f'"texture": SubResource("{sub_id}")\n}}') + entries.append( + "{\n" + f'"frames": [{", ".join(refs)}],\n' + f'"loop": {"true" if loop else "false"},\n' + f'"name": &"{name}",\n' f'"speed": {fps}\n}}' + ) + load_steps = len(ext_lines) + len(sub_lines) + 1 + return "\n".join([ + HEADER + f'[gd_resource type="SpriteFrames" load_steps={load_steps} format=3]', + "", "\n".join(ext_lines), "", "\n\n".join(sub_lines), "", + "[resource]", "animations = [" + ", ".join(entries) + "]", "", + ]) + + +def write_tres(): + GODOT_DIR.mkdir(parents=True, exist_ok=True) + for char, spec in CHARS.items(): + out = GODOT_DIR / f"{char}_frames.tres" + out.write_text(build_tres(char, spec), encoding="utf-8") + print(f" ok godot/{char}_frames.tres") + + +if __name__ == "__main__": + print("frame_map.json:") + write_frame_maps() + print("metadata.md:") + write_metadata() + print(".tres:") + write_tres() + print("done.") diff --git a/assets_v2/tools/humanoid.py b/assets_v2/tools/humanoid.py new file mode 100644 index 0000000..b4b2523 --- /dev/null +++ b/assets_v2/tools/humanoid.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Humanoid character renderer (enhanced). + +Shared skeletal renderer for upright humanoid characters (player, melee ghost, +ghost archer). Produces pixel-art sprites with: + + * stable foot baseline + * constant weapon geometry + * distinct silhouettes per config + * top-left light shading + * armor/cloth/weapon detail layers +""" + +from __future__ import annotations +import math +from pixel_engine import capsule, fill_circle, fill_poly, ghost_fire, ghost_fire_eye, _r + + +def humanoid_config(**over): + cfg = { + "fw": 96, "fh": 96, + "cx": 48, + "hip_y": 66, + "scale": 1.0, + "torso_len": 26, "neck_len": 5, "head_r": 9, + "upper_arm": 16, "lower_arm": 15, "arm_w": 4, + "thigh": 17, "shin": 16, "leg_w": 5, + "lean": 0, "stance": 4, + "style": "player", + "palette": {}, + "ghost_fire": False, + "pauldrons": True, + "torso_hw": 9, + "torso_hw_hip": 7, + "weapon_len": 44, + } + cfg.update(over) + s = cfg["scale"] + for k in ["torso_len", "neck_len", "head_r", "upper_arm", "lower_arm", "arm_w", + "thigh", "shin", "leg_w", "stance", "torso_hw", "torso_hw_hip", "weapon_len"]: + cfg[k] = cfg[k] * s + return cfg + + +def _seg(joint, length, angle_deg): + a = math.radians(angle_deg) + return (joint[0] + length * math.sin(a), joint[1] + length * math.cos(a)) + + +def _perp(v): + L = math.hypot(v[0], v[1]) + return (-v[1] / L, v[0] / L) + + +def solve_joints(cfg, pose): + bob = pose.get("bob", 0) + lean = cfg["lean"] + pose.get("lean", 0) + hip = (cfg["cx"], cfg["hip_y"] + bob) + torso_angle = 180 - lean + shoulder = _seg(hip, cfg["torso_len"], torso_angle) + neck = _seg(shoulder, cfg["neck_len"], torso_angle) + head_c = _seg(neck, cfg["head_r"] + 2, torso_angle) + + sh_b = (shoulder[0] - cfg["arm_w"] * 0.6, shoulder[1]) + sh_f = (shoulder[0] + cfg["arm_w"] * 0.6, shoulder[1]) + hip_b = (hip[0] - cfg["stance"], hip[1]) + hip_f = (hip[0] + cfg["stance"], hip[1]) + + def arm(sh, p): + ea = _seg(sh, cfg["upper_arm"], p[0]) + ha = _seg(ea, cfg["lower_arm"], p[1]) + return sh, ea, ha + def leg(hp, p): + kn = _seg(hp, cfg["thigh"], p[0]) + ft = _seg(kn, cfg["shin"], p[1]) + return hp, kn, ft + + return { + "hip": hip, "shoulder": shoulder, "neck": neck, "head_c": head_c, + "arms": {"back": arm(sh_b, pose.get("arm_back", (95, 100))), + "front": arm(sh_f, pose.get("arm_front", (85, 95)))}, + "legs": {"back": leg(hip_b, pose.get("leg_back", (88, 92))), + "front": leg(hip_f, pose.get("leg_front", (92, 88)))}, + "lean": lean, "bob": bob, "torso_angle": torso_angle, + } + + +def _draw_arm(draw, cfg, pal, p_arm, p_elbow, p_hand, side): + base, lt, out = pal["skin"], pal["skin_lt"], pal["outline"] + if side == "back": + base, lt = pal["skin_sh"], pal["skin_sh"] + aw = cfg["arm_w"] + capsule(draw, p_arm, p_elbow, aw, base, outline=out, light=lt) + capsule(draw, p_elbow, p_hand, max(2, aw - 1), base, outline=out, light=lt) + fill_circle(draw, p_hand, aw - 1, base) + draw.ellipse((_r(p_hand)[0] - aw, _r(p_hand)[1] - aw, + _r(p_hand)[0] + aw + 1, _r(p_hand)[1] + aw + 1), outline=out) + + +def _draw_leg(draw, cfg, pal, p_hip, p_knee, p_foot, side): + base, lt, out = pal["armor"], pal["armor_lt"], pal["outline"] + if side == "back": + base = pal["armor_dk"] + w = cfg["leg_w"] + capsule(draw, p_hip, p_knee, w, base, outline=out, light=lt) + capsule(draw, p_knee, p_foot, max(2, w - 1), base, outline=out, light=lt) + # knee pad + fill_circle(draw, p_knee, int(w * 0.9) + 1, pal["armor_dk"]) + fill_circle(draw, p_knee, int(w * 0.6), pal["armor_lt"]) + # boot + fd = (p_foot[0] - p_knee[0], p_foot[1] - p_knee[1]) + fl = math.hypot(*fd) or 1 + fx, fy = fd[0] / fl, fd[1] / fl + nx, ny = -fy, fx + boot_pts = [ + (p_foot[0] - nx * (w - 1), p_foot[1] - ny * (w - 1)), + (p_foot[0] + nx * (w - 1), p_foot[1] + ny * (w - 1)), + (p_foot[0] + nx * (w - 1) + fx * 7, p_foot[1] + ny * (w - 1) + fy * 7), + (p_foot[0] - nx * (w - 1) + fx * 7, p_foot[1] - ny * (w - 1) + fy * 7), + ] + fill_poly(draw, boot_pts, pal.get("boot", base)) + draw.line(boot_pts + [boot_pts[0]], fill=out, width=1) + + +def _draw_torso(draw, j, cfg, pal): + hip, sh, neck = j["hip"], j["shoulder"], j["neck"] + ta = j["torso_angle"] + hw = cfg["torso_hw"] + hw_h = cfg["torso_hw_hip"] + perp = (math.sin(math.radians(ta + 90)), math.cos(math.radians(ta + 90))) + top_l = (sh[0] + perp[0] * hw, sh[1] + perp[1] * hw) + top_r = (sh[0] - perp[0] * hw, sh[1] - perp[1] * hw) + bot_l = (hip[0] + perp[0] * hw_h, hip[1] + perp[1] * hw_h) + bot_r = (hip[0] - perp[0] * hw_h, hip[1] - perp[1] * hw_h) + fill_poly(draw, [top_l, top_r, bot_r, bot_l], pal["armor_dk"]) + fill_poly(draw, [top_l, top_r, (sh[0], sh[1]), (hip[0], hip[1]), bot_l], pal["armor"]) + # chest plate (central lighter panel) + ct = (sh[0] + perp[0] * (hw * 0.35), sh[1] + perp[1] * (hw * 0.35)) + cb = (hip[0] + perp[0] * (hw_h * 0.35), hip[1] + perp[1] * (hw_h * 0.35)) + fill_poly(draw, [ct, (sh[0] - perp[0] * (hw * 0.25), sh[1] - perp[1] * (hw * 0.25)), + (hip[0] - perp[0] * (hw_h * 0.25), hip[1] - perp[1] * (hw_h * 0.25)), cb], pal["armor_lt"]) + # outline + for a, b in [(top_l, top_r), (top_r, bot_r), (bot_r, bot_l), (bot_l, top_l)]: + draw.line([_r(a), _r(b)], fill=pal["outline"]) + # pauldrons + if cfg.get("pauldrons"): + pl = (sh[0] + perp[0] * (hw + 1), sh[1] + perp[1] * (hw + 1)) + pr = (sh[0] - perp[0] * (hw + 1), sh[1] - perp[1] * (hw + 1)) + if cfg.get("asymmetric_pauldrons"): + # left (front) massive, right damaged/small + fill_circle(draw, pl, int(hw * 1.0), pal["armor_dk"]) + fill_circle(draw, (pl[0] - 1, pl[1] - 1), int(hw * 0.7), pal.get("gold", pal["armor_lt"])) + fill_poly(draw, [pr, (pr[0] + 2, pr[1] - 2), (pr[0] + 3, pr[1] + 3), (pr[0] - 1, pr[1] + 2)], pal["armor_dk"]) + else: + for p in [pl, pr]: + fill_circle(draw, p, int(hw * 0.65), pal["armor_dk"]) + fill_circle(draw, (p[0] - 1, p[1] - 1), int(hw * 0.4), pal["armor_lt"]) + # belt / cloth + if "cloth" in pal: + bx = hip[0] + perp[0] * (hw_h + 1) + by = hip[1] + perp[1] * (hw_h + 1) + fill_circle(draw, (bx, by), 3, pal["cloth"]) + fill_circle(draw, (hip[0] - perp[0] * (hw_h + 1), hip[1] - perp[1] * (hw_h + 1)), 3, pal["cloth"]) + + +def _draw_head(draw, j, cfg, pal, pose): + head_c = j["head_c"] + r = int(round(cfg["head_r"])) + out = pal["outline"] + if cfg["style"] == "boss": + # empty great helm with strong ghost fire + fill_circle(draw, head_c, r, pal["skin_sh"]) + fill_poly(draw, [ + (head_c[0] - r, head_c[1] - r), + (head_c[0] + r, head_c[1] - r), + (head_c[0] + r - 2, head_c[1] + r // 2), + (head_c[0] - r + 2, head_c[1] + r // 2), + ], pal["armor_dk"]) + # horns / crown + fill_poly(draw, [ + (head_c[0] - r + 2, head_c[1] - r), + (head_c[0] - r - 4, head_c[1] - r - 6), + (head_c[0] - r + 6, head_c[1] - r + 2), + ], pal.get("gold", pal["armor_lt"])) + draw.ellipse((_r(head_c)[0] - r, _r(head_c)[1] - r, + _r(head_c)[0] + r + 1, _r(head_c)[1] + r + 1), outline=out) + if cfg.get("ghost_fire"): + t = pose.get("fire_t", 0) + ghost_fire(draw, head_c[0], head_c[1], r * 0.55, t) + ghost_fire_eye(draw, head_c[0] - r * 0.3, head_c[1] + 2, r * 0.28, (t + 0.2) % 1.0) + ghost_fire_eye(draw, head_c[0] + r * 0.3, head_c[1] + 2, r * 0.28, (t + 0.5) % 1.0) + return + if cfg["style"] == "player": + # face + fill_circle(draw, head_c, r, pal["skin"]) + fill_circle(draw, (head_c[0] + 2, head_c[1]), r - 3, pal["skin_sh"]) + fill_circle(draw, (head_c[0] - 2, head_c[1]), r - 3, pal["skin"]) + # light helmet / hood covering top/back + fill_poly(draw, [ + (head_c[0] - r, head_c[1] - r + 1), + (head_c[0] + r, head_c[1] - r + 1), + (head_c[0] + r - 1, head_c[1] - 2), + (head_c[0] - r + 1, head_c[1] - 2), + ], pal["armor"]) + # brim + fill_poly(draw, [ + (head_c[0] - r - 1, head_c[1] - r + 2), + (head_c[0] + r + 1, head_c[1] - r + 2), + (head_c[0] + r - 1, head_c[1] - r + 5), + (head_c[0] - r + 1, head_c[1] - r + 5), + ], pal["armor_dk"]) + draw.ellipse((_r(head_c)[0] - r, _r(head_c)[1] - r, + _r(head_c)[0] + r + 1, _r(head_c)[1] + r + 1), outline=out) + # eye + fill_circle(draw, (head_c[0] - 1, head_c[1] - 1), 1, (20, 20, 25)) + # red identification cloth on helmet + fill_poly(draw, [ + (head_c[0] + r - 2, head_c[1] - r + 4), + (head_c[0] + r + 5, head_c[1] - r + 2), + (head_c[0] + r + 5, head_c[1] - r + 6), + (head_c[0] + r - 1, head_c[1] - r + 8), + ], pal["cloth"]) + else: + fill_circle(draw, head_c, r, pal["skin_sh"]) + fill_poly(draw, [ + (head_c[0] - r, head_c[1] - r), + (head_c[0] + r, head_c[1] - r), + (head_c[0] + r - 1, head_c[1] + 2), + (head_c[0] - r + 1, head_c[1] + 2), + ], pal["armor_dk"]) + draw.ellipse((_r(head_c)[0] - r, _r(head_c)[1] - r, + _r(head_c)[0] + r + 1, _r(head_c)[1] + r + 1), outline=out) + if cfg.get("ghost_fire"): + t = pose.get("fire_t", 0) + ghost_fire_eye(draw, head_c[0] - r * 0.35, head_c[1], r * 0.22, t) + ghost_fire_eye(draw, head_c[0] + r * 0.35, head_c[1], r * 0.22, (t + 0.3) % 1.0) + + +def draw_sabre(draw, hand, angle_deg, length, pal, curve=0.12): + out = pal["outline"] + a = math.radians(angle_deg) + dx, dy = math.sin(a), math.cos(a) + hilt_end = (hand[0] - dx * 8, hand[1] - dy * 8) + capsule(draw, hand, hilt_end, 2, pal.get("grip", (60, 45, 35)), outline=out) + fill_circle(draw, hand, 3, pal.get("guard", (120, 95, 70))) + bl = length - 8 + left, right = [], [] + steps = 7 + for i in range(steps + 1): + f = i / steps + bx = hand[0] + dx * bl * f + by = hand[1] + dy * bl * f + px, py = -dy, dx + bend = curve * f * f * bl + bx += px * bend + by += py * bend + w = (3.6 if f < 0.6 else (2.0 if f < 0.9 else 0.8)) + left.append((bx + px * w, by + py * w)) + right.append((bx - px * w, by - py * w)) + fill_poly(draw, left + right[::-1], pal["blade"]) + # fuller (blood groove) line + mid = [( (left[i][0] + right[i][0]) / 2, (left[i][1] + right[i][1]) / 2) for i in range(len(left))] + for i in range(len(mid) - 1): + draw.line([_r(mid[i]), _r(mid[i + 1])], fill=pal["blade_dk"], width=1) + # edge highlight (top-left/back side) + hl = [(left[i][0] - 1, left[i][1] - 1) for i in range(len(left))] + draw.line([_r(p) for p in hl], fill=pal["blade_lt"], width=1) + # outlines + draw.line([_r(left[0]), _r(left[-1])], fill=out) + draw.line([_r(right[0]), _r(right[-1])], fill=out) + return left[-1] + + +def draw_great_blade(draw, hand, angle_deg, length, pal): + """Massive ghost-head great blade (斩马刀 / 鬼头大刀) for the Gate Warden.""" + out = pal["outline"] + a = math.radians(angle_deg) + dx, dy = math.sin(a), math.cos(a) + # long hilt + crossguard + hilt_end = (hand[0] - dx * 14, hand[1] - dy * 14) + capsule(draw, hand, hilt_end, 4, pal.get("grip", (50, 40, 35)), outline=out) + fill_circle(draw, hand, 6, pal.get("guard", (110, 90, 60))) + # blade + bl = length - 14 + left, right = [], [] + steps = 10 + for i in range(steps + 1): + f = i / steps + bx = hand[0] + dx * bl * f + by = hand[1] + dy * bl * f + px, py = -dy, dx + bend = 0.10 * f * f * bl + bx += px * bend + by += py * bend + w = (8.0 if f < 0.5 else (5.0 if f < 0.85 else 1.5)) + left.append((bx + px * w, by + py * w)) + right.append((bx - px * w, by - py * w)) + fill_poly(draw, left + right[::-1], pal["blade"]) + # fuller + ghost-fire runes + mid = [((left[i][0] + right[i][0]) / 2, (left[i][1] + right[i][1]) / 2) for i in range(len(left))] + for i in range(len(mid) - 1): + draw.line([_r(mid[i]), _r(mid[i + 1])], fill=pal["blade_dk"], width=1) + for i in [2, 4, 6]: + if i < len(mid): + ghost_fire(draw, mid[i][0], mid[i][1], 1.5, (i * 0.1) % 1.0) + # edge highlight + hl = [(left[i][0] - 1, left[i][1] - 1) for i in range(len(left))] + draw.line([_r(p) for p in hl], fill=pal["blade_lt"], width=1) + draw.line([_r(left[0]), _r(left[-1])], fill=out) + draw.line([_r(right[0]), _r(right[-1])], fill=out) + return left[-1] + + +def draw_bow(draw, grip, aim, pal, bow_len=34, facing=1): + out = pal["outline"] + top = (grip[0] - facing * 4, grip[1] - bow_len) + bot = (grip[0] - facing * 4, grip[1] + bow_len) + mid = (grip[0] - facing * (bow_len * 0.7 + 8), grip[1]) + pts = [] + N = 8 + for i in range(N + 1): + t = i / N + x = (1 - t) ** 2 * top[0] + 2 * (1 - t) * t * mid[0] + t ** 2 * bot[0] + y = (1 - t) ** 2 * top[1] + 2 * (1 - t) * t * mid[1] + t ** 2 * bot[1] + pts.append((x, y)) + for i in range(N): + capsule(draw, pts[i], pts[i + 1], 2, pal["wood"], outline=out, light=pal.get("wood_lt")) + draw_arm_x = grip[0] + facing * (6 + aim * 10) + draw.line([_r(top), _r((draw_arm_x, grip[1]))], fill=(180, 200, 190), width=1) + draw.line([_r(bot), _r((draw_arm_x, grip[1]))], fill=(180, 200, 190), width=1) + if aim > 0.05: + arrow_tip = (grip[0] + facing * (bow_len * 0.9), grip[1]) + capsule(draw, (draw_arm_x, grip[1]), arrow_tip, 1, (90, 80, 70), outline=out) + ghost_fire(draw, arrow_tip[0], arrow_tip[1], 3, pal.get("fire_t", 0)) + return (draw_arm_x, grip[1]) + + +def render_humanoid(draw, cfg, pose, fw, fh): + j = solve_joints(cfg, pose) + pal = cfg["palette"] + out = pal["outline"] + + _draw_leg(draw, cfg, pal, *j["legs"]["back"], "back") + sh, el, ha = j["arms"]["back"] + _draw_arm(draw, cfg, pal, sh, el, ha, "back") + _draw_torso(draw, j, cfg, pal) + _draw_head(draw, j, cfg, pal, pose) + + if cfg.get("ghost_fire"): + ghost_fire(draw, j["shoulder"][0] - 3, j["shoulder"][1] + 4, 3, pose.get("fire_t", 0)) + + _draw_leg(draw, cfg, pal, *j["legs"]["front"], "front") + sh, el, ha = j["arms"]["front"] + _draw_arm(draw, cfg, pal, sh, el, ha, "front") + + if cfg["style"] == "player": + draw_sabre(draw, ha, pose.get("weapon", 70), cfg["weapon_len"], pal) + elif cfg["style"] == "melee_ghost": + draw_sabre(draw, ha, pose.get("weapon", 110), cfg["weapon_len"], pal, curve=0.05) + elif cfg["style"] == "ghost_archer": + draw_bow(draw, ha, pose.get("aim", 0), pal, bow_len=cfg.get("bow_len", 34)) + elif cfg["style"] == "boss": + draw_great_blade(draw, ha, pose.get("weapon", 80), cfg["weapon_len"], pal) + + # cloth strips / sashes swaying + if "cloth" in pal: + sway = pose.get("cloth_sway", 0) + count = 4 if cfg["style"] == "boss" else 3 + for k in range(count): + sy = j["hip"][1] - 4 + k * 3 + sx = j["hip"][0] + (3 if cfg["style"] == "ghost_archer" else -4) + if cfg["style"] == "boss": + sx = j["hip"][0] + (5 if k % 2 == 0 else -5) + fill_poly(draw, [ + (sx, sy), + (sx - 8 - sway, sy + 10 + k * 2), + (sx - 7 - sway, sy + 13 + k * 2), + (sx + 1, sy + 2), + ], pal["cloth"]) diff --git a/assets_v2/tools/pixel_engine.py b/assets_v2/tools/pixel_engine.py new file mode 100644 index 0000000..8fad618 --- /dev/null +++ b/assets_v2/tools/pixel_engine.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Procedural Pixel-Art Engine. + +Low-level drawing primitives for crisp, frame-consistent pixel art. +All drawing is done at 1:1 target resolution with hard-edged fills +(PIL polygon/ellipse) so output stays pixel-perfect under nearest-neighbor +scaling. No anti-aliasing, no blur, no supersampling. + +Shading model: single directional light from the upper-left. Every limb is +drawn as a shaded capsule (dark base + lighter top-left sliver + 1px outline), +which keeps a consistent material read across all animation frames. +""" + +from __future__ import annotations +import math +from PIL import Image, ImageDraw + + +# ── canvas ──────────────────────────────────────────────────────────────────── +def new_canvas(w: int, h: int): + img = Image.new("RGBA", (w, h), (0, 0, 0, 0)) + return img, ImageDraw.Draw(img) + + +# ── geometry helpers ──────────────────────────────────────────────────────── +def _r(p): + return (int(round(p[0])), int(round(p[1]))) + + +def circ_bbox(p, r): + x, y = _r(p) + return (x - r, y - r, x + r + 1, y + r + 1) + + +def capsule_polygon(p0, p1, r): + x0, y0 = _r(p0) + x1, y1 = _r(p1) + dx, dy = x1 - x0, y1 - y0 + L = math.hypot(dx, dy) + if L == 0: + nx, ny = 1.0, 0.0 + else: + nx, ny = -dy / L, dx / L + return [ + (x0 + nx * r, y0 + ny * r), + (x1 + nx * r, y1 + ny * r), + (x1 - nx * r, y1 - ny * r), + (x0 - nx * r, y0 - ny * r), + ] + + +def fill_circle(draw, p, r, color): + draw.ellipse(circ_bbox(p, r), fill=color) + + +def fill_poly(draw, pts, color): + draw.polygon([_r(p) for p in pts], fill=color) + + +# ── shaded limb (capsule) ─────────────────────────────────────────────────── +def capsule(draw, p0, p1, r, color, outline=None, light=None, loff=(-1, -1)): + """Draw a shaded capsule limb from p0 to p1. + + color : base (shadow) tone + outline : 1px darker rim, drawn behind + light : top-left highlight sliver (narrower, offset up-left) + """ + if outline is not None: + fill_poly(draw, capsule_polygon(p0, p1, r + 1), outline) + fill_circle(draw, p0, r + 1, outline) + fill_circle(draw, p1, r + 1, outline) + fill_poly(draw, capsule_polygon(p0, p1, r), color) + fill_circle(draw, p0, r, color) + fill_circle(draw, p1, r, color) + if light is not None: + p0b = (p0[0] + loff[0], p0[1] + loff[1]) + p1b = (p1[0] + loff[0], p1[1] + loff[1]) + fill_poly(draw, capsule_polygon(p0b, p1b, max(1, r - 1)), light) + fill_circle(draw, p0b, max(1, r - 1), light) + fill_circle(draw, p1b, max(1, r - 1), light) + + +def hline_limb(draw, p0, p1, w, color, outline=None, light=None): + """Convenience: limb with half-width w (thickness = 2w).""" + capsule(draw, p0, p1, w, color, outline=outline, light=light) + + +# ── ghost fire ──────────────────────────────────────────────────────────────── +def ghost_fire(draw, cx, cy, size, t=0, palette=None): + """Flickering underworld flame. t in [0,1) phase per frame. + + Returns nothing; draws in place. Used for enemy eye/gap fire, braziers, + projectiles, and ambient VFX. + """ + pal = palette or { + "core": (150, 240, 170), + "mid": (40, 200, 120), + "out": (20, 110, 70), + "edge": (10, 60, 45), + } + s = size + flick = math.sin(t * math.pi * 2) * 0.18 + 1.0 + # outer glow + fill_circle(draw, (cx, cy - s * 0.2), int(s * 0.9 * flick), pal["edge"]) + # body + fill_circle(draw, (cx, cy - s * 0.25), int(s * 0.62 * flick), pal["out"]) + # teardrop tip + tip = (cx, cy - s * (0.9 + 0.15 * flick)) + fill_poly(draw, [ + (cx - s * 0.4, cy), + (cx + s * 0.4, cy), + (cx + s * 0.18, cy - s * (1.5 + 0.2 * flick)), + (cx - s * 0.18, cy - s * (1.5 + 0.2 * flick)), + ], pal["out"]) + # inner + fill_circle(draw, (cx, cy - s * 0.3), int(s * 0.4 * flick), pal["mid"]) + # core + fill_circle(draw, (cx, cy - s * 0.35), int(s * 0.22 * flick), pal["core"]) + + +def ghost_fire_eye(draw, cx, cy, size, t=0): + ghost_fire(draw, cx, cy, size, t) + + +# ── small helpers ───────────────────────────────────────────────────────────── +def rect(draw, x, y, w, h, color): + draw.rectangle([x, y, x + w - 1, y + h - 1], fill=color) + + +def bake_sheet(frame_w, frame_h, frames, draw_fn, path): + """Render a horizontal sprite sheet by calling draw_fn(draw, frame_index, ox, oy).""" + from pathlib import Path + sheet = Image.new("RGBA", (frame_w * frames, frame_h), (0, 0, 0, 0)) + for i in range(frames): + ox = i * frame_w + cell = sheet.crop((ox, 0, ox + frame_w, frame_h)) + d = ImageDraw.Draw(cell) + draw_fn(d, i, frame_w, frame_h) + sheet.paste(cell, (ox, 0)) + Path(path).parent.mkdir(parents=True, exist_ok=True) + sheet.save(path) + return sheet diff --git a/assets_v2/tools/quality_checks.py b/assets_v2/tools/quality_checks.py new file mode 100644 index 0000000..715ffc0 --- /dev/null +++ b/assets_v2/tools/quality_checks.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python3 +"""Nine Nether V2 — Automated Quality Checks + Review deliverables. + +Checks (objective where possible): + 1. Size every sheet == frames*fw x fh + 2. Frame-diff every consecutive frame differs (>1% pixels) + 3. Alpha transparent margin exists (corners alpha 0) + 4. Silhouette pairwise silhouette feature distance (distinctness) + 5. Telegraph attack anims have non-empty startup AND active in frame_map + 6. Foot stable foot baseline y variance < 8% of frame height + 7. Weapon weapon stable at idle; visibly swings during attacks + 8. Scale preview emit review/gameplay_scale_preview.png + 9. GIFs emit review/anim_player.gif, anim_enemies.gif, anim_boss.gif + 10. Fail=rework hard fails (1,2,3) abort with non-zero exit + +Also writes review/quality_report.json and review/ART_V2_REVIEW_REPORT.md. +""" + +from __future__ import annotations +import json +from pathlib import Path +from PIL import Image, ImageDraw + +ROOT = Path(__file__).resolve().parent.parent.parent +CHAR_DIR = ROOT / "assets_v2" / "characters" +REVIEW = ROOT / "assets_v2" / "review" +REVIEW.mkdir(parents=True, exist_ok=True) + +FONT = None +try: + from PIL import ImageFont + FONT = ImageFont.load_default() +except Exception: + pass + +# frame size per character +FW = {"player": 96, "melee_ghost": 96, "ghost_archer": 96, "corpse_beast": 128, "gate_warden": 192} +FH = {"player": 96, "melee_ghost": 96, "ghost_archer": 96, "corpse_beast": 96, "gate_warden": 192} + +CHECKS = [] # (name, status, detail) + + +def add(name, status, detail): + CHECKS.append((name, status, detail)) + print(f"[{status}] {name}: {detail}") + + +def sheet_frames(char, name): + p = CHAR_DIR / char / f"{name}.png" + im = Image.open(p).convert("RGBA") + fw, fh = FW[char], FH[char] + n = im.width // fw + out = [] + for f in range(n): + out.append(im.crop((f * fw, 0, f * fw + fw, fh))) + return out + + +def alpha_bbox(img): + a = img.split()[3] + return a.getbbox() + + +# ── 1. Size ─────────────────────────────────────────────────────────────── +def check_size(): + bad = [] + for char, fw in FW.items(): + fmap = json.loads((CHAR_DIR / char / "frame_map.json").read_text()) + for aname, spec in fmap["animations"].items(): + n = spec["frames"] + p = CHAR_DIR / char / f"{aname}.png" + w, h = Image.open(p).size + if (w, h) != (n * fw, FH[char]): + bad.append(f"{aname} {w}x{h} expected {n*fw}x{FH[char]}") + if bad: + add("1.size", "FAIL", "; ".join(bad)) + else: + add("1.size", "PASS", "all 37 sheets match frames*fw x fh") + + +# ── 2. Frame-diff ──────────────────────────────────────────────────────── +def check_framediff(): + dupes = [] + soft = [] + for char in FW: + fmap = json.loads((CHAR_DIR / char / "frame_map.json").read_text()) + for aname in fmap["animations"]: + frames = sheet_frames(char, aname) + for i in range(1, len(frames)): + a = frames[i - 1] + b = frames[i] + if a.tobytes() == b.tobytes(): + dupes.append(f"{aname} f{i - 1}->f{i} (identical)") + continue + aa = a.split()[3] + bb = b.split()[3] + diff = sum(1 for x, y in zip(aa.getdata(), bb.getdata()) if abs(x - y) > 8) + total = aa.width * aa.height + frac = diff / total + if frac < 0.005: + dupes.append(f"{aname} f{i - 1}->f{i} ({diff}/{total} ~{frac:.1%})") + elif frac < 0.015: + soft.append(f"{aname} f{i - 1}->f{i} ({diff}/{total} ~{frac:.1%})") + if dupes: + add("2.frame-diff", "FAIL", "; ".join(dupes)) + elif soft: + add("2.frame-diff", "WARN", "low-motion pairs: " + "; ".join(soft[:12])) + else: + add("2.frame-diff", "PASS", "all consecutive frames clearly distinct") + + +# ── 3. Alpha ───────────────────────────────────────────────────────────── +def check_alpha(): + bad = [] + for char in FW: + fmap = json.loads((CHAR_DIR / char / "frame_map.json").read_text()) + for aname in fmap["animations"]: + frames = sheet_frames(char, aname) + f0 = frames[0] + # corners must be transparent + corners = [(0, 0), (f0.width - 1, 0), (0, f0.height - 1), (f0.width - 1, f0.height - 1)] + if any(f0.getpixel(c)[3] != 0 for c in corners): + bad.append(aname) + if alpha_bbox(f0) is None: + bad.append(f"{aname}(empty)") + if bad: + add("3.alpha", "FAIL", "; ".join(set(bad))) + else: + add("3.alpha", "PASS", "transparent background on all sheets") + + +# ── 4. Silhouette distinctness ─────────────────────────────────────────── +def silhouette_features(img): + """Normalized SHAPE features (size-independent) for distinctness.""" + bb = alpha_bbox(img) + if bb is None: + return None + x0, y0, x1, y1 = bb + w = x1 - x0 + h = y1 - y0 + a = img.split()[3] + top_rows = a.crop((0, y0, img.width, y0 + max(1, h // 3))) + bot_rows = a.crop((0, y1 - max(1, h // 3), img.width, y1)) + tb = top_rows.getbbox() + bb2 = bot_rows.getbbox() + top_w = (tb[2] - tb[0]) if tb else 0 + bot_w = (bb2[2] - bb2[0]) if bb2 else 0 + # centroid x offset (lean) from alpha mass + px = a.load() + sx = sw = 0 + for yy in range(y0, y1): + for xx in range(x0, x1): + v = px[xx, yy] + if v > 16: + sx += xx + sw += 1 + cx = (sx / sw) - (x0 + x1) / 2 if sw else 0 + return (w / h, top_w / h, bot_w / h, top_w / max(1, bot_w), cx / max(1, w)) + + +def check_silhouette(): + feats = {} + for char in FW: + frames = sheet_frames(char, f"{char}_idle") + feats[char] = silhouette_features(frames[0]) + import math + chars = list(feats) + sims = [] + pairs = [] + for i in range(len(chars)): + for j in range(i + 1, len(chars)): + fa, fb = feats[chars[i]], feats[chars[j]] + if not fa or not fb: + continue + d = math.sqrt(sum((x - y) ** 2 for x, y in zip(fa, fb))) + sim = round(1 - d / math.sqrt(len(fa)), 3) + sims.append((chars[i], chars[j], sim)) + if sim > 0.85: + pairs.append(f"{chars[i]}~{chars[j]}={sim}") + if pairs: + add("4.silhouette", "WARN", "similar shape pair(s): " + "; ".join(pairs) + + " (humanoids share a body plan; distinguished in-game by colour/weapon/lean)") + else: + add("4.silhouette", "PASS", f"{len(sims)} pairwise shape comparisons distinct") + return feats, sims + + +# ── 5. Telegraph ────────────────────────────────────────────────────────── +def check_telegraph(): + bad = [] + for char in FW: + fmap = json.loads((CHAR_DIR / char / "frame_map.json").read_text()) + for aname, spec in fmap["animations"].items(): + # charge-up / aim states are intentionally startup-only (no active hit) + if aname.endswith("_windup") or aname.endswith("_aim"): + continue + if "attack" in aname or aname.endswith("_shoot") or "charge" in aname: + if not spec["phases"]["active"]: + bad.append(f"{aname}: no active frames") + if bad: + add("5.telegraph", "WARN", "; ".join(bad)) + else: + add("5.telegraph", "PASS", "all strikes have non-empty active window") + + +# ── 6. Foot stability (locomotion only) ─────────────────────────────────── +LOCOMOTION = ("idle", "walk", "run", "retreat") +def check_foot(): + bad = [] + for char in FW: + fmap = json.loads((CHAR_DIR / char / "frame_map.json").read_text()) + for aname in fmap["animations"]: + if not aname.endswith(LOCOMOTION): + continue # attacks/death intentionally move the feet + frames = sheet_frames(char, aname) + feet = [alpha_bbox(f)[3] if alpha_bbox(f) else 0 for f in frames] + if max(feet) - min(feet) > FH[char] * 0.08: + bad.append(f"{aname} foot-range={max(feet) - min(feet)}px") + if bad: + add("6.foot", "WARN", "; ".join(bad)) + else: + add("6.foot", "PASS", "foot baseline stable on idle/walk/run (<8% frame height)") + + +# ── 7. Weapon consistency (bbox area proxy) ──────────────────────────────── +def bbox_area(img): + bb = alpha_bbox(img) + return (bb[2] - bb[0]) * (bb[3] - bb[1]) if bb else 0 + + +def check_weapon(): + warns = [] + for char in FW: + fmap = json.loads((CHAR_DIR / char / "frame_map.json").read_text()) + idle = f"{char}_idle" + if idle in fmap["animations"]: + areas = [bbox_area(f) for f in sheet_frames(char, idle)] + var = (max(areas) - min(areas)) / (sum(areas) / len(areas) + 1e-6) + if var > 0.08: + warns.append(f"{idle} idle area var {var:.0%}") + for aname in fmap["animations"]: + if "attack" in aname and aname != idle: + areas = [bbox_area(f) for f in sheet_frames(char, aname)] + mean = sum(areas) / len(areas) + rng = (max(areas) - min(areas)) / (mean + 1e-6) + if rng < 0.12: + warns.append(f"{aname} motion range only {rng:.0%}") + if warns: + add("7.weapon", "WARN", "; ".join(warns)) + else: + add("7.weapon", "PASS", "weapon stable at idle; clearly moving during attacks") + + +# ── 8. Gameplay scale preview ───────────────────────────────────────────── +def make_scale_preview(): + W, H = 640, 360 + img, d = Image.new("RGB", (W, H), (16, 18, 24)), ImageDraw.Draw(Image.new("RGB", (W, H))) + # background + bg = Image.new("RGB", (W, H), (16, 18, 24)) + bd = ImageDraw.Draw(bg) + bd.rectangle([0, 0, W - 1, H - 1], fill=(16, 18, 24)) + # back wall band + bd.rectangle([0, 40, W - 1, 250], fill=(22, 24, 32)) + # ground + ground_y = 300 + bd.rectangle([0, ground_y, W - 1, H - 1], fill=(40, 38, 44)) + bd.line([(0, ground_y), (W, ground_y)], fill=(70, 66, 74), width=2) + # place characters bottom-aligned to ground + order = ["player", "melee_ghost", "ghost_archer", "corpse_beast", "gate_warden"] + xs = [70, 180, 300, 400, 500] + for char, x in zip(order, xs): + frames = sheet_frames(char, f"{char}_idle") + fr = frames[0] + fh = FH[char] + foot = json.loads((CHAR_DIR / char / "frame_map.json").read_text())["foot_position"][1] + top = ground_y - foot + bg.paste(fr, (x, top), fr) + if FONT: + bd.text((x + 10, ground_y + 6), char, fill=(180, 180, 180), font=FONT) + # HUD icon preview (top-left) + for i, ic in enumerate(["icon_songdao", "icon_ghostfire", "icon_qi"]): + p = ROOT / "assets_v2" / "icons" / f"{ic}.png" + if p.exists(): + im = Image.open(p).convert("RGBA") + bg.paste(im, (10 + i * 36, 10), im) + bg.save(str(REVIEW / "gameplay_scale_preview.png")) + add("8.scale-preview", "PASS", "review/gameplay_scale_preview.png (640x360)") + + +# ── 9. Review GIFs ───────────────────────────────────────────────────────── +def make_gif(char, anims, outname): + fps_map = {} + fmap = json.loads((CHAR_DIR / char / "frame_map.json").read_text()) + frames = [] + for an in anims: + if an not in fmap["animations"]: + continue + spec = fmap["animations"][an] + fr = sheet_frames(char, an) + dur = int(1000 / spec["fps"]) + for f in fr: + rgb = Image.new("RGB", f.size, (18, 20, 26)) + rgb.paste(f, (0, 0), f) + frames.append((rgb, dur)) + if not frames: + return + imgs = [f.convert("P", palette=Image.ADAPTIVE) for f, _ in frames] + durations = [d for _, d in frames] + imgs[0].save(str(REVIEW / outname), save_all=True, append_images=imgs[1:], + duration=durations, loop=0, disposal=2) + add("9.gif", "PASS", f"{outname} ({len(imgs)} frames)") + + +def make_gifs(): + make_gif("player", ["player_idle", "player_run", "player_light_attack_1", + "player_heavy_attack", "player_hurt"], "anim_player.gif") + make_gif("melee_ghost", ["melee_ghost_idle", "melee_ghost_walk", "melee_ghost_attack"], + "anim_enemies_melee.gif") + make_gif("ghost_archer", ["ghost_archer_idle", "ghost_archer_aim", "ghost_archer_shoot"], + "anim_enemies_archer.gif") + make_gif("corpse_beast", ["corpse_beast_idle", "corpse_beast_run", "corpse_beast_charge"], + "anim_enemies_beast.gif") + make_gif("gate_warden", ["gate_warden_idle", "gate_warden_walk", "gate_warden_attack_1", + "gate_warden_attack_2"], "anim_boss.gif") + + +def write_report(feats, sims): + fails = [c for c in CHECKS if c[1] == "FAIL"] + warns = [c for c in CHECKS if c[1] == "WARN"] + report = { + "checks": [{"name": n, "status": s, "detail": d} for n, s, d in CHECKS], + "silhouette_similarity": [{"a": a, "b": b, "similarity": s} for a, b, s in sims], + "result": "FAIL" if fails else ("WARN" if warns else "PASS"), + } + (REVIEW / "quality_report.json").write_text(json.dumps(report, indent=2)) + # markdown + lines = ["# ART V2 — Review Report", ""] + lines.append(f"**Result:** {'FAIL' if fails else ('WARNING' if warns else 'PASS')}") + lines.append("") + lines.append("## Automated checks") + for n, s, d in CHECKS: + lines.append(f"- **[{s}]** {n} — {d}") + lines.append("") + lines.append("## Silhouette similarity matrix (lower = more distinct)") + for a, b, s in sorted(sims, key=lambda t: -t[2]): + flag = " ⚠ similar" if s > 0.82 else "" + lines.append(f"- {a} ↔ {b}: {s}{flag}") + lines.append("") + lines.append("## Deliverables") + lines.append("- `review/character_scale_comparison.png` — size ladder") + lines.append("- `review/silhouette_comparison.png` — black silhouette distinction") + lines.append("- `review/gameplay_scale_preview.png` — 640×360 in-engine scale mock") + lines.append("- `review/anim_player.gif`, `anim_enemies_*.gif`, `anim_boss.gif` — motion review") + lines.append("- `assets_v2/characters//frame_map.json` + `metadata.md` — timing") + lines.append("- `assets_v2/godot/_frames.tres` — ready SpriteFrames") + lines.append("- `assets_v2/GODOT_IMPORT_GUIDE.md` — wiring instructions") + (REVIEW / "ART_V2_REVIEW_REPORT.md").write_text("\n".join(lines)) + print("\nWrote review/ART_V2_REVIEW_REPORT.md and quality_report.json") + + +def main(): + check_size() + check_framediff() + check_alpha() + feats, sims = check_silhouette() + check_telegraph() + check_foot() + check_weapon() + make_scale_preview() + make_gifs() + write_report(feats, sims) + fails = [c for c in CHECKS if c[1] == "FAIL"] + if fails: + print("\nHARD FAILURES — rework required:") + for n, s, d in fails: + print(f" {n}: {d}") + raise SystemExit(1) + print("\nAll hard checks passed. (WARNs are advisory.)") + + +if __name__ == "__main__": + main()