885 lines
25 KiB
JavaScript
885 lines
25 KiB
JavaScript
/**
|
|
* 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 Eliminate Unit missions
|
|
*/
|
|
static NOUNS_ELIMINATE_UNIT = [
|
|
"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",
|
|
};
|
|
|
|
/**
|
|
* Templates for dynamic narrative generation
|
|
*/
|
|
static NARRATIVE_TEMPLATES = {
|
|
INTRO: {
|
|
IRON_LEGION: {
|
|
speaker: "General Kael",
|
|
portrait: "assets/images/portraits/vanguard.png",
|
|
text: "Commander, Iron Legion scouts report {enemy} activity in {biome}. We need you to {verb} them immediately.",
|
|
},
|
|
ARCANE_DOMINION: {
|
|
speaker: "Magister Varen",
|
|
portrait: "assets/images/portraits/arcanist.png",
|
|
text: "Readings indicate {enemy} disturbances in {biome}. Eliminate the threat before it destabilizes the region.",
|
|
},
|
|
COGWORK_CONCORD: {
|
|
speaker: "Chief Engineer Rix",
|
|
portrait: "assets/images/portraits/engineer.png",
|
|
text: "We've got a situation in {biome}. {enemy} units are interfering with our operations. Shut them down.",
|
|
},
|
|
SHADOW_COVENANT: {
|
|
speaker: "Whisper",
|
|
portrait: "assets/images/portraits/rogue.png",
|
|
text: "A target of interest has appeared in {biome}. {enemy} forces are guarding it. Remove them.",
|
|
},
|
|
},
|
|
OUTRO: {
|
|
IRON_LEGION: {
|
|
speaker: "General Kael",
|
|
portrait: "assets/images/portraits/vanguard.png",
|
|
text: "Target neutralized. Funds transferred. Good hunting, Explorer.",
|
|
},
|
|
ARCANE_DOMINION: {
|
|
speaker: "Magister Varen",
|
|
portrait: "assets/images/portraits/arcanist.png",
|
|
text: "The anomaly has been corrected. Payment has been sent.",
|
|
},
|
|
COGWORK_CONCORD: {
|
|
speaker: "Chief Engineer Rix",
|
|
portrait: "assets/images/portraits/engineer.png",
|
|
text: "Efficiency restored. Your compensation is in the account.",
|
|
},
|
|
SHADOW_COVENANT: {
|
|
speaker: "Whisper",
|
|
portrait: "assets/images/portraits/rogue.png",
|
|
text: "Done and dusted. The Covenant remembers your service.",
|
|
},
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Map of biomes to potential hazards
|
|
*/
|
|
static BIOME_HAZARDS = {
|
|
BIOME_FUNGAL_CAVES: ["HAZARD_POISON_SPORES", "HAZARD_ACID_POOLS"],
|
|
BIOME_RUSTING_WASTES: ["HAZARD_RADIATION_ZONES", "HAZARD_MAGNETIC_STORM"],
|
|
BIOME_CRYSTAL_SPIRES: ["HAZARD_MANA_STORM", "HAZARD_CRYSTAL_RESONANCE"],
|
|
BIOME_VOID_SEEP: ["HAZARD_VOID_POCKETS", "HAZARD_GRAVITY_FLUX"],
|
|
BIOME_CONTESTED_FRONTIER: ["HAZARD_ARTILLERY_STRIKE", "HAZARD_MINEFIELD"],
|
|
};
|
|
|
|
/**
|
|
* 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<string>} 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<T>} 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<string>} 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<string>} unlockedRegions - Array of biome type IDs
|
|
* @param {Array<string>} 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", "ELIMINATE_UNIT", "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 "ELIMINATE_UNIT":
|
|
noun = this.randomChoice(this.NOUNS_ELIMINATE_UNIT);
|
|
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);
|
|
|
|
// Generate enemy spawns based on archetype (especially for ASSASSINATION)
|
|
const enemySpawns = this.generateEnemySpawns(
|
|
archetype,
|
|
objectives,
|
|
validTier
|
|
);
|
|
|
|
// Calculate rewards
|
|
const rewards = this.calculateRewards(validTier, archetype, biomeType);
|
|
|
|
// Determine Faction ID for narrative
|
|
const factionId = this.BIOME_TO_FACTION[biomeType] || "IRON_LEGION";
|
|
|
|
// Generate unique ID (timestamp + random to ensure uniqueness)
|
|
const missionId = `SIDE_OP_${Date.now()}_${Math.random()
|
|
.toString(36)
|
|
.substring(2, 9)}`;
|
|
|
|
// Generate Narrative
|
|
const narrative = this.generateNarrative(
|
|
missionId,
|
|
archetype,
|
|
biomeType,
|
|
factionId
|
|
);
|
|
|
|
// 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,
|
|
},
|
|
narrative: narrative,
|
|
objectives: objectives,
|
|
enemy_spawns: enemySpawns,
|
|
rewards: rewards,
|
|
expiresIn: 3, // Expires in 3 campaign days
|
|
};
|
|
|
|
// Add boss config for Eliminate Unit missions
|
|
if (archetype === "ELIMINATE_UNIT") {
|
|
// Find target ID from objectives
|
|
const targetObj = objectives.primary.find(
|
|
(o) => o.type === "ELIMINATE_UNIT"
|
|
);
|
|
if (targetObj?.target_def_id) {
|
|
mission.config.boss_config = {
|
|
target_def_id: targetObj.target_def_id,
|
|
name: `${this.randomChoice([
|
|
"Krag",
|
|
"Vorak",
|
|
"Xol",
|
|
"Zea",
|
|
])} the ${this.randomChoice(["Breaker", "Rot", "Vile", "Ender"])}`,
|
|
stats: {
|
|
hp_multiplier: 2.0,
|
|
attack_multiplier: 1.5,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
return mission;
|
|
}
|
|
|
|
/**
|
|
* Generates dynamic narrative for the mission
|
|
* @param {string} missionId - The unique ID of the mission
|
|
* @param {string} archetype - Mission archetype
|
|
* @param {string} biomeType - Biome type
|
|
* @param {string} factionId - Faction ID requesting the mission
|
|
* @returns {Object} Narrative object with intro/outro and dynamic data
|
|
*/
|
|
static generateNarrative(missionId, archetype, biomeType, factionId) {
|
|
const introId = `NARRATIVE_${missionId}_INTRO`;
|
|
const outroId = `NARRATIVE_${missionId}_OUTRO`;
|
|
|
|
// Get templates (fallback to Iron Legion if missing)
|
|
const introTemplate =
|
|
this.NARRATIVE_TEMPLATES.INTRO[factionId] ||
|
|
this.NARRATIVE_TEMPLATES.INTRO["IRON_LEGION"];
|
|
const outroTemplate =
|
|
this.NARRATIVE_TEMPLATES.OUTRO[factionId] ||
|
|
this.NARRATIVE_TEMPLATES.OUTRO["IRON_LEGION"];
|
|
|
|
// Format Strings
|
|
const biomeName = biomeType.replace("BIOME_", "").replace("_", " ");
|
|
let verb = "eliminate";
|
|
switch (archetype) {
|
|
case "SALVAGE":
|
|
verb = "recover";
|
|
break;
|
|
case "RECON":
|
|
verb = "scout";
|
|
break;
|
|
case "ELIMINATE_UNIT":
|
|
verb = "assassinate";
|
|
break;
|
|
}
|
|
|
|
const introText = introTemplate.text
|
|
.replace("{enemy}", "hostile")
|
|
.replace("{biome}", biomeName.toLowerCase())
|
|
.replace("{verb}", verb);
|
|
|
|
return {
|
|
intro_sequence: introId,
|
|
outro_success: outroId,
|
|
_dynamic_data: {
|
|
[introId]: {
|
|
id: introId,
|
|
nodes: [
|
|
{
|
|
id: "1",
|
|
type: "DIALOGUE",
|
|
speaker: introTemplate.speaker,
|
|
portrait: introTemplate.portrait,
|
|
text: introText,
|
|
next: "END",
|
|
},
|
|
],
|
|
},
|
|
[outroId]: {
|
|
id: outroId,
|
|
nodes: [
|
|
{
|
|
id: "1",
|
|
type: "DIALOGUE",
|
|
speaker: outroTemplate.speaker,
|
|
portrait: outroTemplate.portrait,
|
|
text: outroTemplate.text,
|
|
next: "END",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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 "ELIMINATE_UNIT":
|
|
// 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,
|
|
target_count: 1, // Explicitly set to 1 for single target elimination
|
|
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)
|
|
// Turn limit: 15 + (tier * 5) turns for RECON missions
|
|
const turnLimit = 15 + tier * 5;
|
|
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", turn_limit: turnLimit },
|
|
],
|
|
};
|
|
|
|
default:
|
|
return {
|
|
primary: [
|
|
{
|
|
id: "OBJ_DEFAULT",
|
|
type: "ELIMINATE_ALL",
|
|
description: "Complete the mission objectives.",
|
|
},
|
|
],
|
|
failure_conditions: [{ type: "SQUAD_WIPE" }],
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates enemy spawns based on archetype and objectives
|
|
* @param {string} archetype - Mission archetype
|
|
* @param {Object} objectives - Generated objectives
|
|
* @param {number} tier - Difficulty tier
|
|
* @returns {Array<EnemySpawn>} Array of enemy spawn definitions
|
|
*/
|
|
static generateEnemySpawns(archetype, objectives, tier) {
|
|
const spawns = [];
|
|
|
|
switch (archetype) {
|
|
case "ELIMINATE_UNIT":
|
|
// For ELIMINATE_UNIT, spawn the target enemy from the ELIMINATE_UNIT objective
|
|
const eliminateUnitObj = objectives.primary?.find(
|
|
(obj) => obj.type === "ELIMINATE_UNIT"
|
|
);
|
|
if (eliminateUnitObj?.target_def_id) {
|
|
spawns.push({
|
|
enemy_def_id: eliminateUnitObj.target_def_id,
|
|
count: 1,
|
|
});
|
|
}
|
|
// Also spawn some regular enemies for support
|
|
const regularEnemies = [
|
|
"ENEMY_SHARDBORN_SENTINEL",
|
|
"ENEMY_GOBLIN_RAIDER",
|
|
"ENEMY_CRYSTAL_SHARD",
|
|
];
|
|
const supportEnemy = this.randomChoice(regularEnemies);
|
|
spawns.push({
|
|
enemy_def_id: supportEnemy,
|
|
count: Math.max(1, Math.floor(tier / 2)), // 1-2 support enemies based on tier
|
|
});
|
|
break;
|
|
|
|
case "SKIRMISH":
|
|
// Skirmish missions spawn a mix of enemies
|
|
const skirmishEnemies = [
|
|
"ENEMY_SHARDBORN_SENTINEL",
|
|
"ENEMY_GOBLIN_RAIDER",
|
|
"ENEMY_CRYSTAL_SHARD",
|
|
];
|
|
const totalSkirmish = 3 + tier; // 4-8 enemies based on tier
|
|
for (let i = 0; i < totalSkirmish; i++) {
|
|
const enemyType = this.randomChoice(skirmishEnemies);
|
|
const existingSpawn = spawns.find(
|
|
(s) => s.enemy_def_id === enemyType
|
|
);
|
|
if (existingSpawn) {
|
|
existingSpawn.count++;
|
|
} else {
|
|
spawns.push({ enemy_def_id: enemyType, count: 1 });
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "SALVAGE":
|
|
case "RECON":
|
|
// These missions have fewer enemies (lower density)
|
|
const lightEnemies = [
|
|
"ENEMY_SHARDBORN_SENTINEL",
|
|
"ENEMY_GOBLIN_RAIDER",
|
|
];
|
|
const totalLight = Math.max(1, tier); // 1-5 enemies based on tier
|
|
for (let i = 0; i < totalLight; i++) {
|
|
const enemyType = this.randomChoice(lightEnemies);
|
|
const existingSpawn = spawns.find(
|
|
(s) => s.enemy_def_id === enemyType
|
|
);
|
|
if (existingSpawn) {
|
|
existingSpawn.count++;
|
|
} else {
|
|
spawns.push({ enemy_def_id: enemyType, count: 1 });
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Default: spawn a few basic enemies
|
|
spawns.push({
|
|
enemy_def_id: "ENEMY_SHARDBORN_SENTINEL",
|
|
count: Math.max(1, tier),
|
|
});
|
|
}
|
|
|
|
return spawns;
|
|
}
|
|
|
|
/**
|
|
* 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 "ELIMINATE_UNIT":
|
|
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";
|
|
}
|
|
|
|
// Roll for Hazards (30% chance)
|
|
const hazards = [];
|
|
const allowedHazards = this.BIOME_HAZARDS[biomeType];
|
|
if (allowedHazards && Math.random() < 0.3) {
|
|
hazards.push(this.randomChoice(allowedHazards));
|
|
}
|
|
|
|
return {
|
|
type: biomeType,
|
|
hazards: hazards,
|
|
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 "ELIMINATE_UNIT":
|
|
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)
|
|
);
|
|
|
|
// Eliminate Unit missions get bonus currency
|
|
let finalCurrency = currencyAmount;
|
|
if (archetype === "ELIMINATE_UNIT") {
|
|
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<MissionDefinition>} currentMissions - Current list of available missions
|
|
* @param {number} tier - Current campaign tier
|
|
* @param {Array<string>} unlockedRegions - Array of unlocked biome type IDs
|
|
* @param {Array<string>} history - Array of completed mission titles or IDs
|
|
* @param {boolean} isDailyReset - If true, decrements expiresIn for all missions
|
|
* @returns {Array<MissionDefinition>} 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];
|
|
}
|
|
}
|