import regions.regions; from resources import MoneyType; import object_creation; import orbitals; import saving; import util.target_search; const int STRATEGIC_RING = -1; const double RECOVERY_TIME = 3.0 * 60.0; const double COMBAT_RECOVER_RATE = 0.25; tidy class OrbitalScript { OrbitalNode@ node; StrategicIconNode@ icon; OrbitalRequirements reqs; Object@ lastHitBy; OrbitalSection@ core; array sections; int nextSectionId = 1; int contestion = 0; bool isFree = false; bool delta = false; bool deltaHP = false; bool deltaOrbit = false; bool disabled = false; double Health = 0; double MaxHealth = 0; double Armor = 0; double MaxArmor = 0; double DR = 2.5; double DPS = 0; Orbital@ master; void save(Orbital& obj, SaveFile& file) { saveObjectStates(obj, file); uint cnt = sections.length; file << cnt; for(uint i = 0; i < cnt; ++i) file << sections[i]; file << nextSectionId; file << Health; file << MaxHealth; file << Armor; file << MaxArmor; file << DR; file << contestion; file << disabled; file << DPS; file << obj.usingLabor; file << isFree; file << master; file << cast(obj.Resources); file << cast(obj.Orbit); file << cast(obj.Statuses); if(obj.hasConstruction) { file << true; file << cast(obj.Construction); } else { file << false; } if(obj.hasLeaderAI) { file << true; file << cast(obj.LeaderAI); } else { file << false; } if(obj.hasAbilities) { file << true; file << cast(obj.Abilities); } else { file << false; } if(obj.hasCargo) { file << true; file << cast(obj.Cargo); } else { file << false; } file << cast(obj.Mover); } void load(Orbital& obj, SaveFile& file) { loadObjectStates(obj, file); uint cnt = 0; file >> cnt; sections.length = cnt; for(uint i = 0; i < cnt; ++i) @sections[i] = OrbitalSection(file); if(sections.length != 0) @core = sections[0]; file >> nextSectionId; file >> Health; file >> MaxHealth; file >> Armor; file >> MaxArmor; file >> DR; if(file >= SV_0014) { file >> contestion; file >> disabled; } if(file >= SV_0042) { file >> DPS; file >> obj.usingLabor; } if(file >= SV_0068) file >> isFree; if(file >= SV_0149) file >> master; file >> cast(obj.Resources); file >> cast(obj.Orbit); file >> cast(obj.Statuses); bool has = false; file >> has; if(has) { obj.activateConstruction(); file >> cast(obj.Construction); } file >> has; if(has) { obj.activateLeaderAI(); file >> cast(obj.LeaderAI); } if(file >= SV_0093) { file >> has; if(has) { obj.activateAbilities(); file >> cast(obj.Abilities); } } if(file >= SV_0125) { file >> has; if(has) { obj.activateCargo(); file >> cast(obj.Cargo); } } if(file >= SV_0108) file >> cast(obj.Mover); else obj.maxAcceleration = 0; } void makeFree(Orbital& obj) { if(isFree) return; for(uint i = 0, cnt = sections.length; i < cnt; ++i) { auto@ sec = sections[i]; if(sec.type.maintenance != 0 && obj.owner !is null && obj.owner.valid) obj.owner.modMaintenance(-sec.type.maintenance, MoT_Orbitals); } isFree = true; } void postInit(Orbital& obj) { obj.maxAcceleration = 0; obj.hasVectorMovement = true; obj.activateLeaderAI(); obj.leaderInit(); } Orbital@ getMaster() { return master; } bool hasMaster() { return master !is null; } bool isMaster(Object@ obj) { return master is obj; } void setMaster(Orbital@ newMaster) { @master = newMaster; delta = true; } void checkOrbit(Orbital& obj) { Region@ prevRegion = obj.region; if(updateRegion(obj)) { Region@ newRegion = obj.region; if(icon !is null) { if(prevRegion !is null) prevRegion.removeStrategicIcon(STRATEGIC_RING, icon); if(newRegion !is null) newRegion.addStrategicIcon(STRATEGIC_RING, obj, icon); } for(uint i = 0, cnt = sections.length; i < cnt; ++i) { if(sections[i].enabled) sections[i].regionChange(obj, prevRegion, newRegion); } obj.changeResourceRegion(prevRegion, newRegion); obj.changeStatusRegion(prevRegion, newRegion); @prevRegion = newRegion; } Region@ reg = obj.region; if(reg !is null) { Object@ orbObj = reg.getOrbitObject(obj.position); if(orbObj !is null) obj.orbitAround(orbObj); else obj.orbitAround(reg.starRadius + obj.radius, reg.position); deltaOrbit = true; } } void postLoad(Orbital& obj) { if(core !is null) { auto@ type = core.type; @node = cast(bindNode(obj, "OrbitalNode")); if(node !is null) node.establish(obj, type.id); if(type.strategicIcon.valid) { @icon = StrategicIconNode(); if(type.strategicIcon.sheet !is null) icon.establish(obj, type.iconSize, type.strategicIcon.sheet, type.strategicIcon.index); else if(type.strategicIcon.mat !is null) icon.establish(obj, type.iconSize, type.strategicIcon.mat); if(obj.region !is null) obj.region.addStrategicIcon(STRATEGIC_RING, obj, icon); } } for(uint i = 0, cnt = sections.length; i < cnt; ++i) sections[i].makeGraphics(obj, node); obj.resourcesPostLoad(); if(obj.hasLeaderAI) obj.leaderPostLoad(); } double get_dps() { return DPS; } double get_efficiency() { return clamp(Health / max(1.0, MaxHealth), 0.0, 1.0); } void modDPS(double mod) { DPS += mod; deltaHP = true; } void _write(const Orbital& obj, Message& msg) { uint cnt = sections.length; msg.writeSmall(cnt); for(uint i = 0; i < cnt; ++i) msg << sections[i]; msg << contestion; msg << disabled; msg << master; } void _writeHP(const Orbital& obj, Message& msg) { msg << Health; msg << MaxHealth; msg << Armor; msg << MaxArmor; msg << DR; msg << DPS; } void syncInitial(const Orbital& obj, Message& msg) { _write(obj, msg); _writeHP(obj, msg); obj.writeResources(msg); obj.writeOrbit(msg); obj.writeStatuses(msg); obj.writeMover(msg); if(obj.hasConstruction) { msg.write1(); obj.writeConstruction(msg); } else { msg.write0(); } if(obj.hasLeaderAI) { msg.write1(); obj.writeLeaderAI(msg); } else { msg.write0(); } if(obj.hasAbilities) { msg.write1(); obj.writeAbilities(msg); } else { msg.write0(); } if(obj.hasCargo) { msg.write1(); obj.writeCargo(msg); } else { msg.write0(); } } bool syncDelta(const Orbital& obj, Message& msg) { bool used = false; if(delta) { used = true; delta = false; msg.write1(); _write(obj, msg); } else msg.write0(); if(deltaHP) { used = true; deltaHP = false; msg.write1(); _writeHP(obj, msg); } else msg.write0(); if(deltaOrbit) { used = true; deltaOrbit = false; msg.write1(); obj.writeOrbit(msg); } else msg.write0(); if(obj.writeResourceDelta(msg)) used = true; else msg.write0(); if(obj.hasConstruction && obj.writeConstructionDelta(msg)) used = true; else msg.write0(); if(obj.hasLeaderAI && obj.writeLeaderAIDelta(msg)) used = true; else msg.write0(); if(obj.hasAbilities && obj.writeAbilityDelta(msg)) used = true; else msg.write0(); if(obj.hasCargo && obj.writeCargoDelta(msg)) used = true; else msg.write0(); if(obj.writeStatusDelta(msg)) used = true; else msg.write0(); if(obj.writeOrbitDelta(msg)) used = true; else msg.write0(); if(obj.writeMoverDelta(msg)) used = true; else msg.write0(); return used; } void syncDetailed(const Orbital& obj, Message& msg) { _write(obj, msg); _writeHP(obj, msg); obj.writeResources(msg); obj.writeOrbit(msg); obj.writeStatuses(msg); obj.writeMover(msg); if(obj.hasConstruction) { msg.write1(); obj.writeConstruction(msg); } else { msg.write0(); } if(obj.hasLeaderAI) { msg.write1(); obj.writeLeaderAI(msg); } else { msg.write0(); } if(obj.hasAbilities) { msg.write1(); obj.writeAbilities(msg); } else { msg.write0(); } if(obj.hasCargo) { msg.write1(); obj.writeCargo(msg); } else { msg.write0(); } } void modMaxArmor(double value) { MaxArmor += value; Armor = clamp(Armor, 0, MaxArmor); deltaHP = true; } void modMaxHealth(double value) { MaxHealth += value; Health = clamp(Health, 0, MaxHealth); deltaHP = true; } void modDR(double value) { DR += value; deltaHP = true; } double getValue(Player& pl, Orbital& obj, uint id) { double value = 0.0; for(uint i = 0, cnt = sections.length; i < cnt; ++i) { auto@ sec = sections[i]; if(!sec.enabled) continue; for(uint j = 0, jcnt = sec.type.hooks.length; j < jcnt; ++j) { if(sec.type.hooks[j].getValue(pl, obj, sec.data[j], id, value)) return value; } } return 0.0; } const Design@ getDesign(Player& pl, Orbital& obj, uint id) { const Design@ value; for(uint i = 0, cnt = sections.length; i < cnt; ++i) { auto@ sec = sections[i]; if(!sec.enabled) continue; for(uint j = 0, jcnt = sec.type.hooks.length; j < jcnt; ++j) { if(sec.type.hooks[j].getDesign(pl, obj, sec.data[j], id, value)) return value; } } return null; } Object@ getObject(Player& pl, Orbital& obj, uint id) { Object@ value; for(uint i = 0, cnt = sections.length; i < cnt; ++i) { auto@ sec = sections[i]; if(!sec.enabled) continue; for(uint j = 0, jcnt = sec.type.hooks.length; j < jcnt; ++j) { if(sec.type.hooks[j].getObject(pl, obj, sec.data[j], id, value)) return value; } } return null; } void sendValue(Player& pl, Orbital& obj, uint id, double value) { if(!obj.valid || obj.destroying) return; for(uint i = 0, cnt = sections.length; i < cnt; ++i) { auto@ sec = sections[i]; if(!sec.enabled) continue; for(uint j = 0, jcnt = sec.type.hooks.length; j < jcnt; ++j) { if(sec.type.hooks[j].sendValue(pl, obj, sec.data[j], id, value)) return; } } } void sendDesign(Player& pl, Orbital& obj, uint id, const Design@ value) { if(!obj.valid || obj.destroying) return; for(uint i = 0, cnt = sections.length; i < cnt; ++i) { auto@ sec = sections[i]; if(!sec.enabled) continue; for(uint j = 0, jcnt = sec.type.hooks.length; j < jcnt; ++j) { if(sec.type.hooks[j].sendDesign(pl, obj, sec.data[j], id, value)) return; } } } void sendObject(Player& pl, Orbital& obj, uint id, Object@ value) { if(!obj.valid || obj.destroying) return; for(uint i = 0, cnt = sections.length; i < cnt; ++i) { auto@ sec = sections[i]; if(!sec.enabled) continue; for(uint j = 0, jcnt = sec.type.hooks.length; j < jcnt; ++j) { if(sec.type.hooks[j].sendObject(pl, obj, sec.data[j], id, value)) return; } } } void triggerDelta() { delta = true; } double get_health(Orbital& orb) { double v = Health; Empire@ owner = orb.owner; if(owner !is null) v *= owner.OrbitalHealthMod; return v; } double get_maxHealth(Orbital& orb) { double v = MaxHealth; Empire@ owner = orb.owner; if(owner !is null) v *= owner.OrbitalHealthMod; return v; } double get_armor(Orbital& orb) { double v = Armor; Empire@ owner = orb.owner; if(owner !is null) v *= owner.OrbitalArmorMod; return v; } double get_maxArmor(Orbital& orb) { double v = MaxArmor; Empire@ owner = orb.owner; if(owner !is null) v *= owner.OrbitalArmorMod; return v; } void addSection(Orbital& obj, uint typeId) { auto@ type = getOrbitalModule(typeId); if(type is null) return; OrbitalSection sec(type); sec.id = nextSectionId++; sections.insertLast(sec); if(type.isCore && core is null) { @node = cast(bindNode(obj, "OrbitalNode")); if(node !is null) node.establish(obj, type.id); @core = sec; obj.orbitSpin(type.spin); if(type.isStandalone) obj.setImportEnabled(false); if(type.strategicIcon.valid) { @icon = StrategicIconNode(); if(type.strategicIcon.sheet !is null) icon.establish(obj, type.iconSize, type.strategicIcon.sheet, type.strategicIcon.index); else if(type.strategicIcon.mat !is null) icon.establish(obj, type.iconSize, type.strategicIcon.mat); if(obj.region !is null) obj.region.addStrategicIcon(STRATEGIC_RING, obj, icon); } obj.noCollide = !type.isSolid; MaxHealth = type.health; MaxArmor = type.armor; Health = MaxHealth; Armor = MaxArmor; if(disabled) { Health *= 0.25; Armor *= 0.25; } } sec.create(obj); if(sec is core && !this.disabled) sec.enable(obj); else sec.enabled = false; sec.makeGraphics(obj, node); checkSections(obj); delta = true; if(type.maintenance != 0 && obj.owner !is null && obj.owner.valid && !isFree) obj.owner.modMaintenance(type.maintenance, MoT_Orbitals); } void checkSections(Orbital& obj) { reqs.init(obj, direct=true); for(uint i = 0, cnt = sections.length; i < cnt; ++i) { auto@ sec = sections[i]; if(sec.enabled) { if(this.disabled || sec.shouldDisable(obj)) { sec.disable(obj); delta = true; if(sec.type.health != 0 || sec.type.armor != 0) { if(sec !is core) { deltaHP = true; MaxHealth -= sec.type.health; Health = min(Health, MaxHealth); MaxArmor -= sec.type.armor; Armor = min(Armor, MaxArmor); } } } else { if(!reqs.add(sec.type)) { sec.disable(obj); delta = true; if(sec.type.health != 0 || sec.type.armor != 0) { if(sec !is core) { deltaHP = true; MaxHealth -= sec.type.health; Health = min(Health, MaxHealth); MaxArmor -= sec.type.armor; Armor = min(Armor, MaxArmor); } } } } } else { if(!this.disabled && sec.shouldEnable(obj)) { if(reqs.add(sec.type)) { sec.enable(obj); delta = true; if(sec.type.health != 0 || sec.type.armor != 0) { if(sec !is core) { MaxHealth += sec.type.health; MaxArmor += sec.type.armor; deltaHP = true; } } } } } } } void getSections() { for(uint i = 0, cnt = sections.length; i < cnt; ++i) yield(sections[i]); } bool hasModule(uint typeId) { for(uint i = 0, cnt = sections.length; i < cnt; ++i) { auto@ sec = sections[i]; if(sec.type.id == typeId) return true; } return false; } //Remote player-accessible void buildModule(Orbital& obj, uint typeId) { if(core is null || core.type.isStandalone) return; auto@ type = getOrbitalModule(typeId); if(type is null) return; if(!type.canBuildOn(obj)) return; if(type.buildCost != 0) { if(obj.owner.consumeBudget(type.buildCost) == -1) return; } addSection(obj, typeId); } void destroyModule(Orbital& obj, int id) { if(contestion != 0) return; for(uint i = 0, cnt = sections.length; i < cnt; ++i) { auto@ sec = sections[i]; if(sec.id == id) { //Can't destroy the core, silly if(sec is core) return; if(sec.enabled) sec.disable(obj); sec.destroy(obj); if(sec.type.maintenance != 0 && obj.owner !is null && obj.owner.valid && !isFree) obj.owner.modMaintenance(-sec.type.maintenance, MoT_Orbitals); MaxHealth -= sec.type.health; Health = min(Health, MaxHealth); MaxArmor -= sec.type.armor; Armor = min(Armor, MaxArmor); sections.removeAt(i); checkSections(obj); delta = true; deltaHP = true; return; } } } void scuttle(Orbital& obj) { if(contestion != 0) return; obj.destroy(); } uint get_coreModule() { auto@ mod = core; if(mod is null) return uint(-1); return mod.type.id; } bool get_isStandalone() { auto@ mod = core; if(mod is null) return true; return mod.type.isStandalone; } bool get_isContested() { return contestion != 0; } bool get_isDisabled() { return disabled || (core !is null && !core.enabled); } void setContested(bool value) { if(value) contestion += 1; else contestion -= 1; delta = true; } void setDisabled(bool value) { disabled = value; delta = true; } void destroy(Orbital& obj) { if(obj.inCombat && !game_ending) playParticleSystem("ShipExplosion", obj.position, obj.rotation, obj.radius, obj.visibleMask); for(uint i = 0, cnt = sections.length; i < cnt; ++i) { auto@ sec = sections[i]; if(sec.enabled) sec.disable(obj); sec.destroy(obj); if(sec.type.maintenance != 0 && obj.owner !is null && obj.owner.valid && !isFree) obj.owner.modMaintenance(-sec.type.maintenance, MoT_Orbitals); } if(icon !is null) { if(obj.region !is null) obj.region.removeStrategicIcon(STRATEGIC_RING, icon); icon.markForDeletion(); @icon = null; } @node = null; leaveRegion(obj); obj.destroyObjResources(); if(obj.hasConstruction) obj.destroyConstruction(); if(obj.hasAbilities) obj.destroyAbilities(); if(obj.hasLeaderAI) obj.leaderDestroy(); if(obj.owner !is null && obj.owner.valid) obj.owner.unregisterOrbital(obj); } bool onOwnerChange(Orbital& obj, Empire@ prevOwner) { regionOwnerChange(obj, prevOwner); obj.changeResourceOwner(prevOwner); for(uint i = 0, cnt = sections.length; i < cnt; ++i) { auto@ sec = sections[i]; if(sec.enabled) sec.ownerChange(obj, prevOwner, obj.owner); if(sec.type.maintenance != 0 && !isFree) { if(prevOwner !is null && prevOwner.valid) prevOwner.modMaintenance(-sec.type.maintenance, MoT_Orbitals); if(obj.owner !is null && obj.owner.valid) obj.owner.modMaintenance(sec.type.maintenance, MoT_Orbitals); } } if(obj.hasLeaderAI) obj.leaderChangeOwner(prevOwner, obj.owner); if(obj.hasConstruction) obj.clearRally(); if(obj.hasAbilities) obj.abilityOwnerChange(prevOwner, obj.owner); obj.changeStatusOwner(prevOwner, obj.owner); if(prevOwner !is null && prevOwner.valid) prevOwner.unregisterOrbital(obj); if(obj.owner !is null && obj.owner.valid) obj.owner.registerOrbital(obj); return false; } float timer = 0.f; double prevFleet = 0.0; float combatTimer = 0.f; void occasional_tick(Orbital& obj) { Region@ prevRegion = obj.region; if(updateRegion(obj)) { Region@ newRegion = obj.region; if(icon !is null) { if(prevRegion !is null) prevRegion.removeStrategicIcon(STRATEGIC_RING, icon); if(newRegion !is null) newRegion.addStrategicIcon(STRATEGIC_RING, obj, icon); } for(uint i = 0, cnt = sections.length; i < cnt; ++i) { if(sections[i].enabled) sections[i].regionChange(obj, prevRegion, newRegion); } obj.changeResourceRegion(prevRegion, newRegion); obj.changeStatusRegion(prevRegion, newRegion); @prevRegion = newRegion; } if(icon !is null) icon.visible = obj.isVisibleTo(playerEmpire); //Update in combat flags bool engaged = obj.engaged; if(engaged) combatTimer = 20.f; else combatTimer -= 1.f; obj.inCombat = combatTimer > 0.f; obj.engaged = false; if(engaged && prevRegion !is null) prevRegion.EngagedMask |= obj.owner.mask; if(node !is null) { double rad = 0.0; if(obj.hasLeaderAI && obj.SupplyCapacity > 0) rad = obj.getFormationRadius(); if(rad != prevFleet) { node.setFleetPlane(rad); prevFleet = rad; } } if(obj.hasLeaderAI) obj.updateFleetStrength(); //Order support ships to attack if(combatTimer > 0.f) { if(obj.hasLeaderAI && obj.supportCount > 0) { Object@ target = findEnemy(obj, obj, obj.owner, obj.position, 700.0); if(target !is null) { //Always target the fleet as a whole { Ship@ othership = cast(target); if(othership !is null) { Object@ leader = othership.Leader; if(leader !is null) @target = leader; } } //Order a random support to assist uint cnt = obj.supportCount; if(cnt > 0) { uint attackWith = max(1, cnt / 8); for(uint i = 0, off = randomi(0,cnt-1); i < attackWith; ++i) { Object@ sup = obj.supportShip[(i+off) % cnt]; if(sup !is null) sup.supportAttack(target); } } } } } else { @lastHitBy = null; } //Update module requirements checkSections(obj); } vec3d get_strategicIconPosition(const Orbital& obj) { if(icon is null) return obj.position; return icon.position; } void repairOrbital(Orbital& obj, double amount) { double armorMod = 1.0, healthMod = 1.0; double armor = Armor, health = Health, maxArmor = MaxArmor, maxHealth = MaxHealth; if(obj.owner !is null) { armorMod = obj.owner.OrbitalArmorMod; healthMod = obj.owner.OrbitalHealthMod; armor *= armorMod; health *= healthMod; maxArmor *= armorMod; maxHealth *= healthMod; } double toArmor = min(maxArmor - armor, amount); armor = min(armor + toArmor, maxArmor); health = min(health + amount - toArmor, maxHealth); deltaHP = true; Armor = armor / armorMod; Health = health / healthMod; } void damage(Orbital& obj, DamageEvent& evt, double position, const vec2d& direction) { if(!obj.valid || obj.destroying) return; double armorMod = 1.0, healthMod = 1.0; double armor = Armor, health = Health; if(obj.owner !is null) { armorMod = obj.owner.OrbitalArmorMod; healthMod = obj.owner.OrbitalHealthMod; armor *= armorMod; health *= healthMod; } obj.engaged = true; if(armor > 0) { evt.damage = max(0.2 * evt.damage, evt.damage - DR); double dealArmor = min(evt.damage, armor); armor = max(0.0, armor - dealArmor); deltaHP = true; } if(evt.damage > health) { evt.damage -= health; health = 0.0; obj.destroy(); Empire@ killer; if(evt.obj !is null) @killer = evt.obj.owner; for(uint i = 0, cnt = sections.length; i < cnt; ++i) { if(sections[i].enabled) sections[i].kill(obj, killer); } return; } if(evt.obj !is null) { if(lastHitBy !is evt.obj && obj.hasLeaderAI) { //Order a random support to block, and another to attack uint cnt = obj.supportCount; if(cnt > 0) { uint ind = randomi(0,cnt-1); Object@ sup = obj.supportShip[ind]; if(sup !is null) sup.supportInterfere(lastHitBy, obj); if(cnt > 1) { @sup = obj.supportShip[ind+1]; if(sup !is null) sup.supportAttack(lastHitBy); } } } @lastHitBy = evt.obj; } health -= evt.damage; deltaHP = true; Armor = armor / armorMod; Health = health / healthMod; } void setBuildPct(Orbital& obj, double pct) { if(obj.inCombat) return; Health = (0.01 + pct * 0.99) * MaxHealth; Armor = (0.01 + pct * 0.99) * MaxArmor; deltaHP = true; } double tick(Orbital& obj, double time) { //Take vision from region if(obj.region !is null) obj.donatedVision |= obj.region.DonateVisionMask; //Tick sections for(uint i = 0, cnt = sections.length; i < cnt; ++i) { if(sections[i].enabled) sections[i].tick(obj, time); } //Tick construction double delay = 0.2; if(obj.hasConstruction) { obj.constructionTick(time); if(obj.hasConstructionUnder(0.2)) delay = 0.0; } //Tick resources obj.resourceTick(time); //Tick orbit obj.moverTick(time); //Tick status obj.statusTick(time); //Tick fleet if(obj.hasLeaderAI) { obj.leaderTick(time); obj.orderTick(time); } //Tick abilities if(obj.hasAbilities) obj.abilityTick(time); //Tick occasional stuff timer -= float(time); if(timer <= 0.f) { occasional_tick(obj); timer = 1.f; } //Repair if(!disabled && ((core !is null && core.type.combatRepair) || !obj.inCombat)) { double recover = time * ((MaxHealth + MaxArmor) / RECOVERY_TIME); if(obj.inCombat) recover *= COMBAT_RECOVER_RATE; if(Health < MaxHealth) { double take = min(recover, MaxHealth - Health); Health = clamp(Health + take, 0, MaxHealth); recover -= take; deltaHP = true; } if(recover > 0 && Armor < MaxArmor) { Armor = clamp(Armor + recover, 0, MaxArmor); deltaHP = true; } } return delay; } };