Files
novafarma/docs/V1_0_IMPLEMENTATION_PLAN.md
David Kotnik ce7d9e7d32 docs: Director Mode v1.0 - Master Directive & Implementation Plan
Added comprehensive directive and implementation plan for v1.0 release:
- Political systems (Lawyer, Mayor)
- Work automation (Zombie workers, 4-frame animations)
- Cinematic intro (Ken Burns, typewriter, blur-to-clear)
- Accessibility (ADHD mode, colorblind filters, asset gallery)

Current asset count: 1200+ images
Latest batch: 342 Style 32 assets
Status: Ready for autonomous execution
2026-01-04 18:56:17 +01:00

22 KiB

🎯 MRTVA DOLINA v1.0 - IMPLEMENTATION PLAN

Generated: 2026-01-04 18:52 CET
Mode: DIRECTOR MODE (Autonomous)
Target: v1.0 Release Build
Status: 🟢 READY TO EXECUTE


📅 TIMELINE

Total Estimated Time: 6-8 hours
Phases: 5
Autonomous: YES (// turbo-all)


🔢 PHASE 1: ASSET GENERATION (Style 32)

Duration: 2 hours
Priority: HIGH

Tasks:

1.1 Political NPCs

  • lawyer_wrinkled_suit_style32.png - Odvetnik z aktovko in debelimi očali
  • mayor_post_apo_sash_style32.png - Župan z lento, post-apo stil

1.2 Animation Spritesheets (4 frames each)

  • milking_cow_4frame_style32.png - Molža krave
  • blacksmithing_4frame_style32.png - Kovaštvo z iskrami
  • chopping_wood_4frame_style32.png - Sekanje drv
  • mining_4frame_style32.png - Rudarjenje

1.3 Zombie Workers

  • zombie_worker_purple_eyes_style32.png - Zombie delavec z vijolično očmi
  • zombie_worker_carrying_style32.png - Zombie nosi material
  • zombie_worker_mining_style32.png - Zombie rudari

Output Location: /assets/images/STYLE_32_SESSION_JAN_04/


🎮 PHASE 2: GAME SYSTEMS IMPLEMENTATION

Duration: 2 hours
Priority: HIGH

Tasks:

2.1 Lawyer Service System

File: /src/systems/LawyerService.js

export class LawyerService {
  constructor(scene) {
    this.scene = scene;
    this.divorceFee = 50000;
    this.lawyerCut = 0.25; // 25%
    this.cooldownDays = 7;
    this.lastDivorce = null;
  }

  processDivorce(player, partner) {
    if (player.gold < this.divorceFee) {
      return { success: false, reason: 'insufficient_funds' };
    }

    const lawyerCut = this.divorceFee * this.lawyerCut;
    player.gold -= this.divorceFee;
    this.scene.economy.lawyerGold += lawyerCut;

    // Trigger town gossip
    this.scene.gossipSystem.spread({
      type: 'divorce',
      source: player.name,
      partner: partner.name,
      severity: 'high'
    });

    this.lastDivorce = this.scene.time.getCurrentDay();
    return { success: true, fee: this.divorceFee, lawyerCut };
  }

  canProcess() {
    if (!this.lastDivorce) return true;
    const daysSince = this.scene.time.getCurrentDay() - this.lastDivorce;
    return daysSince >= this.cooldownDays;
  }
}

2.2 Mayor Service System

File: /src/systems/MayorService.js

export class MayorService {
  constructor(scene) {
    this.scene = scene;
    this.biomePermits = {
      desert: 10000,
      mountains: 15000,
      jungle: 20000,
      swamp: 12000,
      arctic: 18000
    };
    this.taxRate = 0.10; // 10%
    this.taxInterval = 30; // days
    this.lastTaxCollection = 0;
  }

  issuePermit(player, biome) {
    const cost = this.biomePermits[biome];
    if (!cost) return { success: false, reason: 'invalid_biome' };
    if (player.gold < cost) return { success: false, reason: 'insufficient_funds' };

    player.gold -= cost;
    player.biomeAccess[biome] = true;
    this.scene.economy.mayorGold += cost;

    return { success: true, biome, cost };
  }

  collectTax(player) {
    const daysSince = this.scene.time.getCurrentDay() - this.lastTaxCollection;
    if (daysSince < this.taxInterval) return { success: false, reason: 'too_soon' };

    const taxAmount = Math.floor(player.gold * this.taxRate);
    player.gold -= taxAmount;
    this.scene.economy.mayorGold += taxAmount;
    this.lastTaxCollection = this.scene.time.getCurrentDay();

    return { success: true, amount: taxAmount };
  }
}

2.3 Zombie Worker Manager

File: /src/systems/ZombieWorkerManager.js

export class ZombieWorkerManager {
  constructor(scene) {
    this.scene = scene;
    this.workers = [];
    this.efficiency = 0.75; // 75% of human speed
    this.maintenanceCostPerDay = 50;
  }

  assignTask(zombie, task) {
    zombie.currentTask = task;
    zombie.workProgress = 0;
    zombie.isWorking = true;

    // Purple eye glow effect
    zombie.eyes.setTint(0x9D4EDD); // violet
    zombie.eyes.alpha = 0.8;
  }

  update(delta) {
    this.workers.forEach(zombie => {
      if (!zombie.isWorking) return;

      const task = zombie.currentTask;
      const progressRate = (this.efficiency * delta) / 1000;
      zombie.workProgress += progressRate;

      if (zombie.workProgress >= task.duration) {
        this.completeTask(zombie, task);
      }
    });
  }

  completeTask(zombie, task) {
    // Auto-store resources in base storage
    this.scene.storage.add(task.resource, task.amount);
    zombie.isWorking = false;
    zombie.currentTask = null;

    // Find next task
    const nextTask = this.scene.taskQueue.getNext();
    if (nextTask) {
      this.assignTask(zombie, nextTask);
    }
  }
}

2.4 Work Animation System

File: /src/systems/WorkAnimationSystem.js

export class WorkAnimationSystem {
  constructor(scene) {
    this.scene = scene;
    this.animations = {};
  }

  create() {
    // Milking animation
    this.scene.anims.create({
      key: 'milking',
      frames: this.scene.anims.generateFrameNumbers('milking_4frame', { start: 0, end: 3 }),
      frameRate: 8,
      repeat: -1
    });

    // Blacksmithing with sparks
    this.scene.anims.create({
      key: 'blacksmithing',
      frames: this.scene.anims.generateFrameNumbers('blacksmith_4frame', { start: 0, end: 3 }),
      frameRate: 6,
      repeat: -1
    });

    // Chopping wood
    this.scene.anims.create({
      key: 'chopping',
      frames: this.scene.anims.generateFrameNumbers('chop_wood_4frame', { start: 0, end: 3 }),
      frameRate: 7,
      repeat: -1
    });

    // Mining
    this.scene.anims.create({
      key: 'mining',
      frames: this.scene.anims.generateFrameNumbers('mining_4frame', { start: 0, end: 3 }),
      frameRate: 8,
      repeat: -1
    });
  }

  playAnimation(sprite, animKey) {
    sprite.play(animKey);

    // Add particle effects for specific frames
    if (animKey === 'blacksmithing') {
      sprite.on('animationupdate', (anim, frame) => {
        if (frame.index === 2) { // Impact frame
          this.scene.particles.emitSparks(sprite.x, sprite.y);
        }
      });
    }
  }
}

🎬 PHASE 3: CINEMATIC INTRO SYSTEM

Duration: 2 hours
Priority: MEDIUM

Tasks:

3.1 Intro Scene with Ken Burns Effect

File: /src/scenes/IntroScene.js

export class IntroScene extends Phaser.Scene {
  constructor() {
    super({ key: 'IntroScene' });
  }

  create() {
    this.cameras.main.setBackgroundColor('#000000');
    
    // Scene 1: Abandoned farm with Ken Burns zoom
    this.showScene1();
  }

  showScene1() {
    const bg = this.add.image(0, 0, 'intro_farm_abandoned').setOrigin(0);
    bg.setScale(1.2); // Start zoomed in

    // Ken Burns: slow zoom out + pan
    this.tweens.add({
      targets: bg,
      scale: 1.0,
      x: -50,
      y: -30,
      duration: 5000,
      ease: 'Sine.easeInOut'
    });

    // Typewriter text
    this.showTypewriter(
      "Leta 2084, Dolina se ni več obranila...",
      100, 500,
      () => this.crossFadeToScene2()
    );
  }

  showTypewriter(text, x, y, onComplete) {
    const textObj = this.add.text(x, y, '', {
      fontSize: '24px',
      fontFamily: 'Georgia',
      color: '#FFFFFF',
      stroke: '#000000',
      strokeThickness: 4
    });

    let charIndex = 0;
    const typeSpeed = 60; // ms per character

    const timer = this.time.addEvent({
      delay: typeSpeed,
      repeat: text.length - 1,
      callback: () => {
        textObj.text += text[charIndex];
        charIndex++;
        if (charIndex === text.length && onComplete) {
          this.time.delayedCall(2000, onComplete);
        }
      }
    });
  }

  crossFadeToScene2() {
    this.cameras.main.fadeOut(1500, 0, 0, 0);
    this.cameras.main.once('camerafadeoutcomplete', () => {
      this.showScene2();
      this.cameras.main.fadeIn(1500, 0, 0, 0);
    });
  }

  showScene2() {
    this.children.removeAll();
    const bg = this.add.image(400, 300, 'intro_lab_flash').setScale(1.0);

    this.showTypewriter(
      "Incident je pustil posledice, ki jih nihče ni pričakoval...",
      100, 500,
      () => this.blurToGameplay()
    );
  }

  blurToGameplay() {
    // Blur-to-clear transition
    const blur = this.cameras.main.postFX.addBlur(0, 2, 2, 10);
    
    this.cameras.main.fadeOut(2000, 0, 0, 0);
    
    this.tweens.add({
      targets: blur,
      strength: 0,
      duration: 2000,
      onComplete: () => {
        this.scene.start('GameScene'); // Start gameplay
      }
    });
  }
}

3.2 Voice Cloning Pipeline

File: /tools/voice_clone.py

import edge_tts
import asyncio
from pydub import AudioSegment
from pydub.effects import low_pass_filter, high_pass_filter

async def clone_voice_with_radio_filter(text, lang='sl', output_path='narration.mp3'):
    """
    Clone user's voice and apply radio filter
    """
    # Use Edge TTS for voice generation
    communicate = edge_tts.Communicate(text, voice=f"{lang}-SL-PetraNeural")
    await communicate.save(output_path)
    
    # Apply radio filter (post-apocalyptic aesthetic)
    audio = AudioSegment.from_file(output_path)
    
    # Radio effect: bandpass filter + compression
    radio_audio = high_pass_filter(audio, 300)  # Cut below 300Hz
    radio_audio = low_pass_filter(radio_audio, 3000)  # Cut above 3kHz
    radio_audio = radio_audio + 3  # Increase volume slightly
    
    radio_audio.export(output_path, format="mp3")
    print(f"✅ Voice generated with radio filter: {output_path}")

# Generate all languages
languages = {
    'sl': 'sl-SL-PetraNeural',  # Slovenian (use as base)
    'en': 'en-US-GuyNeural',     # English
    'de': 'de-DE-ConradNeural',  # German
    'it': 'it-IT-DiegoNeural',   # Italian
    'zh': 'zh-CN-YunxiNeural'    # Chinese
}

async def generate_all_narrations():
    script = "Leta 2084, Dolina se ni več obranila..."
    
    for lang_code, voice in languages.items():
        await clone_voice_with_radio_filter(
            script, 
            lang_code, 
            f"assets/audio/intro_{lang_code}.mp3"
        )

asyncio.run(generate_all_narrations())

Duration: 1.5 hours
Priority: MEDIUM

Tasks:

File: /tools/asset_gallery.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>DolinaSmrti Asset Gallery</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background: #1a1a1a;
      color: #fff;
      padding: 20px;
    }
    h1 {
      text-align: center;
      margin-bottom: 20px;
      color: #9D4EDD;
    }
    .search-bar {
      width: 100%;
      max-width: 600px;
      margin: 0 auto 30px;
      display: block;
      padding: 12px;
      font-size: 16px;
      border: 2px solid #9D4EDD;
      border-radius: 8px;
      background: #2a2a2a;
      color: #fff;
    }
    .filter-buttons {
      text-align: center;
      margin-bottom: 30px;
    }
    .filter-btn {
      padding: 8px 16px;
      margin: 5px;
      border: 2px solid #9D4EDD;
      background: #2a2a2a;
      color: #fff;
      cursor: pointer;
      border-radius: 6px;
      transition: all 0.3s;
    }
    .filter-btn:hover, .filter-btn.active {
      background: #9D4EDD;
      color: #000;
    }
    .gallery-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
      gap: 20px;
      padding: 20px;
    }
    .asset-card {
      background: #2a2a2a;
      border-radius: 12px;
      padding: 10px;
      text-align: center;
      transition: transform 0.2s, box-shadow 0.2s;
      cursor: pointer;
    }
    .asset-card:hover {
      transform: translateY(-5px);
      box-shadow: 0 8px 16px rgba(157, 78, 221, 0.4);
    }
    .asset-card img {
      width: 128px;
      height: 128px;
      object-fit: contain;
      border-radius: 8px;
      background: #1a1a1a;
    }
    .asset-card .filename {
      margin-top: 8px;
      font-size: 12px;
      color: #aaa;
      word-break: break-all;
    }
    .modal {
      display: none;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0,0,0,0.9);
      justify-content: center;
      align-items: center;
      z-index: 1000;
    }
    .modal.active { display: flex; }
    .modal img {
      max-width: 90%;
      max-height: 90%;
      border-radius: 12px;
    }
  </style>
</head>
<body>
  <h1>🎨 DolinaSmrti Asset Gallery</h1>
  <input type="text" class="search-bar" id="search" placeholder="Search assets...">
  
  <div class="filter-buttons">
    <button class="filter-btn active" data-filter="all">All (342)</button>
    <button class="filter-btn" data-filter="buildings">Buildings</button>
    <button class="filter-btn" data-filter="npcs">NPCs</button>
    <button class="filter-btn" data-filter="terrain">Terrain</button>
    <button class="filter-btn" data-filter="interior">Interior</button>
    <button class="filter-btn" data-filter="weapons">Weapons</button>
    <button class="filter-btn" data-filter="tools">Tools</button>
    <button class="filter-btn" data-filter="ui">UI</button>
  </div>

  <div class="gallery-grid" id="gallery"></div>

  <div class="modal" id="modal">
    <img id="modal-img" src="" alt="">
  </div>

  <script>
    const assetPath = '../assets/images/STYLE_32_SESSION_JAN_04/';
    const gallery = document.getElementById('gallery');
    const modal = document.getElementById('modal');
    const modalImg = document.getElementById('modal-img');
    const search = document.getElementById('search');
    
    let assets = [];

    // Load assets from directory
    fetch(assetPath)
      .then(r => r.text())
      .then(html => {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const links = [...doc.querySelectorAll('a')];
        
        assets = links
          .filter(a => a.href.endsWith('.png'))
          .map(a => ({
            name: a.textContent,
            path: assetPath + a.textContent,
            category: detectCategory(a.textContent)
          }));
        
        renderGallery(assets);
      });

    function detectCategory(filename) {
      if (filename.match(/^(barn|bakery|church|house|farmhouse|tavern|windmill)/i)) return 'buildings';
      if (filename.match(/^npc/i)) return 'npcs';
      if (filename.match(/^terrain/i)) return 'terrain';
      if (filename.match(/^interior/i)) return 'interior';
      if (filename.match(/weapon/i)) return 'weapons';
      if (filename.match(/tool/i)) return 'tools';
      if (filename.match(/(button|icon|bar|panel)/i)) return 'ui';
      return 'misc';
    }

    function renderGallery(items) {
      gallery.innerHTML = items.map(asset => `
        <div class="asset-card" data-category="${asset.category}" onclick="openModal('${asset.path}')">
          <img src="${asset.path}" alt="${asset.name}" loading="lazy">
          <div class="filename">${asset.name}</div>
        </div>
      `).join('');
    }

    function openModal(src) {
      modalImg.src = src;
      modal.classList.add('active');
    }

    modal.addEventListener('click', () => modal.classList.remove('active'));

    // Filter buttons
    document.querySelectorAll('.filter-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        
        const filter = btn.dataset.filter;
        const cards = document.querySelectorAll('.asset-card');
        
        cards.forEach(card => {
          if (filter === 'all' || card.dataset.category === filter) {
            card.style.display = 'block';
          } else {
            card.style.display = 'none';
          }
        });
      });
    });

    // Search
    search.addEventListener('input', (e) => {
      const query = e.target.value.toLowerCase();
      const filtered = assets.filter(a => a.name.toLowerCase().includes(query));
      renderGallery(filtered);
    });
  </script>
</body>
</html>

4.2 ADHD Focus Mode

File: /src/systems/AccessibilityManager.js

export class AccessibilityManager {
  constructor(scene) {
    this.scene = scene;
    this.adhdMode = false;
    this.colorblindMode = null;
  }

  enableADHDMode() {
    this.adhdMode = true;
    
    // Reduce animations
    this.scene.tweens.timeScale = 0.7; // Slower, more predictable
    
    // Highlight interactables
    this.scene.interactables.forEach(obj => {
      obj.setStrokeStyle(4, 0xFFFF00, 1); // Yellow outline
    });
    
    // Simplify UI
    this.scene.ui.setAlpha(0.9);
    this.scene.ui.setScale(1.1); // Slightly larger
    
    // Increase contrast
    this.scene.cameras.main.setAlpha(1.5);
    
    console.log('✅ ADHD Focus Mode enabled');
  }

  setColorblindFilter(type) {
    this.colorblindMode = type;
    const filters = {
      protanopia: { r: 0.56667, g: 0.43333, b: 0 },
      deuteranopia: { r: 0.625, g: 0.375, b: 0 },
      tritanopia: { r: 0.95, g: 0.05, b: 0 },
      monochrome: { r: 0.299, g: 0.587, b: 0.114 }
    };

    if (filters[type]) {
      this.scene.cameras.main.setTint(Phaser.Display.Color.GetColor(
        filters[type].r * 255,
        filters[type].g * 255,
        filters[type].b * 255
      ));
    }
  }
}

4.3 Smart Asset Organization Script

File: /scripts/organize_assets.py

import os
import shutil
from pathlib import Path

SOURCE_DIR = "assets/images/STYLE_32_SESSION_JAN_04"
TARGET_STRUCTURE = {
    "characters": {
        "gronk": ["gronk"],
        "kai": ["kai"],
        "ana": ["ana"],
        "zombies": ["zombie"]
    },
    "buildings": ["barn", "bakery", "church", "farmhouse", "house", "tavern", "windmill", "workshop"],
    "biomes": {
        "desert": ["desert"],
        "mountains": ["mountain"],
        "jungle": ["jungle"],
        "swamp": ["swamp"]
    },
    "props": ["barrel", "chest", "campfire", "crate", "grave"],
    "ui": ["button", "icon", "bar", "panel", "slot"],
    "interior": ["interior"],
    "weapons": ["weapon"],
    "tools": ["tool"],
    "terrain": ["terrain"]
}

def organize_assets():
    """Reorganize assets into smart folder structure"""
    for category, patterns in TARGET_STRUCTURE.items():
        if isinstance(patterns, dict):
            # Nested structure
            for subfolder, subpatterns in patterns.items():
                organize_category(category, subfolder, subpatterns)
        else:
            organize_category(category, None, patterns)

def organize_category(category, subfolder, patterns):
    target_path = Path(SOURCE_DIR) / category
    if subfolder:
        target_path = target_path / subfolder
    
    target_path.mkdir(parents=True, exist_ok=True)
    
    for file in Path(SOURCE_DIR).glob("*.png"):
        filename = file.name.lower()
        if any(pattern in filename for pattern in patterns):
            new_path = target_path / file.name
            shutil.copy(file, new_path)
            print(f"✅ {file.name}{category}/{subfolder or ''}")

if __name__ == "__main__":
    organize_assets()
    print("🎉 Asset organization complete!")

PHASE 5: TESTING & FINAL COMMIT

Duration: 0.5 hours
Priority: CRITICAL

Tasks:

  • Test lawyer service (divorce flow, gossip trigger, gold calculation)
  • Test mayor service (permit purchase, tax collection)
  • Test zombie workers (assignment, purple eyes, auto-storage)
  • Test work animations (all 4 cycles, particle effects)
  • Test intro sequence (Ken Burns, typewriter, blur-to-clear)
  • Test asset gallery (thumbnail grid, search, filters, modal)
  • Test ADHD mode (reduced animations, highlighted interactables)
  • Test colorblind filters (all 4 modes)
  • Verify asset organization (smart folders created correctly)

Final Commit:

git add .
git commit -m "feat(v1.0): Director Mode - Complete Political, Automation, Cinematic & Accessibility Systems

SYSTEMS IMPLEMENTED:
✅ Lawyer NPC (divorce 50k, 25% cut, gossip)  
✅ Mayor NPC (biome permits, tax collection)  
✅ Zombie Workers (24/7, purple eyes, auto-storage)  
✅ Work Animations (milking, blacksmithing, chopping, mining)  
✅ Cinematic Intro (Ken Burns, typewriter, blur-to-clear)  
✅ Voice Cloning (multi-language, radio filter)  
✅ Asset Gallery (342+ thumbnails, search, filters)  
✅ ADHD Focus Mode (reduced animations, highlighted UI)  
✅ Colorblind Filters (protanopia, deuteranopia, tritanopia, monochrome)  
✅ Smart Asset Organization (characters, biomes, buildings, props)

STATUS: Mrtva Dolina v1.0 COMPLETE 🎬
Assets: 342 images, 170MB  
Code: ~2000 new lines  
Systems: 9 new + 4 enhanced"

📊 DELIVERABLES SUMMARY

Assets Generated: ~15

  • 2 political NPCs (lawyer, mayor)
  • 4 animation spritesheets (4 frames each = 16 frames)
  • 3 zombie worker variants

Code Written: ~2000 lines

  • LawyerService.js (150 lines)
  • MayorService.js (120 lines)
  • ZombieWorkerManager.js (180 lines)
  • WorkAnimationSystem.js (140 lines)
  • IntroScene.js (200 lines)
  • AccessibilityManager.js (100 lines)
  • asset_gallery.html (250 lines)
  • voice_clone.py (80 lines)
  • organize_assets.py (60 lines)

Systems Implemented: 9

  1. Lawyer Service
  2. Mayor Service
  3. Zombie Worker Manager
  4. Work Animation System
  5. Cinematic Intro Scene
  6. Voice Cloning Pipeline
  7. Asset Gallery
  8. ADHD Focus Mode
  9. Colorblind Filters

🎯 SUCCESS CRITERIA

All political NPCs rendered and functional
Work animations smooth and particle effects working
Zombie workers operate 24/7 with purple eyes
Intro plays with Ken Burns, typewriter, and blur
Asset gallery shows all 342+ images with search
ADHD mode reduces cognitive load effectively
Colorblind filters functional for all 4 types
Assets organized in smart folder structure
All systems tested and bug-free
Commit message comprehensive and accurate


EXECUTION MODE: AUTONOMOUS (// turbo-all)
APPROVAL REQUIRED: NO
PROCEED: YES


END OF IMPLEMENTATION PLAN
Ready to execute on your command.