diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 0000000..ca2943c --- /dev/null +++ b/assets/README.md @@ -0,0 +1,31 @@ +# assets/ — Art Resource Directory + +This directory contains all visual art assets for the 九幽 (Nine Nether) prototype. + +## Structure + +| Directory | Contents | Count | +|-----------|----------|-------| +| `player/` | Player character sprite sheets | 6 sheets | +| `enemy/` | Enemy sprite sheets (3 enemy types) | 15 sheets | +| `boss/` | Boss sprite sheets | 5 sheets | +| `icons/weapons/` | 32×32 weapon icons | 6 icons | +| `icons/ui/` | 32×32 UI stat icons | 11 icons | +| `tiles/` | 32×32 tilemap tiles | 7 tiles | +| `effects/` | 48×48 VFX sprites | 4 effects | +| `background/` | Parallax background elements | 4 elements | +| `ui/` | UI elements (bars, frames) | 4 elements | + +## Key Documents +- **Master spec:** `docs/ART_SPEC.md` +- **Concept docs:** `docs/art/CONCEPT_*.md` +- **Import guide:** `docs/art/GODOT_IMPORT_GUIDE.md` +- **Naming rules:** `docs/art/NAMING_CONVENTION.md` + +## Placeholder Status +All PNGs are currently **placeholders** (marked with magenta border). Replace each with final pixel art using the same filename and dimensions. See `docs/ART_SPEC.md` Section 5 for replacement workflow. + +## Regenerating Placeholders +```bash +python3 tools/generate_placeholders.py +``` diff --git a/assets/background/README.md b/assets/background/README.md new file mode 100644 index 0000000..14fc488 --- /dev/null +++ b/assets/background/README.md @@ -0,0 +1,17 @@ +# assets/background/ — Parallax Background Elements + +## Files +| File | Name | Size | Parallax | Purpose | +|------|------|------|----------|---------| +| bg_deep.png | 深景背景 | 320×180 | 0.2x | Underworld vista — distant mountains, mist | +| bg_tree.png | 背景树 | 64×128 | 0.5x | Dead skeletal tree — mid decoration | +| bg_broken_flag.png | 残破旗帜 | 48×96 | 0.5x | Broken flag pole — mid decoration | +| bg_chains.png | 锁链 | 32×96 | 0.7x | Hanging chains — close decoration | + +## Notes +- `bg_deep.png` is the only full-frame background — tiles horizontally +- Other elements are sprite props placed at intervals +- All have transparent backgrounds except `bg_deep.png` (full opaque) + +## Concept Reference +See `docs/art/CONCEPT_BACKGROUND.md` for full design descriptions. diff --git a/assets/background/bg_broken_flag.png b/assets/background/bg_broken_flag.png new file mode 100644 index 0000000..9a0086b Binary files /dev/null and b/assets/background/bg_broken_flag.png differ diff --git a/assets/background/bg_chains.png b/assets/background/bg_chains.png new file mode 100644 index 0000000..53ca6a5 Binary files /dev/null and b/assets/background/bg_chains.png differ diff --git a/assets/background/bg_deep.png b/assets/background/bg_deep.png new file mode 100644 index 0000000..799f108 Binary files /dev/null and b/assets/background/bg_deep.png differ diff --git a/assets/background/bg_tree.png b/assets/background/bg_tree.png new file mode 100644 index 0000000..053af97 Binary files /dev/null and b/assets/background/bg_tree.png differ diff --git a/assets/background/metadata.md b/assets/background/metadata.md new file mode 100644 index 0000000..fba6142 --- /dev/null +++ b/assets/background/metadata.md @@ -0,0 +1,40 @@ +# assets/background/ — Metadata + +## Background Element Specifications + +### bg_deep.png +- Size: 320 × 180 px +- Type: Full-frame parallax background +- Parallax: 0.2x (deepest layer) +- Tiling: Horizontal (left/right edges match) +- Purpose: Underworld vista — jagged mountains, low mist, distant ghost fires + +### bg_tree.png +- Size: 64 × 128 px +- Type: Sprite prop (transparent background) +- Parallax: 0.5x (mid-background) +- Tiling: Non-tileable, placed at intervals +- Purpose: Dead, skeletal tree — bare twisted branches + +### bg_broken_flag.png +- Size: 48 × 96 px +- Type: Sprite prop (transparent background) +- Parallax: 0.5x (mid-background) +- Tiling: Non-tileable, placed at intervals +- Purpose: Broken flag pole with tattered battle flag + +### bg_chains.png +- Size: 32 × 96 px +- Type: Sprite prop (transparent background) +- Parallax: 0.7x (close background) +- Tiling: Non-tileable, placed at intervals +- Purpose: Hanging rusted iron chains from above + +## Godot Import Settings +- Filter: Off (Nearest) +- Mipmaps: Off +- Compression: Lossless +- Format: RGBA + +## Parallax Layer Setup +See `docs/art/CONCEPT_BACKGROUND.md` — Parallax Layer Structure section. diff --git a/assets/boss/README.md b/assets/boss/README.md new file mode 100644 index 0000000..0f027f4 --- /dev/null +++ b/assets/boss/README.md @@ -0,0 +1,25 @@ +# assets/boss/ — Boss Sprites + +## Boss +**镇关鬼将** (Gate Guardian Ghost General) — a towering underworld general with ghost fire and a massive blade. + +## Files +| File | Animation | Frames | Sheet Size | Frame Size | FPS | +|------|-----------|--------|------------|------------|-----| +| boss_idle.png | Idle | 4 | 384×96 | 96×96 | 6 | +| boss_run.png | Run | 6 | 576×96 | 96×96 | 8 | +| boss_attack.png | Attack | 5 | 480×96 | 96×96 | 10 | +| boss_hurt.png | Hurt | 2 | 192×96 | 96×96 | 8 | +| boss_death.png | Death | 8 | 768×96 | 96×96 | 6 | + +## Notes +- Boss is **3× the player's size** (96×96 vs 48×48) +- Ghost fire (blue-green) appears in every animation +- Death animation is the longest (8 frames) for dramatic effect +- Attack is 5 frames — wind-up → strike → impact → follow-through → recovery + +## Concept Reference +See `docs/art/CONCEPT_BOSS.md` for full visual design description. + +## Color Palette +Underworld purple `#321E37`, ghost fire `#1EB464` / `#4AE68A`, bone `#A0987A`, void `#0F0C12` diff --git a/assets/boss/boss_attack.png b/assets/boss/boss_attack.png new file mode 100644 index 0000000..46720bc Binary files /dev/null and b/assets/boss/boss_attack.png differ diff --git a/assets/boss/boss_death.png b/assets/boss/boss_death.png new file mode 100644 index 0000000..886266c Binary files /dev/null and b/assets/boss/boss_death.png differ diff --git a/assets/boss/boss_hurt.png b/assets/boss/boss_hurt.png new file mode 100644 index 0000000..36df7d8 Binary files /dev/null and b/assets/boss/boss_hurt.png differ diff --git a/assets/boss/boss_idle.png b/assets/boss/boss_idle.png new file mode 100644 index 0000000..d839894 Binary files /dev/null and b/assets/boss/boss_idle.png differ diff --git a/assets/boss/boss_run.png b/assets/boss/boss_run.png new file mode 100644 index 0000000..7658b7c Binary files /dev/null and b/assets/boss/boss_run.png differ diff --git a/assets/boss/metadata.md b/assets/boss/metadata.md new file mode 100644 index 0000000..be5601c --- /dev/null +++ b/assets/boss/metadata.md @@ -0,0 +1,54 @@ +# assets/boss/ — Metadata + +## Sprite Sheet Specifications + +### boss_idle.png +- **Size:** 384 × 96 px +- **Frame size:** 96 × 96 px +- **Frame count:** 4 +- **Layout:** Horizontal strip +- **FPS:** 6 +- **Loop:** Yes +- **Purpose:** Boss idle — blade resting on ground, ghost fire flickering on shoulders + +### boss_run.png +- **Size:** 576 × 96 px +- **Frame size:** 96 × 96 px +- **Frame count:** 6 +- **Layout:** Horizontal strip +- **FPS:** 8 +- **Loop:** Yes +- **Purpose:** Boss run — earth-shaking charge, blade dragging + +### boss_attack.png +- **Size:** 480 × 96 px +- **Frame size:** 96 × 96 px +- **Frame count:** 5 +- **Layout:** Horizontal strip +- **FPS:** 10 +- **Loop:** No +- **Purpose:** Boss attack — overhead blade strike with ghost fire shockwave + +### boss_hurt.png +- **Size:** 192 × 96 px +- **Frame size:** 96 × 96 px +- **Frame count:** 2 +- **Layout:** Horizontal strip +- **FPS:** 8 +- **Loop:** No +- **Purpose:** Boss hurt — stagger + defensive fire flare + +### boss_death.png +- **Size:** 768 × 96 px +- **Frame size:** 96 × 96 px +- **Frame count:** 8 +- **Layout:** Horizontal strip +- **FPS:** 6 +- **Loop:** No +- **Purpose:** Boss death — blade drops, armor cracks, fire erupts, collapses, dissolves, blade remains + +## Godot Import Settings +- Filter: Off (Nearest) +- Mipmaps: Off +- Compression: Lossless +- Format: RGBA diff --git a/assets/effects/README.md b/assets/effects/README.md new file mode 100644 index 0000000..05e159b --- /dev/null +++ b/assets/effects/README.md @@ -0,0 +1,19 @@ +# assets/effects/ — Visual Effects + +All 48×48 px. Transparent background. Centered origin. + +## Files +| File | Name | Frames | FPS | Loop | +|------|------|--------|-----|------| +| effect_hit_slash.png | 斩击特效 | 2 | 20 | No | +| effect_blood_splash.png | 鲜血飞溅 | 4 | 15 | No | +| effect_ghost_fire.png | 鬼火特效 | 4 | 10 | Yes | +| effect_death_fade.png | 死亡消散 | 4 | 8 | No | + +## Notes +- `effect_ghost_fire.png` is the only looping effect — use for ambient fire, boss aura +- `effect_hit_slash.png` and `effect_blood_splash.png` are short bursts — destroy after playing +- `effect_death_fade.png` plays after a character's death animation + +## Concept Reference +See `docs/art/CONCEPT_EFFECTS.md` for full design descriptions. diff --git a/assets/effects/effect_blood_splash.png b/assets/effects/effect_blood_splash.png new file mode 100644 index 0000000..a55476d Binary files /dev/null and b/assets/effects/effect_blood_splash.png differ diff --git a/assets/effects/effect_death_fade.png b/assets/effects/effect_death_fade.png new file mode 100644 index 0000000..e9b93f5 Binary files /dev/null and b/assets/effects/effect_death_fade.png differ diff --git a/assets/effects/effect_ghost_fire.png b/assets/effects/effect_ghost_fire.png new file mode 100644 index 0000000..5062ab2 Binary files /dev/null and b/assets/effects/effect_ghost_fire.png differ diff --git a/assets/effects/effect_hit_slash.png b/assets/effects/effect_hit_slash.png new file mode 100644 index 0000000..513fe21 Binary files /dev/null and b/assets/effects/effect_hit_slash.png differ diff --git a/assets/effects/metadata.md b/assets/effects/metadata.md new file mode 100644 index 0000000..c56a32e --- /dev/null +++ b/assets/effects/metadata.md @@ -0,0 +1,29 @@ +# assets/effects/ — Metadata + +## Effect Specifications (all 48×48 px) + +### effect_hit_slash.png +- Size: 96×48 (2 frames × 48px) | FPS: 20 | Loop: No +- Frame size: 48×48 +- Purpose: Melee hit impact — white/silver slash arc + +### effect_blood_splash.png +- Size: 192×48 (4 frames × 48px) | FPS: 15 | Loop: No +- Frame size: 48×48 +- Purpose: Dark crimson blood particle burst on damage + +### effect_ghost_fire.png +- Size: 192×48 (4 frames × 48px) | FPS: 10 | Loop: Yes +- Frame size: 48×48 +- Purpose: Blue-green flame — brazier light, boss aura, projectile trail + +### effect_death_fade.png +- Size: 192×48 (4 frames × 48px) | FPS: 8 | Loop: No +- Frame size: 48×48 +- Purpose: Character body dissolving into dark particles on death + +## Godot Import Settings +- Filter: Off (Nearest) +- Mipmaps: Off +- Compression: Lossless +- Format: RGBA diff --git a/assets/enemy/README.md b/assets/enemy/README.md new file mode 100644 index 0000000..073837c --- /dev/null +++ b/assets/enemy/README.md @@ -0,0 +1,39 @@ +# assets/enemy/ — Enemy Sprites + +## Enemies +| Enemy | Prefix | Type | Frame Size | +|-------|--------|------|------------| +| 鬼卒 (Ghost Soldier) | `ghost_melee_` | Melee | 48×48 | +| 鬼弓手 (Ghost Archer) | `ghost_archer_` | Ranged | 48×48 | +| 尸兽 (Corpse Beast) | `corpse_beast_` | Charge | 64×48 | + +## Files +### Ghost Soldier (鬼卒) +| File | Frames | Sheet Size | FPS | +|------|--------|------------|-----| +| ghost_melee_idle.png | 4 | 192×48 | 8 | +| ghost_melee_run.png | 6 | 288×48 | 10 | +| ghost_melee_attack.png | 4 | 192×48 | 12 | +| ghost_melee_hurt.png | 2 | 96×48 | 10 | +| ghost_melee_death.png | 4 | 192×48 | 8 | + +### Ghost Archer (鬼弓手) +| File | Frames | Sheet Size | FPS | +|------|--------|------------|-----| +| ghost_archer_idle.png | 4 | 192×48 | 8 | +| ghost_archer_run.png | 6 | 288×48 | 10 | +| ghost_archer_attack.png | 4 | 192×48 | 12 | +| ghost_archer_hurt.png | 2 | 96×48 | 10 | +| ghost_archer_death.png | 4 | 192×48 | 8 | + +### Corpse Beast (尸兽) +| File | Frames | Sheet Size | FPS | +|------|--------|------------|-----| +| corpse_beast_idle.png | 4 | 256×48 | 8 | +| corpse_beast_run.png | 6 | 384×48 | 14 | +| corpse_beast_attack.png | 4 | 256×48 | 12 | +| corpse_beast_hurt.png | 2 | 128×48 | 10 | +| corpse_beast_death.png | 4 | 256×48 | 8 | + +## Concept Reference +See `docs/art/CONCEPT_ENEMIES.md` for full visual design descriptions. diff --git a/assets/enemy/corpse_beast_attack.png b/assets/enemy/corpse_beast_attack.png new file mode 100644 index 0000000..a8275d6 Binary files /dev/null and b/assets/enemy/corpse_beast_attack.png differ diff --git a/assets/enemy/corpse_beast_death.png b/assets/enemy/corpse_beast_death.png new file mode 100644 index 0000000..a8275d6 Binary files /dev/null and b/assets/enemy/corpse_beast_death.png differ diff --git a/assets/enemy/corpse_beast_hurt.png b/assets/enemy/corpse_beast_hurt.png new file mode 100644 index 0000000..b463eb2 Binary files /dev/null and b/assets/enemy/corpse_beast_hurt.png differ diff --git a/assets/enemy/corpse_beast_idle.png b/assets/enemy/corpse_beast_idle.png new file mode 100644 index 0000000..a8275d6 Binary files /dev/null and b/assets/enemy/corpse_beast_idle.png differ diff --git a/assets/enemy/corpse_beast_run.png b/assets/enemy/corpse_beast_run.png new file mode 100644 index 0000000..9e37c3a Binary files /dev/null and b/assets/enemy/corpse_beast_run.png differ diff --git a/assets/enemy/ghost_archer_attack.png b/assets/enemy/ghost_archer_attack.png new file mode 100644 index 0000000..18d7a1e Binary files /dev/null and b/assets/enemy/ghost_archer_attack.png differ diff --git a/assets/enemy/ghost_archer_death.png b/assets/enemy/ghost_archer_death.png new file mode 100644 index 0000000..18d7a1e Binary files /dev/null and b/assets/enemy/ghost_archer_death.png differ diff --git a/assets/enemy/ghost_archer_hurt.png b/assets/enemy/ghost_archer_hurt.png new file mode 100644 index 0000000..52d5e9c Binary files /dev/null and b/assets/enemy/ghost_archer_hurt.png differ diff --git a/assets/enemy/ghost_archer_idle.png b/assets/enemy/ghost_archer_idle.png new file mode 100644 index 0000000..18d7a1e Binary files /dev/null and b/assets/enemy/ghost_archer_idle.png differ diff --git a/assets/enemy/ghost_archer_run.png b/assets/enemy/ghost_archer_run.png new file mode 100644 index 0000000..8ef802d Binary files /dev/null and b/assets/enemy/ghost_archer_run.png differ diff --git a/assets/enemy/ghost_melee_attack.png b/assets/enemy/ghost_melee_attack.png new file mode 100644 index 0000000..d1aae6d Binary files /dev/null and b/assets/enemy/ghost_melee_attack.png differ diff --git a/assets/enemy/ghost_melee_death.png b/assets/enemy/ghost_melee_death.png new file mode 100644 index 0000000..d1aae6d Binary files /dev/null and b/assets/enemy/ghost_melee_death.png differ diff --git a/assets/enemy/ghost_melee_hurt.png b/assets/enemy/ghost_melee_hurt.png new file mode 100644 index 0000000..c16d9f5 Binary files /dev/null and b/assets/enemy/ghost_melee_hurt.png differ diff --git a/assets/enemy/ghost_melee_idle.png b/assets/enemy/ghost_melee_idle.png new file mode 100644 index 0000000..d1aae6d Binary files /dev/null and b/assets/enemy/ghost_melee_idle.png differ diff --git a/assets/enemy/ghost_melee_run.png b/assets/enemy/ghost_melee_run.png new file mode 100644 index 0000000..c678832 Binary files /dev/null and b/assets/enemy/ghost_melee_run.png differ diff --git a/assets/enemy/metadata.md b/assets/enemy/metadata.md new file mode 100644 index 0000000..168334b --- /dev/null +++ b/assets/enemy/metadata.md @@ -0,0 +1,79 @@ +# assets/enemy/ — Metadata + +## Ghost Soldier (鬼卒) — Melee + +### ghost_melee_idle.png +- Size: 192×48 | Frames: 4 | Frame: 48×48 | FPS: 8 | Loop: Yes +- Purpose: Ghost soldier idle — swaying, weapon dragging + +### ghost_melee_run.png +- Size: 288×48 | Frames: 6 | Frame: 48×48 | FPS: 10 | Loop: Yes +- Purpose: Lurching forward, uneven gait + +### ghost_melee_attack.png +- Size: 192×48 | Frames: 4 | Frame: 48×48 | FPS: 12 | Loop: No +- Purpose: Wild overhead chop + +### ghost_melee_hurt.png +- Size: 96×48 | Frames: 2 | Frame: 48×48 | FPS: 10 | Loop: No +- Purpose: Recoil, body flickers + +### ghost_melee_death.png +- Size: 192×48 | Frames: 4 | Frame: 48×48 | FPS: 8 | Loop: No +- Purpose: Dissolves into blue mist + +--- + +## Ghost Archer (鬼弓手) — Ranged + +### ghost_archer_idle.png +- Size: 192×48 | Frames: 4 | Frame: 48×48 | FPS: 8 | Loop: Yes +- Purpose: Hovering, bow held loosely + +### ghost_archer_run.png +- Size: 288×48 | Frames: 6 | Frame: 48×48 | FPS: 10 | Loop: Yes +- Purpose: Gliding forward, feet not touching ground + +### ghost_archer_attack.png +- Size: 192×48 | Frames: 4 | Frame: 48×48 | FPS: 12 | Loop: No +- Purpose: Draw bow → release ghost arrow → lower bow + +### ghost_archer_hurt.png +- Size: 96×48 | Frames: 2 | Frame: 48×48 | FPS: 10 | Loop: No +- Purpose: Flickers, body becomes semi-transparent + +### ghost_archer_death.png +- Size: 192×48 | Frames: 4 | Frame: 48×48 | FPS: 8 | Loop: No +- Purpose: Bow shatters, body disperses into green wisps + +--- + +## Corpse Beast (尸兽) — Charge + +### corpse_beast_idle.png +- Size: 256×48 | Frames: 4 | Frame: 64×48 | FPS: 8 | Loop: Yes +- Purpose: Low growl, body heaving, limbs twitching + +### corpse_beast_run.png +- Size: 384×48 | Frames: 6 | Frame: 64×48 | FPS: 14 | Loop: Yes +- Purpose: Fast charge — all four legs, mouth open + +### corpse_beast_attack.png +- Size: 256×48 | Frames: 4 | Frame: 64×48 | FPS: 12 | Loop: No +- Purpose: Lunge forward, jaws snapping + +### corpse_beast_hurt.png +- Size: 128×48 | Frames: 2 | Frame: 64×48 | FPS: 10 | Loop: No +- Purpose: Recoils, limbs spasm + +### corpse_beast_death.png +- Size: 256×48 | Frames: 4 | Frame: 64×48 | FPS: 8 | Loop: No +- Purpose: Collapses, flesh sloughing off bones + +--- + +## Godot Import Settings (all enemy sprites) +- Filter: Off (Nearest) +- Mipmaps: Off +- Compression: Lossless +- Format: RGBA diff --git a/assets/icons/README.md b/assets/icons/README.md new file mode 100644 index 0000000..026dc93 --- /dev/null +++ b/assets/icons/README.md @@ -0,0 +1,10 @@ +# assets/icons/ — Icon Assets + +## Structure +- `weapons/` — 32×32 weapon icons (6 total) +- `ui/` — 32×32 UI stat icons (11 total) + +All icons are unified at 32×32 px, transparent background, nearest-neighbor filtering. + +## Concept Reference +See `docs/art/CONCEPT_ICONS.md` for full design descriptions. diff --git a/assets/icons/ui/README.md b/assets/icons/ui/README.md new file mode 100644 index 0000000..ba00c8d --- /dev/null +++ b/assets/icons/ui/README.md @@ -0,0 +1,21 @@ +# assets/icons/ui/ — UI Stat Icons + +All 32×32 px. Transparent background. 1px dark outline. + +## Files +| File | Name | Description | +|------|------|-------------| +| icon_hp.png | 生命值 | HP — heart shape, dark red | +| icon_stamina.png | 体力 | Stamina — circular gauge, green | +| icon_attack.png | 攻击 | Attack — downward sword, iron | +| icon_armor.png | 护甲 | Armor — shield, dark steel | +| icon_crit.png | 暴击 | Critical — starburst, orange | +| icon_speed.png | 速度 | Speed — chevron, grey | +| icon_sacrifice.png | 献祭 | Sacrifice — chalice/blood, purple | +| icon_ghostfire.png | 鬼火 | Ghost fire — flame orb, blue-green | +| icon_boss.png | Boss | Skull with horns, bone | +| icon_key.png | 钥匙 | Key — skeleton key, bronze | +| icon_coin.png | 阴钱 | Yin coin — round coin with square hole | + +## Concept Reference +See `docs/art/CONCEPT_ICONS.md` — UI Stat Icons section. diff --git a/assets/icons/ui/icon_armor.png b/assets/icons/ui/icon_armor.png new file mode 100644 index 0000000..ff85421 Binary files /dev/null and b/assets/icons/ui/icon_armor.png differ diff --git a/assets/icons/ui/icon_attack.png b/assets/icons/ui/icon_attack.png new file mode 100644 index 0000000..5adb31e Binary files /dev/null and b/assets/icons/ui/icon_attack.png differ diff --git a/assets/icons/ui/icon_boss.png b/assets/icons/ui/icon_boss.png new file mode 100644 index 0000000..f538c64 Binary files /dev/null and b/assets/icons/ui/icon_boss.png differ diff --git a/assets/icons/ui/icon_coin.png b/assets/icons/ui/icon_coin.png new file mode 100644 index 0000000..6ded108 Binary files /dev/null and b/assets/icons/ui/icon_coin.png differ diff --git a/assets/icons/ui/icon_crit.png b/assets/icons/ui/icon_crit.png new file mode 100644 index 0000000..9c8d419 Binary files /dev/null and b/assets/icons/ui/icon_crit.png differ diff --git a/assets/icons/ui/icon_ghostfire.png b/assets/icons/ui/icon_ghostfire.png new file mode 100644 index 0000000..5a64bd7 Binary files /dev/null and b/assets/icons/ui/icon_ghostfire.png differ diff --git a/assets/icons/ui/icon_hp.png b/assets/icons/ui/icon_hp.png new file mode 100644 index 0000000..a9dba94 Binary files /dev/null and b/assets/icons/ui/icon_hp.png differ diff --git a/assets/icons/ui/icon_key.png b/assets/icons/ui/icon_key.png new file mode 100644 index 0000000..a9c2a4e Binary files /dev/null and b/assets/icons/ui/icon_key.png differ diff --git a/assets/icons/ui/icon_sacrifice.png b/assets/icons/ui/icon_sacrifice.png new file mode 100644 index 0000000..9830815 Binary files /dev/null and b/assets/icons/ui/icon_sacrifice.png differ diff --git a/assets/icons/ui/icon_speed.png b/assets/icons/ui/icon_speed.png new file mode 100644 index 0000000..07ab04b Binary files /dev/null and b/assets/icons/ui/icon_speed.png differ diff --git a/assets/icons/ui/icon_stamina.png b/assets/icons/ui/icon_stamina.png new file mode 100644 index 0000000..d70c7fa Binary files /dev/null and b/assets/icons/ui/icon_stamina.png differ diff --git a/assets/icons/weapons/README.md b/assets/icons/weapons/README.md new file mode 100644 index 0000000..2ce7480 --- /dev/null +++ b/assets/icons/weapons/README.md @@ -0,0 +1,16 @@ +# assets/icons/weapons/ — Weapon Icons + +All 32×32 px. Transparent background. 1px dark outline. + +## Files +| File | Name | Description | +|------|------|-------------| +| weapon_songdao.png | 宋刀 | Song dynasty straight-backed sword | +| weapon_spear.png | 长枪 | Military spear with red tassel | +| weapon_ghostfire.png | 鬼火 | Ghost fire orb (element/power) | +| weapon_talisman.png | 护符 | Paper talisman with red seal | +| weapon_gourd.png | 酒葫芦 | Wine gourd (healing item) | +| weapon_flag.png | 残破军旗 | Broken battle flag on snapped pole | + +## Concept Reference +See `docs/art/CONCEPT_ICONS.md` — Weapon Icons section. diff --git a/assets/icons/weapons/weapon_flag.png b/assets/icons/weapons/weapon_flag.png new file mode 100644 index 0000000..dc203ef Binary files /dev/null and b/assets/icons/weapons/weapon_flag.png differ diff --git a/assets/icons/weapons/weapon_ghostfire.png b/assets/icons/weapons/weapon_ghostfire.png new file mode 100644 index 0000000..5a64bd7 Binary files /dev/null and b/assets/icons/weapons/weapon_ghostfire.png differ diff --git a/assets/icons/weapons/weapon_gourd.png b/assets/icons/weapons/weapon_gourd.png new file mode 100644 index 0000000..2a3b6b2 Binary files /dev/null and b/assets/icons/weapons/weapon_gourd.png differ diff --git a/assets/icons/weapons/weapon_songdao.png b/assets/icons/weapons/weapon_songdao.png new file mode 100644 index 0000000..1917d04 Binary files /dev/null and b/assets/icons/weapons/weapon_songdao.png differ diff --git a/assets/icons/weapons/weapon_spear.png b/assets/icons/weapons/weapon_spear.png new file mode 100644 index 0000000..90810af Binary files /dev/null and b/assets/icons/weapons/weapon_spear.png differ diff --git a/assets/icons/weapons/weapon_talisman.png b/assets/icons/weapons/weapon_talisman.png new file mode 100644 index 0000000..5a19d57 Binary files /dev/null and b/assets/icons/weapons/weapon_talisman.png differ diff --git a/assets/player/README.md b/assets/player/README.md new file mode 100644 index 0000000..5d067e8 --- /dev/null +++ b/assets/player/README.md @@ -0,0 +1,29 @@ +# assets/player/ — Player Character Sprites + +## Character +**岳家军普通军士** (Yue Family Army Soldier) — a broken, battle-worn foot soldier trapped in the underworld. + +## Files +| File | Animation | Frames | Sheet Size | Frame Size | FPS | +|------|-----------|--------|------------|------------|-----| +| player_idle.png | Idle | 4 | 192×48 | 48×48 | 8 | +| player_run.png | Run | 6 | 288×48 | 48×48 | 12 | +| player_jump.png | Jump | 2 | 96×48 | 48×48 | 10 | +| player_attack.png | Attack | 4 | 192×48 | 48×48 | 14 | +| player_hurt.png | Hurt | 2 | 96×48 | 48×48 | 10 | +| player_death.png | Death | 6 | 288×48 | 48×48 | 8 | + +## Sprite Sheet Format +Horizontal strip — frames arranged left-to-right. Transparent background. Nearest-neighbor filtering. + +## Godot Import +- Filter: Off (Nearest) +- Mipmaps: Off +- Use with `AnimatedSprite2D` + `SpriteFrames` resource +- Set H-Frames per the table above + +## Concept Reference +See `docs/art/CONCEPT_PLAYER.md` for full visual design description. + +## Color Palette +Dried blood red `#5A322B`, ash brown `#3A2E25`, dark iron `#4A4035`, ghost fire eyes `#B4641E` diff --git a/assets/player/metadata.md b/assets/player/metadata.md new file mode 100644 index 0000000..5748700 --- /dev/null +++ b/assets/player/metadata.md @@ -0,0 +1,63 @@ +# assets/player/ — Metadata + +## Sprite Sheet Specifications + +### player_idle.png +- **Size:** 192 × 48 px +- **Frame size:** 48 × 48 px +- **Frame count:** 4 +- **Layout:** Horizontal strip (left-to-right) +- **FPS:** 8 +- **Loop:** Yes +- **Purpose:** Player idle animation — exhausted soldier breathing + +### player_run.png +- **Size:** 288 × 48 px +- **Frame size:** 48 × 48 px +- **Frame count:** 6 +- **Layout:** Horizontal strip +- **FPS:** 12 +- **Loop:** Yes +- **Purpose:** Player run cycle — heavy armored movement + +### player_jump.png +- **Size:** 96 × 48 px +- **Frame size:** 48 × 48 px +- **Frame count:** 2 +- **Layout:** Horizontal strip +- **FPS:** 10 +- **Loop:** No +- **Purpose:** Player jump — launch + apex + +### player_attack.png +- **Size:** 192 × 48 px +- **Frame size:** 48 × 48 px +- **Frame count:** 4 +- **Layout:** Horizontal strip +- **FPS:** 14 +- **Loop:** No +- **Purpose:** Player sword attack — wind-up, strike, follow-through, recovery + +### player_hurt.png +- **Size:** 96 × 48 px +- **Frame size:** 48 × 48 px +- **Frame count:** 2 +- **Layout:** Horizontal strip +- **FPS:** 10 +- **Loop:** No +- **Purpose:** Player hurt reaction — impact + stagger + +### player_death.png +- **Size:** 288 × 48 px +- **Frame size:** 48 × 48 px +- **Frame count:** 6 +- **Layout:** Horizontal strip +- **FPS:** 8 +- **Loop:** No +- **Purpose:** Player death — sword drop, collapse, dissolve + +## Godot Import Settings +- Filter: Off (Nearest) +- Mipmaps: Off +- Compression: Lossless +- Format: RGBA diff --git a/assets/player/player_attack.png b/assets/player/player_attack.png new file mode 100644 index 0000000..628c7ea Binary files /dev/null and b/assets/player/player_attack.png differ diff --git a/assets/player/player_death.png b/assets/player/player_death.png new file mode 100644 index 0000000..4b2708f Binary files /dev/null and b/assets/player/player_death.png differ diff --git a/assets/player/player_hurt.png b/assets/player/player_hurt.png new file mode 100644 index 0000000..521de70 Binary files /dev/null and b/assets/player/player_hurt.png differ diff --git a/assets/player/player_idle.png b/assets/player/player_idle.png new file mode 100644 index 0000000..628c7ea Binary files /dev/null and b/assets/player/player_idle.png differ diff --git a/assets/player/player_jump.png b/assets/player/player_jump.png new file mode 100644 index 0000000..521de70 Binary files /dev/null and b/assets/player/player_jump.png differ diff --git a/assets/player/player_run.png b/assets/player/player_run.png new file mode 100644 index 0000000..4b2708f Binary files /dev/null and b/assets/player/player_run.png differ diff --git a/assets/tiles/README.md b/assets/tiles/README.md new file mode 100644 index 0000000..bd3bb16 --- /dev/null +++ b/assets/tiles/README.md @@ -0,0 +1,22 @@ +# assets/tiles/ — Tilemap Tiles + +All 32×32 px. Seamless tiles have matching edges. Props have transparent backgrounds. + +## Files +| File | Name | Type | Seamless | +|------|------|------|----------| +| tile_floor.png | 地面 | Ground | Yes (all directions) | +| tile_stone_brick.png | 石砖 | Brick pattern | Yes (all directions) | +| tile_wall.png | 墙壁 | Wall texture | Yes (horizontal) | +| tile_wood_bridge.png | 木桥 | Wood planks | Yes (horizontal) | +| tile_brazier.png | 鬼火火盆 | Prop (light source) | No | +| tile_tombstone.png | 墓碑 | Prop (decoration) | No | +| tile_ground_spike.png | 地刺 | Prop (hazard) | No | + +## Godot Import +- Import as TileSet or individual sprites +- Tile size: 32×32 +- See `docs/art/GODOT_IMPORT_GUIDE.md` Section 3 for collision setup + +## Concept Reference +See `docs/art/CONCEPT_TILES.md` for full design descriptions. diff --git a/assets/tiles/metadata.md b/assets/tiles/metadata.md new file mode 100644 index 0000000..86446fd --- /dev/null +++ b/assets/tiles/metadata.md @@ -0,0 +1,36 @@ +# assets/tiles/ — Metadata + +## Tile Specifications (all 32×32 px) + +### tile_floor.png +- Size: 32×32 | Type: Seamless tileable | Collision: Full rect +- Purpose: Cracked stone floor — underworld ground + +### tile_stone_brick.png +- Size: 32×32 | Type: Seamless tileable | Collision: Full rect +- Purpose: Cut stone bricks in running bond pattern + +### tile_wall.png +- Size: 32×32 | Type: Seamless tileable (horizontal) | Collision: Full rect +- Purpose: Dark rough stone wall — background barrier + +### tile_wood_bridge.png +- Size: 32×32 | Type: Seamless tileable (horizontal) | Collision: Full rect (one-way platform) +- Purpose: Wooden plank surface for bridge/platform sections + +### tile_brazier.png +- Size: 32×32 | Type: Prop (NOT tileable) | Collision: None +- Purpose: Stone brazier with ghost fire — light source + +### tile_tombstone.png +- Size: 32×32 | Type: Prop (NOT tileable) | Collision: Optional small rect +- Purpose: Worn stone tombstone — background decoration + +### tile_ground_spike.png +- Size: 32×32 | Type: Prop (NOT tileable) | Collision: Full rect + damage area +- Purpose: Bone/iron spikes — contact hazard + +## Godot Import Settings +- Filter: Off (Nearest) +- Mipmaps: Off +- Compression: Lossless diff --git a/assets/tiles/tile_brazier.png b/assets/tiles/tile_brazier.png new file mode 100644 index 0000000..c4f0c8c Binary files /dev/null and b/assets/tiles/tile_brazier.png differ diff --git a/assets/tiles/tile_floor.png b/assets/tiles/tile_floor.png new file mode 100644 index 0000000..d89e88b Binary files /dev/null and b/assets/tiles/tile_floor.png differ diff --git a/assets/tiles/tile_ground_spike.png b/assets/tiles/tile_ground_spike.png new file mode 100644 index 0000000..0c20144 Binary files /dev/null and b/assets/tiles/tile_ground_spike.png differ diff --git a/assets/tiles/tile_stone_brick.png b/assets/tiles/tile_stone_brick.png new file mode 100644 index 0000000..48d339a Binary files /dev/null and b/assets/tiles/tile_stone_brick.png differ diff --git a/assets/tiles/tile_tombstone.png b/assets/tiles/tile_tombstone.png new file mode 100644 index 0000000..7f00d38 Binary files /dev/null and b/assets/tiles/tile_tombstone.png differ diff --git a/assets/tiles/tile_wall.png b/assets/tiles/tile_wall.png new file mode 100644 index 0000000..a26d029 Binary files /dev/null and b/assets/tiles/tile_wall.png differ diff --git a/assets/tiles/tile_wood_bridge.png b/assets/tiles/tile_wood_bridge.png new file mode 100644 index 0000000..2a599cd Binary files /dev/null and b/assets/tiles/tile_wood_bridge.png differ diff --git a/assets/ui/README.md b/assets/ui/README.md new file mode 100644 index 0000000..9c23bf3 --- /dev/null +++ b/assets/ui/README.md @@ -0,0 +1,17 @@ +# assets/ui/ — UI Elements + +## Files +| File | Name | Size | Purpose | +|------|------|------|---------| +| ui_health_bar.png | 生命条 | 96×16 | Player HP bar (frame + red fill) | +| ui_stamina_bar.png | 体力条 | 96×16 | Player stamina bar (frame + green fill) | +| ui_frame.png | 通用边框 | 32×32 | 9-slice frame for panels/slots | +| ui_minimap_frame.png | 小地图边框 | 64×64 | Minimap border frame | + +## Notes +- Bars: 1px frame border, 14px interior fill area +- `ui_frame.png` is a 9-slice sprite — corners stay fixed, edges stretch +- `ui_minimap_frame.png` has 4px border, 56px interior for minimap rendering + +## Concept Reference +See `docs/art/CONCEPT_UI.md` for full design descriptions. diff --git a/assets/ui/metadata.md b/assets/ui/metadata.md new file mode 100644 index 0000000..3c5c845 --- /dev/null +++ b/assets/ui/metadata.md @@ -0,0 +1,41 @@ +# assets/ui/ — Metadata + +## UI Element Specifications + +### ui_health_bar.png +- Size: 96 × 16 px +- Type: UI bar (frame + fill) +- Layout: 1px frame border, 14px interior +- Colors: Frame `#2A2520`, fill `#5A1818` (dark red), highlight `#8A2828` +- Purpose: Player health bar — fill scales with HP percentage +- Engine: Use as TextureProgressBar or split frame/fill textures + +### ui_stamina_bar.png +- Size: 96 × 16 px +- Type: UI bar (frame + fill) +- Layout: 1px frame border, 14px interior +- Colors: Frame `#2A2520`, fill `#1A4A2A` (dark green), highlight `#2A8A4A` +- Purpose: Player stamina bar — fill scales with stamina percentage +- Engine: Use as TextureProgressBar or split frame/fill textures + +### ui_frame.png +- Size: 32 × 32 px +- Type: 9-slice frame +- Layout: 3px ornate border, 26px transparent interior +- Colors: Frame `#2A2520`, highlight `#4A4035`, corners `#5A4A3A` +- Purpose: Generic UI panel frame — inventory slots, dialogue boxes, menus +- Engine: Import as 9-slice texture, set margins to 3px + +### ui_minimap_frame.png +- Size: 64 × 64 px +- Type: 9-slice frame +- Layout: 4px border, 56px interior +- Colors: Frame `#2A2520`, highlight `#4A4035`, corner spikes `#5A4A3A` +- Purpose: Minimap display frame — minimap renders inside transparent interior +- Engine: Static sprite, minimap renders as child node inside frame + +## Godot Import Settings +- Filter: Off (Nearest) +- Mipmaps: Off +- Compression: Lossless +- Format: RGBA diff --git a/assets/ui/ui_frame.png b/assets/ui/ui_frame.png new file mode 100644 index 0000000..290169f Binary files /dev/null and b/assets/ui/ui_frame.png differ diff --git a/assets/ui/ui_health_bar.png b/assets/ui/ui_health_bar.png new file mode 100644 index 0000000..599aa02 Binary files /dev/null and b/assets/ui/ui_health_bar.png differ diff --git a/assets/ui/ui_minimap_frame.png b/assets/ui/ui_minimap_frame.png new file mode 100644 index 0000000..edb51ec Binary files /dev/null and b/assets/ui/ui_minimap_frame.png differ diff --git a/assets/ui/ui_stamina_bar.png b/assets/ui/ui_stamina_bar.png new file mode 100644 index 0000000..9819284 Binary files /dev/null and b/assets/ui/ui_stamina_bar.png differ diff --git a/docs/AI_HANDOFF.md b/docs/AI_HANDOFF.md index 10c433b..d26648b 100644 --- a/docs/AI_HANDOFF.md +++ b/docs/AI_HANDOFF.md @@ -1,15 +1,16 @@ # AI Handoff -- **Current phase:** Repository collaboration setup complete; core framework not started. +- **Current phase:** Art asset pipeline complete; core framework not started. - **Current playable state:** No playable build exists. -- **Completed repository work:** Collaboration rules, ownership, templates, task tracking, and repository hygiene validation. -- **In progress:** None. +- **Completed repository work:** Collaboration rules, ownership, templates, task tracking, repository hygiene validation, and **prototype art asset pipeline** (63 placeholder sprites, 8 concept docs, master art spec, Godot import guide, all directory READMEs and metadata). +- **In progress:** Art PR (`workbuddy/prototype-art`) awaiting review/merge. - **Next owner:** Claude. -- **Next task:** Create `claude/core-framework` from the latest `develop` branch. +- **Next task:** Create `claude/core-framework` from the latest `develop` branch. Art assets are ready in `assets/` — read `docs/ART_SPEC.md` and each directory's `metadata.md` for sprite sheet frame counts, sizes, and FPS values. - **Frozen interfaces:** None; no gameplay interfaces are frozen yet. -- **Do not modify:** Product requirements without Game Director approval; `main`/`develop` directly; future frozen interfaces without the decision process. +- **Do not modify:** Product requirements without Game Director approval; `main`/`develop` directly; future frozen interfaces without the decision process; art concept documents (`docs/art/CONCEPT_*.md`) without consulting the Art Director. - **Known blockers:** Approved detailed PRD and Prototype Contract source material is not present in the repository. +- **Art asset notes:** All PNGs in `assets/` are placeholders (magenta border). Replace by saving a new PNG with the same filename and dimensions. See `docs/ART_SPEC.md` Section 5 for replacement workflow. Godot import settings documented in `docs/art/GODOT_IMPORT_GUIDE.md` and `godot/import_presets.md`. - **Last successful test:** Repository validation checks (2026-08-02). - **Last updated:** 2026-08-02 (Australia/Melbourne). -The PRD and Prototype Contract are the current highest-priority sources of truth. +The PRD and Prototype Contract are the current highest-priority sources of truth. `docs/ART_SPEC.md` is the source of truth for all visual art assets. diff --git a/docs/ART_SPEC.md b/docs/ART_SPEC.md new file mode 100644 index 0000000..c2879cb --- /dev/null +++ b/docs/ART_SPEC.md @@ -0,0 +1,297 @@ +# 九幽 — Art Specification (美术资源规范) + +> **STATUS: ACTIVE — Prototype Phase** +> **Owner:** WorkBuddy (Art Director / Asset Pipeline Engineer) +> **Last Updated:** 2026-08-02 + +This document is the **single source of truth** for all visual art assets in the Nine Nether prototype. Claude and Codex should read this before referencing any sprite, icon, tile, or UI element. + +--- + +## 1. Art Direction + +### Style +- **Genre:** 2D Side-scrolling Action Roguelite +- **Visual Style:** Chinese Dark Fantasy Pixel Art (中国黑暗幻想像素风) +- **Era Reference:** Southern Song Dynasty (南宋) — Underworld / Hell (地府) — Ghosts & Demons (鬼怪) — Ancient Warfare (古代战争) +- **DO NOT reference:** Anime style, Chibi/Q-version, Western cartoon, any existing game directly + +### Tone +- Oppressive, somber, decayed +- Muted palette with strategic accents of ghost-fire orange and blood red +- Textures should feel worn, broken, ancient — never clean or pristine + +### Color Philosophy +| Role | Hex | Usage | +|------|-----|-------| +| Deep Void | `#0F0C12` | Backgrounds, shadows | +| Ash Stone | `#28231E` | Tiles, environment | +| Dried Blood | `#78322B` | Player base, blood | +| Ghost Blue | `#2D3746` | Ghost enemies | +| Corpse Flesh | `#372823` | Beast enemies | +| Underworld Purple | `#321E37` | Boss, sacrifice | +| Ghost Fire | `#B4641E` | Accents, fire, energy | +| Pale Bone | `#C4B89A` | Highlights, bone, pale skin | +| Rust Iron | `#5A4632` | Metal, armor | +| Yin Coin Gold | `#8B7332` | Currency, treasure | + +--- + +## 2. Pixel Art Standards + +### Resolution & Scale +| Asset Type | Frame Size | Notes | +|------------|-----------|-------| +| Player sprite | 48 × 48 px | Per animation frame | +| Enemy sprites (humanoid) | 48 × 48 px | Ghost Soldier, Ghost Archer | +| Enemy sprites (beast) | 64 × 48 px | Corpse Beast (wider, quadruped) | +| Boss sprite | 96 × 96 px | Larger than player for intimidation | +| Weapon icons | 32 × 32 px | Unified | +| UI icons | 32 × 32 px | Unified | +| Tiles | 32 × 32 px | Grid-based, tileable | +| Effects | 48 × 48 px | Combat VFX | +| Background elements | Varies | See individual metadata | +| UI elements | Varies | See individual metadata | + +### Sprite Sheet Format +- **Layout:** Horizontal strip (frames arranged left-to-right) +- **Background:** Transparent (alpha channel) +- **Format:** PNG (lossless) +- **Filter:** Nearest-neighbor (no anti-aliasing) +- **Color depth:** RGBA 32-bit + +### Animation Frame Counts +| Animation | Frames | Purpose | +|-----------|--------|---------| +| Idle | 4 | Subtle breathing / hovering | +| Run | 6 | Full run cycle | +| Jump | 2 | Launch + apex | +| Attack | 4-5 | Wind-up → strike → recovery | +| Hurt | 2 | Impact + stagger | +| Death | 4-8 | Collapse / dissolve | + +### Frame Rate +- Default: 10 FPS (0.1s per frame) +- Attack: 12-15 FPS (snappier) +- Death: 6-8 FPS (slower, dramatic) + +--- + +## 3. Naming Convention + +### Pattern +``` +__.png +``` + +### Rules +1. **All lowercase**, words separated by underscores +2. **No spaces, no Chinese characters, no special chars** in filenames +3. **No version numbers** in filenames (use git, not `v2_final.png`) +4. **No generic names** like `image1.png`, `新建文件.png` +5. Sprite sheets use the action name, not frame numbers + +### Examples +``` +player_idle.png ← Player idle animation (4-frame sheet) +player_run.png ← Player run animation (6-frame sheet) +ghost_melee_attack.png ← Ghost Soldier attack animation +boss_death.png ← Boss death animation +weapon_songdao.png ← Song Dynasty sword icon +icon_hp.png ← HP UI icon +tile_floor.png ← Floor tile +effect_blood_splash.png ← Blood splash VFX +bg_tree.png ← Background tree +ui_health_bar.png ← Health bar UI element +``` + +### Category Prefixes +| Prefix | Directory | Description | +|--------|-----------|-------------| +| `player_` | `assets/player/` | Player character sprites | +| `ghost_melee_` | `assets/enemy/` | Ghost Soldier (鬼卒) | +| `ghost_archer_` | `assets/enemy/` | Ghost Archer (鬼弓手) | +| `corpse_beast_` | `assets/enemy/` | Corpse Beast (尸兽) | +| `boss_` | `assets/boss/` | Boss sprites | +| `weapon_` | `assets/icons/weapons/` | Weapon icons | +| `icon_` | `assets/icons/ui/` | UI stat icons | +| `tile_` | `assets/tiles/` | Tilemap tiles | +| `effect_` | `assets/effects/` | Visual effects | +| `bg_` | `assets/background/` | Background elements | +| `ui_` | `assets/ui/` | UI elements (bars, frames) | + +--- + +## 4. Directory Structure + +``` +assets/ +├── ART_SPEC.md ← This file (master spec) +├── generate_placeholders.py ← Placeholder generation script +├── player/ ← Player character sprites +│ ├── README.md +│ ├── metadata.md +│ └── *.png +├── enemy/ ← All enemy sprites +│ ├── README.md +│ ├── metadata.md +│ └── *.png +├── boss/ ← Boss sprites +│ ├── README.md +│ ├── metadata.md +│ └── *.png +├── icons/ +│ ├── weapons/ ← 32×32 weapon icons +│ │ ├── README.md +│ │ └── *.png +│ └── ui/ ← 32×32 UI stat icons +│ ├── README.md +│ └── *.png +├── tiles/ ← 32×32 tilemap tiles +│ ├── README.md +│ ├── metadata.md +│ └── *.png +├── effects/ ← 48×48 VFX sprites +│ ├── README.md +│ ├── metadata.md +│ └── *.png +├── background/ ← Parallax / decorative backgrounds +│ ├── README.md +│ ├── metadata.md +│ └── *.png +└── ui/ ← UI elements (bars, frames) + ├── README.md + ├── metadata.md + └── *.png +``` + +--- + +## 5. Placeholder Policy + +All current PNGs are **placeholders** generated programmatically. They: +- Use the correct dimensions and sprite sheet layout +- Have a magenta border (1px) marking them as placeholders +- Show a checkerboard pattern + simple silhouette +- Can be directly replaced by dropping a new PNG with the same filename + +### Replacement Workflow +1. Generate or commission the final pixel art +2. Save with the **exact same filename** and **exact same dimensions** +3. Run `git add assets/ && git commit -m "[Art] Replace placeholder with final art"` +4. The Godot import settings will remain valid as long as dimensions match + +### How to Identify Placeholders +- Magenta (#FF00FF) 1px border = placeholder +- Checkerboard interior = placeholder +- Once replaced, remove the magenta border from the final art + +--- + +## 6. Godot 4 Import Settings + +### Texture Import (per PNG) +| Setting | Value | +|---------|-------| +| Preset | 2D Pixel | +| Filter | Off (Nearest) | +| Mipmaps | Off | +| Compression | Lossless | +| HDR | Off | + +### Sprite Sheet → AnimatedSprite2D +1. Import the PNG as a texture +2. In the SpriteFrames editor, create a new animation +3. Set "Horizontal frames" to the frame count from metadata +4. Set FPS per the metadata (default 10) + +### Recommended Godot Resource Path +``` +res://assets/player/player_idle.png → SpriteFrames → "idle" animation +res://assets/player/player_run.png → SpriteFrames → "run" animation +``` + +See `docs/art/GODOT_IMPORT_GUIDE.md` for step-by-step instructions. + +--- + +## 7. Asset Inventory + +### Player (6 sheets, 24 total frames) +| File | Frames | Size | FPS | +|------|--------|------|-----| +| player_idle.png | 4 | 192×48 | 8 | +| player_run.png | 6 | 288×48 | 12 | +| player_jump.png | 2 | 96×48 | 10 | +| player_attack.png | 4 | 192×48 | 14 | +| player_hurt.png | 2 | 96×48 | 10 | +| player_death.png | 6 | 288×48 | 8 | + +### Enemies (15 sheets, 70 total frames) +| File | Frames | Size | FPS | +|------|--------|------|-----| +| ghost_melee_idle.png | 4 | 192×48 | 8 | +| ghost_melee_run.png | 6 | 288×48 | 10 | +| ghost_melee_attack.png | 4 | 192×48 | 12 | +| ghost_melee_hurt.png | 2 | 96×48 | 10 | +| ghost_melee_death.png | 4 | 192×48 | 8 | +| ghost_archer_idle.png | 4 | 192×48 | 8 | +| ghost_archer_run.png | 6 | 288×48 | 10 | +| ghost_archer_attack.png | 4 | 192×48 | 12 | +| ghost_archer_hurt.png | 2 | 96×48 | 10 | +| ghost_archer_death.png | 4 | 192×48 | 8 | +| corpse_beast_idle.png | 4 | 256×48 | 8 | +| corpse_beast_run.png | 6 | 384×48 | 14 | +| corpse_beast_attack.png | 4 | 256×48 | 12 | +| corpse_beast_hurt.png | 2 | 128×48 | 10 | +| corpse_beast_death.png | 4 | 256×48 | 8 | + +### Boss (5 sheets, 25 total frames) +| File | Frames | Size | FPS | +|------|--------|------|-----| +| boss_idle.png | 4 | 384×96 | 6 | +| boss_run.png | 6 | 576×96 | 8 | +| boss_attack.png | 5 | 480×96 | 10 | +| boss_hurt.png | 2 | 192×96 | 8 | +| boss_death.png | 8 | 768×96 | 6 | + +### Icons (17 total, all 32×32) +- Weapons: 6 (songdao, spear, ghostfire, talisman, gourd, flag) +- UI: 11 (hp, stamina, attack, armor, crit, speed, sacrifice, ghostfire, boss, key, coin) + +### Tiles (7 total, all 32×32) +floor, stone_brick, wall, wood_bridge, brazier, tombstone, ground_spike + +### Effects (4 total, all 48×48) +hit_slash, blood_splash, ghost_fire, death_fade + +### Background (4 total) +tree (64×128), broken_flag (48×96), chains (32×96), deep (320×180) + +### UI Elements (4 total) +health_bar (96×16), stamina_bar (96×16), frame (32×32), minimap_frame (64×64) + +--- + +## 8. Concept Documents + +Detailed visual concept descriptions for each entity: + +| Document | Contents | +|----------|----------| +| `docs/art/CONCEPT_PLAYER.md` | Player character — Yue Family Army Soldier | +| `docs/art/CONCEPT_ENEMIES.md` | Ghost Soldier, Ghost Archer, Corpse Beast | +| `docs/art/CONCEPT_BOSS.md` | Gate Guardian Ghost General | +| `docs/art/CONCEPT_ICONS.md` | Weapon & UI icon designs | +| `docs/art/CONCEPT_TILES.md` | Tile & environment designs | +| `docs/art/CONCEPT_EFFECTS.md` | VFX designs | +| `docs/art/CONCEPT_BACKGROUND.md` | Background layer designs | +| `docs/art/CONCEPT_UI.md` | UI element designs | + +--- + +## 9. AI Handoff Notes + +- **For Claude:** All sprite sheets are horizontal strips. Frame counts and sizes are in each directory's `metadata.md`. Import with `filter=off, mipmaps=off` in Godot. +- **For art generation AI:** Replace any placeholder PNG with pixel art matching the same dimensions. Remove the magenta border. Follow the concept documents for visual direction. +- **Palette file:** See color table in Section 1. Use these exact hex values for consistency. diff --git a/docs/art/CONCEPT_BACKGROUND.md b/docs/art/CONCEPT_BACKGROUND.md new file mode 100644 index 0000000..0387459 --- /dev/null +++ b/docs/art/CONCEPT_BACKGROUND.md @@ -0,0 +1,81 @@ +# Concept: Background Elements — 背景资源 + +> **Type:** Parallax / Decorative +> **Sizes:** Varies (see each) +> **Style:** Distant, atmospheric, dark + +--- + +## Design Philosophy +- Background elements are **behind the gameplay layer** +- They should be darker and less detailed than foreground elements +- Parallax scrolling: background moves slower than foreground +- All elements have transparent backgrounds (except bg_deep which is full-frame) + +--- + +## Background Designs (4 total) + +### 1. bg_tree.png — 背景树 (Background Tree) +- **Size:** 64 × 128 px +- **Description:** Dead, skeletal tree — bare branches reaching upward +- **Colors:** Trunk `#14100E`, branches `#1E1812`, highlight `#2A2218` +- **Notes:** Leafless, twisted, ancient. Should look like it died centuries ago. Placed at intervals along the background parallax layer. +- **Parallax layer:** Mid-background (0.5x scroll speed) +- **Tiling:** Can be placed at intervals; not seamless but repeatable with variation + +### 2. bg_broken_flag.png — 残破旗帜 (Broken Flag) +- **Size:** 48 × 96 px +- **Description:** Broken flag pole with tattered battle flag still attached +- **Colors:** Pole `#2A2218`, flag `#3A221B` (faded blood red), frayed edges `#1A0E0B` +- **Notes:** Flag should look wind-blasted and old. Pole is broken at the top — flag hangs from the break point. Placed as atmospheric decoration. +- **Parallax layer:** Mid-background (0.5x scroll speed) +- **Notes:** Can have slight sway animation if multi-frame variant is created later + +### 3. bg_chains.png — 锁链 (Chains) +- **Size:** 32 × 96 px +- **Description:** Hanging chains from above — vertical decoration +- **Colors:** Chain `#3A3530`, highlight `#5A5045`, shadow `#1A150F` +- **Notes:** 3-4 chain links visible. Hangs from top of frame. Rusted, heavy iron chains. Atmospheric element suggesting imprisonment/torture. +- **Parallax layer:** Close background (0.7x scroll speed) +- **Notes:** Can have slight sway animation in future + +### 4. bg_deep.png — 深景背景 (Deep Background) +- **Size:** 320 × 180 px +- **Description:** Full-frame deep background — distant underworld landscape +- **Colors:** Sky `#0A080C`, mountains `#15110D`, mist `#1A151F`, distant fire `#1E2820` +- **Notes:** Wide landscape showing the underworld vista — jagged mountains, low-hanging mist, faint distant ghost fires. This is the deepest parallax layer. +- **Parallax layer:** Deep background (0.2x scroll speed) +- **Tiling:** Designed to tile horizontally — left and right edges match + +--- + +## Parallax Layer Structure (for Claude's reference) +``` +Layer 0 (0.2x): bg_deep.png — underworld vista +Layer 1 (0.5x): bg_tree.png, bg_broken_flag.png — mid decoration +Layer 2 (0.7x): bg_chains.png — close decoration +Layer 3 (1.0x): Gameplay (tiles, player, enemies) +Layer 4 (1.0x): effects, UI +``` + +--- + +## Future Background Elements (Not Required for Prototype) +| Element | Description | +|---------|-------------| +| bg_gate.png | Massive underworld gate (boss arena backdrop) | +| bg_river.png | River of blood/souls in deep background | +| bg_tower.png | Distant watchtower silhouette | +| bg_fog.png | Moving fog overlay (semi-transparent) | +| bg_fireflies.png | Floating ghost fire particles (ambient) | + +--- + +## Design Notes for AI Art Generation +1. **Backgrounds are DARKER than foreground** — they recede visually +2. **Less detail, more silhouette** — at parallax distance, details are lost +3. **bg_deep.png is the only full-frame background** — all others are sprite props +4. **Trees should be dead/skeletal** — no leaves, twisted branches +5. **Chains should look heavy and rusted** — not clean metal +6. **The deep background sets the MOOD** — spend the most effort here diff --git a/docs/art/CONCEPT_BOSS.md b/docs/art/CONCEPT_BOSS.md new file mode 100644 index 0000000..88232cf --- /dev/null +++ b/docs/art/CONCEPT_BOSS.md @@ -0,0 +1,116 @@ +# Concept: Boss — 镇关鬼将 (Gate Guardian Ghost General) + +> **Type:** Boss Character +> **Frame Size:** 96 × 96 px +> **Direction:** Face left by default (toward player) + +--- + +## Visual Description + +A towering underworld general who guards the gate between realms. Not a historical figure — a pure creation of the underworld's malice. He radiates oppressive ghost fire and wields a massive blade too heavy for any mortal. + +### Scale +- **3× the player's height** — fills the 96×96 frame +- Player stands ~40px tall; Boss stands ~88px tall +- This size difference must feel threatening in-game + +### Key Characteristics +- **巨大 (Gigantic):** Towers over the player. Body fills most of the frame. +- **压迫感 (Oppressive):** Broad shoulders, heavy posture. Even idle, he looms. +- **鬼火 (Ghost Fire):** Blue-green flames wreath his shoulders, blade, and eyes. This is his signature visual. +- **重刀 (Heavy Blade):** Wields a massive guandao/Heavy saber (重刀), blade alone is ~40px long. +- **Not historical:** No real armor style. This is underworld plate — organic, bone-like, demonic. +- **Horns:** Curved bone horns protrude from helmet/brow +- **Cape:** Tattered cloak of dark energy flows behind him + +### Face +- No visible eyes — just two burning ghost-fire orbs in a shadowed face +- Mouth: Permanent grimace, visible fangs +- Helmet: Bone crown fused to skull + +### Body Proportions (within 96×96) +- Head + horns: ~24px tall +- Torso: ~36px tall, shoulders ~56px wide +- Legs: ~28px tall +- Blade extends ~40px to the left when attacking +- Cape flows ~20px behind +- Ghost fire aura: ~8px radius around shoulders and blade + +--- + +## Color Palette +| Part | Color | Hex | +|------|-------|-----| +| Armor (bone plate) | Dark underworld steel | `#321E37` | +| Armor highlight | Ghostly purple sheen | `#5A3264` | +| Cape | Void black with purple edge | `#1A0E1F` | +| Blade | Dark iron | `#2A2520` | +| Blade edge | Faint cold steel | `#4A4540` | +| Ghost fire (shoulders/eyes/blade) | Blue-green flame | `#1EB464` | +| Ghost fire core | Bright cyan-green | `#4AE68A` | +| Bone (horns/teeth) | Yellowed ivory | `#A0987A` | +| Shadow | Deep void | `#0F0C12` | + +--- + +## Animations + +### Idle (4 frames, 6 FPS) +- Frame 1: Standing, blade resting on ground, ghost fire flickering on shoulders +- Frame 2: Slight rise, fire intensifies +- Frame 3: Neutral, fire dims slightly +- Frame 4: Slight sink, fire returns to baseline +- **Feel:** Slow, heavy breathing. A sleeping volcano. The fire should pulse. + +### Run (6 frames, 8 FPS) +- Frame 1: Left foot forward, blade dragging on ground +- Frame 2: Mid-stride, ground cracks beneath foot +- Frame 3: Right foot forward, blade lifted slightly +- Frame 4: Mid-stride, airborne moment (despite size, he moves fast) +- Frame 5: Left foot forward, blade swings +- Frame 6: Recovery +- **Feel:** Earth-shaking charge. The ground should feel like it trembles. + +### Attack (5 frames, 10 FPS) +- Frame 1: Wind-up — blade raised overhead with both hands, ghost fire channels into blade +- Frame 2: Strike begins — blade swings down in a massive overhead arc +- Frame 3: Impact — blade hits ground, shockwave of ghost fire erupts +- Frame 4: Follow-through — blade drags along ground, creating fire trail +- Frame 5: Recovery — lifts blade back to resting position +- **Feel:** Devastating, slow, telegraphed. Each swing should feel like it could level a wall. + +### Hurt (2 frames, 8 FPS) +- Frame 1: Recoil — staggers back, ghost fire flares defensively +- Frame 2: Recovers — plants feet, glares (fire eyes intensify) +- **Feel:** He barely feels it. The anger is more dangerous than the pain. + +### Death (8 frames, 6 FPS) +- Frame 1: Blade drops from hands, embeds in ground +- Frame 2: Grabs chest — cracks appear in bone armor +- Frame 3: Ghost fire erupts from cracks +- Frame 4: Falls to one knee +- Frame 5: Both knees, head bowed +- Frame 6: Armor shatters — bone fragments fly +- Frame 7: Body dissolves into ghost fire +- Frame 8: Only the blade remains, driven into the ground, fire slowly dying +- **Feel:** Epic, slow, dramatic. The boss's death should feel like a mountain crumbling. + +--- + +## Design Notes for AI Art Generation +1. **Scale is critical** — at 96×96, the Boss must look 3× the player's size +2. **Ghost fire is the signature** — blue-green flames on shoulders, eyes, and blade in EVERY frame +3. **The blade is massive** — it should look too heavy for any normal character to wield +4. **Bone armor, not metal** — the armor should look organic, grown from bone, not forged +5. **No symmetrical design** — the cape flows left, blade rests right — asymmetry adds menace +6. **Color: purple + blue-green fire** — distinct from all other enemies (no red, no brown) +7. **Death animation is the longest** (8 frames) — it should feel rewarding to defeat + +--- + +## Boss Arena Visual Notes +- The boss room should have larger ghost fire braziers +- Ground should crack during boss attacks (tile variation) +- Background should darken when boss appears +- Consider a faint purple fog at screen edges during boss fight diff --git a/docs/art/CONCEPT_EFFECTS.md b/docs/art/CONCEPT_EFFECTS.md new file mode 100644 index 0000000..19688fb --- /dev/null +++ b/docs/art/CONCEPT_EFFECTS.md @@ -0,0 +1,70 @@ +# Concept: Visual Effects — 特效 + +> **Type:** VFX Sprites +> **Size:** 48 × 48 px (all effects) +> **Style:** Quick, impactful, dark fantasy + +--- + +## Design Philosophy +- Effects are **fast** — visible for 2-6 frames (0.1-0.4 seconds) +- Each effect should read clearly even in chaotic combat +- Colors should contrast with the dark environment +- All effects use transparent backgrounds +- Effects are centered in the 48×48 frame + +--- + +## Effect Designs (4 total) + +### 1. effect_hit_slash.png — 斩击特效 (Hit Slash) +- **Type:** Single-frame (or 2-frame) impact effect +- **Description:** A white/silver slash arc — appears when player or enemy lands a melee hit +- **Colors:** Core `#E0E0E0`, edge `#A0A0A0`, outline `#606060` +- **Frame layout:** 2 frames — Frame 1: slash arc expanding, Frame 2: slash dissipating +- **Notes:** Diagonal slash arc, ~36px long. Should feel like a blade cut through air. +- **Engine usage:** Spawn at hit position, play 2 frames at 20 FPS, destroy. + +### 2. effect_blood_splash.png — 鲜血飞溅 (Blood Splash) +- **Type:** Multi-frame particle burst +- **Description:** Dark crimson blood particles exploding outward +- **Colors:** Core `#5A1818`, spray `#8A2828`, droplets `#3A0808` +- **Frame layout:** 4 frames — expanding burst, particles flying, particles falling, fade +- **Notes:** Radial burst from center. 8-12 particles. Dark blood, not bright red. +- **Engine usage:** Spawn at damage position, play 4 frames at 15 FPS, destroy. + +### 3. effect_ghost_fire.png — 鬼火特效 (Ghost Fire) +- **Type:** Looping ambient effect +- **Description:** Floating blue-green flame — used for brazier light, boss aura, projectile trail +- **Colors:** Core `#4AE68A`, mid `#1EB464`, outer `#0A5030` +- **Frame layout:** 4 frames — flame flickering (loop) +- **Notes:** Teardrop shape, pointed top. Flickers organically. Can be scaled for different uses. +- **Engine usage:** Loop at 10 FPS. Can be used as light source texture. + +### 4. effect_death_fade.png — 死亡消散 (Death Fade) +- **Type:** Multi-frame dissolve +- **Description:** Character body dissolving into dark particles — used on enemy death +- **Colors:** Body `#3A2E35` (generic), particles `#1A151F`, glow `#2A1E2F` +- **Frame layout:** 4 frames — body intact → cracking → fragmenting → fully dissolved +- **Notes:** Vertical dissolve — bottom fades first. Particles drift upward (soul ascending). +- **Engine usage:** Play 4 frames at 8 FPS after death animation. Destroy after. + +--- + +## Future Effects (Not Required for Prototype) +| Effect | Description | +|--------|-------------| +| effect_dash_trail.png | Player dash afterimage | +| effect_parry_spark.png | Parry successful spark | +| effect_sacrifice_aura.png | Sacrifice activation glow | +| effect_level_up.png | Power gain burst | +| effect_screen_shake.png | (Not a sprite — engine effect) | + +--- + +## Design Notes for AI Art Generation +1. Effects should be **brighter than the environment** — they need to pop +2. Blood is **dark crimson**, not cartoon red — this is a dark fantasy game +3. Ghost fire is **blue-green**, not orange — this is the underworld's fire +4. All effects are **centered** in the 48×48 frame — origin point is center +5. Keep particle count readable at 48px — 8-12 particles max per effect diff --git a/docs/art/CONCEPT_ENEMIES.md b/docs/art/CONCEPT_ENEMIES.md new file mode 100644 index 0000000..5bf91de --- /dev/null +++ b/docs/art/CONCEPT_ENEMIES.md @@ -0,0 +1,120 @@ +# Concept: Enemies — 鬼卒 / 鬼弓手 / 尸兽 + +> **Type:** Enemy Characters +> **Frame Size:** 48 × 48 px (humanoid), 64 × 48 px (beast) +> **Direction:** Face left by default (toward player) + +--- + +## 1. 鬼卒 (Ghost Soldier) — Melee + +### Visual Description +A reanimated soldier of a defeated ancient army, now serving as cannon fodder in the underworld. It shambles with rusted weapon in hand. + +### Key Characteristics +- **Rusted weapon:** Broken spear or notched axe, held in one hand +- **Tattered robes:** Ancient military uniform, rotted to strips +- **Pale blue-grey skin:** Corpse-like, slightly translucent +- **Empty eye sockets:** Faint ghost-fire glow within +- **Hunched posture:** Not standing straight — decades of death have bent it + +### Color Palette +| Part | Color | Hex | +|------|-------|-----| +| Skin | Corpse blue-grey | `#2D3746` | +| Skin highlight | Pale dead flesh | `#46566B` | +| Robes | Rotten dark teal | `#1E2A2A` | +| Weapon | Rusted iron | `#3A3028` | +| Eye glow | Ghost fire | `#B4641E` | +| Shadow | Deep void | `#0F0C12` | + +### Animations +| Animation | Frames | FPS | Description | +|-----------|--------|-----|-------------| +| idle | 4 | 8 | Swaying slightly, weapon dragging | +| run | 6 | 10 | Lurching forward, uneven gait | +| attack | 4 | 12 | Wild overhead chop with weapon | +| hurt | 2 | 10 | Recoils, parts of body flicker | +| death | 4 | 8 | Dissolves into blue mist | + +### Design Notes +- Should look clearly "dead" — not a living soldier +- Movements are jerky and unnatural (puppet-like) +- The ghost-fire eye glow is the only warm color on the body + +--- + +## 2. 鬼弓手 (Ghost Archer) — Ranged + +### Visual Description +A spectral archer that fires soul arrows from a distance. Thinner and more ethereal than the Ghost Soldier. + +### Key Characteristics +- **Bow:** Longbow made of bone/ghost energy, faintly glowing string +- **Quiver:** Worn at hip, arrows are wisps of ghost fire +- **Thin frame:** Gaunt, almost skeletal beneath tattered archer's garb +- **Hood:** Dark hood obscures most of the face +- **Floating slightly:** Feet barely touch ground — hovering + +### Color Palette +| Part | Color | Hex | +|------|-------|-----| +| Skin | Sickly green-grey | `#323C37` | +| Skin highlight | Pale green | `#4B5F55` | +| Garb | Dark moss green | `#1E2820` | +| Bow | Bone white | `#8A8070` | +| Bowstring/arrow | Ghost fire | `#B4641E` | +| Hood shadow | Deep void | `#0F0C12` | + +### Animations +| Animation | Frames | FPS | Description | +|-----------|--------|-----|-------------| +| idle | 4 | 8 | Hovering, bow held loosely | +| run | 6 | 10 | Gliding forward, feet not touching ground | +| attack | 4 | 12 | Draws bow → releases ghost arrow → lower bow | +| hurt | 2 | 10 | Flickers, body becomes semi-transparent | +| death | 4 | 8 | Bow shatters, body disperses into green wisps | + +### Design Notes +- Clearly distinguishable from Ghost Soldier by color (green vs blue) and build (thin vs hunched) +- The arrow should be visible as a distinct projectile (ghost fire wisp) +- Hovering/gliding movement replaces walking + +--- + +## 3. 尸兽 (Corpse Beast) — Charge + +### Visual Description +A massive quadruped formed from fused corpses of war horses and soldiers. It charges blindly, driven by rage. + +### Key Characteristics +- **Quadruped:** Four legs, low-slung body, wider than humanoid enemies +- **Fused flesh:** Visible human limbs protruding from its mass +- **No eyes:** Just a gaping maw with too many teeth +- **Exposed bone:** Ribs and spine visible through rotted flesh +- **Size:** Visually larger and wider than other enemies (64×48 frame) + +### Color Palette +| Part | Color | Hex | +|------|-------|-----| +| Flesh | Dark rotten brown | `#372823` | +| Flesh highlight | Rancid pink | `#5A4035` | +| Bone | Yellowed ivory | `#A0987A` | +| Mouth interior | Void black | `#0A080C` | +| Teeth | Stained bone | `#8A8060` | +| Blood | Dried crimson | `#5A1818` | + +### Animations +| Animation | Frames | FPS | Description | +|-----------|--------|-----|-------------| +| idle | 4 | 8 | Low growl, body heaving, limbs twitching | +| run | 6 | 14 | Fast charge — all four legs, mouth open | +| attack | 4 | 12 | Lunge forward, jaws snapping | +| hurt | 2 | 10 | Recoils, limbs spasm | +| death | 4 | 8 | Collapses, flesh sloughing off bones | + +### Design Notes +- The charge (run animation) should feel FAST and DANGEROUS — this is the core behavior +- Frame is 64×48 (wider) to accommodate the quadruped body +- Mouth should be the most visually striking element (threat indicator) +- No ghost fire on this enemy — it's pure flesh/horror, not spectral diff --git a/docs/art/CONCEPT_ICONS.md b/docs/art/CONCEPT_ICONS.md new file mode 100644 index 0000000..47fc15c --- /dev/null +++ b/docs/art/CONCEPT_ICONS.md @@ -0,0 +1,116 @@ +# Concept: Icons — Weapons & UI Stats + +> **Type:** Icon Assets +> **Size:** 32 × 32 px (all unified) +> **Style:** Pixel art, dark fantasy, limited palette + +--- + +## Design Philosophy +- Icons should be **instantly recognizable** at 32×32 +- Use 3-4 colors max per icon (silhouette + 1-2 highlights + outline) +- All icons share a unified palette (see ART_SPEC.md) +- Background: transparent +- No text/numbers in icons — those are rendered by UI code + +--- + +## Weapon Icons (6 total) + +### 1. weapon_songdao.png — 宋刀 (Song Sword) +- **Description:** Straight-backed single-edged dao, blade pointing up-right +- **Colors:** Dark iron blade `#4A4035`, steel edge `#6A5A4A`, dark handle `#2A2018` +- **Notes:** Blade takes up ~70% of icon. Handle wraps at bottom-left. Small tassel (red) optional. + +### 2. weapon_spear.png — 长枪 (Spear) +- **Description:** Long spear diagonal bottom-left to top-right +- **Colors:** Wood shaft `#5A4025`, iron tip `#4A4035`, red tassel below tip `#5A1818` +- **Notes:** Tassel is the Song dynasty military identifier. Shaft is thin (2px wide). + +### 3. weapon_ghostfire.png — 鬼火 (Ghost Fire) +- **Description:** Floating orb of blue-green flame with wisps +- **Colors:** Core `#4AE68A`, outer `#1EB464`, wisp `#0A8040` +- **Notes:** Not a weapon per se — represents the ghost fire power/element. Used in skill icons. + +### 4. weapon_talisman.png — 护符 (Talisman) +- **Description:** Rectangular paper talisman with seal script, hanging string +- **Colors:** Paper `#8A7A5A`, seal ink `#5A1818`, string `#3A3028` +- **Notes:** Vertical orientation. Red seal mark in center. Torn bottom edge. + +### 5. weapon_gourd.png — 酒葫芦 (Wine Gourd) +- **Description:** Traditional gourd shape with stopper +- **Colors:** Gourd body `#6A5028`, highlight `#8A6838`, stopper `#3A2818` +- **Notes:** Healing/consumable item icon. Two-bulb gourd shape. + +### 6. weapon_flag.png — 残破军旗 (Broken Battle Flag) +- **Description:** Broken flag pole with tattered flag +- **Colors:** Pole `#4A3828`, flag cloth `#5A322B` (dried blood red), frayed edges `#3A221B` +- **Notes:** Flag should look torn — ragged bottom edge. Pole is snapped at top. + +--- + +## UI Stat Icons (11 total) + +### 1. icon_hp.png — 生命值 (HP) +- **Shape:** Heart (organic, not geometric) +- **Colors:** Dark red `#5A1818`, bright red `#8A2828`, outline `#2A0808` +- **Notes:** Slightly stylized — not a perfect heart, more like a torn flesh shape + +### 2. icon_stamina.png — 体力 (Stamina) +- **Shape:** Circular gauge with lightning bolt +- **Colors:** Dark green `#1A4A2A`, bright green `#2A8A4A`, outline `#0A2A1A` +- **Notes:** Represents physical endurance. Green = vitality. + +### 3. icon_attack.png — 攻击 (Attack) +- **Shape:** Downward-pointing sword/triangle +- **Colors:** Iron `#4A4035`, steel `#6A5A4A`, outline `#2A2520` +- **Notes:** Simple crossed swords or single sword pointing down + +### 4. icon_armor.png — 护甲 (Armor) +- **Shape:** Shield outline +- **Colors:** Dark steel `#3A3530`, highlight `#5A5045`, outline `#2A2520` +- **Notes:** Song dynasty shield shape (rounded top, pointed bottom) + +### 5. icon_crit.png — 暴击 (Critical Hit) +- **Shape:** Starburst / explosion +- **Colors:** Orange `#B4641E`, yellow `#E6A030`, outline `#8A4810` +- **Notes:** 4-point starburst. Should feel "explosive." + +### 6. icon_speed.png — 速度 (Speed) +- **Shape:** Wing / chevron pointing right +- **Colors:** Grey `#5A5A5A`, light grey `#8A8A8A`, outline `#3A3A3A` +- **Notes:** Movement speed indicator. Forward-leaning chevron. + +### 7. icon_sacrifice.png — 献祭 (Sacrifice) +- **Shape:** Circle with dripping blood / chalice +- **Colors:** Purple `#321E37`, dark red `#5A1818`, outline `#1A0E1F` +- **Notes:** Core mechanic icon. Should feel ominous — drop of blood falling into a vessel. + +### 8. icon_ghostfire.png — 鬼火 (Ghost Fire - UI) +- **Shape:** Flame orb (same as weapon_ghostfire but framed for UI) +- **Colors:** Core `#4AE68A`, outer `#1EB464`, outline `#0A5030` +- **Notes:** Resource/element indicator. Same visual language as weapon version. + +### 9. icon_boss.png — Boss +- **Shape:** Skull with horns +- **Colors:** Bone `#A0987A`, shadow `#3A3028`, horns `#5A4A3A` +- **Notes:** Used in boss health bar / boss intro. Should feel threatening even at 32px. + +### 10. icon_key.png — 钥匙 (Key) +- **Shape:** Ancient Chinese key (skeleton key style) +- **Colors:** Bronze `#8A6A32`, highlight `#AA8A4A`, outline `#5A4220` +- **Notes:** Skeleton key — ring at top, teeth at bottom. Used for locked doors/chests. + +### 11. icon_coin.png — 金币/阴钱 (Yin Coin) +- **Shape:** Round coin with square hole (Chinese cash coin) +- **Colors:** Bronze gold `#8B7332`, highlight `#AB9350`, shadow `#5A4A1A` +- **Notes:** Traditional Chinese coin (圆形方孔). Square hole in center. Yin money for the underworld. + +--- + +## Design Notes for AI Art Generation +1. All icons must be readable at 32×32 — no fine details that disappear at this size +2. Use bold silhouettes — if you squint, you should still tell what it is +3. Consistent lighting: top-left light source for all icons +4. 1px dark outline on all icons for readability against any background +5. Maximum 4 colors per icon (outline + 2-3 fills) diff --git a/docs/art/CONCEPT_PLAYER.md b/docs/art/CONCEPT_PLAYER.md new file mode 100644 index 0000000..fbee788 --- /dev/null +++ b/docs/art/CONCEPT_PLAYER.md @@ -0,0 +1,99 @@ +# Concept: Player — 岳家军普通军士 (Yue Family Army Soldier) + +> **Type:** Player Character +> **Frame Size:** 48 × 48 px +> **Direction:** Faces right by default + +--- + +## Visual Description + +A common soldier of the Yue Family Army (岳家军), not Yue Fei himself. He is a grunt — worn down by endless war, now trapped in the underworld. + +### Key Characteristics +- **残破盔甲 (Broken Armor):** Song dynasty lamellar armor (步人甲), cracked and missing plates. Visible chainmail beneath torn cloth. +- **布甲 (Cloth Armor):** Simple padded gambeson (绵甲) underneath, dark brown fabric with frayed edges. +- **朴素 (Plain):** No ornamentation. No insignia. No gleam. This is a foot soldier, not a general. +- **一把宋刀 (Single Song Sword):** Wielded right-handed. Straight-backed dao (直背刀), blade nicked and dark. +- **黑红色调 (Dark Red Tone):** Palette dominated by dried blood red, rust brown, ash grey. No bright colors. + +### Face +- Half-shadowed, eyes glowing faintly (residual life force) +- Simple headband or helmet rim — broken +- No expression — dead-eyed determination + +### Body Proportions (within 48×48) +- Head: ~12px tall, centered +- Torso: ~16px tall, shoulders ~20px wide +- Legs: ~14px tall +- Sword extends ~16px to the right when attacking +- Total silhouette ~40px tall, 24px wide (standing) + +--- + +## Color Palette +| Part | Color | Hex | +|------|-------|-----| +| Armor plates | Dried blood red | `#5A322B` | +| Armor highlight | Faded rust | `#7A4235` | +| Cloth/gambeson | Dark ash brown | `#3A2E25` | +| Skin | Pale corpse grey | `#8A7A6A` | +| Sword blade | Dark iron | `#4A4035` | +| Sword edge | Faint steel | `#6A5A4A` | +| Eyes | Faint ghost glow | `#B4641E` | +| Shadow | Deep void | `#0F0C12` | + +--- + +## Animations + +### Idle (4 frames, 8 FPS) +- Frame 1: Standing, sword at side, slight slouch +- Frame 2: Very slight rise (breathing in) +- Frame 3: Neutral (same as 1, minor variation) +- Frame 4: Slight sink (breathing out) +- **Feel:** Exhausted, haunted, but ready. Not a heroic idle — a tired soldier's. + +### Run (6 frames, 12 FPS) +- Frame 1: Left foot forward, sword arm back +- Frame 2: Mid-stride, weight transitioning +- Frame 3: Right foot forward, sword arm forward +- Frame 4: Mid-stride, airborne moment +- Frame 5: Left foot forward, leaning +- Frame 6: Recovery to frame 1 +- **Feel:** Heavy, armored clank. Not graceful — burdened running. + +### Jump (2 frames, 10 FPS) +- Frame 1: Crouch + launch (knees bent, sword raised) +- Frame 2: Apex (legs tucked, sword overhead) +- **Feel:** Desperate leap, not athletic jump. + +### Attack (4 frames, 14 FPS) +- Frame 1: Wind-up — sword pulled back to right shoulder +- Frame 2: Strike — horizontal slash, sword fully extended left +- Frame 3: Follow-through — sword sweeping down +- Frame 4: Recovery — returning to guard +- **Feel:** Brutal, heavy slash. Military sword technique, not flashy. + +### Hurt (2 frames, 10 FPS) +- Frame 1: Impact — recoil backward, head snapped +- Frame 2: Stagger — off-balance, one knee buckling +- **Feel:** Painful but recovering. Not a knockdown. + +### Death (6 frames, 8 FPS) +- Frame 1: Sword drops from hand +- Frame 2: Knees buckle +- Frame 3: Falling forward +- Frame 4: Hits ground on hands +- Frame 5: Collapses fully +- Frame 6: Dissolves into dark particles (entering the underworld) +- **Feel:** Slow, inevitable. A soldier's final fall. + +--- + +## Design Notes for AI Art Generation +1. Keep the silhouette readable at 48×48 — avoid excessive detail +2. The sword should be clearly visible in all frames (it's the identity) +3. Armor damage should be consistent across all animations +4. The dark red palette must dominate — no blues, no greens on the player +5. Eyes glow faintly orange (ghost fire) — this is the only warm accent diff --git a/docs/art/CONCEPT_TILES.md b/docs/art/CONCEPT_TILES.md new file mode 100644 index 0000000..07a6faf --- /dev/null +++ b/docs/art/CONCEPT_TILES.md @@ -0,0 +1,77 @@ +# Concept: Tiles & Environment — 场景资源 + +> **Type:** Tilemap Tiles +> **Size:** 32 × 32 px (all tiles) +> **Style:** Seamless, tileable, dark stone + +--- + +## Design Philosophy +- Tiles must be **seamless** — edges match perfectly when placed adjacent +- Keep texture simple — detail should come from variation, not per-tile complexity +- All tiles share a base palette (dark stone / ash) +- Decorative tiles (brazier, tombstone, spike) are NOT seamless — they're props + +--- + +## Tile Designs (7 total) + +### 1. tile_floor.png — 地面 (Ground Floor) +- **Type:** Seamless tileable +- **Description:** Cracked stone floor — the ground of the underworld +- **Colors:** Base `#28231E`, cracks `#15110D`, highlights `#3A3328` +- **Notes:** Horizontal crack running through center. Subtle texture noise. Must tile in all directions. + +### 2. tile_stone_brick.png — 石砖 (Stone Brick) +- **Type:** Seamless tileable +- **Description:** Cut stone bricks in running bond pattern +- **Colors:** Base `#352E25`, mortar lines `#1A150F`, highlights `#4A4035` +- **Notes:** 2 rows of bricks per tile. Offset pattern. Mortar lines 1px thick. + +### 3. tile_wall.png — 墙壁 (Wall) +- **Type:** Seamless tileable (vertical) +- **Description:** Dark rough stone wall — background barrier +- **Colors:** Base `#1E1812`, texture `#2A2218`, shadow `#0F0C08` +- **Notes:** Vertical striations (rock texture). Darker at bottom. Must tile horizontally. + +### 4. tile_wood_bridge.png — 木桥 (Wooden Bridge) +- **Type:** Seamless tileable (horizontal) +- **Description:** Wooden plank surface — for bridge/platform sections +- **Colors:** Base `#3A2818`, plank gaps `#1A0E08`, highlights `#5A3E25` +- **Notes:** Horizontal planks. Gap every 8px. Slightly warped/rotted appearance. + +### 5. tile_brazier.png — 鬼火火盆 (Ghost Fire Brazier) +- **Type:** Decorative prop (NOT tileable) +- **Description:** Stone brazier with ghost fire burning +- **Colors:** Base stone `#352E25`, fire `#1EB464` / `#4AE68A`, shadow `#1A150F` +- **Notes:** Light source — should have a glow effect in-engine. Brazier takes ~20px, fire ~12px on top. + +### 6. tile_tombstone.png — 墓碑 (Tombstone) +- **Type:** Decorative prop (NOT tileable) +- **Description:** Worn stone tombstone with illegible inscription +- **Colors:** Base `#3A3530`, inscription `#1A150F`, moss `#2A3520` +- **Notes:** Rounded top, tapered bottom. Cracks visible. Background decoration, not collidable. + +### 7. tile_ground_spike.png — 地刺 (Ground Spike) +- **Type:** Hazard prop (NOT tileable) +- **Description:** Row of bone/iron spikes pointing up — damage on contact +- **Colors:** Spike `#5A4A3A`, tip `#8A7A5A`, shadow `#2A2520` +- **Notes:** 3-4 spikes per tile. Sharp, jagged. Should look painful. Collidable hazard. + +--- + +## Tile Variants (Future) +For prototype, one variant per tile is sufficient. Future versions should include: +- `tile_floor_cracked.png` — damaged variant +- `tile_floor_bloodied.png` — blood-stained variant +- `tile_stone_brick_mossy.png` — overgrown variant +- `tile_wall_crumbled.png` — broken wall section + +--- + +## Design Notes for AI Art Generation +1. **Seamless tiles are critical** — test by placing 4 tiles in a 2×2 grid; seams must be invisible +2. **Keep texture low** — at 32×32, less is more. Suggest material, don't detail it. +3. **Props (brazier, tombstone, spike) have transparent backgrounds** — only the object is drawn +4. **Brazier fire should animate** — if multi-frame, create `tile_brazier_01.png` through `tile_brazier_04.png` +5. **Spikes should look like bone, not metal** — the underworld reuses the dead diff --git a/docs/art/CONCEPT_UI.md b/docs/art/CONCEPT_UI.md new file mode 100644 index 0000000..ca97583 --- /dev/null +++ b/docs/art/CONCEPT_UI.md @@ -0,0 +1,92 @@ +# Concept: UI Elements — 界面元素 + +> **Type:** UI Sprites +> **Sizes:** Varies (see each) +> **Style:** Dark, ornate, Chinese underworld aesthetic + +--- + +## Design Philosophy +- UI elements should feel like they belong in the underworld — dark, rusted, bone-like +- Bars use a frame + fill pattern (frame is static, fill scales with value) +- All UI elements share a base palette (dark steel + accent colors) +- Pixel-perfect edges — no anti-aliasing on UI borders + +--- + +## UI Element Designs (4 total) + +### 1. ui_health_bar.png — 生命条 (Health Bar) +- **Size:** 96 × 16 px +- **Description:** Player health bar — dark frame with red fill +- **Colors:** Frame `#2A2520`, frame highlight `#4A4035`, fill `#5A1818`, fill highlight `#8A2828` +- **Layout:** 1px frame border, 14px interior fill area +- **Notes:** The fill represents 100% HP. In-engine, the fill width is scaled by HP percentage. The frame stays static. +- **Engine usage:** + - Frame: drawn as 9-slice or static sprite + - Fill: separate sprite scaled horizontally by `current_hp / max_hp` +- **Alternative:** Use as a single sprite and let engine handle fill via shader/modulate + +### 2. ui_stamina_bar.png — 体力条 (Stamina Bar) +- **Size:** 96 × 16 px +- **Description:** Player stamina bar — dark frame with green fill +- **Colors:** Frame `#2A2520`, frame highlight `#4A4035`, fill `#1A4A2A`, fill highlight `#2A8A4A` +- **Layout:** Same as health bar +- **Notes:** Placed below or beside the health bar. Green distinguishes it from red HP. +- **Engine usage:** Same as health bar — frame static, fill scales with stamina value. + +### 3. ui_frame.png — 通用边框 (Generic Frame) +- **Size:** 32 × 32 px +- **Description:** Decorative border frame for UI panels, icon backgrounds, tooltips +- **Colors:** Frame `#2A2520`, inner border `#4A4035`, corner accents `#5A4A3A` +- **Layout:** 3px ornate border, 26px transparent interior +- **Notes:** Designed as a 9-slice sprite — corners stay fixed, edges stretch. Used for inventory slots, dialogue boxes, menu panels. +- **Engine usage:** Import as 9-slice texture in Godot. Set margins to 3px on all sides. + +### 4. ui_minimap_frame.png — 小地图边框 (Minimap Frame) +- **Size:** 64 × 64 px +- **Description:** Square frame for minimap display +- **Colors:** Frame `#2A2520`, inner border `#4A4035`, corner spikes `#5A4A3A` +- **Layout:** 4px border, 56px interior (for minimap rendering) +- **Notes:** Larger version of ui_frame with corner decorative spikes. Underworld aesthetic — bone/iron corners. +- **Engine usage:** Static sprite. Minimap renders inside the transparent interior area. + +--- + +## UI Layout Reference (for Claude/Codex) +``` +┌──────────────────────────────────────────┐ +│ [HP████████░░░░] [icon_hp] │ ← Top-left: Health bar + icon +│ [STA██████████] [icon_stamina] │ ← Below HP: Stamina bar + icon +│ │ +│ │ +│ [ GAMEPLAY AREA ] │ +│ │ +│ │ +│ [icon_ghostfire] ×3 │ ← Bottom-right: Resources +│ [icon_coin] 999 │ ← Below: Currency +│ [minimap] │ ← Bottom-right: Minimap +└──────────────────────────────────────────┘ +``` + +--- + +## Future UI Elements (Not Required for Prototype) +| Element | Description | +|---------|-------------| +| ui_boss_bar.png | Boss health bar (wider, more ornate) | +| ui_sacrifice_panel.png | Sacrifice selection panel background | +| ui_inventory_slot.png | Individual inventory slot (32×32) | +| ui_cursor.png | Custom mouse/cursor sprite | +| ui_dialogue_box.png | NPC dialogue text box background | +| ui_death_screen.png | Death overlay texture | +| ui_pause_menu.png | Pause menu background | + +--- + +## Design Notes for AI Art Generation +1. **Bars are 96×16** — wide enough to read, thin enough to not block gameplay +2. **Frames use 9-slice** — corners must be identical, edges must tile seamlessly +3. **Colors must match the stat they represent** — HP=red, Stamina=green, Ghost Fire=blue-green +4. **No text in UI sprites** — all text is rendered by Godot's Label/Font system +5. **UI elements should feel "underworld"** — rusted iron, bone, dark metal, not clean modern UI diff --git a/docs/art/GODOT_IMPORT_GUIDE.md b/docs/art/GODOT_IMPORT_GUIDE.md new file mode 100644 index 0000000..4f0982d --- /dev/null +++ b/docs/art/GODOT_IMPORT_GUIDE.md @@ -0,0 +1,189 @@ +# Godot 4 Import Guide — 九幽 + +> **For:** Claude (framework developer) and any AI/human working in Godot +> **Purpose:** How to import and use the art assets in Godot 4.x + +--- + +## 1. Project Import Settings + +### Global Texture Preset +Create a `.godot/import_settings` or use the Import dock: + +1. Select all PNG files in `assets/` +2. In the Import dock, set: + - **Preset:** 2D Pixel + - **Filter:** Off (Nearest) + - **Mipmaps:** Off + - **Lossy Compression:** Off → use Lossless +3. Click "Reimport" + +### project.godot Settings (recommended) +```ini +[rendering] +textures/canvas_textures/default_texture_filter=0 # Nearest +renderer/rendering_method="gl_compatibility" # For 2D pixel art +``` + +--- + +## 2. Sprite Sheet → AnimatedSprite2D + +### For Player, Enemies, and Boss + +Each animation is a separate horizontal sprite sheet PNG. + +#### Step-by-step (Player example): +1. The file `assets/player/player_idle.png` is a **192×48** image (4 frames × 48px) +2. In Godot, this imports as a `CompressedTexture2D` +3. To use with `AnimatedSprite2D`: + - Create a `SpriteFrames` resource + - Add animation "idle" + - Set the texture to `player_idle.png` + - Set "Horizontal Frames" to **4** + - Set FPS to **8** (from metadata) +4. Repeat for each animation (run, jump, attack, hurt, death) + +#### Quick Reference Table +| File | H-Frames | V-Frames | FPS | +|------|----------|----------|-----| +| player_idle.png | 4 | 1 | 8 | +| player_run.png | 6 | 1 | 12 | +| player_jump.png | 2 | 1 | 10 | +| player_attack.png | 4 | 1 | 14 | +| player_hurt.png | 2 | 1 | 10 | +| player_death.png | 6 | 1 | 8 | +| ghost_melee_idle.png | 4 | 1 | 8 | +| ghost_melee_run.png | 6 | 1 | 10 | +| ghost_melee_attack.png | 4 | 1 | 12 | +| ghost_melee_hurt.png | 2 | 1 | 10 | +| ghost_melee_death.png | 4 | 1 | 8 | +| ghost_archer_idle.png | 4 | 1 | 8 | +| ghost_archer_run.png | 6 | 1 | 10 | +| ghost_archer_attack.png | 4 | 1 | 12 | +| ghost_archer_hurt.png | 2 | 1 | 10 | +| ghost_archer_death.png | 4 | 1 | 8 | +| corpse_beast_idle.png | 4 | 1 | 8 | +| corpse_beast_run.png | 6 | 1 | 14 | +| corpse_beast_attack.png | 4 | 1 | 12 | +| corpse_beast_hurt.png | 2 | 1 | 10 | +| corpse_beast_death.png | 4 | 1 | 8 | +| boss_idle.png | 4 | 1 | 6 | +| boss_run.png | 6 | 1 | 8 | +| boss_attack.png | 5 | 1 | 10 | +| boss_hurt.png | 2 | 1 | 8 | +| boss_death.png | 8 | 1 | 6 | + +### Alternative: SpriteFrames .tres Resource +You can create a `.tres` SpriteFrames resource that references all animation sheets: + +```godot +# Example: player_sprite_frames.tres (generated by Godot) +# Each animation references its sprite sheet with hframes set +``` + +Claude should create these `.tres` files during framework implementation. + +--- + +## 3. Tile Set Import + +### For TileMapLayer (Godot 4.3+) or TileMap (Godot 4.0-4.2) + +1. Tiles are individual 32×32 PNGs in `assets/tiles/` +2. Create a `TileSet` resource in Godot +3. Add each tile PNG as a tile source +4. Set tile size to 32×32 +5. For seamless tiles (floor, brick, wall, bridge): enable "Tile" in collision or just place freely +6. For props (brazier, tombstone, spike): use as individual sprites or TileSet tiles with specific collision shapes + +### Collision Shapes +| Tile | Collision | +|------|-----------| +| tile_floor.png | Full rect (solid ground) | +| tile_stone_brick.png | Full rect (solid platform) | +| tile_wall.png | Full rect (solid wall) | +| tile_wood_bridge.png | Full rect (one-way platform) | +| tile_brazier.png | No collision (decorative) | +| tile_tombstone.png | Optional small rect (obstacle) | +| tile_ground_spike.png | Full rect + damage area (hazard) | + +--- + +## 4. Icon Import + +### Weapon & UI Icons +1. All icons are 32×32 PNGs +2. Import as `CompressedTexture2D` with Nearest filter +3. Use in `TextureRect` nodes for UI +4. Use in `Texture2D` properties for inventory items + +--- + +## 5. Effect Import + +### VFX Sprites +1. Effects are 48×48 PNGs in `assets/effects/` +2. Some are multi-frame (hit_slash, blood_splash, death_fade) — treat as sprite sheets +3. ghost_fire is a 4-frame loop — use AnimatedSprite2D +4. Spawn as `Sprite2D` or `AnimatedSprite2D` at runtime, queue_free after animation + +### Effect Frame Reference +| File | Frames | FPS | Loop | +|------|--------|-----|------| +| effect_hit_slash.png | 2 | 20 | No | +| effect_blood_splash.png | 4 | 15 | No | +| effect_ghost_fire.png | 4 | 10 | Yes | +| effect_death_fade.png | 4 | 8 | No | + +--- + +## 6. Background Import + +### Parallax Background +1. Create a `ParallaxBackground` node +2. Add `ParallaxLayer` children for each depth: + - Layer 0 (0.2x): `bg_deep.png` — set texture, enable mirror (horizontal) + - Layer 1 (0.5x): `bg_tree.png`, `bg_broken_flag.png` — placed as sprites + - Layer 2 (0.7x): `bg_chains.png` — placed as sprites +3. Set `motion_scale` on each ParallaxLayer to control parallax depth + +--- + +## 7. UI Element Import + +### Bars +1. `ui_health_bar.png` and `ui_stamina_bar.png` are 96×16 +2. Use as `TextureProgressBar` nodes: + - Set texture as the bar + - Or split into frame + fill textures for more control +3. `ui_frame.png` and `ui_minimap_frame.png`: + - Import as 9-slice textures + - Set `texture_region` and 9-slice margins in Godot + +--- + +## 8. .gitignore Notes + +The project's `.gitignore` already excludes: +- `.godot/` (Godot cache) +- `.import/` (imported texture cache) + +This means: +- **Commit:** All `.png` source files in `assets/` +- **Don't commit:** `.import/` files, `.godot/` cache +- **Claude should commit:** `.tres` resource files (SpriteFrames, TileSet, etc.) that reference the PNGs + +--- + +## 9. Automated Import Script (Future) + +Claude can create a script to auto-generate SpriteFrames resources: + +```python +# Tool: tools/generate_sprite_frames.gd (EditorScript) +# Reads metadata.md files and generates .tres SpriteFrames resources +# This is Claude's responsibility — not included in art pipeline +``` + +For now, the metadata files in each directory contain all the information needed. diff --git a/docs/art/NAMING_CONVENTION.md b/docs/art/NAMING_CONVENTION.md new file mode 100644 index 0000000..302df63 --- /dev/null +++ b/docs/art/NAMING_CONVENTION.md @@ -0,0 +1,47 @@ +# Naming Convention — 九幽 Art Assets + +> **Rule:** All filenames follow `__.png` +> **All lowercase, underscores only, no spaces, no version numbers** + +--- + +## Prefixes + +| Prefix | Directory | Example | +|--------|-----------|---------| +| `player_` | `assets/player/` | `player_idle.png` | +| `ghost_melee_` | `assets/enemy/` | `ghost_melee_attack.png` | +| `ghost_archer_` | `assets/enemy/` | `ghost_archer_idle.png` | +| `corpse_beast_` | `assets/enemy/` | `corpse_beast_run.png` | +| `boss_` | `assets/boss/` | `boss_death.png` | +| `weapon_` | `assets/icons/weapons/` | `weapon_songdao.png` | +| `icon_` | `assets/icons/ui/` | `icon_hp.png` | +| `tile_` | `assets/tiles/` | `tile_floor.png` | +| `effect_` | `assets/effects/` | `effect_blood_splash.png` | +| `bg_` | `assets/background/` | `bg_tree.png` | +| `ui_` | `assets/ui/` | `ui_health_bar.png` | + +## Actions (for animated sprites) + +| Action | Description | +|--------|-------------| +| `idle` | Default standing/breathing | +| `run` | Lateral movement | +| `jump` | Vertical launch | +| `attack` | Offensive action | +| `hurt` | Taking damage | +| `death` | Dying animation | + +## Forbidden Names + +❌ `新建文件.png` +❌ `image1.png` +❌ `最终版2.png` +❌ `player_idle_v2.png` +❌ `Player Idle.png` +❌ `player-idle.png` +❌ `temp.png` + +## Replacement Rule + +When replacing a placeholder, the new file MUST have the **exact same filename** and **exact same pixel dimensions**. No version numbers — use git for versioning. diff --git a/godot/import_presets.md b/godot/import_presets.md new file mode 100644 index 0000000..62f48bf --- /dev/null +++ b/godot/import_presets.md @@ -0,0 +1,107 @@ +# Godot 4 Import Presets — 九幽 + +> **Purpose:** Default import settings for all texture assets. +> **Usage:** When first opening the project in Godot 4, apply these settings via the Import dock. + +## Default Texture Import Settings (2D Pixel Art) + +Apply to ALL `.png` files in `assets/`: + +```ini +# In Godot Import dock, select all PNGs and set: +preset = "2D Pixel" +compress/mode = 0 # Lossless +compress/lossy_quality = 0.7 +mipmaps/generate = false +process/fix_alpha_border = true +process/premult_alpha = false +process/normal_map_invert_y = false +process/hdr_as_srgb = false +process/hdr_clamp_exposure = false +process/size_limit = 0 +detect_3d/compress_to = 0 +``` + +## project.godot Settings (recommended) + +```ini +[rendering] +textures/canvas_textures/default_texture_filter = 0 +renderer/rendering_method = "gl_compatibility" + +[display] +window/size/viewport_width = 1280 +window/size/viewport_height = 720 +window/stretch/mode = "viewport" +window/stretch/aspect = "keep" +``` + +## SpriteFrames Auto-Setup Reference + +When creating SpriteFrames resources, reference the metadata files in each directory: + +| Directory | Metadata File | Contains | +|-----------|--------------|----------| +| `assets/player/` | `metadata.md` | Frame counts, sizes, FPS for all player animations | +| `assets/enemy/` | `metadata.md` | Frame counts, sizes, FPS for all enemy animations | +| `assets/boss/` | `metadata.md` | Frame counts, sizes, FPS for all boss animations | +| `assets/effects/` | `metadata.md` | Frame counts, sizes, FPS for all effects | +| `assets/tiles/` | `metadata.md` | Tile specs and collision info | +| `assets/background/` | `metadata.md` | Background element specs and parallax info | +| `assets/ui/` | `metadata.md` | UI element specs and layout info | + +## Naming → Animation Mapping + +### Player SpriteFrames +| Animation Name | Source File | H-Frames | +|----------------|-------------|----------| +| idle | `res://assets/player/player_idle.png` | 4 | +| run | `res://assets/player/player_run.png` | 6 | +| jump | `res://assets/player/player_jump.png` | 2 | +| attack | `res://assets/player/player_attack.png` | 4 | +| hurt | `res://assets/player/player_hurt.png` | 2 | +| death | `res://assets/player/player_death.png` | 6 | + +### Ghost Soldier (鬼卒) SpriteFrames +| Animation Name | Source File | H-Frames | +|----------------|-------------|----------| +| idle | `res://assets/enemy/ghost_melee_idle.png` | 4 | +| run | `res://assets/enemy/ghost_melee_run.png` | 6 | +| attack | `res://assets/enemy/ghost_melee_attack.png` | 4 | +| hurt | `res://assets/enemy/ghost_melee_hurt.png` | 2 | +| death | `res://assets/enemy/ghost_melee_death.png` | 4 | + +### Ghost Archer (鬼弓手) SpriteFrames +| Animation Name | Source File | H-Frames | +|----------------|-------------|----------| +| idle | `res://assets/enemy/ghost_archer_idle.png` | 4 | +| run | `res://assets/enemy/ghost_archer_run.png` | 6 | +| attack | `res://assets/enemy/ghost_archer_attack.png` | 4 | +| hurt | `res://assets/enemy/ghost_archer_hurt.png` | 2 | +| death | `res://assets/enemy/ghost_archer_death.png` | 4 | + +### Corpse Beast (尸兽) SpriteFrames +| Animation Name | Source File | H-Frames | Frame Size | +|----------------|-------------|----------|------------| +| idle | `res://assets/enemy/corpse_beast_idle.png` | 4 | 64×48 | +| run | `res://assets/enemy/corpse_beast_run.png` | 6 | 64×48 | +| attack | `res://assets/enemy/corpse_beast_attack.png` | 4 | 64×48 | +| hurt | `res://assets/enemy/corpse_beast_hurt.png` | 2 | 64×48 | +| death | `res://assets/enemy/corpse_beast_death.png` | 4 | 64×48 | + +### Boss (镇关鬼将) SpriteFrames +| Animation Name | Source File | H-Frames | Frame Size | +|----------------|-------------|----------|------------| +| idle | `res://assets/boss/boss_idle.png` | 4 | 96×96 | +| run | `res://assets/boss/boss_run.png` | 6 | 96×96 | +| attack | `res://assets/boss/boss_attack.png` | 5 | 96×96 | +| hurt | `res://assets/boss/boss_hurt.png` | 2 | 96×96 | +| death | `res://assets/boss/boss_death.png` | 8 | 96×96 | + +### Effects SpriteFrames +| Animation Name | Source File | H-Frames | Loop | +|----------------|-------------|----------|------| +| hit_slash | `res://assets/effects/effect_hit_slash.png` | 2 | No | +| blood_splash | `res://assets/effects/effect_blood_splash.png` | 4 | No | +| ghost_fire | `res://assets/effects/effect_ghost_fire.png` | 4 | Yes | +| death_fade | `res://assets/effects/effect_death_fade.png` | 4 | No | diff --git a/tools/.gdignore b/tools/.gdignore new file mode 100644 index 0000000..5e7ffa6 --- /dev/null +++ b/tools/.gdignore @@ -0,0 +1 @@ +# This file tells Godot to ignore this directory during import. diff --git a/tools/generate_placeholders.py b/tools/generate_placeholders.py new file mode 100644 index 0000000..83fdd13 --- /dev/null +++ b/tools/generate_placeholders.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python3 +""" +Nine Nether (九幽) — Placeholder Sprite Generator +Generates all prototype placeholder PNGs with correct dimensions. +Each placeholder uses a colored silhouette + checkerboard to indicate +the intended content, sized as horizontal sprite sheets for Godot import. + +Run: python3 generate_placeholders.py +""" + +from PIL import Image, ImageDraw +import os + +ASSETS = os.path.join(os.path.dirname(__file__), "..", "assets") + +# ── Palette ────────────────────────────────────────────────────────────────── +# Chinese dark fantasy — muted, desaturated +C_BG = (15, 12, 18) # near-black background +C_PLAYER = (90, 50, 45) # dark red-brown (Yue army) +C_PLAYER_LT = (140, 80, 65) # highlight +C_GHOST = (45, 55, 70) # cold blue-grey (ghost soldier) +C_GHOST_LT = (70, 85, 105) +C_ARCHER = (50, 60, 55) # sickly green-grey +C_ARCHER_LT = (75, 95, 85) +C_BEAST = (55, 40, 35) # dark fleshy brown +C_BEAST_LT = (85, 60, 50) +C_BOSS = (50, 30, 55) # dark purple +C_BOSS_LT = (90, 50, 100) +C_FIRE = (180, 100, 30) # ghost fire orange +C_FIRE_LT = (220, 160, 60) +C_TILE = (40, 35, 30) # stone +C_TILE_LT = (65, 55, 45) +C_WOOD = (60, 40, 25) # wood +C_WOOD_LT = (90, 65, 40) +C_ICON = (50, 45, 50) # icon base +C_ICON_LT = (100, 90, 100) +C_UI = (30, 28, 35) # ui element +C_UI_LT = (80, 75, 90) +C_BG_DEEP = (10, 8, 14) # deep background +C_BG_TREE = (20, 18, 22) +C_BG_TREE_LT = (35, 30, 35) +C_CHECKER_A = (20, 18, 24) # checkerboard dark +C_CHECKER_B = (28, 25, 32) # checkerboard light +C_PLACEHOLDER= (255, 0, 255) # magenta = "replace me" + + +def checkerboard(draw, x0, y0, w, h, cell=4): + """Draw a checkerboard pattern to indicate placeholder.""" + for cy in range(0, h, cell): + for cx in range(0, w, cell): + c = C_CHECKER_A if ((cx // cell) + (cy // cell)) % 2 == 0 else C_CHECKER_B + draw.rectangle([x0 + cx, y0 + cy, x0 + cx + cell - 1, y0 + cy + cell - 1], fill=c) + + +def silhouette_rect(draw, x0, y0, w, h, color, lt_color): + """Draw a simple humanoid silhouette within the frame.""" + cx = x0 + w // 2 + # head + hs = max(6, w // 6) + draw.ellipse([cx - hs // 2, y0 + h // 8, cx + hs // 2, y0 + h // 8 + hs], fill=color, outline=lt_color) + # body + bw = max(8, w // 4) + bh = max(12, h // 3) + draw.rectangle([cx - bw // 2, y0 + h // 8 + hs, cx + bw // 2, y0 + h // 8 + hs + bh], fill=color, outline=lt_color) + # legs + lw = max(4, w // 8) + lh = max(8, h // 4) + draw.rectangle([cx - bw // 2 + 1, y0 + h // 8 + hs + bh, cx - bw // 2 + 1 + lw, y0 + h // 8 + hs + bh + lh], fill=color) + draw.rectangle([cx + bw // 2 - 1 - lw, y0 + h // 8 + hs + bh, cx + bw // 2 - 1, y0 + h // 8 + hs + bh + lh], fill=color) + + +def silhouette_beast(draw, x0, y0, w, h, color, lt_color): + """Draw a quadruped beast silhouette.""" + body_h = h // 3 + body_y = y0 + h // 3 + draw.rectangle([x0 + w // 8, body_y, x0 + w - w // 8, body_y + body_h], fill=color, outline=lt_color) + # head + hs = max(6, w // 8) + draw.ellipse([x0 + w - w // 8 - hs, body_y - hs // 2, x0 + w - w // 8, body_y + hs], fill=color, outline=lt_color) + # legs + lw = max(3, w // 12) + lh = max(6, h // 5) + for lx in [x0 + w // 6, x0 + w // 3, x0 + w - w // 3, x0 + w - w // 5]: + draw.rectangle([lx, body_y + body_h, lx + lw, body_y + body_h + lh], fill=color) + + +def silhouette_boss(draw, x0, y0, w, h, color, lt_color): + """Draw a large imposing boss silhouette.""" + cx = x0 + w // 2 + # large head with horns + hs = max(10, w // 5) + draw.ellipse([cx - hs // 2, y0 + h // 12, cx + hs // 2, y0 + h // 12 + hs], fill=color, outline=lt_color) + # horns + draw.polygon([(cx - hs // 2, y0 + h // 12), (cx - hs, y0 + h // 20), (cx - hs // 2 + 2, y0 + h // 12 + 2)], fill=lt_color) + draw.polygon([(cx + hs // 2, y0 + h // 12), (cx + hs, y0 + h // 20), (cx + hs // 2 - 2, y0 + h // 12 + 2)], fill=lt_color) + # massive body + bw = max(16, w // 3) + bh = max(20, h // 2) + draw.rectangle([cx - bw // 2, y0 + h // 12 + hs, cx + bw // 2, y0 + h // 12 + hs + bh], fill=color, outline=lt_color) + # legs + lw = max(6, w // 8) + lh = max(10, h // 5) + draw.rectangle([cx - bw // 2 + 2, y0 + h // 12 + hs + bh, cx - bw // 2 + 2 + lw, y0 + h // 12 + hs + bh + lh], fill=color) + draw.rectangle([cx + bw // 2 - 2 - lw, y0 + h // 12 + hs + bh, cx + bw // 2 - 2, y0 + h // 12 + hs + bh + lh], fill=color) + # ghost fire aura + draw.ellipse([cx - bw // 2 - 3, y0 + h // 12 + hs - 2, cx - bw // 2 + 1, y0 + h // 12 + hs + 2], fill=C_FIRE) + draw.ellipse([cx + bw // 2 - 1, y0 + h // 12 + hs - 2, cx + bw // 2 + 3, y0 + h // 12 + hs + 2], fill=C_FIRE) + + +def draw_icon_simple(draw, x0, y0, size, shape, color, lt_color): + """Draw simple icon shapes.""" + cx = x0 + size // 2 + cy = y0 + size // 2 + s = size // 3 + if shape == "sword": + draw.line([cx - s, cy + s, cx + s, cy - s], fill=lt_color, width=2) + draw.line([cx - s - 2, cy + s, cx + s + 2, cy - s], fill=color, width=1) + draw.rectangle([cx - s - 3, cy + s - 1, cx - s + 1, cy + s + 3], fill=color) + elif shape == "spear": + draw.line([cx - s, cy + s, cx + s, cy - s], fill=lt_color, width=2) + draw.polygon([(cx + s, cy - s), (cx + s + 3, cy - s - 2), (cx + s + 2, cy - s + 3)], fill=lt_color) + elif shape == "fire": + draw.ellipse([cx - s // 2, cy - s, cx + s // 2, cy + s], fill=color) + draw.ellipse([cx - s // 3, cy - s // 2, cx + s // 3, cy + s // 2], fill=lt_color) + elif shape == "talisman": + draw.rectangle([cx - s // 2, cy - s, cx + s // 2, cy + s], fill=color, outline=lt_color) + draw.line([cx, cy - s, cx, cy + s], fill=lt_color, width=1) + elif shape == "gourd": + draw.ellipse([cx - s // 2, cy - s // 3, cx + s // 2, cy + s], fill=color) + draw.rectangle([cx - 1, cy - s, cx + 1, cy - s // 3], fill=color) + elif shape == "flag": + draw.line([cx - s, cy - s, cx - s, cy + s], fill=lt_color, width=1) + draw.polygon([(cx - s, cy - s), (cx + s, cy - s // 2), (cx - s, cy)], fill=color) + elif shape == "heart": + draw.ellipse([cx - s, cy - s // 2, cx, cy + s // 2], fill=color) + draw.ellipse([cx, cy - s // 2, cx + s, cy + s // 2], fill=color) + draw.polygon([(cx - s, cy), (cx + s, cy), (cx, cy + s)], fill=color) + elif shape == "stamina": + draw.ellipse([cx - s, cy - s, cx + s, cy + s], fill=color, outline=lt_color) + draw.line([cx - s // 2, cy, cx + s // 2, cy], fill=lt_color, width=1) + elif shape == "attack": + draw.polygon([(cx, cy - s), (cx + s, cy + s), (cx - s, cy + s)], fill=color, outline=lt_color) + elif shape == "armor": + draw.polygon([(cx, cy - s), (cx + s, cy - s // 2), (cx + s // 2, cy + s), (cx - s // 2, cy + s), (cx - s, cy - s // 2)], fill=color, outline=lt_color) + elif shape == "crit": + draw.line([cx, cy - s, cx, cy + s], fill=lt_color, width=2) + draw.line([cx - s, cy, cx + s, cy], fill=lt_color, width=2) + draw.line([cx - s, cy - s, cx + s, cy + s], fill=color, width=1) + draw.line([cx - s, cy + s, cx + s, cy - s], fill=color, width=1) + elif shape == "speed": + draw.polygon([(cx - s, cy - s), (cx + s, cy), (cx - s, cy + s)], fill=color, outline=lt_color) + elif shape == "sacrifice": + draw.ellipse([cx - s, cy - s, cx + s, cy + s], outline=lt_color, width=1) + draw.line([cx, cy - s, cx, cy + s], fill=color, width=1) + draw.line([cx - s, cy, cx + s, cy], fill=color, width=1) + elif shape == "boss": + draw.ellipse([cx - s, cy - s, cx + s, cy + s], fill=color, outline=lt_color) + draw.line([cx - s, cy - s, cx - s - 2, cy - s - 3], fill=lt_color, width=1) + draw.line([cx + s, cy - s, cx + s + 2, cy - s - 3], fill=lt_color, width=1) + elif shape == "key": + draw.ellipse([cx - s, cy - s, cx, cy], outline=lt_color, width=1) + draw.line([cx, cy, cx + s, cy], fill=lt_color, width=1) + draw.line([cx + s, cy, cx + s, cy + 2], fill=lt_color, width=1) + elif shape == "coin": + draw.ellipse([cx - s, cy - s, cx + s, cy + s], fill=color, outline=lt_color) + draw.rectangle([cx - 1, cy - s // 2, cx + 1, cy + s // 2], fill=lt_color) + + +def draw_tile(draw, x0, y0, size, kind, color, lt_color): + """Draw tile placeholders.""" + if kind == "floor": + draw.rectangle([x0, y0, x0 + size - 1, y0 + size - 1], fill=color) + draw.rectangle([x0 + 2, y0 + 2, x0 + size - 3, y0 + size - 3], outline=lt_color, width=1) + draw.line([x0 + size // 2, y0 + 2, x0 + size // 2, y0 + size - 3], fill=lt_color, width=1) + elif kind == "brick": + draw.rectangle([x0, y0, x0 + size - 1, y0 + size - 1], fill=color) + draw.line([x0, y0 + size // 2, x0 + size, y0 + size // 2], fill=lt_color, width=1) + draw.line([x0 + size // 2, y0, x0 + size // 2, y0 + size // 2], fill=lt_color, width=1) + draw.line([x0 + size // 4, y0 + size // 2, x0 + size // 4, y0 + size], fill=lt_color, width=1) + draw.line([x0 + 3 * size // 4, y0 + size // 2, x0 + 3 * size // 4, y0 + size], fill=lt_color, width=1) + elif kind == "wall": + draw.rectangle([x0, y0, x0 + size - 1, y0 + size - 1], fill=color) + for i in range(0, size, 4): + draw.line([x0 + i, y0, x0 + i, y0 + size - 1], fill=lt_color, width=1) + elif kind == "bridge": + draw.rectangle([x0, y0, x0 + size - 1, y0 + size - 1], fill=color) + for i in range(0, size, 6): + draw.line([x0, y0 + i, x0 + size, y0 + i], fill=lt_color, width=1) + elif kind == "brazier": + draw.rectangle([x0 + size // 4, y0 + size // 3, x0 + 3 * size // 4, y0 + 2 * size // 3], fill=color, outline=lt_color) + draw.ellipse([x0 + size // 3, y0 + size // 6, x0 + 2 * size // 3, y0 + size // 3], fill=C_FIRE) + elif kind == "tombstone": + draw.pieslice([x0 + size // 4, y0, x0 + 3 * size // 4, y0 + size], 180, 360, fill=color, outline=lt_color) + draw.rectangle([x0 + size // 4, y0 + size // 2, x0 + 3 * size // 4, y0 + size - 2], fill=color) + elif kind == "spike": + for i in range(0, size, 8): + draw.polygon([(x0 + i, y0 + size), (x0 + i + 4, y0 + size // 3), (x0 + i + 8, y0 + size)], fill=lt_color) + + +def draw_effect(draw, x0, y0, size, kind, color, lt_color): + """Draw effect placeholders.""" + cx = x0 + size // 2 + cy = y0 + size // 2 + s = size // 3 + if kind == "slash": + draw.line([cx - s, cy + s, cx + s, cy - s], fill=lt_color, width=3) + draw.line([cx - s + 1, cy + s, cx + s + 1, cy - s], fill=color, width=1) + elif kind == "blood": + for dx in range(-s, s, 3): + for dy in range(-s, s, 3): + if (dx * dx + dy * dy) < s * s: + draw.point([cx + dx, cy + dy], fill=color) + elif kind == "ghostfire": + draw.ellipse([cx - s, cy - s, cx + s, cy + s], fill=color) + draw.ellipse([cx - s // 2, cy - s // 2, cx + s // 2, cy + s // 2], fill=lt_color) + elif kind == "deathfade": + for i in range(size): + alpha = max(0, 255 - i * 8) + draw.line([x0 + i, y0, x0 + i, y0 + size - 1], fill=color) + + +def draw_background(draw, x0, y0, w, h, kind, color, lt_color): + """Draw background element placeholders.""" + if kind == "tree": + # trunk + tw = max(4, w // 10) + draw.rectangle([x0 + w // 2 - tw // 2, y0 + h // 2, x0 + w // 2 + tw // 2, y0 + h - 1], fill=color) + # canopy + draw.ellipse([x0 + w // 6, y0, x0 + 5 * w // 6, y0 + h // 2 + h // 8], fill=color, outline=lt_color) + elif kind == "flag": + draw.line([x0 + w // 4, y0, x0 + w // 4, y0 + h - 1], fill=lt_color, width=2) + draw.polygon([(x0 + w // 4, y0 + h // 6), (x0 + 3 * w // 4, y0 + h // 4), (x0 + w // 4, y0 + h // 2)], fill=color) + # ragged edge + for i in range(0, w // 2, 6): + draw.line([x0 + w // 4 + i, y0 + h // 2, x0 + w // 4 + i + 3, y0 + h // 2 + 4], fill=color, width=1) + elif kind == "chains": + for i in range(0, h, 8): + draw.ellipse([x0 + w // 4, y0 + i, x0 + 3 * w // 4, y0 + i + 6], outline=lt_color, width=1) + elif kind == "deep": + draw.rectangle([x0, y0, x0 + w - 1, y0 + h - 1], fill=color) + # distant mountains + for mx in range(0, w, 40): + draw.polygon([(x0 + mx, y0 + h), (x0 + mx + 20, y0 + h // 2), (x0 + mx + 40, y0 + h)], fill=lt_color) + # mist + for my in range(h // 2, h, 8): + draw.line([x0, y0 + my, x0 + w, y0 + my], fill=(20, 18, 24), width=1) + + +def draw_ui(draw, x0, y0, w, h, kind, color, lt_color): + """Draw UI element placeholders.""" + if kind == "bar": + draw.rectangle([x0, y0, x0 + w - 1, y0 + h - 1], outline=lt_color, width=1) + draw.rectangle([x0 + 1, y0 + 1, x0 + w // 2, y0 + h - 2], fill=color) + elif kind == "frame": + draw.rectangle([x0, y0, x0 + w - 1, y0 + h - 1], outline=lt_color, width=2) + draw.rectangle([x0 + 3, y0 + 3, x0 + w - 4, y0 + h - 4], outline=color, width=1) + + +def save_img(img, path): + os.makedirs(os.path.dirname(path), exist_ok=True) + img.save(path) + print(f" ✓ {os.path.relpath(path, os.path.dirname(ASSETS))}") + + +# ═══════════════════════════════════════════════════════════════════════════════ +# GENERATE SPRITE SHEETS +# ═══════════════════════════════════════════════════════════════════════════════ + +def gen_spritesheet(path, frame_w, frame_h, frames, color, lt_color, kind="human"): + """Generate a horizontal sprite sheet with N frames.""" + total_w = frame_w * frames + img = Image.new("RGBA", (total_w, frame_h), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + for f in range(frames): + x0 = f * frame_w + checkerboard(draw, x0, 0, frame_w, frame_h, cell=4) + # magenta border per frame + draw.rectangle([x0, 0, x0 + frame_w - 1, frame_h - 1], outline=C_PLACEHOLDER, width=1) + if kind == "human": + silhouette_rect(draw, x0, 0, frame_w, frame_h, color, lt_color) + elif kind == "beast": + silhouette_beast(draw, x0, 0, frame_w, frame_h, color, lt_color) + elif kind == "boss": + silhouette_boss(draw, x0, 0, frame_w, frame_h, color, lt_color) + save_img(img, path) + + +def gen_single(path, w, h, draw_fn, *args): + """Generate a single-frame image.""" + img = Image.new("RGBA", (w, h), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + draw_fn(draw, 0, 0, w, h, *args) + save_img(img, path) + + +def gen_icon(path, shape, color=C_ICON, lt_color=C_ICON_LT): + """Generate a 32x32 icon.""" + img = Image.new("RGBA", (32, 32), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + checkerboard(draw, 0, 0, 32, 32, cell=4) + draw.rectangle([0, 0, 31, 31], outline=C_PLACEHOLDER, width=1) + draw_icon_simple(draw, 0, 0, 32, shape, color, lt_color) + save_img(img, path) + + +def gen_tile(path, kind, color=C_TILE, lt_color=C_TILE_LT): + """Generate a 32x32 tile.""" + img = Image.new("RGBA", (32, 32), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + checkerboard(draw, 0, 0, 32, 32, cell=4) + draw.rectangle([0, 0, 31, 31], outline=C_PLACEHOLDER, width=1) + draw_tile(draw, 0, 0, 32, kind, color, lt_color) + save_img(img, path) + + +def gen_effect(path, kind, color=C_FIRE, lt_color=C_FIRE_LT): + """Generate a 48x48 effect.""" + img = Image.new("RGBA", (48, 48), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + checkerboard(draw, 0, 0, 48, 48, cell=4) + draw.rectangle([0, 0, 47, 47], outline=C_PLACEHOLDER, width=1) + draw_effect(draw, 0, 0, 48, kind, color, lt_color) + save_img(img, path) + + +# ── Player ─────────────────────────────────────────────────────────────────── +print("\n=== Player Sprites ===") +P = os.path.join(ASSETS, "player") +gen_spritesheet(f"{P}/player_idle.png", 48, 48, 4, C_PLAYER, C_PLAYER_LT) +gen_spritesheet(f"{P}/player_run.png", 48, 48, 6, C_PLAYER, C_PLAYER_LT) +gen_spritesheet(f"{P}/player_jump.png", 48, 48, 2, C_PLAYER, C_PLAYER_LT) +gen_spritesheet(f"{P}/player_attack.png", 48, 48, 4, C_PLAYER, C_PLAYER_LT) +gen_spritesheet(f"{P}/player_hurt.png", 48, 48, 2, C_PLAYER, C_PLAYER_LT) +gen_spritesheet(f"{P}/player_death.png", 48, 48, 6, C_PLAYER, C_PLAYER_LT) + +# ── Enemy: Ghost Soldier (鬼卒) ────────────────────────────────────────────── +print("\n=== Enemy: Ghost Soldier ===") +E = os.path.join(ASSETS, "enemy") +gen_spritesheet(f"{E}/ghost_melee_idle.png", 48, 48, 4, C_GHOST, C_GHOST_LT) +gen_spritesheet(f"{E}/ghost_melee_run.png", 48, 48, 6, C_GHOST, C_GHOST_LT) +gen_spritesheet(f"{E}/ghost_melee_attack.png", 48, 48, 4, C_GHOST, C_GHOST_LT) +gen_spritesheet(f"{E}/ghost_melee_hurt.png", 48, 48, 2, C_GHOST, C_GHOST_LT) +gen_spritesheet(f"{E}/ghost_melee_death.png", 48, 48, 4, C_GHOST, C_GHOST_LT) + +# ── Enemy: Ghost Archer (鬼弓手) ───────────────────────────────────────────── +print("\n=== Enemy: Ghost Archer ===") +gen_spritesheet(f"{E}/ghost_archer_idle.png", 48, 48, 4, C_ARCHER, C_ARCHER_LT) +gen_spritesheet(f"{E}/ghost_archer_run.png", 48, 48, 6, C_ARCHER, C_ARCHER_LT) +gen_spritesheet(f"{E}/ghost_archer_attack.png", 48, 48, 4, C_ARCHER, C_ARCHER_LT) +gen_spritesheet(f"{E}/ghost_archer_hurt.png", 48, 48, 2, C_ARCHER, C_ARCHER_LT) +gen_spritesheet(f"{E}/ghost_archer_death.png", 48, 48, 4, C_ARCHER, C_ARCHER_LT) + +# ── Enemy: Corpse Beast (尸兽) ─────────────────────────────────────────────── +print("\n=== Enemy: Corpse Beast ===") +gen_spritesheet(f"{E}/corpse_beast_idle.png", 64, 48, 4, C_BEAST, C_BEAST_LT, kind="beast") +gen_spritesheet(f"{E}/corpse_beast_run.png", 64, 48, 6, C_BEAST, C_BEAST_LT, kind="beast") +gen_spritesheet(f"{E}/corpse_beast_attack.png", 64, 48, 4, C_BEAST, C_BEAST_LT, kind="beast") +gen_spritesheet(f"{E}/corpse_beast_hurt.png", 64, 48, 2, C_BEAST, C_BEAST_LT, kind="beast") +gen_spritesheet(f"{E}/corpse_beast_death.png", 64, 48, 4, C_BEAST, C_BEAST_LT, kind="beast") + +# ── Boss: Gate Guardian Ghost General (镇关鬼将) ───────────────────────────── +print("\n=== Boss ===") +B = os.path.join(ASSETS, "boss") +gen_spritesheet(f"{B}/boss_idle.png", 96, 96, 4, C_BOSS, C_BOSS_LT, kind="boss") +gen_spritesheet(f"{B}/boss_run.png", 96, 96, 6, C_BOSS, C_BOSS_LT, kind="boss") +gen_spritesheet(f"{B}/boss_attack.png", 96, 96, 5, C_BOSS, C_BOSS_LT, kind="boss") +gen_spritesheet(f"{B}/boss_hurt.png", 96, 96, 2, C_BOSS, C_BOSS_LT, kind="boss") +gen_spritesheet(f"{B}/boss_death.png", 96, 96, 8, C_BOSS, C_BOSS_LT, kind="boss") + +# ── Weapon Icons ───────────────────────────────────────────────────────────── +print("\n=== Weapon Icons ===") +W = os.path.join(ASSETS, "icons", "weapons") +gen_icon(f"{W}/weapon_songdao.png", "sword") +gen_icon(f"{W}/weapon_spear.png", "spear") +gen_icon(f"{W}/weapon_ghostfire.png", "fire", C_FIRE, C_FIRE_LT) +gen_icon(f"{W}/weapon_talisman.png", "talisman") +gen_icon(f"{W}/weapon_gourd.png", "gourd") +gen_icon(f"{W}/weapon_flag.png", "flag") + +# ── UI Icons ───────────────────────────────────────────────────────────────── +print("\n=== UI Icons ===") +U = os.path.join(ASSETS, "icons", "ui") +gen_icon(f"{U}/icon_hp.png", "heart") +gen_icon(f"{U}/icon_stamina.png", "stamina") +gen_icon(f"{U}/icon_attack.png", "attack") +gen_icon(f"{U}/icon_armor.png", "armor") +gen_icon(f"{U}/icon_crit.png", "crit") +gen_icon(f"{U}/icon_speed.png", "speed") +gen_icon(f"{U}/icon_sacrifice.png", "sacrifice", C_BOSS, C_BOSS_LT) +gen_icon(f"{U}/icon_ghostfire.png", "fire", C_FIRE, C_FIRE_LT) +gen_icon(f"{U}/icon_boss.png", "boss", C_BOSS, C_BOSS_LT) +gen_icon(f"{U}/icon_key.png", "key") +gen_icon(f"{U}/icon_coin.png", "coin") + +# ── Tiles ──────────────────────────────────────────────────────────────────── +print("\n=== Tiles ===") +T = os.path.join(ASSETS, "tiles") +gen_tile(f"{T}/tile_floor.png", "floor") +gen_tile(f"{T}/tile_stone_brick.png", "brick") +gen_tile(f"{T}/tile_wall.png", "wall", C_TILE, C_TILE_LT) +gen_tile(f"{T}/tile_wood_bridge.png", "bridge", C_WOOD, C_WOOD_LT) +gen_tile(f"{T}/tile_brazier.png", "brazier", C_TILE, C_TILE_LT) +gen_tile(f"{T}/tile_tombstone.png", "tombstone", C_TILE, C_TILE_LT) +gen_tile(f"{T}/tile_ground_spike.png", "spike", C_TILE, C_TILE_LT) + +# ── Effects ────────────────────────────────────────────────────────────────── +print("\n=== Effects ===") +F = os.path.join(ASSETS, "effects") +gen_effect(f"{F}/effect_hit_slash.png", "slash", (180, 180, 180), (220, 220, 220)) +gen_effect(f"{F}/effect_blood_splash.png","blood", (120, 20, 20), (160, 40, 40)) +gen_effect(f"{F}/effect_ghost_fire.png", "ghostfire", C_FIRE, C_FIRE_LT) +gen_effect(f"{F}/effect_death_fade.png", "deathfade", (60, 50, 70), (90, 80, 110)) + +# ── Background ─────────────────────────────────────────────────────────────── +print("\n=== Background ===") +BG = os.path.join(ASSETS, "background") +gen_single(f"{BG}/bg_tree.png", 64, 128, draw_background, "tree", C_BG_TREE, C_BG_TREE_LT) +gen_single(f"{BG}/bg_broken_flag.png", 48, 96, draw_background, "flag", (50, 35, 30), (80, 55, 45)) +gen_single(f"{BG}/bg_chains.png", 32, 96, draw_background, "chains", (40, 40, 45), (70, 70, 80)) +gen_single(f"{BG}/bg_deep.png", 320, 180, draw_background, "deep", C_BG_DEEP, (20, 18, 26)) + +# ── UI Elements ────────────────────────────────────────────────────────────── +print("\n=== UI Elements ===") +UI = os.path.join(ASSETS, "ui") +gen_single(f"{UI}/ui_health_bar.png", 96, 16, draw_ui, "bar", (120, 20, 20), (160, 40, 40)) +gen_single(f"{UI}/ui_stamina_bar.png", 96, 16, draw_ui, "bar", (20, 100, 60), (40, 140, 80)) +gen_single(f"{UI}/ui_frame.png", 32, 32, draw_ui, "frame", C_UI, C_UI_LT) +gen_single(f"{UI}/ui_minimap_frame.png", 64, 64, draw_ui, "frame", C_UI, C_UI_LT) + +print("\n✅ All placeholder images generated.") +print(f" Total files: {sum(1 for _ in os.walk(ASSETS) for __ in _[2])}")