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