@zerospacegg/iolin
Version:
Pure TypeScript implementation of ZeroSpace game data processing (PKL-free)
248 lines • 9.81 kB
JavaScript
/**
* 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