- ASSET_COUNT_STATUS_01_01_2026.md - asset tracking - CHARACTER_PRODUCTION_PLAN.md - character animation plan - CHARACTER_GENERATION_FINAL_PLAN.md - API alternatives research - COMFYUI_SETUP_TODAY.md - ComfyUI setup guide - TASKS_01_01_2026.md - consolidated task list - FULL_STORY_OVERVIEW.md - game narrative summary - preview_animations.html - animation preview gallery - Test scripts for API exploration (test_minimal.py, test_imagen.py) - Character generation scripts (generate_all_characters_complete.py, generate_characters_working.py) These were created during API troubleshooting and production planning.
394 lines
16 KiB
Python
394 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
COMPLETE CHARACTER ANIMATION GENERATOR
|
|
Generates ALL animations for Kai, Ana, and Gronk (435 PNG total)
|
|
Date: 1.1.2026
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
from pathlib import Path
|
|
import google.generativeai as genai
|
|
from PIL import Image
|
|
import io
|
|
|
|
# ==================== CONFIGURATION ====================
|
|
# Try both environment variable names
|
|
API_KEY = os.getenv('GEMINI_API_KEY') or os.getenv('GOOGLE_API_KEY')
|
|
if not API_KEY:
|
|
print("❌ ERROR: API key not found!")
|
|
print("Please set one of:")
|
|
print(" export GEMINI_API_KEY='your-key-here'")
|
|
print(" export GOOGLE_API_KEY='your-key-here'")
|
|
raise ValueError("API key not set!")
|
|
|
|
# NOTE: Using deprecated google.generativeai for compatibility
|
|
# TODO: Migrate to google.genai in future
|
|
import warnings
|
|
warnings.filterwarnings('ignore', category=FutureWarning)
|
|
|
|
genai.configure(api_key=API_KEY)
|
|
print(f"✅ API Key configured (first 10 chars): {API_KEY[:10]}...")
|
|
|
|
OUTPUT_DIR = Path("assets/slike")
|
|
DELAY_BETWEEN_CALLS = 15 # seconds (safe rate limiting)
|
|
|
|
# Art Style Constants
|
|
STYLE_BASE = """
|
|
Dark Hand-Drawn 2D Stylized Indie Game Art.
|
|
CRITICAL REQUIREMENTS:
|
|
- THICK bold black outlines (3-4px)
|
|
- Exaggerated proportions, warped perspective
|
|
- High-contrast noir elements
|
|
- Centered subject with 10px transparent margin
|
|
- 32-bit PNG with alpha channel
|
|
- Clean background (pure transparency)
|
|
"""
|
|
|
|
# ==================== CHARACTER DEFINITIONS ====================
|
|
|
|
KAI_BASE = """
|
|
Character: KAI MARKOVIĆ (Age 17, Male Protagonist)
|
|
- Green & Pink dreadlocks (distinctive!)
|
|
- Tactical survivor outfit (torn, weathered)
|
|
- Lean athletic build
|
|
- Determined expression
|
|
- REFERENCE: Check konsistentno/kai_master_styleA_reference.png
|
|
"""
|
|
|
|
ANA_BASE = """
|
|
Character: ANA MARKOVIĆ (Age 17, Female Twin)
|
|
- Pink dreadlocks (matches Kai's style but all pink)
|
|
- Explorer vest, cargo pants
|
|
- Slender athletic build
|
|
- Intelligent, caring expression
|
|
- REFERENCE: Check konsistentno/ana_master_styleA_reference.png
|
|
"""
|
|
|
|
GRONK_BASE = """
|
|
Character: GRONK (Zen Troll Companion)
|
|
- Large green troll, 7ft tall
|
|
- Pink mohawk hair
|
|
- Multiple piercings (ears, nose)
|
|
- Vape pen accessory (signature item!)
|
|
- Baggy pants, no shirt
|
|
- Chill, relaxed expression
|
|
- REFERENCE: Check konsistentno/gronk_master_styleA_reference.png
|
|
"""
|
|
|
|
# ==================== ANIMATION DEFINITIONS ====================
|
|
|
|
KAI_ANIMATIONS = {
|
|
# PHASE 1: Core Gameplay (72 PNG)
|
|
"walk_north": {"frames": 4, "desc": "walking away from camera, back view"},
|
|
"walk_northeast": {"frames": 4, "desc": "walking diagonal up-right"},
|
|
"walk_east": {"frames": 4, "desc": "walking right, side view (right profile)"},
|
|
"walk_southeast": {"frames": 4, "desc": "walking diagonal down-right"},
|
|
"walk_south": {"frames": 4, "desc": "walking toward camera, front view"},
|
|
"walk_southwest": {"frames": 4, "desc": "walking diagonal down-left"},
|
|
"walk_west": {"frames": 4, "desc": "walking left, side view (left profile)"},
|
|
"walk_northwest": {"frames": 4, "desc": "walking diagonal up-left"},
|
|
|
|
"idle_front": {"frames": 4, "desc": "idle standing, front view, breathing animation"},
|
|
"idle_side": {"frames": 4, "desc": "idle standing, side profile, breathing animation"},
|
|
|
|
"attack_north": {"frames": 4, "desc": "sword swing upward (attacking north)"},
|
|
"attack_east": {"frames": 4, "desc": "sword swing right (attacking east)"},
|
|
"attack_south": {"frames": 4, "desc": "sword swing downward (attacking south)"},
|
|
"attack_west": {"frames": 4, "desc": "sword swing left (attacking west)"},
|
|
|
|
"dig_north": {"frames": 4, "desc": "digging with shovel upward"},
|
|
"dig_east": {"frames": 4, "desc": "digging with shovel to the right"},
|
|
"dig_south": {"frames": 4, "desc": "digging with shovel downward"},
|
|
"dig_west": {"frames": 4, "desc": "digging with shovel to the left"},
|
|
|
|
# PHASE 2: Expanded (47 PNG)
|
|
"run_north": {"frames": 4, "desc": "running away, back view, faster pace"},
|
|
"run_northeast": {"frames": 4, "desc": "running diagonal up-right"},
|
|
"run_east": {"frames": 4, "desc": "running right, side view"},
|
|
"run_southeast": {"frames": 4, "desc": "running diagonal down-right"},
|
|
"run_south": {"frames": 4, "desc": "running toward camera, front view"},
|
|
"run_southwest": {"frames": 4, "desc": "running diagonal down-left"},
|
|
"run_west": {"frames": 4, "desc": "running left, side view"},
|
|
"run_northwest": {"frames": 4, "desc": "running diagonal up-left"},
|
|
|
|
"plant_front": {"frames": 3, "desc": "planting seeds, kneeling, front view"},
|
|
"plant_side": {"frames": 3, "desc": "planting seeds, kneeling, side view"},
|
|
|
|
"harvest_front": {"frames": 3, "desc": "harvesting crops, bending down, front view"},
|
|
"harvest_side": {"frames": 3, "desc": "harvesting crops, bending down, side view"},
|
|
|
|
"hurt": {"frames": 3, "desc": "taking damage, recoiling, pain expression"},
|
|
|
|
# PHASE 3: Polish (27 PNG)
|
|
"sad": {"frames": 4, "desc": "crying, hands on face, emotional"},
|
|
"happy": {"frames": 4, "desc": "celebrating, arms raised, joyful"},
|
|
"thinking": {"frames": 3, "desc": "hand on chin, pondering"},
|
|
"shocked": {"frames": 2, "desc": "surprised, eyes wide, mouth open"},
|
|
|
|
"use_item_front": {"frames": 3, "desc": "using item from inventory, front view"},
|
|
"use_item_side": {"frames": 3, "desc": "using item from inventory, side view"},
|
|
|
|
"eat": {"frames": 3, "desc": "eating food, bringing to mouth"},
|
|
|
|
"death": {"frames": 5, "desc": "death animation, collapsing to ground"},
|
|
|
|
# PHASE 4: Advanced (10 PNG)
|
|
"zombie_command": {"frames": 4, "desc": "telepathic gesture, hand raised, concentration"},
|
|
"telepathy_effect": {"frames": 6, "desc": "psychic energy emanating, glowing effect"},
|
|
}
|
|
|
|
ANA_ANIMATIONS = {
|
|
# PHASE 1: Core (56 PNG)
|
|
"walk_north": {"frames": 4, "desc": "walking away, back view"},
|
|
"walk_northeast": {"frames": 4, "desc": "walking diagonal up-right"},
|
|
"walk_east": {"frames": 4, "desc": "walking right, side view"},
|
|
"walk_southeast": {"frames": 4, "desc": "walking diagonal down-right"},
|
|
"walk_south": {"frames": 4, "desc": "walking toward camera, front view"},
|
|
"walk_southwest": {"frames": 4, "desc": "walking diagonal down-left"},
|
|
"walk_west": {"frames": 4, "desc": "walking left, side view"},
|
|
"walk_northwest": {"frames": 4, "desc": "walking diagonal up-left"},
|
|
|
|
"idle_front": {"frames": 4, "desc": "idle standing, front view"},
|
|
"idle_side": {"frames": 4, "desc": "idle standing, side profile"},
|
|
|
|
"research_front": {"frames": 4, "desc": "writing in notebook, studying"},
|
|
"research_side": {"frames": 4, "desc": "writing in notebook, side view"},
|
|
|
|
"heal_front": {"frames": 4, "desc": "applying bandage, medical care"},
|
|
"heal_side": {"frames": 4, "desc": "applying bandage, side view"},
|
|
|
|
# PHASE 2: Expanded (46 PNG)
|
|
"run_north": {"frames": 4, "desc": "running away, back view"},
|
|
"run_northeast": {"frames": 4, "desc": "running diagonal up-right"},
|
|
"run_east": {"frames": 4, "desc": "running right"},
|
|
"run_southeast": {"frames": 4, "desc": "running diagonal down-right"},
|
|
"run_south": {"frames": 4, "desc": "running toward camera"},
|
|
"run_southwest": {"frames": 4, "desc": "running diagonal down-left"},
|
|
"run_west": {"frames": 4, "desc": "running left"},
|
|
"run_northwest": {"frames": 4, "desc": "running diagonal up-left"},
|
|
|
|
"examine_front": {"frames": 3, "desc": "examining with magnifying glass"},
|
|
"examine_side": {"frames": 3, "desc": "examining, side view"},
|
|
|
|
"mix_potion": {"frames": 4, "desc": "mixing ingredients in flask"},
|
|
|
|
"worried": {"frames": 4, "desc": "worried expression, hand to head"},
|
|
|
|
# PHASE 3: Polish (18 PNG)
|
|
"relief": {"frames": 4, "desc": "relief, exhaling, relaxed"},
|
|
"twin_bond_glow": {"frames": 6, "desc": "glowing psychic connection effect"},
|
|
"flashback_pose": {"frames": 3, "desc": "memory pose, ethereal"},
|
|
"death": {"frames": 5, "desc": "death animation, collapsing"},
|
|
|
|
# PHASE 4: Advanced (18 PNG)
|
|
"collect_sample_front": {"frames": 3, "desc": "collecting sample with vial"},
|
|
"collect_sample_side": {"frames": 3, "desc": "collecting sample, side view"},
|
|
|
|
"defend": {"frames": 4, "desc": "defensive stance with staff"},
|
|
"cure_cast": {"frames": 5, "desc": "casting cure spell, magical gesture"},
|
|
"hurt": {"frames": 3, "desc": "taking damage, recoiling"},
|
|
}
|
|
|
|
GRONK_ANIMATIONS = {
|
|
# PHASE 1: Core (72 PNG)
|
|
"walk_north": {"frames": 4, "desc": "heavy lumbering walk, back view"},
|
|
"walk_northeast": {"frames": 4, "desc": "heavy walk diagonal up-right"},
|
|
"walk_east": {"frames": 4, "desc": "heavy walk right, side view"},
|
|
"walk_southeast": {"frames": 4, "desc": "heavy walk diagonal down-right"},
|
|
"walk_south": {"frames": 4, "desc": "heavy walk toward camera, front view"},
|
|
"walk_southwest": {"frames": 4, "desc": "heavy walk diagonal down-left"},
|
|
"walk_west": {"frames": 4, "desc": "heavy walk left, side view"},
|
|
"walk_northwest": {"frames": 4, "desc": "heavy walk diagonal up-left"},
|
|
|
|
"idle_front": {"frames": 4, "desc": "idle, chill stance, front view"},
|
|
"idle_side": {"frames": 4, "desc": "idle, chill stance, side view"},
|
|
|
|
"vape_front": {"frames": 6, "desc": "vaping, exhaling smoke cloud, front view"},
|
|
"vape_side": {"frames": 6, "desc": "vaping, exhaling smoke, side view"},
|
|
|
|
"smash_north": {"frames": 5, "desc": "club smash upward"},
|
|
"smash_east": {"frames": 5, "desc": "club smash right"},
|
|
"smash_south": {"frames": 5, "desc": "club smash downward"},
|
|
"smash_west": {"frames": 5, "desc": "club smash left"},
|
|
|
|
# PHASE 2: Expanded (47 PNG)
|
|
"run_north": {"frames": 4, "desc": "slow lumbering run, back view"},
|
|
"run_northeast": {"frames": 4, "desc": "lumbering run diagonal up-right"},
|
|
"run_east": {"frames": 4, "desc": "lumbering run right"},
|
|
"run_southeast": {"frames": 4, "desc": "lumbering run diagonal down-right"},
|
|
"run_south": {"frames": 4, "desc": "lumbering run toward camera"},
|
|
"run_southwest": {"frames": 4, "desc": "lumbering run diagonal down-left"},
|
|
"run_west": {"frames": 4, "desc": "lumbering run left"},
|
|
"run_northwest": {"frames": 4, "desc": "lumbering run diagonal up-left"},
|
|
|
|
"lift_heavy_front": {"frames": 4, "desc": "lifting heavy object, straining"},
|
|
"lift_heavy_side": {"frames": 4, "desc": "lifting heavy object, side view"},
|
|
|
|
"laugh": {"frames": 4, "desc": "laughing, belly laugh, jovial"},
|
|
"hurt": {"frames": 3, "desc": "taking damage, grimacing"},
|
|
|
|
# PHASE 3: Polish (16 PNG)
|
|
"meditate": {"frames": 4, "desc": "zen meditation pose, sitting, serene"},
|
|
"confused": {"frames": 3, "desc": "confused, scratching head"},
|
|
"chill": {"frames": 3, "desc": "ultra relaxed, peace sign"},
|
|
|
|
"death": {"frames": 5, "desc": "death animation, collapsing heavily"},
|
|
|
|
# PHASE 4: Advanced (6 PNG)
|
|
"block": {"frames": 3, "desc": "blocking with raised arms"},
|
|
"taunt": {"frames": 4, "desc": "taunting enemies, beckoning"},
|
|
}
|
|
|
|
# ==================== GENERATION FUNCTIONS ====================
|
|
|
|
def generate_animation_frame(character_name, character_base, animation_name, frame_num, animation_desc, style=STYLE_BASE):
|
|
"""Generate a single animation frame"""
|
|
|
|
prompt = f"""
|
|
{style}
|
|
|
|
{character_base}
|
|
|
|
ANIMATION: {animation_name} - Frame {frame_num}
|
|
Description: {animation_desc}
|
|
|
|
Frame {frame_num} timing notes:
|
|
- Frame 1: Start pose
|
|
- Frame 2: Mid-motion
|
|
- Frame 3: Peak/impact
|
|
- Frame 4: Follow-through/return
|
|
|
|
Generate this specific frame showing clear progression in the animation cycle.
|
|
Maintain character consistency (check reference in konsistentno/ folder).
|
|
"""
|
|
|
|
try:
|
|
print(f" 🎨 Generating {character_name} - {animation_name} - Frame {frame_num}...")
|
|
|
|
model = genai.GenerativeModel('gemini-2.0-flash-exp')
|
|
response = model.generate_content([prompt])
|
|
|
|
if response.parts and len(response.parts) > 0:
|
|
image_data = response.parts[0].inline_data.data
|
|
image = Image.open(io.BytesIO(image_data))
|
|
return image
|
|
else:
|
|
print(f" ❌ No image generated for {animation_name} frame {frame_num}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
print(f" ❌ Error generating {animation_name} frame {frame_num}: {e}")
|
|
return None
|
|
|
|
|
|
def generate_character_animations(character_name, character_base, animations_dict):
|
|
"""Generate all animations for a character"""
|
|
|
|
char_dir = OUTPUT_DIR / character_name.lower()
|
|
char_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
total_frames = sum(anim["frames"] for anim in animations_dict.values())
|
|
current_frame = 0
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"🎬 GENERATING {character_name.upper()} ANIMATIONS")
|
|
print(f"{'='*60}")
|
|
print(f"Total animations: {len(animations_dict)}")
|
|
print(f"Total frames: {total_frames}")
|
|
print(f"Output directory: {char_dir}")
|
|
print()
|
|
|
|
for anim_name, anim_data in animations_dict.items():
|
|
frames_count = anim_data["frames"]
|
|
desc = anim_data["desc"]
|
|
|
|
print(f"\n📹 Animation: {anim_name} ({frames_count} frames)")
|
|
|
|
for frame_num in range(1, frames_count + 1):
|
|
current_frame += 1
|
|
|
|
# Generate frame
|
|
image = generate_animation_frame(
|
|
character_name,
|
|
character_base,
|
|
anim_name,
|
|
frame_num,
|
|
desc
|
|
)
|
|
|
|
if image:
|
|
# Save original
|
|
filename = f"{character_name.lower()}_{anim_name}_frame{frame_num}_1024x1024.png"
|
|
filepath = char_dir / filename
|
|
image.save(filepath, "PNG")
|
|
print(f" ✅ Saved: {filename}")
|
|
|
|
# Create preview (256x256)
|
|
preview = image.resize((256, 256), Image.Resampling.LANCZOS)
|
|
preview_filename = f"{character_name.lower()}_{anim_name}_frame{frame_num}_preview_256x256.png"
|
|
preview_filepath = char_dir / preview_filename
|
|
preview.save(preview_filepath, "PNG")
|
|
print(f" ✅ Saved preview: {preview_filename}")
|
|
|
|
# Progress
|
|
progress = (current_frame / total_frames) * 100
|
|
print(f" 📊 Progress: {current_frame}/{total_frames} ({progress:.1f}%)")
|
|
|
|
# Rate limiting
|
|
print(f" ⏳ Waiting {DELAY_BETWEEN_CALLS}s...")
|
|
time.sleep(DELAY_BETWEEN_CALLS)
|
|
|
|
print(f"\n✅ {character_name.upper()} COMPLETE! Generated {total_frames} frames ({total_frames * 2} files with previews)")
|
|
return total_frames
|
|
|
|
|
|
# ==================== MAIN EXECUTION ====================
|
|
|
|
def main():
|
|
"""Generate all character animations"""
|
|
|
|
print("=" * 80)
|
|
print("🎮 DOLINASMRTI - COMPLETE CHARACTER ANIMATION GENERATOR")
|
|
print("=" * 80)
|
|
print(f"Date: 2026-01-01")
|
|
print(f"Target: 435 PNG (all 3 characters, all animations)")
|
|
print(f"API Delay: {DELAY_BETWEEN_CALLS}s between calls")
|
|
print()
|
|
|
|
start_time = time.time()
|
|
total_generated = 0
|
|
|
|
# Generate Kai
|
|
kai_frames = generate_character_animations("Kai", KAI_BASE, KAI_ANIMATIONS)
|
|
total_generated += kai_frames
|
|
|
|
# Generate Ana
|
|
ana_frames = generate_character_animations("Ana", ANA_BASE, ANA_ANIMATIONS)
|
|
total_generated += ana_frames
|
|
|
|
# Generate Gronk
|
|
gronk_frames = generate_character_animations("Gronk", GRONK_BASE, GRONK_ANIMATIONS)
|
|
total_generated += gronk_frames
|
|
|
|
# Summary
|
|
elapsed = time.time() - start_time
|
|
hours = int(elapsed // 3600)
|
|
minutes = int((elapsed % 3600) // 60)
|
|
|
|
print("\n" + "=" * 80)
|
|
print("🎆 GENERATION COMPLETE!")
|
|
print("=" * 80)
|
|
print(f"Total frames generated: {total_generated}")
|
|
print(f"Total files created: {total_generated * 2} (with previews)")
|
|
print(f"Time elapsed: {hours}h {minutes}min")
|
|
print(f"\nCharacter breakdown:")
|
|
print(f" - Kai: {kai_frames} frames")
|
|
print(f" - Ana: {ana_frames} frames")
|
|
print(f" - Gronk: {gronk_frames} frames")
|
|
print("\n✅ All character animations ready for game integration!")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|