UNPKG

@zerospacegg/iolin

Version:

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

354 lines 12.5 kB
/** * Unit - RTS unit class for ZeroSpace units * Ported from unit.pkl with functional constructor pattern */ import { Entity } from "./entity.js"; export const InfuseState = { none: 0, single: 1, double: 2, }; export const ReanimateState = { none: 0, alive: 1, zombie: 2, }; /** * RTS Unit class base - extends Entity with unit-specific properties * Usage: new ArmyUnit("Stinger", (unit) => { unit.hp = 100; }) */ export class Unit extends Entity { get type() { return "unit"; } // Ugly accessor methods for transformation state _getInfused() { return this._infused; } _setInfused(value) { this._infused = value; } _getReanimated() { return this._reanimated; } _setReanimated(value) { this._reanimated = value; } constructor(props) { super(props); // Transformation state using private fields with ugly accessor methods this._infused = InfuseState.none; this._reanimated = ReanimateState.none; this.vision = 1600; // Default vision for all units this.pushability = 1; // Default pushability for all units - determines pathing priority // Add unit-specific dynamic tags to avoid circular dependency this.dynamicTaggers.push([(e) => e instanceof Unit && e.canBeInfused, "can-be-infused"], [(e) => e instanceof Unit && e.canBeReanimated, "can-be-reanimated"], [(e) => e instanceof Unit && e.canBeMindControlled, "can-be-mind-controlled"], // Transformation state tags [(e) => e instanceof Unit && e._getInfused() >= InfuseState.single, "infused"], [ (e) => e instanceof Unit && e._getInfused() === InfuseState.double, "double-infused", ], [ (e) => e instanceof Unit && e._getReanimated() === ReanimateState.alive, "reanimated:alive", ], [ (e) => e instanceof Unit && e._getReanimated() === ReanimateState.zombie, "reanimated:zombie", ], // Unit type tags from internal dev data [ (e) => (e instanceof Unit && e.internalTags?.includes("Attackable.Biological")) ?? false, "unit:bio", ], [ (e) => (e instanceof Unit && e.internalSecondaryTags?.includes("Mech")) ?? false, "unit:mech", ], [ (e) => (e instanceof Unit && e.internalSecondaryTags?.includes("Vehicle")) ?? false, "unit:vehicle", ], [ (e) => (e instanceof Unit && e.internalSecondaryTags?.includes("Infantry")) ?? false, "unit:infantry", ]); } get subtype() { return this.unitType; } // Computed properties from PKL get canBeInfused() { if (this.canBeInfusedOverride !== undefined) { return this.canBeInfusedOverride; } // Can't infuse beyond double infusion if (this._infused >= InfuseState.double) { return false; } // Check static tags only to avoid circular dependency return (this.unitType === "army" || this.unitType === "merc") && !this.manualTags.includes("massive"); } get canBeReanimated() { return this.unitType === "army" || this.unitType === "merc" || this.unitType === "hero"; } get canBeMindControlled() { return this.unitType !== "hero"; } // Cost calculation as getters from PKL get reanimateCost() { if (!this.canBeReanimated) { return undefined; } const h = this.hexiteCost ?? 0; const f = (this.fluxCost ?? 0) * 1.25; const n = (h + f) * 0.0675; return Math.round(n * 10) / 10; } get infuseCost() { if (!this.canBeInfused) { return undefined; } const s = this.supply ?? 0; return Math.floor(s * 2); } /** Get all child entity IDs for ecosystem linking - includes upgrades */ get children() { const children = super.children; // Add all upgrade IDs with their navigation paths for (const [key, upgrade] of Object.entries(this.upgrades)) { if (upgrade.id) children[upgrade.id] = ["upgrades", key]; } return children; } /** * JSON.stringify() calls this automatically */ toJSON() { return { ...super.toJSON(), unitType: this.unitType, canBeInfused: this.canBeInfused, canBeReanimated: this.canBeReanimated, canBeMindControlled: this.canBeMindControlled, reanimateCost: this.reanimateCost, infuseCost: this.infuseCost, }; } // Ugly transformation methods that actually work _applyInfusion(value) { if (value !== this._infused) { if (value === InfuseState.none) { // Reset to base stats const base = new this.constructor(); this.hp = base.hp; // Reset attack damage for (const [key, attack] of Object.entries(this.attacks)) { if (base.attacks[key]?.damage) { attack.damage = base.attacks[key].damage; } } } else { // Apply infusion multipliers const multiplier = value === InfuseState.double ? 2.25 : 1.5; // 1.5x for single, 2.25x for double // Apply to HP if (this._infused === InfuseState.none) { this.hp = Math.floor((this.hp ?? 0) * multiplier); } else { // Transitioning from single to double this.hp = Math.floor((this.hp ?? 0) * 1.5); // 1.5 * 1.5 = 2.25 } // Apply to attack damage const damageMultiplier = value === InfuseState.double ? 2.25 : 1.5; for (const attack of Object.values(this.attacks)) { if (attack.damage) { if (this._infused === InfuseState.none) { attack.damage = Math.floor(attack.damage * damageMultiplier); } else { // Transitioning from single to double attack.damage = Math.floor(attack.damage * 1.5); } } } } this._infused = value; } } _applyReanimation(value) { if (value !== this._reanimated) { if (value === ReanimateState.none) { // Reset to base stats const base = new this.constructor(); this.hp = base.hp; this.speed = base.speed; // Reset attack speed for (const [key, attack] of Object.entries(this.attacks)) { if (base.attacks[key]?.cooldown) { attack.cooldown = base.attacks[key].cooldown; } } } else { // Apply reanimation effects const hpMultiplier = value === ReanimateState.zombie ? 0.7 : 1.3; // Zombie penalty or alive bonus const speedMultiplier = value === ReanimateState.zombie ? 0.7 : 1.3; const attackSpeedMultiplier = value === ReanimateState.zombie ? 0.7 : 1.3; // Apply to HP and speed if (this._reanimated === ReanimateState.none) { this.hp = Math.floor((this.hp ?? 0) * hpMultiplier); this.speed = Math.floor((this.speed ?? 0) * speedMultiplier); } // Apply to attack speed (reduce cooldown for faster attacks) for (const attack of Object.values(this.attacks)) { if (attack.cooldown) { if (this._reanimated === ReanimateState.none) { attack.cooldown = attack.cooldown / attackSpeedMultiplier; } } } } this._reanimated = value; } } /** * Infuse this unit (chainable method) * No-ops if unit is not infusable */ infuse(doubleInfuse = false) { if (!this.canBeInfused) { return this; // No-op if not infusable } this._applyInfusion(doubleInfuse ? InfuseState.double : InfuseState.single); return this; } /** * Reanimate this unit (chainable method) * No-ops if unit is not reanimatable */ reanimate(zombieMode = false) { if (!this.canBeReanimated) { return this; // No-op if not reanimatable } if (zombieMode) { this._applyInfusion(InfuseState.none); // Zombie loses infusion first this._applyReanimation(ReanimateState.zombie); // Then becomes zombie } else { this._applyReanimation(ReanimateState.alive); // Alive reanimation } return this; } } export class ArmyUnit extends Unit { constructor(props) { super(props); } get unitType() { return "army"; } } export class HeroUnit extends Unit { constructor() { super(...arguments); this.supply = 10; // All heroes cost 10 supply this.buildTime = 20; // All heroes take 20 seconds to build this.rebuildable = true; // All heroes can be rebuilt when they die this.rebuildTime = 60; // All heroes take 60 seconds to rebuild } get unitType() { return "hero"; } } export class CoopCommanderUnit extends Unit { get unitType() { return "coop-commander"; } constructor() { super(); this.tier = "T2"; // All coop commanders are T2 like heroes // Commander progression system this.maxLevel = 15; // Default max level for commanders this.currentLevel = 1; // Starting level this.commanderTopbars = {}; this.levels = {}; } // Auto-populate children with levels and topbars - kawaii style! (◕‿◕)♡ get children() { const children = super.children; // Add all commander levels for (const [key, level] of Object.entries(this.levels)) { if (level && level.id) children[level.id] = ["levels", key]; } // Add all topbar abilities for (const [key, topbar] of Object.entries(this.commanderTopbars)) { if (topbar && topbar.id) children[topbar.id] = ["commanderTopbars", key]; } return children; } /** Get topbars available at current level */ getAvailableTopbars() { if (!this.commanderTopbars) { this.commanderTopbars = {}; } const available = {}; for (const [name, topbar] of Object.entries(this.commanderTopbars)) { if (!topbar.requiredLevel || topbar.requiredLevel <= this.currentLevel) { available[name] = topbar; } } return available; } /** Set commander level (affects available topbars) - chainable! */ withLevel(level) { this.currentLevel = Math.min(Math.max(1, level), this.maxLevel); return this; } toJSON() { return { ...super.toJSON(), commanderType: this.commanderType, commanderFaction: this.commanderFaction, commanderTopbars: this.commanderTopbars, levels: this.levels, maxLevel: this.maxLevel, currentLevel: this.currentLevel, }; } } export class HarvesterUnit extends Unit { get unitType() { return "harvester"; } } export class BuilderUnit extends Unit { get unitType() { return "builder"; } } export class SpecialUnit extends Unit { get unitType() { return "special"; } } export class UltimateUnit extends Unit { get unitType() { return "ultimate"; } } export class MercUnit extends Unit { get unitType() { return "merc"; } } export class MercTopbarUnit extends Unit { get unitType() { return "merc-topbar"; } } export class MobileMercOutpostUnit extends Unit { get unitType() { return "mobile-merc-outpost"; } } //# sourceMappingURL=unit.js.map