UNPKG

@zerospacegg/iolin

Version:

Pure TypeScript implementation of ZeroSpace game data processing (PKL-free)

248 lines 9.81 kB
/** * Faction - Base faction class for ZeroSpace factions * Ported from faction.pkl with static properties for subclasses */ import { FactionTalent } from "./ability.js"; import { Entity } from "./entity.js"; /** * Base Faction class - extends Entity with faction-specific properties * Usage: new Faction("Grell", (faction) => { faction.mercHeroesAllowed = false; }) */ export class Faction extends Entity { constructor() { super(...arguments); // Faction-specific properties from PKL this.mercHeroesAllowed = true; // ID arrays for faction content references this.heroes = []; this.units = []; this.buildings = []; this.guestHeroes = []; // Object collections for owned abilities this.topbars = {}; this.talents = {}; this.commanders = {}; } get type() { return "faction"; } get slug() { // Use static factionSlug from subclass if available const constructor = this.constructor; if (constructor.factionSlug) { return constructor.factionSlug; } // For factions, derive slug from name since this IS the faction // (not a unit/building that belongs to a faction) return super.slug; } /** * Wire up unlock relationships by reading unlockedBy and setting unlocked states * Call this at the end of faction constructor to set up dependencies */ wireUpUnlocks() { // Read talent unlockedBy and set topbar unlocked states Object.values(this.talents).forEach(talent => { talent.unlockedBy.forEach(topbarId => { const topbar = Object.values(this.topbars).find(t => t.id === topbarId); if (topbar) { topbar.unlocked = false; // Will be unlocked when talent.apply() is called } }); }); // TODO: Handle ability unlockedBy for upgrades/turrets // Object.values(this.upgrades || {}).forEach(upgrade => { // upgrade.unlockedBy.forEach(abilityId => { // const ability = findAbilityById(abilityId); // if (ability) { // ability.unlocked = false; // Will be unlocked when upgrade.apply() is called // } // }); // }); } /** Get all child entity IDs for ecosystem linking - includes topbars and talents */ get children() { const children = super.children; // Add all topbar IDs with their navigation paths for (const [key, topbar] of Object.entries(this.topbars)) { if (topbar && topbar.id) children[topbar.id] = ["topbars", key]; } // Add all talent IDs with their navigation paths for (const [key, talent] of Object.entries(this.talents)) { if (talent && talent.id) children[talent.id] = ["talents", key]; } // Add all commander IDs with their navigation paths for (const [key, commander] of Object.entries(this.commanders)) { if (commander && commander.id) children[commander.id] = ["commanders", key]; } return children; } /** * JSON.stringify() calls this automatically */ toJSON() { return { ...super.toJSON(), mercHeroesAllowed: this.mercHeroesAllowed, heroes: this.heroes, units: this.units, buildings: this.buildings, guestHeroes: this.guestHeroes, talents: this.talents, topbars: this.topbars, commanders: this.commanders, }; } } export class MainFaction extends Faction { get subtype() { return "main"; } get factionType() { return this.subtype; } constructor() { super(); this.addBaseFactionTalents(); } addBaseFactionTalents() { // Create anonymous class that extends FactionTalent const self = this; class BaseFactionTalent extends FactionTalent { get type() { return "ability"; } get abilityOf() { return self.slug; } } // Base faction talents (level 7) - from engine/faction.pkl this.talents.resourceControl = new BaseFactionTalent({ name: "Resource Control", level: 7, description: "Control Towers generate resource and flux", apply() { }, }); this.talents.powerControl = new BaseFactionTalent({ name: "Power Control", level: 7, description: "Control Towers generate faction power", apply() { }, }); } // Tech tree getter - generates dependency tree starting from main base get techTree() { const allEntities = [...this.unitClasses, ...this.buildingClasses]; // Create entity instances to access their properties const entityInstances = allEntities.map(EntityClass => new EntityClass()); // Find key entities by type/subtype const mainBase = entityInstances.find(e => e.type === "building" && e.subtype === "base"); const builder = entityInstances.find(e => e.type === "unit" && e.subtype === "builder"); const harvester = entityInstances.find(e => e.type === "unit" && e.subtype === "harvester"); const supply = entityInstances.find(e => e.type === "building" && e.subtype === "supply"); if (!mainBase) { console.error(`❌ No main base building found for faction ${this.slug}`); console.error(`❌ Available buildings:`, entityInstances.filter(e => e.type === "building").map(e => `${e.name} (${e.subtype})`)); throw new Error(`No main base building found for faction ${this.slug}`); } // Build nested tree structure starting from main base const processedNodes = new Set(); // Helper function to build nested tree recursively const buildTreeNode = (entity) => { const nodeId = entity.id || entity.constructor.id || entity.slug; if (processedNodes.has(nodeId)) { // Return reference to avoid infinite loops return { id: nodeId, name: entity.name, type: entity.type, subtype: entity.subtype, createdBy: entity.createdBy || [], upgradedBy: entity.upgradedBy || [], children: [], // Empty to avoid cycles }; } processedNodes.add(nodeId); const node = { id: nodeId, name: entity.name, type: entity.type, subtype: entity.subtype, createdBy: entity.createdBy || [], upgradedBy: entity.upgradedBy || [], children: [], }; // Process only unlocked entities as children if (entity.unlocks?.length) { entity.unlocks.forEach((unlockedId) => { const unlockedEntity = entityInstances.find(e => (e.constructor.id || e.slug) === unlockedId); if (unlockedEntity) { node.children.push(buildTreeNode(unlockedEntity)); } }); } // Sort children: units first, then research buildings, then other buildings if (node.children.length > 0) { node.children.sort((a, b) => { // Priority order: units first, then other buildings, then tech buildings, then production buildings const getTypePriority = (child) => { if (child.type === "unit") return 1; if (child.type === "building" && child.subtype !== "tech" && child.subtype !== "production") return 2; if (child.type === "building" && child.subtype === "tech") return 3; if (child.type === "building" && child.subtype === "production") return 4; return 5; // fallback }; const aPriority = getTypePriority(a); const bPriority = getTypePriority(b); if (aPriority !== bPriority) { return aPriority - bPriority; } // Within same type, sort alphabetically by name return (a.name || "").localeCompare(b.name || ""); }); } return node; }; // Start building tree from main base const tree = buildTreeNode(mainBase); return { faction: this.slug, mainBase: mainBase.constructor.id || mainBase.slug, builder: builder?.constructor.id || builder?.slug, harvester: harvester?.constructor.id || harvester?.slug, supply: supply?.constructor.id || supply?.slug, tree, }; } // Optional properties for entity classes (not IDs) - for tech tree generation get unitClasses() { return []; } get buildingClasses() { return []; } } export class MercFaction extends Faction { get subtype() { return "mercenary"; } get factionType() { return this.subtype; } } export class NonPlayerFaction extends Faction { get subtype() { return "nonplayer"; } get factionType() { return this.subtype; } } //# sourceMappingURL=faction.js.map