/** * MissionGenerator.js * Factory that produces temporary Mission objects for Side Ops (procedural missions). * * @typedef {import("../managers/types.js").MissionDefinition} MissionDefinition */ /** * MissionGenerator * Generates procedural side missions based on campaign tier and unlocked regions. */ export class MissionGenerator { /** * Adjectives for mission naming (general flavor) */ static ADJECTIVES = [ "Silent", "Broken", "Red", "Crimson", "Shattered", "Frozen", "Burning", "Dark", "Blind", "Hidden", "Lost", "Ancient", "Hollow", "Swift" ]; /** * Nouns for Skirmish (Combat) missions */ static NOUNS_SKIRMISH = [ "Thunder", "Storm", "Iron", "Fury", "Shield", "Hammer", "Wrath", "Wall", "Strike", "Anvil" ]; /** * Nouns for Salvage (Loot) missions */ static NOUNS_SALVAGE = [ "Cache", "Vault", "Echo", "Spark", "Core", "Grip", "Harvest", "Trove", "Fragment", "Salvage" ]; /** * Nouns for Assassination (Kill) missions */ static NOUNS_ASSASSINATION = [ "Viper", "Dagger", "Fang", "Night", "Shadow", "End", "Hunt", "Razor", "Ghost", "Sting" ]; /** * Nouns for Recon (Explore) missions */ static NOUNS_RECON = [ "Eye", "Watch", "Path", "Horizon", "Whisper", "Dawn", "Light", "Step", "Vision", "Scope" ]; /** * Tier configuration: [Name, Enemy Level Range, Reward Multiplier] */ static TIER_CONFIG = { 1: { name: "Recon", enemyLevel: [1, 2], multiplier: 1.0 }, 2: { name: "Patrol", enemyLevel: [3, 4], multiplier: 1.5 }, 3: { name: "Conflict", enemyLevel: [5, 6], multiplier: 2.5 }, 4: { name: "War", enemyLevel: [7, 8], multiplier: 4.0 }, 5: { name: "Suicide", enemyLevel: [9, 10], multiplier: 6.0 } }; /** * Maps biome types to faction IDs for reputation rewards */ static BIOME_TO_FACTION = { "BIOME_FUNGAL_CAVES": "ARCANE_DOMINION", "BIOME_RUSTING_WASTES": "COGWORK_CONCORD", "BIOME_CRYSTAL_SPIRES": "ARCANE_DOMINION", "BIOME_VOID_SEEP": "SHADOW_COVENANT", "BIOME_CONTESTED_FRONTIER": "IRON_LEGION" }; /** * Converts a number to Roman numeral * @param {number} num - Number to convert (2-4) * @returns {string} Roman numeral */ static toRomanNumeral(num) { const roman = ["", "I", "II", "III", "IV", "V"]; return roman[num] || ""; } /** * Extracts the base name (adjective + noun) from a mission title * @param {string} title - Mission title (e.g., "Operation: Silent Viper II") * @returns {string} Base name (e.g., "Silent Viper") */ static extractBaseName(title) { // Remove "Operation: " prefix and any Roman numeral suffix const match = title.match(/Operation:\s*(.+?)(?:\s+[IVX]+)?$/); if (match) { return match[1].trim(); } return title.replace(/Operation:\s*/, "").replace(/\s+[IVX]+$/, "").trim(); } /** * Finds the highest Roman numeral in history for a given base name * @param {string} baseName - Base name (e.g., "Silent Viper") * @param {Array} history - Array of completed mission titles or IDs * @returns {number} Highest numeral found (0 if none) */ static findHighestNumeral(baseName, history) { let highest = 0; const pattern = new RegExp(`Operation:\\s*${baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s+([IVX]+)`, "i"); for (const entry of history) { const match = entry.match(pattern); if (match) { const roman = match[1]; const num = this.romanToNumber(roman); if (num > highest) { highest = num; } } } return highest; } /** * Converts Roman numeral to number * @param {string} roman - Roman numeral string * @returns {number} Number value */ static romanToNumber(roman) { const map = { "I": 1, "II": 2, "III": 3, "IV": 4, "V": 5 }; return map[roman] || 0; } /** * Selects a random element from an array * @param {Array} array - Array to select from * @returns {T} Random element * @template T */ static randomChoice(array) { return array[Math.floor(Math.random() * array.length)]; } /** * Generates a random number between min and max (inclusive) * @param {number} min - Minimum value * @param {number} max - Maximum value * @returns {number} Random number */ static randomRange(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } /** * Generates a random float between min and max * @param {number} min - Minimum value * @param {number} max - Maximum value * @returns {number} Random float */ static randomFloat(min, max) { return Math.random() * (max - min) + min; } /** * Selects a biome from unlocked regions with weighting * @param {Array} unlockedRegions - Array of biome type IDs * @returns {string} Selected biome type */ static selectBiome(unlockedRegions) { if (unlockedRegions.length === 0) { // Default fallback return "BIOME_RUSTING_WASTES"; } // 40% chance for the most recently unlocked region (last in array) if (Math.random() < 0.4 && unlockedRegions.length > 0) { return unlockedRegions[unlockedRegions.length - 1]; } // Otherwise random selection return this.randomChoice(unlockedRegions); } /** * Generates a Side Op mission * @param {number} tier - Campaign tier (1-5) * @param {Array} unlockedRegions - Array of biome type IDs * @param {Array} history - Array of completed mission titles or IDs * @returns {MissionDefinition} Generated mission object */ static generateSideOp(tier, unlockedRegions, history = []) { // Validate tier const validTier = Math.max(1, Math.min(5, tier)); const tierConfig = this.TIER_CONFIG[validTier]; // Select archetype const archetypes = ["SKIRMISH", "SALVAGE", "ASSASSINATION", "RECON"]; const archetype = this.randomChoice(archetypes); // Select noun based on archetype let noun; switch (archetype) { case "SKIRMISH": noun = this.randomChoice(this.NOUNS_SKIRMISH); break; case "SALVAGE": noun = this.randomChoice(this.NOUNS_SALVAGE); break; case "ASSASSINATION": noun = this.randomChoice(this.NOUNS_ASSASSINATION); break; case "RECON": noun = this.randomChoice(this.NOUNS_RECON); break; default: noun = this.randomChoice(this.NOUNS_SKIRMISH); } // Select adjective const adjective = this.randomChoice(this.ADJECTIVES); // Check history for series const baseName = `${adjective} ${noun}`; const highestNumeral = this.findHighestNumeral(baseName, history); const nextNumeral = highestNumeral + 1; const romanSuffix = nextNumeral > 1 ? ` ${this.toRomanNumeral(nextNumeral)}` : ""; // Build title const title = `Operation: ${baseName}${romanSuffix}`; // Select biome const biomeType = this.selectBiome(unlockedRegions); // Generate objectives based on archetype const objectives = this.generateObjectives(archetype, validTier); // Generate biome config based on archetype const biomeConfig = this.generateBiomeConfig(archetype, biomeType); // Calculate rewards const rewards = this.calculateRewards(validTier, archetype, biomeType); // Generate unique ID (timestamp + random to ensure uniqueness) const missionId = `SIDE_OP_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; // Build mission object const mission = { id: missionId, type: "SIDE_QUEST", config: { title: title, description: this.generateDescription(archetype, biomeType), difficulty_tier: validTier, recommended_level: tierConfig.enemyLevel[1] // Use max enemy level as recommended }, biome: biomeConfig, deployment: { squad_size_limit: 4 }, objectives: objectives, rewards: rewards, expiresIn: 3 // Expires in 3 campaign days }; return mission; } /** * Generates objectives based on archetype * @param {string} archetype - Mission archetype * @param {number} tier - Difficulty tier * @returns {Object} Objectives object */ static generateObjectives(archetype, tier) { switch (archetype) { case "SKIRMISH": return { primary: [{ id: "OBJ_ELIMINATE_ALL", type: "ELIMINATE_ALL", description: "Clear the sector of hostile forces." }], failure_conditions: [{ type: "SQUAD_WIPE" }] }; case "SALVAGE": const crateCount = this.randomRange(3, 5); return { primary: [{ id: "OBJ_SALVAGE", type: "INTERACT", target_object_id: "OBJ_SUPPLY_CRATE", target_count: crateCount, description: `Recover ${crateCount} supply crates before the enemy secures them.` }], failure_conditions: [{ type: "SQUAD_WIPE" }] }; case "ASSASSINATION": // Generate a random elite enemy ID const eliteEnemies = [ "ENEMY_ELITE_ECHO", "ENEMY_ELITE_BREAKER", "ENEMY_ELITE_STALKER", "ENEMY_ELITE_WARDEN" ]; const targetId = this.randomChoice(eliteEnemies); return { primary: [{ id: "OBJ_HUNT", type: "ELIMINATE_UNIT", target_def_id: targetId, description: "A High-Value Target has been spotted. Eliminate them." }], failure_conditions: [{ type: "SQUAD_WIPE" }] }; case "RECON": // Generate 3 zone coordinates (simplified - actual zones would be set during mission generation) return { primary: [{ id: "OBJ_RECON", type: "REACH_ZONE", target_count: 3, description: "Survey the designated coordinates." }], failure_conditions: [ { type: "SQUAD_WIPE" }, { type: "TURN_LIMIT_EXCEEDED" } ] }; default: return { primary: [{ id: "OBJ_DEFAULT", type: "ELIMINATE_ALL", description: "Complete the mission objectives." }], failure_conditions: [{ type: "SQUAD_WIPE" }] }; } } /** * Generates biome configuration based on archetype * @param {string} archetype - Mission archetype * @param {string} biomeType - Biome type ID * @returns {Object} Biome configuration */ static generateBiomeConfig(archetype, biomeType) { let size, roomCount, density; switch (archetype) { case "SKIRMISH": size = { x: 20, y: 12, z: 20 }; roomCount = 6; density = "MEDIUM"; break; case "SALVAGE": size = { x: 18, y: 12, z: 18 }; roomCount = 5; density = "HIGH"; // High density for obstacles/cover break; case "ASSASSINATION": size = { x: 22, y: 12, z: 22 }; roomCount = 7; density = "MEDIUM"; break; case "RECON": size = { x: 24, y: 12, z: 24 }; // Large map roomCount = 8; density = "LOW"; // Low enemy density break; default: size = { x: 20, y: 12, z: 20 }; roomCount = 6; density = "MEDIUM"; } return { type: biomeType, generator_config: { seed_type: "RANDOM", size: size, room_count: roomCount, density: density } }; } /** * Generates mission description based on archetype and biome * @param {string} archetype - Mission archetype * @param {string} biomeType - Biome type ID * @returns {string} Description text */ static generateDescription(archetype, biomeType) { const biomeNames = { "BIOME_FUNGAL_CAVES": "Fungal Caves", "BIOME_RUSTING_WASTES": "Rusting Wastes", "BIOME_CRYSTAL_SPIRES": "Crystal Spires", "BIOME_VOID_SEEP": "Void Seep", "BIOME_CONTESTED_FRONTIER": "Contested Frontier" }; const biomeName = biomeNames[biomeType] || "the region"; switch (archetype) { case "SKIRMISH": return `Clear the sector of hostile forces in ${biomeName}.`; case "SALVAGE": return `Recover lost supplies before the enemy secures them in ${biomeName}.`; case "ASSASSINATION": return `A High-Value Target has been spotted in ${biomeName}. Eliminate them.`; case "RECON": return `Survey the designated coordinates in ${biomeName}.`; default: return `Complete the mission objectives in ${biomeName}.`; } } /** * Calculates rewards based on tier and archetype * @param {number} tier - Difficulty tier * @param {string} archetype - Mission archetype * @param {string} biomeType - Biome type ID * @returns {Object} Rewards object */ static calculateRewards(tier, archetype, biomeType) { const tierConfig = this.TIER_CONFIG[tier]; const multiplier = tierConfig.multiplier; // Base currency calculation: Base (50) * TierMultiplier * Random(0.8, 1.2) const baseCurrency = 50; const randomFactor = this.randomFloat(0.8, 1.2); const currencyAmount = Math.round(baseCurrency * multiplier * randomFactor); // XP calculation (base 100 * multiplier) const baseXP = 100; const xpAmount = Math.round(baseXP * multiplier * this.randomFloat(0.9, 1.1)); // Assassination missions get bonus currency let finalCurrency = currencyAmount; if (archetype === "ASSASSINATION") { finalCurrency = Math.round(finalCurrency * 1.5); } // Items: 20% chance per Tier to drop a Chest Key or Item const items = []; const itemChance = tier * 0.2; if (Math.random() < itemChance) { // Randomly choose between chest key or item if (Math.random() < 0.5) { items.push("ITEM_CHEST_KEY"); } else { // Generic item - would need item registry in real implementation items.push("ITEM_MATERIAL_SCRAP"); } } // Reputation: +10 with the Region's owner const factionId = this.BIOME_TO_FACTION[biomeType] || "IRON_LEGION"; const reputation = 10; const rewards = { guaranteed: { xp: xpAmount, currency: { aether_shards: finalCurrency } }, faction_reputation: { [factionId]: reputation } }; // Add items if any if (items.length > 0) { rewards.guaranteed.items = items; } return rewards; } /** * Refreshes the mission board, filling it up to 5 entries and removing expired missions * @param {Array} currentMissions - Current list of available missions * @param {number} tier - Current campaign tier * @param {Array} unlockedRegions - Array of unlocked biome type IDs * @param {Array} history - Array of completed mission titles or IDs * @param {boolean} isDailyReset - If true, decrements expiresIn for all missions * @returns {Array} Updated mission list */ static refreshBoard(currentMissions = [], tier, unlockedRegions, history = [], isDailyReset = false) { // On daily reset, decrement expiresIn for all missions let validMissions = currentMissions; if (isDailyReset) { validMissions = currentMissions.map(mission => { if (mission.expiresIn !== undefined) { const updated = { ...mission }; updated.expiresIn = (mission.expiresIn || 3) - 1; return updated; } return mission; }); } // Remove missions that have expired (expiresIn <= 0) validMissions = validMissions.filter(mission => { if (mission.expiresIn !== undefined) { return mission.expiresIn > 0; } // Keep missions without expiration tracking return true; }); // Fill up to 5 missions const targetCount = 5; const needed = Math.max(0, targetCount - validMissions.length); const newMissions = []; for (let i = 0; i < needed; i++) { const mission = this.generateSideOp(tier, unlockedRegions, history); newMissions.push(mission); } // Combine and return return [...validMissions, ...newMissions]; } }