@zerospacegg/iolin
Version:
Pure TypeScript implementation of ZeroSpace game data processing (PKL-free)
508 lines • 19.5 kB
JavaScript
"use strict";
/**
* Entity - Base class for top-level game objects with their own files
* Extends Universal with game-specific properties and path-based ID generation
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Entity = void 0;
exports.typeDesc = typeDesc;
const core_js_1 = require("./core.cjs");
const universal_js_1 = require("./universal.cjs");
/**
* Generate type description for an entity or entity summary
* Works with both full Entity instances and lightweight EntitySummary objects
*
* @param entityOrSummary - Either an Entity instance or EntitySummary object
* @returns Formatted type description (e.g., "Grell Co-Op Unit", "Protectorate Hero")
*/
function typeDesc(entityOrSummary) {
// Handle both Entity instances and EntitySummary objects
const type = entityOrSummary.type;
const subtype = entityOrSummary.subtype;
const faction = entityOrSummary.faction;
const tier = entityOrSummary.tier;
const id = entityOrSummary.id;
if (!type || !subtype)
return "";
// Special handling for faction types
if (type === "faction") {
if (subtype === "main")
return "Main Faction";
if (subtype === "mercenary")
return "Mercenary Faction";
if (subtype === "nonplayer")
return "Non-Player Faction";
return "Faction";
}
// Helper function to capitalize words
const capitalizeWords = (str) => {
return str
.split(/[-\s]+/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
};
const baseType = capitalizeWords(`${subtype} ${type}`);
const withTier = tier && subtype !== "coop-commander" ? `${capitalizeWords(tier)} ${baseType}` : baseType;
// Check if this is a co-op entity by looking at the ID
const isCoopEntity = id.startsWith("coop/");
// Get faction name - capitalize first letter
const factionName = faction ? faction.charAt(0).toUpperCase() + faction.slice(1) : "";
let result = withTier;
if (factionName && type !== "faction") {
if (isCoopEntity) {
result = `${factionName} Co-Op ${withTier}`;
}
else {
result = `${factionName} ${withTier}`;
}
}
return result;
}
/**
* Entity class for top-level game objects that have their own files
* Usage: new Entity("Name", (entity) => { entity.description = "..."; }, import.meta.filename)
*/
class Entity extends universal_js_1.Universal {
// Static getters that parse from src
static get id() {
return this.src.replace(/^src\/zerospace\//, "").replace(/\.ts$/, "");
}
static get slug() {
const parts = this.src.split("/");
return parts[parts.length - 1].replace(/\.ts$/, "");
}
/**
* Returns the parent entity's UUID if this is a coop variant, undefined otherwise.
* Used for artwork inheritance from PvP to coop variants.
*/
get parentUUID() {
// Extract parent UUID from composite UUID pattern (e.g., "parent-uuid:coop-mera")
if (this.uuid.includes(":")) {
return this.uuid.replace(/:.*/, "");
}
return undefined;
}
/**
* Sets a variant UUID by appending a suffix to the current UUID.
* Used by coop entities to create composite UUIDs like "parent-uuid:coop-mera".
*
* @param variantSuffix - The suffix to append (e.g., "coop-mera")
*/
setVariantUUID(variantSuffix) {
// @ts-ignore - TypeScript can't figure out that super() assigns this.uuid, but it does
this.uuid = `${this.uuid}:${variantSuffix}`;
}
// Ugly accessor methods for private fields
_setIdParts(value) {
this._idParts = value;
}
_addTags(...tags) {
this._tags.push(...tags);
}
_removeTags(...tags) {
this._tags = this._tags.filter(tag => !tags.includes(tag));
}
_removeTagsStartingWith(prefix) {
this._tags = this._tags.filter(tag => !tag.startsWith(prefix));
}
// Store path-based values (assigned dynamically, no field declarations)
// Computed properties using clean getters
/** Short display name - defaults to name */
get shortName() {
return this._shortName || this.name;
}
/** Set short display name */
set shortName(value) {
this._shortName = value;
}
constructor(props) {
super();
this._tags = [];
/**
* Dynamic tag predicates - array of [predicate, tag] pairs for computed tags
*/
this.dynamicTaggers = [
// Built-in dynamic tags
[(e) => !!e.type, (e) => e.type],
[(e) => !!e.type && !!e.subtype && e.type !== e.subtype, (e) => `${e.type}:${e.subtype}`],
[(e) => !!e.faction, (e) => `faction:${e.faction}`],
[(e) => !!e.tier && e.tier !== "undefined", (e) => `tier:${e.tier.replace("T", "")}`],
[(e) => e.armorType !== "none", (e) => `armor:${e.armorType}`],
[(e) => (e.gathersRichFlux ?? 0) > 0 || (e.gathersFlux ?? 0) > 0, "gathers:flux"],
[(e) => (e.gathersHexite ?? 0) > 0 || (e.gathersEmptyHexite ?? 0) > 0, "gathers:hexite"],
[(e) => e.domain === "air", "flyer"],
[(e) => (e.armor ?? 0) > 0, "armored"],
[(e) => (e.providesSupply ?? 0) > 0, "provides:supply"],
[(e) => (e.providesBiomass ?? 0) > 0, "biomass"],
[(e) => e.providesDetection ?? false, "detection"],
[(e) => (e.providesUpgradesFor?.length ?? 0) > 0, "provides:upgrades"],
[(e) => (e.shields ?? 0) > 0, "shielded"],
[(e) => (e.abesEnergy ?? 0) > 0, "abes"],
[(e) => (e.energy ?? 0) > 0, "energy"],
[(e) => (e.stunResist ?? 0) > 0, "status-resist"],
[(e) => e.hasAbilityType("attack"), "attacker"],
[(e) => e.hasAbilityType("heal"), "healer"],
[(e) => e.hasAbilityType("spell"), "spellcaster"],
[(e) => e.hasAbilityType("passive"), "has:passive"],
[(e) => !!e.untargetable, "untargetable"],
[(e) => e.rebuildable === true, "respawns"],
[(e) => e.turrets && Object.keys(e.turrets).length > 0, "has:turret"],
[(e) => e.upgrades && Object.keys(e.upgrades).length > 0, "upgradable"],
[(e) => e.sacrifices && Object.keys(e.sacrifices).length > 0, "sacrifices"],
];
// Public ability collections with strict typing - direct assignment
this.attacks = {};
this.heals = {};
this.spells = {};
this.passives = {};
this.sieges = {};
this.weaponSwitches = {};
this.upgrades = {};
this.turrets = {};
this.sacrifices = {};
this.onDeath = {};
this.tier = "";
this.domain = "ground";
this.hotkey = "";
this.armorType = "none";
// Special properties (mutable - can be modified by upgrades)
this.untargetable = false;
this.maxAddOns = 0;
// Game state (design decisions)
this.inGame = true;
this.fromFuture = false;
this.unlockedBy = [];
}
/** Auto-generated slug from filename or name */
get slug() {
// Use static slug if available (set via static filename)
const staticSlug = this.constructor.slug;
if (staticSlug) {
return staticSlug;
}
// Use static src to derive slug from filename
const staticSrc = this.constructor.src;
if (staticSrc) {
// Extract filename from path: "src/zerospace/faction/grell/unit/stinger.ts" -> "stinger"
const pathWithoutExt = staticSrc.replace(/\.ts$/, "");
const parts = pathWithoutExt.split("/");
return parts[parts.length - 1];
}
return (0, core_js_1.slugify)(this.name);
}
/** Auto-generated ID from src path or type/subtype/slug */
get id() {
const staticId = this.constructor.id;
if (staticId) {
if (!this._idParts) {
this._idParts = Object.freeze(staticId.split("/"));
}
return staticId;
}
let idString;
// Derive from static src property
const staticSrc = this.constructor.src;
if (staticSrc) {
idString = staticSrc.replace(/^src\/zerospace\//, "").replace(/\.ts$/, "");
}
else if (this.type && this.subtype) {
idString = `${this.type}/${this.subtype}/${this.slug}`;
}
else {
idString = this.slug;
}
// Initialize frozen ID parts if not already done
if (!this._idParts) {
this._idParts = Object.freeze(idString.split("/"));
}
return idString;
}
/** Source file path from static src property */
get src() {
return this.constructor.src;
}
/** ZeroSpace.gg library path */
get zsggPath() {
return `https://zerospace.gg/library/${this.id}/`;
}
/**
* Protected getter for manual tags only (to avoid circular dependency)
*/
get manualTags() {
return this._tags;
}
/**
* Generate commander level ID for coop entities
* @param level - Commander level number
* @returns Commander level ID string
* @throws Error if entity is not a coop commander entity
*/
commanderLevelId(level) {
const entityId = this.constructor.id;
// Check if this is a coop commander entity
const coopMatch = entityId.match(/^coop\/commander\/([^\/]+)\//);
if (!coopMatch) {
throw new Error(`Entity ${entityId} is not a coop commander entity - cannot generate commander level ID`);
}
const commanderName = coopMatch[1];
return `coop/commander/${commanderName}-commander/level/${level}`;
}
/**
* Complete tag list combining manual and dynamic tags
*/
get tags() {
return [
...this._tags,
...this.dynamicTaggers
.map(([check, tag]) => (check(this) ? (typeof tag === "function" ? tag(this) : tag) : null))
.filter(Boolean),
];
}
/** Pretty formatted type description for display */
get typeDesc() {
return typeDesc(this);
}
/** Get all child entity IDs for ecosystem linking */
get children() {
const children = {};
// Add all ability IDs with their navigation paths
for (const [key, attack] of Object.entries(this.attacks)) {
if (attack.id)
children[attack.id] = ["attacks", key];
}
for (const [key, heal] of Object.entries(this.heals)) {
if (heal.id)
children[heal.id] = ["heals", key];
}
for (const [key, spell] of Object.entries(this.spells)) {
if (spell.id)
children[spell.id] = ["spells", key];
}
for (const [key, passive] of Object.entries(this.passives)) {
if (passive.id)
children[passive.id] = ["passives", key];
}
for (const [key, onDeath] of Object.entries(this.onDeath)) {
if (onDeath.id)
children[onDeath.id] = ["onDeath", key];
}
for (const [key, siege] of Object.entries(this.sieges)) {
if (siege.id)
children[siege.id] = ["sieges", key];
}
for (const [key, weaponSwitch] of Object.entries(this.weaponSwitches)) {
if (weaponSwitch.id)
children[weaponSwitch.id] = ["weaponSwitches", key];
}
for (const [key, sacrifice] of Object.entries(this.sacrifices)) {
if (sacrifice.id)
children[sacrifice.id] = ["sacrifices", key];
}
for (const [key, turret] of Object.entries(this.turrets)) {
if (turret.id)
children[turret.id] = ["turrets", key];
}
return children;
}
/** Apply turret transformations and return new instance - chainable! */
withTurrets() {
// Create a simple transformed object with current stats
// this is completely wrong, wer're not using it yet anywhere so meh.
// we eventually want a this.clone() or something to return a new instance we can modify
// or we generalize this. idk.
const transformed = {
...this,
hp: this.hp,
armor: this.armor,
speed: this.speed,
attacks: { ...this.attacks },
heals: { ...this.heals },
spells: { ...this.spells },
passives: { ...this.passives },
onDeath: { ...this.onDeath },
sieges: { ...this.sieges },
weaponSwitches: { ...this.weaponSwitches },
};
// Apply turret transformations using new apply() pattern
for (const turret of Object.values(this.turrets)) {
if (turret.apply) {
turret.apply();
}
}
return transformed;
}
/** Apply upgrade transformations and return new instance - chainable! */
withUpgrades(upgradeNames) {
// Create a simple transformed object
const transformed = {
...this,
hp: this.hp,
armor: this.armor,
speed: this.speed,
attacks: { ...this.attacks },
heals: { ...this.heals },
spells: { ...this.spells },
passives: { ...this.passives },
onDeath: { ...this.onDeath },
sieges: { ...this.sieges },
weaponSwitches: { ...this.weaponSwitches },
};
// For now, upgrades work differently than turrets - they're ability stubs
// We'll implement this when we have actual upgrade transformations
// This is a placeholder for the chainable API
return transformed;
}
/** Create a snapshot of this entity for transformation chaining */
createSnapshot() {
// Create a simple object with all current values
// this is stupid for the same reason the thing above is stupid
// at the very least we should be able to just call .toJSON() :p
return {
...this,
attacks: { ...this.attacks },
heals: { ...this.heals },
spells: { ...this.spells },
passives: { ...this.passives },
onDeath: { ...this.onDeath },
sieges: { ...this.sieges },
weaponSwitches: { ...this.weaponSwitches },
turrets: { ...this.turrets },
sacrifices: { ...this.sacrifices },
};
}
/**
* Add tags to this entity.
* @param tags - Tags to add to the entity
*/
tag(...tags) {
this._addTags(...tags);
}
// Lore getter/setter with text normalization
get lore() {
return this._lore;
}
set lore(value) {
this._lore = this.normalizeText(value);
}
// Description getter/setter with text normalization
get description() {
return this._description;
}
set description(value) {
this._description = this.normalizeText(value);
}
/** Check if entity has ability of specific type */
hasAbilityType(type) {
for (const attack of Object.values(this.attacks)) {
if (attack.abilityType === type)
return true;
}
for (const heal of Object.values(this.heals)) {
if (heal.abilityType === type)
return true;
}
for (const spell of Object.values(this.spells)) {
if (spell.abilityType === type)
return true;
}
for (const passive of Object.values(this.passives)) {
if (passive.abilityType === type)
return true;
}
for (const onDeath of Object.values(this.onDeath)) {
if (onDeath.abilityType === type)
return true;
}
for (const siege of Object.values(this.sieges)) {
if (siege.abilityType === type)
return true;
}
for (const weaponSwitch of Object.values(this.weaponSwitches)) {
if (weaponSwitch.abilityType === type)
return true;
}
return false;
}
/**
* JSON.stringify() calls this automatically
*/
toJSON() {
return {
id: this.id,
slug: this.slug,
name: this.name,
shortName: this.shortName,
description: this.description,
lore: this.lore,
type: this.type,
subtype: this.subtype,
faction: this.faction,
factionName: this.factionName,
tier: this.tier,
tags: this.tags,
typeDesc: this.typeDesc,
zsggPath: this.zsggPath,
inGame: this.inGame,
fromFuture: this.fromFuture,
maxOwned: this.maxOwned,
attacks: this.attacks,
heals: this.heals,
spells: this.spells,
passives: this.passives,
onDeath: this.onDeath,
sieges: this.sieges,
weaponSwitches: this.weaponSwitches,
turrets: this.turrets,
upgrades: this.upgrades,
children: this.children,
// Game stats
uuid: this.uuid,
hexiteCost: this.hexiteCost,
fluxCost: this.fluxCost,
buildCount: this.buildCount,
cooldown: this.cooldown,
energyCost: this.energyCost,
energyCostType: this.energyCostType,
buildTime: this.buildTime,
rebuildable: this.rebuildable,
rebuildTime: this.rebuildTime,
domain: this.domain,
hotkey: this.hotkey,
timedLife: this.timedLife,
gathersFlux: this.gathersFlux,
gathersRichFlux: this.gathersRichFlux,
gathersHexite: this.gathersHexite,
gathersEmptyHexite: this.gathersEmptyHexite,
supply: this.supply,
hp: this.hp,
vision: this.vision,
speed: this.speed,
shields: this.shields,
abesEnergy: this.abesEnergy,
energy: this.energy,
startingEnergy: this.startingEnergy,
armor: this.armor,
armorType: this.armorType,
stunResist: this.stunResist,
providesSupply: this.providesSupply,
providesBiomass: this.providesBiomass,
providesDetection: this.providesDetection,
providesUpgradesFor: this.providesUpgradesFor,
upgradedBy: this.upgradedBy,
untargetable: this.untargetable,
carryCapacity: this.carryCapacity,
maxAddOns: this.maxAddOns,
creates: this.creates,
createdBy: this.createdBy,
unlocks: this.unlocks,
unlockedBy: this.unlockedBy,
unlocksMercTier: this.unlocksMercTier,
// Internal game engine data NOT exported - accessed directly from TypeScript instances
// (internalId, internalPath, internalTags, internalSecondaryTags)
// Additional dev data stats NOT exported - accessed directly from TypeScript instances
// (turnSpeed, pushability, healthRegen)
};
}
}
exports.Entity = Entity;
//# sourceMappingURL=entity.js.map