diff --git a/src/systems/CinematicVoiceSystem.js b/src/systems/CinematicVoiceSystem.js
new file mode 100644
index 000000000..7c4ddc840
--- /dev/null
+++ b/src/systems/CinematicVoiceSystem.js
@@ -0,0 +1,364 @@
+/**
+ * CINEMATIC VOICE SYSTEM
+ * Mrtva Dolina - Filmski pristop k dialogom
+ *
+ * Features:
+ * - Emocionalna globina (vdihi, premori, poudarki)
+ * - Reverb za flashbacke (Kaijevi spomini)
+ * - Ambient blending (veter, ruševine)
+ * - Typewriter sync (glas + tekst)
+ * - Dynamic background audio (glasba se poduši)
+ */
+
+export class CinematicVoiceSystem {
+ constructor(scene) {
+ this.scene = scene;
+ this.audioContext = null;
+ this.currentVoice = null;
+ this.isFlashback = false;
+
+ // Voice parameters
+ this.emotionalParams = {
+ kai_confused: { rate: 0.9, pitch: 1.0, breathPauses: true, emphasis: 'low' },
+ kai_determined: { rate: 1.0, pitch: 1.1, breathPauses: false, emphasis: 'strong' },
+ ana_gentle: { rate: 0.95, pitch: 1.15, breathPauses: true, emphasis: 'soft' },
+ ana_urgent: { rate: 1.1, pitch: 1.2, breathPauses: false, emphasis: 'strong' },
+ zombie_scout_hungry: { rate: 0.7, pitch: 0.6, breathPauses: false, emphasis: 'guttural' },
+ zombie_scout_happy: { rate: 0.8, pitch: 0.7, breathPauses: false, emphasis: 'friendly' }
+ };
+
+ // Ambient sounds
+ this.ambientSounds = new Map();
+ this.currentAmbient = null;
+
+ this.initializeAudioContext();
+ this.loadAmbientSounds();
+ }
+
+ initializeAudioContext() {
+ try {
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
+ console.log('✅ CinematicVoiceSystem: Audio Context initialized');
+ } catch (error) {
+ console.error('❌ Failed to initialize Audio Context:', error);
+ }
+ }
+
+ loadAmbientSounds() {
+ // Define ambient sound layers
+ const ambients = [
+ { id: 'wind_ruins', file: 'assets/audio/ambient/wind_ruins.mp3', volume: 0.3 },
+ { id: 'crackling_fire', file: 'assets/audio/ambient/fire.mp3', volume: 0.2 },
+ { id: 'rain_outside', file: 'assets/audio/ambient/rain.mp3', volume: 0.4 },
+ { id: 'rain_inside', file: 'assets/audio/ambient/rain_muffled.mp3', volume: 0.2 }
+ ];
+
+ ambients.forEach(ambient => {
+ // These will be loaded on-demand
+ this.ambientSounds.set(ambient.id, {
+ file: ambient.file,
+ volume: ambient.volume,
+ audio: null
+ });
+ });
+ }
+
+ /**
+ * Speak dialogue with cinematic voice
+ * @param {string} text - Text to speak
+ * @param {string} character - Character name (kai, ana, zombie_scout)
+ * @param {string} emotion - Emotion type (confused, determined, gentle, urgent, hungry, happy)
+ * @param {object} options - Additional options (typewriterElement, flashback, ambient)
+ */
+ async speak(text, character, emotion, options = {}) {
+ const voiceKey = `${character}_${emotion}`;
+ const params = this.emotionalParams[voiceKey] || this.emotionalParams.kai_confused;
+
+ // Add breath pauses if enabled
+ let processedText = text;
+ if (params.breathPauses) {
+ processedText = this.addBreathPauses(text);
+ }
+
+ // Add emphasis to key words
+ processedText = this.addEmphasis(processedText, params.emphasis);
+
+ // Set ambient if specified
+ if (options.ambient) {
+ this.setAmbient(options.ambient);
+ }
+
+ // Apply flashback effect if needed
+ this.isFlashback = options.flashback || false;
+
+ // Create speech synthesis
+ const utterance = new SpeechSynthesisUtterance(processedText);
+ utterance.rate = params.rate;
+ utterance.pitch = params.pitch;
+ utterance.volume = options.volume || 0.8;
+
+ // Select voice based on character
+ const voice = this.selectVoice(character);
+ if (voice) {
+ utterance.voice = voice;
+ }
+
+ // Sync with typewriter if provided
+ if (options.typewriterElement) {
+ this.syncWithTypewriter(utterance, options.typewriterElement, text);
+ }
+
+ // Duck background music
+ if (this.scene.sound && this.scene.sound.get('background_music')) {
+ this.duckMusic(0.3, 500); // Lower to 30% over 500ms
+ }
+
+ // Apply reverb for flashbacks
+ if (this.isFlashback && this.audioContext) {
+ await this.applyReverbEffect(utterance);
+ } else {
+ // Standard speech
+ window.speechSynthesis.speak(utterance);
+ }
+
+ // Return promise that resolves when speech ends
+ return new Promise((resolve) => {
+ utterance.onend = () => {
+ // Restore music volume
+ if (this.scene.sound && this.scene.sound.get('background_music')) {
+ this.duckMusic(1.0, 800); // Restore to 100% over 800ms
+ }
+ resolve();
+ };
+ });
+ }
+
+ /**
+ * Add natural breath pauses to text
+ */
+ addBreathPauses(text) {
+ // Add slight pauses after commas and periods
+ return text
+ .replace(/,/g, ',')
+ .replace(/\./g, '.');
+ }
+
+ /**
+ * Add emphasis to key words
+ */
+ addEmphasis(text, emphasisType) {
+ if (emphasisType === 'strong') {
+ // Emphasize question words and important terms
+ const keywords = ['kje', 'kaj', 'kdo', 'zakaj', 'kako', 'Ana', 'Kai', 'spomin'];
+ keywords.forEach(word => {
+ const regex = new RegExp(`\\b${word}\\b`, 'gi');
+ text = text.replace(regex, `${word}`);
+ });
+ } else if (emphasisType === 'soft') {
+ // Soft emphasis for gentle speech
+ const regex = /([A-ZČŠŽ][a-zčšž]+)/g;
+ text = text.replace(regex, '$1');
+ }
+ return text;
+ }
+
+ /**
+ * Select appropriate voice for character
+ */
+ selectVoice(character) {
+ const voices = window.speechSynthesis.getVoices();
+
+ // Prefer Slovenian voices, fallback to similar languages
+ const preferredLangs = ['sl-SI', 'hr-HR', 'sr-RS', 'en-US'];
+
+ if (character === 'kai') {
+ // Male voice
+ return voices.find(v =>
+ preferredLangs.some(lang => v.lang.startsWith(lang.split('-')[0])) &&
+ v.name.toLowerCase().includes('male')
+ ) || voices[0];
+ } else if (character === 'ana') {
+ // Female voice
+ return voices.find(v =>
+ preferredLangs.some(lang => v.lang.startsWith(lang.split('-')[0])) &&
+ v.name.toLowerCase().includes('female')
+ ) || voices[1];
+ } else if (character === 'zombie_scout') {
+ // Deep, gravelly voice
+ return voices.find(v =>
+ v.name.toLowerCase().includes('deep') ||
+ v.name.toLowerCase().includes('bass')
+ ) || voices[0];
+ }
+
+ return voices[0];
+ }
+
+ /**
+ * Sync voice with typewriter text animation
+ */
+ syncWithTypewriter(utterance, element, fullText) {
+ const charDuration = (utterance.rate > 0) ? (60 / utterance.rate) : 60; // ms per character
+
+ utterance.onboundary = (event) => {
+ // Update displayed text as speech progresses
+ const charIndex = event.charIndex;
+ if (element && charIndex < fullText.length) {
+ element.textContent = fullText.substring(0, charIndex + 1);
+ }
+ };
+ }
+
+ /**
+ * Apply reverb effect for flashback sequences
+ */
+ async applyReverbEffect(utterance) {
+ if (!this.audioContext) return;
+
+ try {
+ // Create convolver for reverb
+ const convolver = this.audioContext.createConvolver();
+ const reverbTime = 2.0; // 2 seconds reverb
+
+ // Generate impulse response
+ const sampleRate = this.audioContext.sampleRate;
+ const length = sampleRate * reverbTime;
+ const impulse = this.audioContext.createBuffer(2, length, sampleRate);
+
+ for (let channel = 0; channel < 2; channel++) {
+ const channelData = impulse.getChannelData(channel);
+ for (let i = 0; i < length; i++) {
+ channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 2);
+ }
+ }
+
+ convolver.buffer = impulse;
+
+ // Note: SpeechSynthesis doesn't directly support Web Audio routing
+ // This is a placeholder for when we implement proper audio streaming
+ console.log('🎙️ Reverb effect would be applied here (requires audio streaming)');
+
+ // Fallback: Just speak with modified parameters for now
+ window.speechSynthesis.speak(utterance);
+
+ } catch (error) {
+ console.error('❌ Reverb effect failed:', error);
+ window.speechSynthesis.speak(utterance);
+ }
+ }
+
+ /**
+ * Duck/restore background music volume
+ */
+ duckMusic(targetVolume, duration) {
+ const music = this.scene.sound.get('background_music');
+ if (!music) return;
+
+ this.scene.tweens.add({
+ targets: music,
+ volume: targetVolume,
+ duration: duration,
+ ease: 'Sine.easeInOut'
+ });
+ }
+
+ /**
+ * Set ambient background sound
+ */
+ setAmbient(ambientId) {
+ // Stop current ambient
+ if (this.currentAmbient) {
+ if (this.currentAmbient.audio) {
+ this.currentAmbient.audio.pause();
+ }
+ }
+
+ // Start new ambient
+ const ambient = this.ambientSounds.get(ambientId);
+ if (ambient) {
+ if (!ambient.audio) {
+ ambient.audio = new Audio(ambient.file);
+ ambient.audio.loop = true;
+ ambient.audio.volume = ambient.volume;
+ }
+
+ ambient.audio.play().catch(err => {
+ console.warn('Ambient sound play failed:', err);
+ });
+
+ this.currentAmbient = ambient;
+ }
+ }
+
+ /**
+ * Blend voice with ambient (main feature)
+ */
+ blendWithAmbient(voiceVolume = 0.8, ambientVolume = 0.3) {
+ if (this.currentAmbient && this.currentAmbient.audio) {
+ this.currentAmbient.audio.volume = ambientVolume;
+ }
+ // Voice volume is set in speak() method
+ }
+
+ /**
+ * Stop all audio
+ */
+ stopAll() {
+ window.speechSynthesis.cancel();
+
+ if (this.currentAmbient && this.currentAmbient.audio) {
+ this.currentAmbient.audio.pause();
+ }
+ }
+
+ /**
+ * ZOMBIE SCOUT SPECIFIC SOUNDS
+ */
+ zombieScoutHunger() {
+ const hungerLines = [
+ 'Braaaaains...',
+ 'Možgaaaaani...',
+ 'Hrrrngh... lačen...',
+ '*zombie groan*'
+ ];
+
+ const randomLine = hungerLines[Math.floor(Math.random() * hungerLines.length)];
+ this.speak(randomLine, 'zombie_scout', 'hungry', {
+ volume: 0.6,
+ ambient: 'wind_ruins'
+ });
+ }
+
+ zombieScoutDiscovery() {
+ const discoveryLines = [
+ '*tiho godrnjanje*',
+ 'Hrrm! Tukaj!',
+ '*zadovoljno zavijanje*'
+ ];
+
+ const randomLine = discoveryLines[Math.floor(Math.random() * discoveryLines.length)];
+ this.speak(randomLine, 'zombie_scout', 'happy', {
+ volume: 0.7
+ });
+ }
+
+ /**
+ * Play zombie scout footstep with gear sounds
+ */
+ zombieScoutFootstep() {
+ // Composite sound: footstep + gear rattle
+ const footstep = this.scene.sound.add('zombie_footstep', { volume: 0.4 });
+ const gearRattle = this.scene.sound.add('gear_rattle', { volume: 0.2 });
+
+ footstep.play();
+ setTimeout(() => gearRattle.play(), 50); // Slight delay for realism
+ }
+
+ destroy() {
+ this.stopAll();
+
+ if (this.audioContext) {
+ this.audioContext.close();
+ }
+ }
+}
diff --git a/src/systems/DynamicEnvironmentAudio.js b/src/systems/DynamicEnvironmentAudio.js
new file mode 100644
index 000000000..711229153
--- /dev/null
+++ b/src/systems/DynamicEnvironmentAudio.js
@@ -0,0 +1,391 @@
+/**
+ * DYNAMIC ENVIRONMENT AUDIO SYSTEM
+ * Mrtva Dolina - Fluidni okolju prilagojeni zvoki
+ *
+ * Features:
+ * - Material-based door sounds (metal ruins vs wood farm)
+ * - Adaptive weather audio (rain outside vs inside)
+ * - Puddle system with splash footsteps
+ * - Dynamic footstep sounds based on surface
+ * - Smooth audio transitions (no AI jumps)
+ */
+
+export class DynamicEnvironmentAudio {
+ constructor(scene) {
+ this.scene = scene;
+
+ // Door sounds by material
+ this.doorSounds = {
+ metal_ruins: { open: 'door_metal_creak', close: 'door_metal_slam', volume: 0.7 },
+ wood_farm: { open: 'door_wood_open', close: 'door_wood_close', volume: 0.5 },
+ tech_workshop: { open: 'door_tech_hiss', close: 'door_tech_lock', volume: 0.6 },
+ default: { open: 'door_generic_open', close: 'door_generic_close', volume: 0.5 }
+ };
+
+ // Footstep sounds by surface
+ this.footstepSounds = {
+ grass: ['footstep_grass_1', 'footstep_grass_2', 'footstep_grass_3'],
+ dirt: ['footstep_dirt_1', 'footstep_dirt_2', 'footstep_dirt_3'],
+ stone: ['footstep_stone_1', 'footstep_stone_2', 'footstep_stone_3'],
+ wood: ['footstep_wood_1', 'footstep_wood_2', 'footstep_wood_3'],
+ puddle: ['splash_puddle_1', 'splash_puddle_2', 'splash_puddle_3'],
+ metal: ['footstep_metal_1', 'footstep_metal_2']
+ };
+
+ // Weather system
+ this.weatherActive = false;
+ this.isIndoors = false;
+ this.rainSound = null;
+ this.rainSoundIndoors = null;
+
+ // Puddle tracking
+ this.puddles = [];
+ this.puddlesLayer = null;
+
+ this.init();
+ }
+
+ init() {
+ // Create weather audio objects (will be loaded separately)
+ this.rainSound = {
+ key: 'rain_outside',
+ volume: 0.6,
+ loop: true,
+ audio: null
+ };
+
+ this.rainSoundIndoors = {
+ key: 'rain_inside_muffled',
+ volume: 0.3,
+ loop: true,
+ audio: null
+ };
+
+ console.log('✅ DynamicEnvironmentAudio initialized');
+ }
+
+ /**
+ * DOOR SYSTEM - Material-based sounds
+ */
+ playDoorSound(doorType, action = 'open') {
+ const door = this.doorSounds[doorType] || this.doorSounds.default;
+ const soundKey = door[action];
+
+ if (this.scene.sound && this.scene.sound.get(soundKey)) {
+ const sound = this.scene.sound.add(soundKey, {
+ volume: door.volume
+ });
+ sound.play();
+
+ // Add subtle environmental reverb based on location
+ if (doorType === 'metal_ruins') {
+ // More echo in ruins
+ this.addReverb(sound, 0.4);
+ }
+ } else {
+ console.warn(`🔇 Door sound not found: ${soundKey}`);
+ }
+ }
+
+ /**
+ * WEATHER SYSTEM - Adaptive rain audio
+ */
+ startRain(intensity = 1.0) {
+ this.weatherActive = true;
+
+ // Play appropriate rain sound based on indoor/outdoor status
+ this.updateRainAudio();
+
+ // Start creating puddles on the ground
+ this.startPuddleGeneration();
+
+ console.log('🌧️ Rain started');
+ }
+
+ stopRain() {
+ this.weatherActive = false;
+
+ // Fade out rain sounds
+ this.fadeOutRain();
+
+ // Stop puddle generation (existing puddles remain)
+ this.stopPuddleGeneration();
+
+ console.log('☀️ Rain stopped');
+ }
+
+ updateRainAudio() {
+ if (!this.weatherActive) return;
+
+ if (this.isIndoors) {
+ // Fade to muffled indoor rain
+ this.crossFadeRain(this.rainSoundIndoors, this.rainSound);
+ } else {
+ // Fade to outdoor rain
+ this.crossFadeRain(this.rainSound, this.rainSoundIndoors);
+ }
+ }
+
+ crossFadeRain(soundIn, soundOut) {
+ // Fade out old sound
+ if (soundOut.audio) {
+ this.scene.tweens.add({
+ targets: soundOut.audio,
+ volume: 0,
+ duration: 800,
+ ease: 'Sine.easeInOut',
+ onComplete: () => {
+ soundOut.audio.pause();
+ }
+ });
+ }
+
+ // Fade in new sound
+ if (!soundIn.audio) {
+ soundIn.audio = this.scene.sound.add(soundIn.key, {
+ volume: 0,
+ loop: soundIn.loop
+ });
+ }
+
+ soundIn.audio.play();
+ this.scene.tweens.add({
+ targets: soundIn.audio,
+ volume: soundIn.volume,
+ duration: 800,
+ ease: 'Sine.easeInOut'
+ });
+ }
+
+ fadeOutRain() {
+ const sounds = [this.rainSound, this.rainSoundIndoors];
+
+ sounds.forEach(sound => {
+ if (sound.audio) {
+ this.scene.tweens.add({
+ targets: sound.audio,
+ volume: 0,
+ duration: 1500,
+ ease: 'Sine.easeOut',
+ onComplete: () => {
+ sound.audio.stop();
+ sound.audio = null;
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Set player indoor/outdoor status
+ */
+ setIndoors(isIndoors) {
+ if (this.isIndoors === isIndoors) return;
+
+ this.isIndoors = isIndoors;
+
+ // Update rain audio if weather is active
+ if (this.weatherActive) {
+ this.updateRainAudio();
+ }
+
+ console.log(`🏠 Player is now ${isIndoors ? 'indoors' : 'outdoors'}`);
+ }
+
+ /**
+ * PUDDLE SYSTEM
+ */
+ startPuddleGeneration() {
+ // Create puddles over time
+ this.puddleInterval = setInterval(() => {
+ this.createPuddle();
+ }, 3000); // New puddle every 3 seconds
+ }
+
+ stopPuddleGeneration() {
+ if (this.puddleInterval) {
+ clearInterval(this.puddleInterval);
+ this.puddleInterval = null;
+ }
+ }
+
+ createPuddle() {
+ // Create random puddle on ground
+ const x = Phaser.Math.Between(0, this.scene.cameras.main.width);
+ const y = Phaser.Math.Between(0, this.scene.cameras.main.height);
+
+ const puddle = this.scene.add.graphics();
+ puddle.fillStyle(0x4a6fa5, 0.4); // Blue-ish, semi-transparent
+
+ // Random puddle shape
+ const radius = Phaser.Math.Between(20, 50);
+ puddle.fillEllipse(x, y, radius, radius * 0.7);
+
+ // Add ripple animation
+ this.addPuddleRipples(x, y, radius);
+
+ // Store puddle for collision detection
+ this.puddles.push({
+ x: x,
+ y: y,
+ radius: radius,
+ graphics: puddle
+ });
+
+ // Puddles slowly evaporate after rain stops
+ if (!this.weatherActive) {
+ this.scene.tweens.add({
+ targets: puddle,
+ alpha: 0,
+ duration: 10000,
+ delay: 5000,
+ onComplete: () => {
+ puddle.destroy();
+ this.puddles = this.puddles.filter(p => p.graphics !== puddle);
+ }
+ });
+ }
+ }
+
+ addPuddleRipples(x, y, radius) {
+ // Periodic ripple circles from raindrops
+ const rippleInterval = setInterval(() => {
+ if (!this.weatherActive) {
+ clearInterval(rippleInterval);
+ return;
+ }
+
+ const ripple = this.scene.add.graphics();
+ ripple.lineStyle(2, 0xffffff, 0.6);
+ ripple.strokeCircle(x, y, 5);
+
+ this.scene.tweens.add({
+ targets: ripple,
+ alpha: 0,
+ scaleX: radius / 5,
+ scaleY: radius / 5,
+ duration: 1000,
+ ease: 'Sine.easeOut',
+ onComplete: () => ripple.destroy()
+ });
+ }, Phaser.Math.Between(500, 2000));
+ }
+
+ checkPuddleCollision(x, y) {
+ // Check if player is stepping in a puddle
+ for (const puddle of this.puddles) {
+ const distance = Phaser.Math.Distance.Between(x, y, puddle.x, puddle.y);
+ if (distance < puddle.radius) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * FOOTSTEP SYSTEM
+ */
+ playFootstep(x, y, surface = 'grass') {
+ // Check if stepping in puddle
+ if (this.checkPuddleCollision(x, y)) {
+ surface = 'puddle';
+ }
+
+ const soundArray = this.footstepSounds[surface] || this.footstepSounds.grass;
+ const randomSound = Phaser.Utils.Array.GetRandom(soundArray);
+
+ if (this.scene.sound && this.scene.sound.get(randomSound)) {
+ const volume = surface === 'puddle' ? 0.5 : 0.3;
+ this.scene.sound.play(randomSound, { volume: volume });
+ } else {
+ console.warn(`🔇 Footstep sound not found: ${randomSound}`);
+ }
+ }
+
+ /**
+ * Play character-specific footstep (for Zombie Scout with gear)
+ */
+ playCharacterFootstep(character, x, y, surface = 'grass') {
+ // Play base footstep
+ this.playFootstep(x, y, surface);
+
+ // Add character-specific sounds
+ if (character === 'zombie_scout') {
+ // Add gear rattle and backpack sounds
+ setTimeout(() => {
+ if (this.scene.sound && this.scene.sound.get('gear_rattle')) {
+ this.scene.sound.play('gear_rattle', { volume: 0.2 });
+ }
+ }, 50);
+ }
+ }
+
+ /**
+ * Simple reverb effect (placeholder - real reverb needs Web Audio API)
+ */
+ addReverb(sound, amount = 0.3) {
+ // This is a simplified approach
+ // Real implementation would use ConvolverNode in Web Audio API
+ console.log(`🎙️ Adding ${amount * 100}% reverb to sound`);
+ }
+
+ /**
+ * Update loop - check player position for puddles
+ */
+ update() {
+ // Called every frame by main scene
+ if (this.scene.player) {
+ const player = this.scene.player;
+
+ // Detect when player steps in puddle
+ if (player.isMoving && this.checkPuddleCollision(player.x, player.y)) {
+ // Trigger splash VFX
+ this.scene.events.emit('player:stepped_in_puddle', player.x, player.y);
+ }
+ }
+ }
+
+ /**
+ * Cleanup
+ */
+ destroy() {
+ this.stopRain();
+ this.stopPuddleGeneration();
+
+ // Clean up puddles
+ this.puddles.forEach(puddle => {
+ if (puddle.graphics) {
+ puddle.graphics.destroy();
+ }
+ });
+ this.puddles = [];
+ }
+}
+
+/**
+ * INTEGRATION EXAMPLE:
+ *
+ * // In MainScene.js create()
+ * this.envAudio = new DynamicEnvironmentAudio(this);
+ *
+ * // When player opens door
+ * this.envAudio.playDoorSound('metal_ruins', 'open');
+ *
+ * // When weather changes
+ * this.envAudio.startRain(1.0);
+ *
+ * // When player enters building
+ * this.envAudio.setIndoors(true);
+ *
+ * // In update loop
+ * if (this.player.isMoving && this.player.stepCount % 10 === 0) {
+ * const surface = this.getCurrentSurface(this.player.x, this.player.y);
+ * this.envAudio.playFootstep(this.player.x, this.player.y, surface);
+ * }
+ *
+ * // For Zombie Scout companion
+ * if (this.zombieScout.isMoving) {
+ * this.envAudio.playCharacterFootstep('zombie_scout',
+ * this.zombieScout.x, this.zombieScout.y, surface);
+ * }
+ */
diff --git a/src/systems/ElectionSystem.js b/src/systems/ElectionSystem.js
new file mode 100644
index 000000000..17d8b3f92
--- /dev/null
+++ b/src/systems/ElectionSystem.js
@@ -0,0 +1,435 @@
+/**
+ * ELECTION & SOCIAL ORDER SYSTEM
+ * Mrtva Dolina - City Evolution Through Democracy
+ *
+ * Features:
+ * - Chaos phase (no leader, messy city)
+ * - Election trigger (after 5+ NPCs arrive)
+ * - Vote gathering & influence system
+ * - Mayor inauguration with visual/audio changes
+ * - Unlocks city improvements (walls, patrols)
+ */
+
+export class ElectionSystem {
+ constructor(scene) {
+ this.scene = scene;
+
+ // Election state
+ this.electionPhase = 'none'; // none, chaos, campaign, complete
+ this.mayorElected = false;
+ this.currentMayor = null;
+
+ // Candidates
+ this.candidates = [
+ {
+ id: 'mayor_default',
+ name: 'Župan',
+ votes: 0,
+ platform: 'Obzidje in varnost',
+ supportingNPCs: []
+ },
+ {
+ id: 'ivan_kovac',
+ name: 'Ivan Kovač',
+ votes: 0,
+ platform: 'Proizvodni razvoj',
+ supportingNPCs: []
+ },
+ {
+ id: 'tehnik',
+ name: 'Tehnik',
+ votes: 0,
+ platform: 'Tehnološki napredek',
+ supportingNPCs: []
+ }
+ ];
+
+ // City visual state
+ this.cityState = {
+ cleanliness: 0, // 0-100
+ security: 0, // 0-100
+ morale: 0 // 0-100
+ };
+
+ // Trash/debris objects for visual chaos
+ this.debrisObjects = [];
+
+ // Population tracking
+ this.npcCount = 0;
+ this.electionThreshold = 5; // Trigger election at 5 NPCs
+
+ this.init();
+ }
+
+ init() {
+ // Listen for NPC arrival events
+ this.scene.events.on('npc:arrived', this.onNPCArrival, this);
+ this.scene.events.on('quest:completed', this.onQuestCompleted, this);
+
+ console.log('✅ ElectionSystem initialized');
+ }
+
+ /**
+ * NPC ARRIVAL - Track population
+ */
+ onNPCArrival(npcData) {
+ this.npcCount++;
+
+ console.log(`👤 NPC arrived: ${npcData.name}. Total: ${this.npcCount}`);
+
+ // Check if chaos phase should start
+ if (this.npcCount >= 3 && this.electionPhase === 'none') {
+ this.startChaosPhase();
+ }
+
+ // Check if election should trigger
+ if (this.npcCount >= this.electionThreshold && this.electionPhase === 'chaos') {
+ this.triggerElection();
+ }
+ }
+
+ /**
+ * CHAOS PHASE - City is disorganized
+ */
+ startChaosPhase() {
+ this.electionPhase = 'chaos';
+
+ console.log('💥 CHAOS PHASE STARTED - City needs leadership!');
+
+ // Spawn trash/debris around town
+ this.spawnDebris(15); // 15 trash piles
+
+ // Lower city stats
+ this.cityState.cleanliness = 20;
+ this.cityState.security = 10;
+ this.cityState.morale = 30;
+
+ // NPCs start discussing need for leader
+ this.startChaosDialogues();
+
+ // Show notification
+ this.scene.events.emit('show-notification', {
+ title: 'Stanje Kaosa',
+ message: 'Ljudje potrebujejo vodjo! Uredite red v mestu.',
+ icon: '⚠️',
+ duration: 5000
+ });
+
+ // Update status board
+ this.updateStatusBoard();
+ }
+
+ spawnDebris(count) {
+ const debrisTypes = ['trash_pile', 'broken_crate', 'rubble', 'scattered_papers'];
+
+ for (let i = 0; i < count; i++) {
+ const x = Phaser.Math.Between(100, this.scene.cameras.main.width - 100);
+ const y = Phaser.Math.Between(100, this.scene.cameras.main.height - 100);
+
+ const type = Phaser.Utils.Array.GetRandom(debrisTypes);
+ const debris = this.scene.add.sprite(x, y, type);
+ debris.setDepth(1);
+
+ this.debrisObjects.push(debris);
+ }
+ }
+
+ startChaosDialogues() {
+ // NPCs randomly discuss the chaos
+ const dialogues = [
+ { npc: 'sivilja', text: 'Ta kaos je neznosn! Rabimo vodjo!' },
+ { npc: 'pek', text: 'Kdo bo prinesel red v to mesto?' },
+ { npc: 'ivan_kovac', text: 'Brez organizacije ne moremo preživeti.' }
+ ];
+
+ // Emit dialogue events periodically
+ this.chaosDialogueTimer = setInterval(() => {
+ const dialogue = Phaser.Utils.Array.GetRandom(dialogues);
+ this.scene.events.emit('npc:dialogue', dialogue);
+ }, 30000); // Every 30 seconds
+ }
+
+ /**
+ * TRIGGER ELECTION
+ */
+ triggerElection() {
+ this.electionPhase = 'campaign';
+
+ console.log('🗳️ ELECTION TRIGGERED - Campaign begins!');
+
+ // Stop chaos dialogues
+ if (this.chaosDialogueTimer) {
+ clearInterval(this.chaosDialogueTimer);
+ }
+
+ // Create election quest
+ this.createElectionQuest();
+
+ // Show notification
+ this.scene.events.emit('show-notification', {
+ title: 'Volitve za Župana',
+ message: 'Mesto potrebuje vodjo! Pomagaj pri zbiranju glasov.',
+ icon: '🗳️',
+ duration: 5000
+ });
+
+ // NPCs start campaign dialogues
+ this.startCampaignDialogues();
+ }
+
+ createElectionQuest() {
+ if (!this.scene.questSystem) return;
+
+ const electionQuest = {
+ id: 'election_campaign',
+ title: 'Zbiranje Glasov za Župana',
+ type: 'social',
+ priority: 5,
+ description: 'Pomagaj izbrati župana za Mrtvo Dolino.',
+ objectives: [
+ {
+ id: 'talk_to_npcs',
+ text: 'Pogovor s 5 NPC-ji o volitvah',
+ type: 'interaction',
+ required: 5,
+ current: 0
+ },
+ {
+ id: 'support_candidate',
+ text: 'Podpri kandidata z opravljanjem questov',
+ type: 'flag',
+ complete: false
+ }
+ ],
+ rewards: {
+ xp: 1000,
+ unlocks: ['mayor_office', 'city_improvements']
+ },
+ dialogue: {
+ start: ['Ljudi potrebujejo vodjo. Kdo bo župan?'],
+ complete: ['Volitve so končane! Novi župan je izvoljen!']
+ },
+ npc: 'mayor'
+ };
+
+ this.scene.questSystem.registerQuest(electionQuest);
+ this.scene.questSystem.startQuest('election_campaign');
+ }
+
+ startCampaignDialogues() {
+ // Each candidate promotes their platform
+ const campaignLines = {
+ mayor_default: 'Glasujte zame! Zgradil bom obzidje in patruljo!',
+ ivan_kovac: 'Potrebujemo proizvodnjo! Podprite me!',
+ tehnik: 'Tehnologija je prihodnost! Volite tehnološki napredek!'
+ };
+
+ // NPCs express support for different candidates
+ this.campaignDialogueTimer = setInterval(() => {
+ const candidate = Phaser.Utils.Array.GetRandom(this.candidates);
+ this.scene.events.emit('npc:dialogue', {
+ npc: candidate.id,
+ text: campaignLines[candidate.id]
+ });
+ }, 45000); // Every 45 seconds
+ }
+
+ /**
+ * VOTING - Player influences votes through quests
+ */
+ onQuestCompleted(questId) {
+ if (this.electionPhase !== 'campaign') return;
+
+ // Check which candidate benefits from this quest
+ const questCandidateMap = {
+ 'obzidje': 'mayor_default',
+ 'pekov_recept': 'mayor_default',
+ 'tehnikova_naprava': 'tehnik',
+ 'siviljina_prosnja': 'ivan_kovac'
+ };
+
+ const candidateId = questCandidateMap[questId];
+ if (candidateId) {
+ this.addVote(candidateId, 1);
+
+ // Show feedback
+ this.scene.events.emit('show-floating-text', {
+ x: this.scene.player.x,
+ y: this.scene.player.y - 50,
+ text: `+1 glas za ${candidateId}`,
+ color: '#FFD700'
+ });
+ }
+ }
+
+ addVote(candidateId, votes = 1) {
+ const candidate = this.candidates.find(c => c.id === candidateId);
+ if (candidate) {
+ candidate.votes += votes;
+ console.log(`🗳️ ${candidate.name} dobil ${votes} glas(ov). Skupaj: ${candidate.votes}`);
+ }
+ }
+
+ /**
+ * COMPLETE ELECTION - Inaugurate mayor
+ */
+ completeElection() {
+ if (this.mayorElected) return;
+
+ // Count votes and determine winner
+ const winner = this.candidates.reduce((prev, current) =>
+ (prev.votes > current.votes) ? prev : current
+ );
+
+ this.currentMayor = winner;
+ this.mayorElected = true;
+ this.electionPhase = 'complete';
+
+ console.log(`🏛️ ${winner.name} je izvoljen za župana!`);
+
+ // Inauguration sequence
+ this.inauguration(winner);
+ }
+
+ inauguration(mayor) {
+ // Visual changes
+ this.cleanUpCity();
+
+ // Mayor moves to town hall
+ if (this.scene.npcs && this.scene.npcs[mayor.id]) {
+ const mayorNPC = this.scene.npcs[mayor.id];
+ this.scene.tweens.add({
+ targets: mayorNPC,
+ x: this.scene.townHallX || 400,
+ y: this.scene.townHallY || 300,
+ duration: 3000,
+ ease: 'Sine.easeInOut'
+ });
+ }
+
+ // Change music to ordered/military theme
+ if (this.scene.sound && this.scene.sound.get('background_music')) {
+ this.scene.sound.get('background_music').stop();
+ }
+ this.scene.sound.play('mayor_anthem', { loop: true, volume: 0.5 });
+
+ // Unlock new features
+ this.unlockMayorFeatures();
+
+ // Show inauguration cutscene
+ this.scene.events.emit('show-notification', {
+ title: `Župan ${mayor.name}`,
+ message: `${mayor.name} je uradno inauguriran! Mesto je zdaj pod vodstvom.`,
+ icon: '🏛️',
+ duration: 7000
+ });
+
+ // Update city stats
+ this.cityState.cleanliness = 80;
+ this.cityState.security = 70;
+ this.cityState.morale = 90;
+
+ this.updateStatusBoard();
+
+ // Complete election quest
+ if (this.scene.questSystem) {
+ this.scene.questSystem.completeQuest('election_campaign');
+ }
+ }
+
+ cleanUpCity() {
+ // Remove all debris with animation
+ this.debrisObjects.forEach((debris, index) => {
+ this.scene.tweens.add({
+ targets: debris,
+ alpha: 0,
+ scaleX: 0,
+ scaleY: 0,
+ duration: 1000,
+ delay: index * 100,
+ onComplete: () => debris.destroy()
+ });
+ });
+
+ this.debrisObjects = [];
+
+ // Add clean visual elements (flags, guards, etc.)
+ this.addCityImprovements();
+ }
+
+ addCityImprovements() {
+ // Add flags
+ const flagPositions = [
+ { x: 200, y: 150 },
+ { x: 400, y: 150 },
+ { x: 600, y: 150 }
+ ];
+
+ flagPositions.forEach(pos => {
+ const flag = this.scene.add.sprite(pos.x, pos.y, 'city_flag');
+ flag.setDepth(10);
+
+ // Waving animation
+ this.scene.tweens.add({
+ targets: flag,
+ scaleX: 1.1,
+ duration: 1000,
+ yoyo: true,
+ repeat: -1,
+ ease: 'Sine.easeInOut'
+ });
+ });
+
+ // Add guards (if available)
+ // ... patrol implementation
+ }
+
+ unlockMayorFeatures() {
+ // Unlock wall building
+ if (this.scene.buildingSystem) {
+ this.scene.buildingSystem.unlock('wall_wooden');
+ this.scene.buildingSystem.unlock('wall_stone');
+ }
+
+ // Unlock patrol system
+ if (this.scene.defenseSystem) {
+ this.scene.defenseSystem.unlockPatrols();
+ }
+
+ // Unlock mayor's office
+ this.scene.events.emit('building:unlocked', 'mayor_office');
+
+ console.log('🔓 Mayor features unlocked: walls, patrols, office');
+ }
+
+ updateStatusBoard() {
+ // Update city status display
+ this.scene.events.emit('city:stats_updated', {
+ cleanliness: this.cityState.cleanliness,
+ security: this.cityState.security,
+ morale: this.cityState.morale,
+ population: this.npcCount,
+ mayor: this.currentMayor ? this.currentMayor.name : 'None'
+ });
+ }
+
+ /**
+ * Get election results for UI display
+ */
+ getElectionResults() {
+ return {
+ phase: this.electionPhase,
+ candidates: this.candidates,
+ winner: this.currentMayor,
+ cityState: this.cityState
+ };
+ }
+
+ destroy() {
+ if (this.chaosDialogueTimer) clearInterval(this.chaosDialogueTimer);
+ if (this.campaignDialogueTimer) clearInterval(this.campaignDialogueTimer);
+
+ this.debrisObjects.forEach(obj => obj.destroy());
+ this.debrisObjects = [];
+ }
+}