507 lines
15 KiB
JavaScript
507 lines
15 KiB
JavaScript
/**
|
|
* STORY & QUEST SYSTEM
|
|
* Complete quest system with story acts, dialogue, cutscenes, and multiple endings
|
|
*/
|
|
class StoryQuestSystem {
|
|
constructor(scene) {
|
|
this.scene = scene;
|
|
this.enabled = true;
|
|
|
|
// Quests
|
|
this.quests = new Map();
|
|
this.activeQuests = new Set();
|
|
this.completedQuests = new Set();
|
|
|
|
// Story progress
|
|
this.currentAct = 1;
|
|
this.storyFlags = new Set();
|
|
|
|
// Characters
|
|
this.characters = new Map();
|
|
|
|
// Dialogue
|
|
this.currentDialogue = null;
|
|
|
|
// Endings
|
|
this.playerChoices = [];
|
|
this.ending = null;
|
|
|
|
this.loadProgress();
|
|
this.init();
|
|
|
|
console.log('✅ Story & Quest System initialized');
|
|
}
|
|
|
|
init() {
|
|
this.defineCharacters();
|
|
this.defineQuests();
|
|
console.log('📖 Story & Quest system ready');
|
|
}
|
|
|
|
// ========== CHARACTERS ==========
|
|
|
|
defineCharacters() {
|
|
this.characters.set('jakob', {
|
|
name: 'Old Jakob',
|
|
role: 'Merchant',
|
|
relationship: 0,
|
|
dialogues: new Map()
|
|
});
|
|
|
|
this.characters.set('lyra', {
|
|
name: 'Lyra',
|
|
role: 'Mutant Elf',
|
|
relationship: 0,
|
|
dialogues: new Map()
|
|
});
|
|
|
|
this.characters.set('grok', {
|
|
name: 'Grok',
|
|
role: 'Troll Guardian',
|
|
relationship: 0,
|
|
dialogues: new Map()
|
|
});
|
|
|
|
this.characters.set('dr_chen', {
|
|
name: 'Dr. Chen',
|
|
role: 'Radio Voice',
|
|
relationship: 0,
|
|
dialogues: new Map()
|
|
});
|
|
}
|
|
|
|
// ========== QUESTS ==========
|
|
|
|
defineQuests() {
|
|
// ACT 1: Survival (Day 1-10)
|
|
this.defineQuest('first_harvest', {
|
|
name: 'Prvi Pridelek',
|
|
act: 1,
|
|
description: 'Harvest 10 wheat to survive',
|
|
objectives: [
|
|
{ type: 'harvest', item: 'wheat', amount: 10, current: 0 }
|
|
],
|
|
rewards: { xp: 100, item: 'iron_hoe' },
|
|
unlocks: ['safe_haven']
|
|
});
|
|
|
|
this.defineQuest('safe_haven', {
|
|
name: 'Varno Zatočišče',
|
|
act: 1,
|
|
description: 'Build fence around farm',
|
|
objectives: [
|
|
{ type: 'build', item: 'fence', amount: 20, current: 0 }
|
|
],
|
|
rewards: { xp: 150, blueprint: 'reinforced_fence' },
|
|
unlocks: ['night_watch']
|
|
});
|
|
|
|
this.defineQuest('night_watch', {
|
|
name: 'Nočna Straža',
|
|
act: 1,
|
|
description: 'Survive first zombie night',
|
|
objectives: [
|
|
{ type: 'survive', nights: 1, current: 0 }
|
|
],
|
|
rewards: { xp: 200, item: 'torch_pack' },
|
|
unlocks: ['meet_merchant']
|
|
});
|
|
|
|
this.defineQuest('meet_merchant', {
|
|
name: 'Meet the Merchant',
|
|
act: 1,
|
|
description: 'Find Jakob the trader',
|
|
objectives: [
|
|
{ type: 'talk', npc: 'jakob' }
|
|
],
|
|
rewards: { xp: 100, unlocks: 'trading' },
|
|
unlocks: ['strange_transmission']
|
|
});
|
|
|
|
// ACT 2: Discovery (Day 11-20)
|
|
this.defineQuest('strange_transmission', {
|
|
name: 'Strange Transmission',
|
|
act: 2,
|
|
description: 'Find radio in city',
|
|
objectives: [
|
|
{ type: 'find', item: 'radio', location: 'city_ruins' }
|
|
],
|
|
rewards: { xp: 300, item: 'radio' },
|
|
unlocks: ['first_tame']
|
|
});
|
|
|
|
this.defineQuest('first_tame', {
|
|
name: 'Prvi Poskus',
|
|
act: 2,
|
|
description: 'Tame first zombie',
|
|
objectives: [
|
|
{ type: 'tame', creature: 'zombie', amount: 1 }
|
|
],
|
|
rewards: { xp: 250, unlocks: 'zombie_workers' },
|
|
unlocks: ['lab_ruins']
|
|
});
|
|
|
|
this.defineQuest('lab_ruins', {
|
|
name: 'Lab Ruins',
|
|
act: 2,
|
|
description: 'Explore abandoned research facility',
|
|
objectives: [
|
|
{ type: 'explore', location: 'research_lab' }
|
|
],
|
|
rewards: { xp: 400, item: 'lab_key' },
|
|
unlocks: ['mutant_contact']
|
|
});
|
|
|
|
this.defineQuest('mutant_contact', {
|
|
name: 'Mutant Contact',
|
|
act: 2,
|
|
description: 'Meet friendly mutant Lyra',
|
|
objectives: [
|
|
{ type: 'talk', npc: 'lyra' }
|
|
],
|
|
rewards: { xp: 300, unlocks: 'mutation_research' },
|
|
unlocks: ['lab_notes']
|
|
});
|
|
|
|
// ACT 3: The Truth (Day 21-30)
|
|
this.defineQuest('lab_notes', {
|
|
name: 'Lab Notes',
|
|
act: 3,
|
|
description: 'Collect 5 research documents',
|
|
objectives: [
|
|
{ type: 'collect', item: 'research_document', amount: 5, current: 0 }
|
|
],
|
|
rewards: { xp: 500 },
|
|
unlocks: ['patient_zero']
|
|
});
|
|
|
|
this.defineQuest('patient_zero', {
|
|
name: 'Patient Zero',
|
|
act: 3,
|
|
description: 'Find virus source',
|
|
objectives: [
|
|
{ type: 'find', item: 'virus_sample', location: 'deep_lab' }
|
|
],
|
|
rewards: { xp: 600 },
|
|
unlocks: ['difficult_choice']
|
|
});
|
|
|
|
this.defineQuest('difficult_choice', {
|
|
name: 'Difficult Choice',
|
|
act: 3,
|
|
description: 'Choose faction',
|
|
objectives: [
|
|
{ type: 'choice', options: ['human', 'zombie', 'hybrid'] }
|
|
],
|
|
rewards: { xp: 1000 },
|
|
unlocks: ['final_confrontation']
|
|
});
|
|
|
|
this.defineQuest('final_confrontation', {
|
|
name: 'Final Confrontation',
|
|
act: 3,
|
|
description: 'Boss battle',
|
|
objectives: [
|
|
{ type: 'defeat', boss: 'zombie_king' }
|
|
],
|
|
rewards: { xp: 2000 },
|
|
ending: true
|
|
});
|
|
}
|
|
|
|
defineQuest(id, data) {
|
|
this.quests.set(id, {
|
|
id,
|
|
active: false,
|
|
completed: false,
|
|
...data
|
|
});
|
|
}
|
|
|
|
// ========== QUEST MANAGEMENT ==========
|
|
|
|
startQuest(questId) {
|
|
const quest = this.quests.get(questId);
|
|
if (!quest || quest.active || quest.completed) return false;
|
|
|
|
quest.active = true;
|
|
this.activeQuests.add(questId);
|
|
|
|
console.log(`📜 Quest started: ${quest.name}`);
|
|
return true;
|
|
}
|
|
|
|
updateQuestProgress(questId, objectiveIndex, progress) {
|
|
const quest = this.quests.get(questId);
|
|
if (!quest || !quest.active) return;
|
|
|
|
const objective = quest.objectives[objectiveIndex];
|
|
if (!objective) return;
|
|
|
|
objective.current = progress;
|
|
|
|
// Check if objective complete
|
|
if (this.isObjectiveComplete(objective)) {
|
|
console.log(`✅ Objective complete: ${objective.type}`);
|
|
|
|
// Check if all objectives complete
|
|
if (quest.objectives.every(obj => this.isObjectiveComplete(obj))) {
|
|
this.completeQuest(questId);
|
|
}
|
|
}
|
|
}
|
|
|
|
isObjectiveComplete(objective) {
|
|
switch (objective.type) {
|
|
case 'harvest':
|
|
case 'build':
|
|
case 'collect':
|
|
return objective.current >= objective.amount;
|
|
case 'talk':
|
|
case 'find':
|
|
case 'explore':
|
|
case 'defeat':
|
|
return objective.current === true;
|
|
case 'survive':
|
|
return objective.current >= objective.nights;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
completeQuest(questId) {
|
|
const quest = this.quests.get(questId);
|
|
if (!quest) return;
|
|
|
|
quest.active = false;
|
|
quest.completed = true;
|
|
this.activeQuests.delete(questId);
|
|
this.completedQuests.add(questId);
|
|
|
|
// Grant rewards
|
|
this.grantQuestRewards(quest);
|
|
|
|
// Unlock next quests
|
|
if (quest.unlocks) {
|
|
const unlocks = Array.isArray(quest.unlocks) ? quest.unlocks : [quest.unlocks];
|
|
unlocks.forEach(nextQuest => this.startQuest(nextQuest));
|
|
}
|
|
|
|
// Check for ending
|
|
if (quest.ending) {
|
|
this.triggerEnding();
|
|
}
|
|
|
|
console.log(`🎉 Quest completed: ${quest.name}!`);
|
|
this.saveProgress();
|
|
}
|
|
|
|
grantQuestRewards(quest) {
|
|
const rewards = quest.rewards;
|
|
|
|
if (rewards.xp && this.scene.skillTree) {
|
|
this.scene.skillTree.addXP(rewards.xp);
|
|
}
|
|
|
|
if (rewards.item && this.scene.inventorySystem) {
|
|
this.scene.inventorySystem.addItem(rewards.item, 1);
|
|
}
|
|
|
|
if (rewards.blueprint) {
|
|
console.log(`📋 Unlocked blueprint: ${rewards.blueprint}`);
|
|
}
|
|
}
|
|
|
|
// ========== DIALOGUE SYSTEM ==========
|
|
|
|
startDialogue(npcId, dialogueId) {
|
|
const character = this.characters.get(npcId);
|
|
if (!character) return false;
|
|
|
|
this.currentDialogue = {
|
|
npc: npcId,
|
|
dialogueId,
|
|
currentNode: 0,
|
|
choices: []
|
|
};
|
|
|
|
console.log(`💬 Dialogue started with ${character.name}`);
|
|
return true;
|
|
}
|
|
|
|
selectDialogueChoice(choiceIndex) {
|
|
if (!this.currentDialogue) return;
|
|
|
|
const choice = this.currentDialogue.choices[choiceIndex];
|
|
if (!choice) return;
|
|
|
|
// Track player choice
|
|
this.playerChoices.push({
|
|
npc: this.currentDialogue.npc,
|
|
choice: choice.text,
|
|
consequence: choice.consequence
|
|
});
|
|
|
|
// Update relationship
|
|
const character = this.characters.get(this.currentDialogue.npc);
|
|
if (character && choice.relationshipDelta) {
|
|
character.relationship += choice.relationshipDelta;
|
|
}
|
|
|
|
// Apply consequences
|
|
if (choice.consequence) {
|
|
this.applyChoiceConsequence(choice.consequence);
|
|
}
|
|
|
|
console.log(`💬 Choice selected: ${choice.text}`);
|
|
}
|
|
|
|
applyChoiceConsequence(consequence) {
|
|
// Set story flags
|
|
if (consequence.flag) {
|
|
this.storyFlags.add(consequence.flag);
|
|
}
|
|
|
|
// Unlock quests
|
|
if (consequence.unlockQuest) {
|
|
this.startQuest(consequence.unlockQuest);
|
|
}
|
|
|
|
// Change faction
|
|
if (consequence.faction) {
|
|
this.playerChoices.push({ type: 'faction', value: consequence.faction });
|
|
}
|
|
}
|
|
|
|
endDialogue() {
|
|
this.currentDialogue = null;
|
|
}
|
|
|
|
// ========== CUTSCENES ==========
|
|
|
|
playCutscene(cutsceneId) {
|
|
console.log(`🎬 Playing cutscene: ${cutsceneId}`);
|
|
|
|
const cutscenes = {
|
|
'arrival': this.cutsceneArrival.bind(this),
|
|
'first_zombie': this.cutsceneFirstZombie.bind(this),
|
|
'city_discovery': this.cutsceneCityDiscovery.bind(this),
|
|
'boss_reveal': this.cutsceneBossReveal.bind(this)
|
|
};
|
|
|
|
const cutscene = cutscenes[cutsceneId];
|
|
if (cutscene) {
|
|
cutscene();
|
|
}
|
|
}
|
|
|
|
cutsceneArrival() {
|
|
console.log('🎬 Arrival cutscene - Farm overview');
|
|
}
|
|
|
|
cutsceneFirstZombie() {
|
|
console.log('🎬 First zombie encounter - Tutorial');
|
|
}
|
|
|
|
cutsceneCityDiscovery() {
|
|
console.log('🎬 City discovery - Ruins pan');
|
|
}
|
|
|
|
cutsceneBossReveal() {
|
|
console.log('🎬 Boss reveal - Zombie King emergence');
|
|
}
|
|
|
|
// ========== ENDINGS ==========
|
|
|
|
triggerEnding() {
|
|
// Determine ending based on player choices
|
|
const faction = this.getPlayerFaction();
|
|
const relationships = this.getRelationships();
|
|
|
|
if (faction === 'human' && relationships.jakob > 50) {
|
|
this.ending = 'cure';
|
|
this.playCutscene('cure_ending');
|
|
} else if (faction === 'zombie') {
|
|
this.ending = 'zombie_king';
|
|
this.playCutscene('zombie_king_ending');
|
|
} else if (faction === 'hybrid') {
|
|
this.ending = 'mutation';
|
|
this.playCutscene('mutation_ending');
|
|
} else if (relationships.lyra > 70) {
|
|
this.ending = 'escape';
|
|
this.playCutscene('escape_ending');
|
|
} else {
|
|
this.ending = 'farmer';
|
|
this.playCutscene('farmer_ending');
|
|
}
|
|
|
|
console.log(`🎬 Ending: ${this.ending}`);
|
|
this.saveProgress();
|
|
}
|
|
|
|
getPlayerFaction() {
|
|
const factionChoice = this.playerChoices.find(c => c.type === 'faction');
|
|
return factionChoice ? factionChoice.value : null;
|
|
}
|
|
|
|
getRelationships() {
|
|
const relationships = {};
|
|
for (const [id, character] of this.characters.entries()) {
|
|
relationships[id] = character.relationship;
|
|
}
|
|
return relationships;
|
|
}
|
|
|
|
// ========== PERSISTENCE ==========
|
|
|
|
saveProgress() {
|
|
const data = {
|
|
currentAct: this.currentAct,
|
|
activeQuests: Array.from(this.activeQuests),
|
|
completedQuests: Array.from(this.completedQuests),
|
|
storyFlags: Array.from(this.storyFlags),
|
|
playerChoices: this.playerChoices,
|
|
ending: this.ending,
|
|
characters: Array.from(this.characters.entries()).map(([id, char]) => ({
|
|
id,
|
|
relationship: char.relationship
|
|
}))
|
|
};
|
|
|
|
localStorage.setItem('novafarma_story', JSON.stringify(data));
|
|
}
|
|
|
|
loadProgress() {
|
|
const saved = localStorage.getItem('novafarma_story');
|
|
if (saved) {
|
|
try {
|
|
const data = JSON.parse(saved);
|
|
this.currentAct = data.currentAct || 1;
|
|
this.activeQuests = new Set(data.activeQuests || []);
|
|
this.completedQuests = new Set(data.completedQuests || []);
|
|
this.storyFlags = new Set(data.storyFlags || []);
|
|
this.playerChoices = data.playerChoices || [];
|
|
this.ending = data.ending || null;
|
|
|
|
if (data.characters) {
|
|
data.characters.forEach(char => {
|
|
const character = this.characters.get(char.id);
|
|
if (character) {
|
|
character.relationship = char.relationship;
|
|
}
|
|
});
|
|
}
|
|
|
|
console.log('✅ Story progress loaded');
|
|
} catch (error) {
|
|
console.error('Failed to load story progress:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.saveProgress();
|
|
console.log('📖 Story & Quest System destroyed');
|
|
}
|
|
}
|