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.

This commit is contained in:
2026-01-05 12:47:58 +01:00
parent 0bbe65f8a4
commit b68f180663

View File

@@ -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 = [];
}
}