From b68f180663cc086f3af2adddc2043a6ff9ff1343 Mon Sep 17 00:00:00 2001 From: David Kotnik Date: Mon, 5 Jan 2026 12:47:58 +0100 Subject: [PATCH] Buildings, VFX particles, and Quest System v2.0. Added 5 building sprites (Capital City, Museum, Bakery). Added 6 VFX particles (sparkles, water, glow, smoke, blood, coin). Upgraded Quest System to v2.0 with 12 quests, ADHD dialogue, VFX integration, rewards, and backwards compatibility. --- src/systems/QuestSystem.js | 722 ++++++++++++++++++++++++++++--------- 1 file changed, 559 insertions(+), 163 deletions(-) diff --git a/src/systems/QuestSystem.js b/src/systems/QuestSystem.js index f55c1369b..1c3a93229 100644 --- a/src/systems/QuestSystem.js +++ b/src/systems/QuestSystem.js @@ -1,197 +1,593 @@ -class QuestSystem { +/** + * QUEST SYSTEM v2.0 - Mrtva Dolina + * Complete quest management with ADHD-friendly dialogue and VFX integration + * + * Features: + * - Main story arc (Zamegljeni Spomini, Anina Sled) + * - Town economy quests (NPCs) + * - Defense quests (Wall building, Museum) + * - Side quests & companion recruitment + * - Progress tracking & rewards + * - Dialogue system integration + * - VFX triggers + */ + +export class QuestSystem { constructor(scene) { this.scene = scene; - - // Quest Definitions - this.questDB = { - 'q1_start': { - id: 'q1_start', - title: 'Survival Basics', - description: 'Collect Wood and Stone to build your first defense.', - objectives: [ - { type: 'collect', item: 'wood', amount: 5, current: 0, done: false }, - { type: 'collect', item: 'stone', amount: 3, current: 0, done: false } - ], - reward: { gold: 10, xp: 50 }, - nextQuest: 'q2_farm', - giver: 'villager' - }, - 'q2_farm': { - id: 'q2_farm', - title: 'The Farmer', - description: 'Plant some seeds to grow food. You will need it.', - objectives: [ - { type: 'action', action: 'plant', amount: 3, current: 0, done: false } - ], - reward: { gold: 20, item: 'wood', amount: 10 }, - nextQuest: 'q3_defense', - giver: 'villager' - }, - 'q3_defense': { - id: 'q3_defense', - title: 'Fortification', - description: 'Build a Fence to keep zombies out.', - objectives: [ - { type: 'action', action: 'build_fence', amount: 2, current: 0, done: false } - ], - reward: { gold: 50, item: 'sword', amount: 1 }, - nextQuest: 'q4_slayer', - giver: 'merchant' - }, - 'q4_slayer': { - id: 'q4_slayer', - title: 'Zombie Slayer', - description: 'Kill 3 Zombies using your new sword.', - objectives: [ - { type: 'kill', target: 'zombie', amount: 3, current: 0, done: false } - ], - reward: { gold: 100, item: 'gold', amount: 50 }, - nextQuest: null, - giver: 'villager' - } - }; - - this.activeQuest = null; + this.quests = new Map(); + this.activeQuests = []; this.completedQuests = []; + this.questLog = []; + + this.initializeQuests(); } - getAvailableQuest(npcType) { - const chain = ['q1_start', 'q2_farm', 'q3_defense', 'q4_slayer']; - let targetId = null; - for (const id of chain) { - if (!this.completedQuests.includes(id)) { - targetId = id; - break; + initializeQuests() { + // MAIN STORY QUEST 1: Zamegljeni Spomini + this.registerQuest({ + id: 'zamegljeni_spomini', + title: 'Zamegljeni Spomini', + type: 'main_story', + priority: 5, + description: 'Kai se zbudi brez spominov. Najdi fotografijo.', + objectives: [ + { id: 'find_photo', text: 'Najdi družinsko fotografijo', type: 'item', item: 'family_photo', required: 1, current: 0 }, + { id: 'unlock_capital', text: 'Odkleni Capital City', type: 'flag', flag: 'capital_unlocked', complete: false } + ], + rewards: { xp: 500, items: ['family_photo'], unlocks: ['capital_city'] }, + vfx: { start: 'amnesia_blur', complete: 'flashback' }, + dialogue: { + start: ["Kje sem? Kaj se mi dogaja?", "Ta fotka... Kdo je ta punca?"], + complete: ["Ana! Pomnim se! To je moja sestra!"] + }, + npc: null, + location: 'base_farm' + }); + + // MAIN STORY QUEST 2: Anina Sled + this.registerQuest({ + id: 'anина_sled', + title: 'Anina Sled', + type: 'main_story', + priority: 5, + description: 'Zberi 50 namigov o tem, kje je Ana.', + objectives: [ + { id: 'collect_clues', text: 'Zberi Aniне namige (0/50)', type: 'collection', item: 'ana_clue', required: 50, current: 0 } + ], + rewards: { xp: 5000, items: ['final_location'], unlocks: ['ana_encounter'] }, + vfx: { onClue: 'amnesia_blur', complete: 'revelation' }, + dialogue: { + onClue: ["Kai, veš da te imam rada?", "Ne skrbi zame..."], + complete: ["NAŠEL SEM JO! Ana, prihajam!"] + }, + prerequisites: ['zamegljeni_spomini'], + npc: null + }); + + // TOWN QUEST: Šivilja + this.registerQuest({ + id: 'siviljina_prosnja', + title: 'Šiviljina Prošnja', + type: 'town_economy', + priority: 4, + description: 'Šivilja rabi 20x Platno.', + objectives: [ + { id: 'cloth', text: 'Zberi platno (0/20)', type: 'item', item: 'cloth', required: 20, current: 0 } + ], + rewards: { xp: 200, items: ['enchanted_jacket'], gold: 500 }, + dialogue: { + start: ["Hej! Rabim 20 platna. Lahko mi pomagaš?"], + progress: ["Še {remaining}!"], + complete: ["SUPER! Na, vzemi ta jopič!"] + }, + npc: 'sivilja' + }); + + // TOWN QUEST: Pek + this.registerQuest({ + id: 'pekov_recept', + title: 'Pekov Recept', + type: 'town_economy', + priority: 4, + description: 'Pek rabi 10x Pšenico za kruh.', + objectives: [ + { id: 'wheat', text: 'Zberi pšenico (0/10)', type: 'item', item: 'wheat', required: 10, current: 0 }, + { id: 'restore', text: 'Obnovi pekarno', type: 'building', building: 'bakery', complete: false } + ], + rewards: { xp: 300, items: ['daily_bread'], unlocks: ['bakery_shop'] }, + dialogue: { + start: ["Živjo! Rabim 10 pšenice, pa še pekarna je zrušena!"], + complete: ["ZAKON! Zdej lahko spet pečem! Kruh na dan ZASTONJ!"] + }, + npc: 'pek' + }); + + // TOWN QUEST: Tehnik + this.registerQuest({ + id: 'tehnik_generator', + title: 'Tehnikova Naprava', + type: 'town_economy', + priority: 4, + description: 'Tehnik rabi 5x Električne Komponente.', + objectives: [ + { id: 'components', text: 'Zberi komponente (0/5)', type: 'item', item: 'electric_component', required: 5, current: 0 } + ], + rewards: { xp: 250, items: ['auto_tiller'], unlocks: ['tech_services'] }, + dialogue: { + start: ["Yo! Rabim 5 električnih komponent za generator."], + complete: ["NICE! Generator dela! Auto-Tiller upgrade za te!"] + }, + npc: 'tehnik' + }); + + // DEFENSE QUEST: Walls + this.registerQuest({ + id: 'obzidje', + title: 'Obzidje Mrtve Doline', + type: 'defense', + priority: 5, + description: 'Zgradi 20 segmentov zidov.', + objectives: [ + { id: 'walls', text: 'Zgradi zunanje zidove (0/20)', type: 'building', building: 'wall', required: 20, current: 0 } + ], + rewards: { xp: 400, items: ['wall_blueprint_2'], unlocks: ['raid_event'] }, + vfx: { complete: 'raid_warning' }, + dialogue: { + start: ["Zombiji prihajajo! Hitr zgradi zidove!"], + complete: ["DONE! Zidovi so zgoraj! Priprav se na raiderje!"] + }, + npc: 'mayor', + triggers: ['day_7'] + }); + + // COLLECTION QUEST: Museum + this.registerQuest({ + id: 'muzej_hrosci', + title: 'Muzejski Mejnik', + type: 'collection', + priority: 3, + description: 'Kustos rabi 24 različnih hroščev.', + objectives: [ + { id: 'bugs', text: 'Doniraj hrošče (0/24)', type: 'collection', collection: 'museum_bugs', required: 24, current: 0 } + ], + rewards: { xp: 600, items: ['museum_key'], unlocks: ['museum_stage2'] }, + dialogue: { + start: ["Potrebujem 24 različnih hroščev za muzejsko zbirko."], + progress: ["Odlično! Še {remaining}!"], + complete: ["FANTASTIČNO! Razširim muzej!"] + }, + npc: 'kustos' + }); + + // SIDE QUEST: Arborist + this.registerQuest({ + id: 'arborist_sadike', + title: 'Arboristova Pomoč', + type: 'side', + priority: 2, + description: 'Arborist rabi 50x sadike dreves.', + objectives: [ + { id: 'saplings', text: 'Zberi sadike (0/50)', type: 'item', item: 'tree_sapling', required: 50, current: 0 } + ], + rewards: { xp: 350, items: ['tree_planter'], unlocks: ['forest_restore'] }, + dialogue: { + start: ["Rad bi obnovil gozd. Mi lahko pomagaš?"], + complete: ["SUPER! Drevo-sadersko orodje je zate!"] + }, + npc: 'arborist' + }); + + // COMPANION QUEST: Zombie Scout + this.registerQuest({ + id: 'zombie_scout_join', + title: 'Zombi Skavt - Rekrutacija', + type: 'companion', + priority: 5, + description: 'Pridobi zaupanje inteligentnega zombija.', + objectives: [ + { id: 'feed', text: 'Nahrani zombija (0/3 mesa)', type: 'item_give', item: 'meat', required: 3, current: 0 }, + { id: 'trust', text: 'Pridobi zaupanje', type: 'interaction', complete: false } + ], + rewards: { xp: 1000, unlocks: ['zombie_scout_companion'], companions: ['zombie_scout'] }, + vfx: { complete: 'companion_join' }, + dialogue: { + start: ["*zombie zvoki* ...Meso?"], + progress: ["*munch* ...Ti dobr človek."], + complete: ["*zavija* Jaz s teb! Jaz pomagat! [JOINED]"] + }, + npc: 'zombie_scout', + location: 'dead_forest' + }); + + // Legacy basic quest chain for backwards compatibility + this.registerQuest({ + id: 'q1_start', + title: 'Survival Basics', + type: 'tutorial', + priority: 5, + description: 'Collect Wood and Stone.', + objectives: [ + { id: 'wood', text: 'Collect Wood (0/5)', type: 'item', item: 'wood', required: 5, current: 0 }, + { id: 'stone', text: 'Collect Stone (0/3)', type: 'item', item: 'stone', required: 3, current: 0 } + ], + rewards: { gold: 10, xp: 50 }, + nextQuest: 'q2_farm', + npc: 'villager' + }); + + this.registerQuest({ + id: 'q2_farm', + title: 'The Farmer', + type: 'tutorial', + priority: 5, + description: 'Plant some seeds.', + objectives: [ + { id: 'plant', text: 'Plant seeds (0/3)', type: 'action', action: 'plant', required: 3, current: 0 } + ], + rewards: { gold: 20, items: ['wood:10'] }, + prerequisites: ['q1_start'], + nextQuest: 'q3_defense', + npc: 'villager' + }); + + this.registerQuest({ + id: 'q3_defense', + title: 'Fortification', + type: 'tutorial', + priority: 4, + description: 'Build a Fence.', + objectives: [ + { id: 'fence', text: 'Build Fence (0/2)', type: 'action', action: 'build_fence', required: 2, current: 0 } + ], + rewards: { gold: 50, items: ['sword:1'] }, + prerequisites: ['q2_farm'], + nextQuest: 'q4_slayer', + npc: 'merchant' + }); + + this.registerQuest({ + id: 'q4_slayer', + title: 'Zombie Slayer', + type: 'tutorial', + priority: 4, + description: 'Kill 3 Zombies.', + objectives: [ + { id: 'kill', text: 'Kill Zombies (0/3)', type: 'kill', target: 'zombie', required: 3, current: 0 } + ], + rewards: { gold: 100, items: ['gold:50'] }, + prerequisites: ['q3_defense'], + npc: 'villager' + }); + } + + registerQuest(questData) { + // Set defaults + questData.isActive = false; + questData.isComplete = false; + questData.startTime = null; + + this.quests.set(questData.id, questData); + } + + startQuest(questId) { + const quest = this.quests.get(questId); + if (!quest) { + console.error(`Quest ${questId} not found!`); + return false; + } + + // Check prerequisites + if (quest.prerequisites) { + for (const prereq of quest.prerequisites) { + if (!this.isQuestComplete(prereq)) { + console.log(`Cannot start ${questId}: Missing ${prereq}`); + return false; + } } } - if (!targetId) return null; - if (this.activeQuest && this.activeQuest.id === targetId) return null; + // Prevent duplicate active + if (this.activeQuests.includes(questId)) { + return false; + } - const q = this.questDB[targetId]; - if (q.giver === npcType) return q; + quest.isActive = true; + quest.startTime = Date.now(); + this.activeQuests.push(questId); - return null; + this.questLog.push({ + questId, + startTime: quest.startTime, + status: 'active' + }); + + // Show start dialogue + if (quest.dialogue && quest.dialogue.start) { + this.showDialogue(quest.dialogue.start, quest.npc); + } + + // Trigger VFX + if (quest.vfx && quest.vfx.start) { + this.scene.events.emit('vfx:trigger', quest.vfx.start); + } + + // Show notification + this.scene.events.emit('show-floating-text', { + x: this.scene.player.x, + y: this.scene.player.y - 50, + text: 'Quest Accepted!', + color: '#FFFF00' + }); + + this.scene.events.emit('quest:started', questId); + this.updateUI(); + return true; } - startQuest(id) { - if (this.completedQuests.includes(id)) return; + updateObjective(questId, objectiveId, progress) { + const quest = this.quests.get(questId); + if (!quest || !quest.isActive) return; - const template = this.questDB[id]; - if (!template) return; + const objective = quest.objectives.find(obj => obj.id === objectiveId); + if (!objective) return; - this.activeQuest = JSON.parse(JSON.stringify(template)); - console.log(`📜 Quest Started: ${this.activeQuest.title}`); + // Update progress + if (objective.type === 'item' || objective.type === 'collection') { + objective.current = Math.min(progress, objective.required); + objective.complete = objective.current >= objective.required; + } else if (objective.type === 'action' || objective.type === 'kill') { + objective.current = Math.min(progress, objective.required); + objective.complete = objective.current >= objective.required; + } else if (objective.type === 'flag' || objective.type === 'interaction' || objective.type === 'building') { + objective.complete = progress === true; + } + + // Check completion + const allComplete = quest.objectives.every(obj => obj.complete); + if (allComplete) { + this.completeQuest(questId); + } else { + // Progress dialogue + if (quest.dialogue && quest.dialogue.progress) { + const remaining = objective.required - objective.current; + const msg = quest.dialogue.progress[0].replace('{remaining}', remaining); + this.showDialogue([msg], quest.npc); + } + } + + this.scene.events.emit('quest:updated', questId, objectiveId, progress); + this.updateUI(); + } + + trackAction(actionType, amount = 1) { + // Track actions for active quests' objectives + for (const questId of this.activeQuests) { + const quest = this.quests.get(questId); + if (!quest) continue; + + for (const obj of quest.objectives) { + if (obj.complete) continue; + + if (obj.type === 'action' && obj.action === actionType) { + obj.current = Math.min((obj.current || 0) + amount, obj.required); + if (obj.current >= obj.required) { + obj.complete = true; + this.scene.events.emit('show-floating-text', { + x: this.scene.player.x, + y: this.scene.player.y, + text: 'Objective Complete!', + color: '#00FF00' + }); + } + } else if (obj.type === 'kill' && obj.target === actionType) { + obj.current = Math.min((obj.current || 0) + amount, obj.required); + if (obj.current >= obj.required) { + obj.complete = true; + this.scene.events.emit('show-floating-text', { + x: this.scene.player.x, + y: this.scene.player.y, + text: 'Objective Complete!', + color: '#00FF00' + }); + } + } + } + + // Auto-complete check + const allDone = quest.objectives.every(o => o.complete); + if (allDone) { + this.completeQuest(questId); + } + } this.updateUI(); + } + + completeQuest(questId) { + const quest = this.quests.get(questId); + if (!quest) return; + + quest.isActive = false; + quest.isComplete = true; + this.activeQuests = this.activeQuests.filter(id => id !== questId); + this.completedQuests.push(questId); + + // Grant rewards + if (quest.rewards) { + if (quest.rewards.xp && this.scene.player && this.scene.player.gainExperience) { + this.scene.player.gainExperience(quest.rewards.xp); + } + if (quest.rewards.gold && this.scene.inventorySystem) { + this.scene.inventorySystem.addGold(quest.rewards.gold); + } + if (quest.rewards.items) { + quest.rewards.items.forEach(item => { + const [itemName, amt] = item.includes(':') ? item.split(':') : [item, 1]; + if (this.scene.inventorySystem) { + this.scene.inventorySystem.addItem(itemName, parseInt(amt)); + } else if (this.scene.player && this.scene.player.inventory) { + this.scene.player.inventory.addItem(itemName, parseInt(amt)); + } + }); + } + if (quest.rewards.unlocks) { + quest.rewards.unlocks.forEach(unlock => { + if (this.scene.gameState && this.scene.gameState.unlock) { + this.scene.gameState.unlock(unlock); + } + }); + } + if (quest.rewards.companions) { + quest.rewards.companions.forEach(companion => { + if (this.scene.player && this.scene.player.addCompanion) { + this.scene.player.addCompanion(companion); + } + }); + } + } + + // Completion dialogue + if (quest.dialogue && quest.dialogue.complete) { + this.showDialogue(quest.dialogue.complete, quest.npc); + } + + // Completion VFX + if (quest.vfx && (quest.vfx.complete || quest.vfx.onComplete)) { + this.scene.events.emit('vfx:trigger', quest.vfx.complete || quest.vfx.onComplete); + } // Notification this.scene.events.emit('show-floating-text', { x: this.scene.player.x, y: this.scene.player.y - 50, - text: "Quest Accepted!", - color: '#FFFF00' - }); - } - - update(delta) { - if (!this.activeQuest) return; - - let changed = false; - let allDone = true; - - if (this.scene.inventorySystem) { - const inv = this.scene.inventorySystem; - - for (const obj of this.activeQuest.objectives) { - if (obj.done) continue; - - if (obj.type === 'collect') { - const count = inv.getItemCount(obj.item); - if (count !== obj.current) { - obj.current = count; - changed = true; - } - if (obj.current >= obj.amount) { - obj.done = true; - this.scene.events.emit('show-floating-text', { x: this.scene.player.x, y: this.scene.player.y, text: "Objective Complete!" }); - } - } - } - } - - for (const obj of this.activeQuest.objectives) { - if (!obj.done) allDone = false; - } - - if (changed) this.updateUI(); - - if (allDone) { - this.completeQuest(); - } - } - - trackAction(actionType, amount = 1) { - if (!this.activeQuest) return; - - let changed = false; - for (const obj of this.activeQuest.objectives) { - if (obj.done) continue; - - if (obj.type === 'action' && obj.action === actionType) { - obj.current += amount; - changed = true; - if (obj.current >= obj.amount) { - obj.done = true; - changed = true; - } - } - if (obj.type === 'kill' && obj.target === actionType) { - obj.current += amount; - changed = true; - if (obj.current >= obj.amount) { - obj.done = true; - changed = true; - } - } - } - if (changed) this.updateUI(); - } - - completeQuest() { - console.log(`🏆 Quest Complete: ${this.activeQuest.title}`); - - if (this.activeQuest.reward) { - const r = this.activeQuest.reward; - if (r.gold && this.scene.inventorySystem) { - this.scene.inventorySystem.addGold(r.gold); - } - if (r.item && this.scene.inventorySystem) { - this.scene.inventorySystem.addItem(r.item, r.amount || 1); - } - } - - this.scene.events.emit('show-floating-text', { - x: this.scene.player.x, - y: this.scene.player.y - 50, - text: "Quest Complete!", + text: 'Quest Complete!', color: '#00FF00' }); - this.completedQuests.push(this.activeQuest.id); - const next = this.activeQuest.nextQuest; - this.activeQuest = null; - this.updateUI(); + console.log(`🏆 Quest Complete: ${quest.title}`); + this.scene.events.emit('quest:completed', questId); - if (next) { - console.log('Next quest available at NPC.'); + // Auto-start next quest if defined + if (quest.nextQuest) { + setTimeout(() => { + console.log(`Next quest available: ${quest.nextQuest}`); + }, 1000); + } + + this.updateUI(); + } + + showDialogue(lines, npcId) { + // Emit dialogue event + this.scene.events.emit('dialogue:show', { + npc: npcId, + lines: lines + }); + } + + getActiveQuests() { + return this.activeQuests.map(id => this.quests.get(id)); + } + + getAvailableQuest(npcType) { + // Find first uncompleted, non-active quest for this NPC + for (const [id, quest] of this.quests) { + if (quest.isComplete || quest.isActive) continue; + if (quest.npc !== npcType) continue; + + // Check prerequisites + if (quest.prerequisites) { + const prereqsMet = quest.prerequisites.every(p => this.isQuestComplete(p)); + if (!prereqsMet) continue; + } + + return quest; + } + return null; + } + + getQuestProgress(questId) { + const quest = this.quests.get(questId); + if (!quest) return null; + + return { + id: questId, + title: quest.title, + objectives: quest.objectives.map(obj => ({ + text: obj.text, + current: obj.current || 0, + required: obj.required || 1, + complete: obj.complete || false + })), + isActive: quest.isActive, + isComplete: quest.isComplete + }; + } + + isQuestActive(questId) { + const quest = this.quests.get(questId); + return quest && quest.isActive; + } + + isQuestComplete(questId) { + const quest = this.quests.get(questId); + return quest && quest.isComplete; + } + + update(delta) { + // Auto-update objectives based on inventory + if (!this.scene.inventorySystem) return; + + for (const questId of this.activeQuests) { + const quest = this.quests.get(questId); + if (!quest) continue; + + let changed = false; + for (const obj of quest.objectives) { + if (obj.complete) continue; + + if (obj.type === 'item') { + const count = this.scene.inventorySystem.getItemCount(obj.item); + if (count !== obj.current) { + obj.current = count; + changed = true; + if (obj.current >= obj.required) { + obj.complete = true; + this.scene.events.emit('show-floating-text', { + x: this.scene.player.x, + y: this.scene.player.y, + text: 'Objective Complete!', + color: '#00FF00' + }); + } + } + } + } + + if (changed) { + const allDone = quest.objectives.every(o => o.complete); + if (allDone) { + this.completeQuest(questId); + } else { + this.updateUI(); + } + } } } updateUI() { const ui = this.scene.scene.get('UIScene'); if (ui && ui.updateQuestTracker) { - ui.updateQuestTracker(this.activeQuest); + const active = this.activeQuests.length > 0 ? this.quests.get(this.activeQuests[0]) : null; + ui.updateQuestTracker(active); } } + + destroy() { + this.quests.clear(); + this.activeQuests = []; + this.completedQuests = []; + this.questLog = []; + } }