diff --git a/CHARACTER_ANIMATION_MANIFEST.md b/CHARACTER_ANIMATION_MANIFEST.md new file mode 100644 index 000000000..1c3c824a8 --- /dev/null +++ b/CHARACTER_ANIMATION_MANIFEST.md @@ -0,0 +1,184 @@ +# šŸŽ¬ COMPLETE ANIMATION MANIFEST +**Main Characters**: Kai, Ana, Gronk +**Styles**: A (cartoon) & B (noir) +**Target**: Full game-ready animation sets + +--- + +## šŸ“Š ANIMATION BREAKDOWN + +### **PRIORITY 1: CORE MOVEMENT** (Essential for gameplay) + +#### **Walk Cycle** (4 directions Ɨ 4 frames = 16 frames) +- `walk_north_1.png` through `walk_north_4.png` +- `walk_south_1.png` through `walk_south_4.png` +- `walk_east_1.png` through `walk_east_4.png` +- `walk_west_1.png` through `walk_west_4.png` + +#### **Idle Stance** (4 directions Ɨ 1 frame = 4 frames) +- `idle_north.png` +- `idle_south.png` +- `idle_east.png` +- `idle_west.png` + +**Subtotal Priority 1**: 20 frames Ɨ 2 styles = **40 PNG per character** +**Total Priority 1**: 40 Ɨ 3 characters = **120 PNG** + +--- + +### **PRIORITY 2: FARMING ACTIONS** (Core gameplay) + +#### **Hoeing** (4 directions Ɨ 2 frames = 8 frames) +- `hoe_north_1.png`, `hoe_north_2.png` +- `hoe_south_1.png`, `hoe_south_2.png` +- `hoe_east_1.png`, `hoe_east_2.png` +- `hoe_west_1.png`, `hoe_west_2.png` + +#### **Watering** (4 directions Ɨ 2 frames = 8 frames) +- `water_north_1.png`, `water_north_2.png` +- `water_south_1.png`, `water_south_2.png` +- `water_east_1.png`, `water_east_2.png` +- `water_west_1.png`, `water_west_2.png` + +#### **Planting/Picking** (4 directions Ɨ 2 frames = 8 frames) +- `plant_north_1.png`, `plant_north_2.png` +- `plant_south_1.png`, `plant_south_2.png` +- `plant_east_1.png`, `plant_east_2.png` +- `plant_west_1.png`, `plant_west_2.png` + +**Subtotal Priority 2**: 24 frames Ɨ 2 styles = **48 PNG per character** +**Total Priority 2**: 48 Ɨ 3 characters = **144 PNG** + +--- + +### **PRIORITY 3: COMBAT** (Action gameplay) + +#### **Melee Attack** (4 directions Ɨ 3 frames = 12 frames) +- `attack_north_1.png` through `attack_north_3.png` +- `attack_south_1.png` through `attack_south_3.png` +- `attack_east_1.png` through `attack_east_3.png` +- `attack_west_1.png` through `attack_west_3.png` + +#### **Hit/Hurt** (1 universal frame) +- `hurt.png` + +#### **Death** (1 frame) +- `death.png` + +**Subtotal Priority 3**: 14 frames Ɨ 2 styles = **28 PNG per character** +**Total Priority 3**: 28 Ɨ 3 characters = **84 PNG** + +--- + +### **PRIORITY 4: INTERACTIONS** (Polish) + +#### **Interact/Pick Up** (4 directions Ɨ 1 frame = 4 frames) +- `interact_north.png` +- `interact_south.png` +- `interact_east.png` +- `interact_west.png` + +#### **Carry Item** (4 directions Ɨ 1 frame = 4 frames) +- `carry_north.png` +- `carry_south.png` +- `carry_east.png` +- `carry_west.png` + +**Subtotal Priority 4**: 8 frames Ɨ 2 styles = **16 PNG per character** +**Total Priority 4**: 16 Ɨ 3 characters = **48 PNG** + +--- + +### **PRIORITY 5: RUNNING** (Optional enhancement) + +#### **Run Cycle** (4 directions Ɨ 4 frames = 16 frames) +- `run_north_1.png` through `run_north_4.png` +- `run_south_1.png` through `run_south_4.png` +- `run_east_1.png` through `run_east_4.png` +- `run_west_1.png` through `run_west_4.png` + +**Subtotal Priority 5**: 16 frames Ɨ 2 styles = **32 PNG per character** +**Total Priority 5**: 32 Ɨ 3 characters = **96 PNG** + +--- + +## šŸ“ˆ GRAND TOTAL + +| Priority | Description | Frames/Char | PNG/Char | Total PNG | +|:---------|:------------|------------:|---------:|----------:| +| **P1** | Core Movement | 20 | 40 | 120 | +| **P2** | Farming Actions | 24 | 48 | 144 | +| **P3** | Combat | 14 | 28 | 84 | +| **P4** | Interactions | 8 | 16 | 48 | +| **P5** | Running | 16 | 32 | 96 | +| **TOTAL** | **All Animations** | **82** | **164** | **492** | + +--- + +## šŸŽÆ GENERATION STRATEGY + +### **Phase 1** (Essential - Day 1): +- Priority 1 (Core Movement): **120 PNG** +- Priority 2 (Farming): **144 PNG** +**= 264 PNG** (Playable demo ready!) + +### **Phase 2** (Combat - Day 2): +- Priority 3 (Combat): **84 PNG** + +### **Phase 3** (Polish - Day 3): +- Priority 4 (Interactions): **48 PNG** +- Priority 5 (Running): **96 PNG** + +--- + +## šŸ“‹ NAMING CONVENTION + +Format: `{character}_{action}_{direction}_{frame}_{style}.png` + +**Examples**: +- `kai_walk_north_1_stylea.png` +- `ana_hoe_south_2_styleb.png` +- `gronk_attack_east_3_stylea.png` + +--- + +## šŸ”§ GENERATION PROMPTS + +**Base prompt structure**: +``` +Character: [Kai/Ana/Gronk] from reference image +Action: [walking/hoeing/attacking] [direction] +Frame: [1/2/3/4] of animation cycle +Style: [Style A cartoon vector / Style B noir gritty] +Maintain exact character consistency with master reference +Full body, centered, isolated white background, game sprite asset +``` + +--- + +## ā±ļø TIME ESTIMATES + +**With API quota** (5 req/min = 60/hour): +- Phase 1 (264 PNG): ~4.5 hours +- Phase 2 (84 PNG): ~1.5 hours +- Phase 3 (144 PNG): ~2.5 hours + +**Total**: ~8.5 hours for complete animation sets! + +--- + +## āœ… DELIVERABLES + +**Per character** (Kai, Ana, Gronk): +- āœ… 82 unique animation frames +- āœ… 2 styles each (Style A & B) +- āœ… **164 PNG files per character** +- āœ… Complete animation set ready for Phaser/Tiled integration + +**All 3 characters**: **492 PNG total** šŸŽ® + +--- + +**Created**: 31.12.2025 +**Ready for**: 01.01.2026 (quota reset) +**Status**: ā° Awaiting generation start diff --git a/scripts/generate_character_animations.py b/scripts/generate_character_animations.py new file mode 100644 index 000000000..eb59cec4f --- /dev/null +++ b/scripts/generate_character_animations.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +""" +šŸŽ¬ CHARACTER ANIMATION GENERATOR +Generates complete animation sets for Kai, Ana, Gronk +Based on CHARACTER_ANIMATION_MANIFEST.md +""" + +import os +import sys +import time +from pathlib import Path +from PIL import Image + +try: + import google.generativeai as genai +except ImportError: + print("āŒ google-generativeai not installed!") + sys.exit(1) + +# Configuration +REPO = Path("/Users/davidkotnik/repos/novafarma") +LIKI = REPO / "assets/slike/liki" + +# API Setup +api_key = os.environ.get("GEMINI_API_KEY") +if not api_key: + print("āŒ GEMINI_API_KEY not set!") + sys.exit(1) + +genai.configure(api_key=api_key) + +# Characters and their master references +CHARACTERS = { + "kai": { + "styleA_ref": "kai_master_styleA_reference.png", + "styleB_ref": "kai_master_styleB_reference.png", + "description": "young male adventurer with green hair and backpack" + }, + "ana": { + "styleA_ref": "ana_master_styleA_reference.png", + "styleB_ref": "ana_master_styleB_reference.png", + "description": "young girl explorer with pink dreadlocks, green vest, cargo shorts" + }, + "gronk": { + "styleA_ref": "gronk_master_styleA_reference.png", + "styleB_ref": "gronk_master_styleB_reference.png", + "description": "large friendly orc with green skin, pink dreadlocks, pink hoodie, vape pen" + } +} + +# Animation definitions - PRIORITY 1: CORE MOVEMENT +ANIMATIONS_P1 = { + "walk": { + "frames": 4, + "directions": ["north", "south", "east", "west"], + "prompts": { + "north": "walking away from camera, back view, {frame} of 4 walk cycle", + "south": "walking toward camera, front view, {frame} of 4 walk cycle", + "east": "walking to the right, side view facing right, {frame} of 4 walk cycle", + "west": "walking to the left, side view facing left, {frame} of 4 walk cycle" + } + }, + "idle": { + "frames": 1, + "directions": ["north", "south", "east", "west"], + "prompts": { + "north": "standing idle, back view", + "south": "standing idle, front view", + "east": "standing idle, side view facing right", + "west": "standing idle, side view facing left" + } + } +} + +STYLE_PROMPTS = { + "stylea": "2D game character sprite, cartoon vector art style with bold black outlines, flat colors, cute playful aesthetic, isolated on pure white background", + "styleb": "2D game character sprite, dark hand-drawn gritty noir style with dramatic shadows, high contrast, sketchy atmospheric lines, isolated on pure white background" +} + +def create_preview(image_path: Path, size=256): + """Create preview version""" + try: + img = Image.open(image_path) + preview = img.resize((size, size), Image.Resampling.LANCZOS) + preview_path = image_path.parent / f"{image_path.stem}_preview_{size}x{size}.png" + preview.save(preview_path, 'PNG', optimize=True) + return preview_path + except Exception as e: + print(f" āš ļø Preview failed: {e}") + return None + +def generate_animation_frame(character, action, direction, frame, style, log_file): + """Generate single animation frame""" + + char_data = CHARACTERS[character] + style_suffix = style + + # Build filename + if frame > 0: + filename = f"{character}_{action}_{direction}_{frame}_{style_suffix}.png" + else: + filename = f"{character}_{action}_{direction}_{style_suffix}.png" + + output_path = LIKI / character / filename + + # Skip if exists + if output_path.exists(): + print(f" ā­ļø {filename} exists") + return True + + # Build prompt + anim_data = ANIMATIONS_P1[action] + action_prompt = anim_data["prompts"][direction] + + if "{frame}" in action_prompt: + action_prompt = action_prompt.format(frame=frame) + + style_prompt = STYLE_PROMPTS[style_suffix] + + full_prompt = f"""{style_prompt} + +Character: {char_data['description']} +EXACTLY match the visual style and appearance from the master reference image. + +Action: {action_prompt} +Full body visible, centered composition, game asset sprite. +Maintain complete visual consistency with reference - same colors, proportions, clothing details. +""" + + start = time.time() + + try: + print(f" šŸŽØ Generating: {filename}") + log_file.write(f"{time.strftime('%H:%M:%S')} - {filename}\n") + log_file.flush() + + model = genai.GenerativeModel("gemini-2.5-flash") + response = model.generate_content([full_prompt]) + + if hasattr(response, '_result') and response._result.candidates: + image_data = response._result.candidates[0].content.parts[0].inline_data.data + + # Save + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, 'wb') as f: + f.write(image_data) + + # Preview + create_preview(output_path) + + elapsed = time.time() - start + print(f" āœ… Saved ({elapsed:.1f}s)") + log_file.write(f"{time.strftime('%H:%M:%S')} - SUCCESS ({elapsed:.1f}s)\n") + log_file.flush() + + return True + else: + print(f" āŒ No image data") + log_file.write(f"{time.strftime('%H:%M:%S')} - FAILED - No data\n") + log_file.flush() + return False + + except Exception as e: + print(f" āŒ Error: {e}") + log_file.write(f"{time.strftime('%H:%M:%S')} - ERROR: {e}\n") + log_file.flush() + return False + +def main(): + # Create log + log_dir = REPO / "logs" + log_dir.mkdir(exist_ok=True) + log_file = open(log_dir / f"character_animations_{time.strftime('%Y%m%d_%H%M%S')}.log", 'w') + + print("="*70) + print("šŸŽ¬ CHARACTER ANIMATION GENERATOR - PRIORITY 1") + print("="*70) + print("\n3 Characters Ɨ 20 frames Ɨ 2 styles = 120 PNG\n") + + log_file.write(f"CHARACTER ANIMATION GENERATION - {time.strftime('%Y-%m-%d %H:%M:%S')}\n") + log_file.write("="*70 + "\n\n") + log_file.flush() + + stats = {'total': 0, 'success': 0, 'failed': 0} + + try: + for character in CHARACTERS.keys(): + print(f"\n{'='*70}") + print(f"šŸ‘¤ {character.upper()}") + print(f"{'='*70}") + + for style in ["stylea", "styleb"]: + print(f"\nšŸŽØ Style: {style}") + + for action, anim_data in ANIMATIONS_P1.items(): + print(f"\n šŸ“¹ {action.upper()}") + + for direction in anim_data["directions"]: + frames = anim_data["frames"] + + if frames == 1: + stats['total'] += 1 + if generate_animation_frame(character, action, direction, 0, style, log_file): + stats['success'] += 1 + else: + stats['failed'] += 1 + time.sleep(15) # Rate limiting + else: + for frame in range(1, frames + 1): + stats['total'] += 1 + if generate_animation_frame(character, action, direction, frame, style, log_file): + stats['success'] += 1 + else: + stats['failed'] += 1 + time.sleep(15) # Rate limiting + + # Progress + progress = (stats['total'] / 120) * 100 + print(f"\nšŸ“Š Overall Progress: {progress:.1f}% | āœ… {stats['success']} | āŒ {stats['failed']}") + + # Final summary + print("\n" + "="*70) + print("šŸŽ‰ GENERATION COMPLETE!") + print("="*70) + print(f"āœ… Success: {stats['success']}/120") + print(f"āŒ Failed: {stats['failed']}/120") + print(f"šŸ“Š Success rate: {(stats['success']/120)*100:.1f}%") + + log_file.write(f"\n{'='*70}\n") + log_file.write(f"COMPLETE - Success: {stats['success']}, Failed: {stats['failed']}\n") + + except KeyboardInterrupt: + print(f"\n\nāš ļø INTERRUPTED at {stats['total']}/120") + log_file.write(f"\n\nINTERRUPTED at {stats['total']}/120\n") + + finally: + log_file.close() + +if __name__ == "__main__": + main()