import saving; import design_settings; //Time needed for a support ship to expire if left without a leader const double SUPPORT_EXPIRE_TIME = 3.0 * 60.0; //Max distance a support ship can find a leader to attach to const double MAX_LEADER_RESCUE_DIST = 1000.0; const double MAX_LEADER_RESCUE_DIST_SQ = MAX_LEADER_RESCUE_DIST * MAX_LEADER_RESCUE_DIST; const double MAX_SUPPORT_ABANDON_DIST = 2500.0; const double MAX_SUPPORT_ABANDON_DIST_SQ = MAX_SUPPORT_ABANDON_DIST * MAX_SUPPORT_ABANDON_DIST; enum SupportOrder { SO_Idle, SO_Attack, SO_Interfere, SO_Retreat, SO_Raid, SO_AttackBlind }; tidy class SupportAI : Component_SupportAI, Savable { double expire_progress = 0; bool findLeader = true; bool leaderDelta = false; vec3d newOffset; Object@ leader; int leaderId = 0; float psr = randomf(0.f,1.f); float returnTimer = 0.f; uint range = SR_Auto; bool detached = false, freeToRaid = true; double engageRange = -1.0; SupportOrder order = SO_Idle; uint goal = SG_Cannon; Object@ target, relTarget, checkTarget; SupportAI() { } void load(SaveFile& msg) { msg >> expire_progress; msg >> findLeader; msg >> newOffset; if(msg < SV_0054) { order = SO_Attack; //The ship will automatically reset to the correct state engageRange = 75.0; } else { uint o = SO_Idle; msg >> o; order = SupportOrder(o); msg >> target >> relTarget; msg >> engageRange; if(msg < SV_0068 && engageRange < 0.0) engageRange = 75.0; } if(msg >= SV_0120) { msg >> goal; msg >> range; msg >> detached; } else if(range == SR_Auto) { range = SR_Far; } if(msg >= SV_0138) msg >> leaderId; } void supportPostLoad(Object& obj) { if(obj.isShip) @leader = cast(obj).Leader; } void save(SaveFile& msg) { msg << expire_progress; msg << findLeader; msg << newOffset; msg << uint(order); msg << target << relTarget; msg << engageRange; msg << goal; msg << range; msg << detached; msg << leaderId; } int get_LeaderID() { return leaderId; } void supportIdle(Object& obj) { obj.leaderLock = true; order = SO_Idle; @target = null; @relTarget = null; } void supportIdle(Object& obj, bool immediate) { if(detached && !immediate) returnTimer = 6.f; else obj.leaderLock = true; order = SO_Idle; @target = null; @relTarget = null; } void supportAttack(Object& obj, Object@ targ) { auto@ ship = cast(obj); //Ignore attack orders if there are no weapons if(engageRange <= 0 || ship.DPS <= 1.0e-4) return; switch(goal) { case SG_Brawler: //Raid supports freeToRaid = true; case SG_Cavalry: if(ship.MaxSupply > 1.0e-4 || (leader !is null && leader.freeRaiding)) order = SO_Raid; else order = SO_Attack; break; case SG_Artillery: order = SO_Attack; break; case SG_Cannon: if(leader !is null && leader.freeRaiding) { order = SO_Raid; freeToRaid = true; } else order = SO_AttackBlind; break; case SG_Satellite: break; default: return; } if(obj.isShip) { Ship@ ship = cast(obj); if(targ !is null) ship.blueprint.target(obj, targ, TF_Preference); } @target = targ; @relTarget = null; } void supportInterfere(Object& obj, Object@ targ, Object@ protect) { if(goal == SG_Shield) { order = SO_Interfere; @target = targ; @relTarget = protect; } } void cavalryCharge(Object& obj, Object@ targ) { if(goal == SG_Cavalry) { freeToRaid = true; supportAttack(obj, targ); } else { @checkTarget = targ; } } void set_doRaids(bool raid) { freeToRaid = raid; } bool get_isDetached(Object& obj) { return detached; } bool get_isRaiding() { return detached || order == SO_Raid; } void resupplyFromFleet(Object& obj) { Ship@ ship = cast(obj); Ship@ leaderShip = cast(leader); if(ship.Supply < ship.MaxSupply) { if(leaderShip is null) return; double require = ship.MaxSupply - ship.Supply; if(leaderShip.Supply < require) return; leaderShip.consumeSupply(require); ship.Supply = min(ship.MaxSupply, ship.Supply + require); } } void dumpSupplyToFleet(Object& obj) { Ship@ ship = cast(obj); Ship@ leaderShip = cast(leader); if(ship.Supply >= 0.001) { if(leaderShip is null) return; leaderShip.refundSupply(ship.Supply); ship.Supply = 0; } } void supportRetreat(Object& obj) { order = SO_Retreat; @target = null; @relTarget = null; } void set_supportEngageRange(double range) { engageRange = range; } void preventExpire() { expire_progress = -FLOAT_INFINITY; } void supportInit(Object& obj) { Ship@ ship = cast(obj); const Design@ dsg = ship.blueprint.design; auto@ settings = cast(dsg.settings); if(settings !is null) { if(dsg.hasTag(ST_Satellite)) goal = SG_Satellite; else goal = settings.behavior; range = settings.range; } //TODO: Properly resolve auto range here? if(range == SR_Auto) range = SR_Far; } //Complete a leader change received from the leader void completeRegisterLeader(Object& obj, Object@ newLeader) { Ship@ ship = cast(obj); Object@ prevLeader = leader; if(prevLeader !is null) prevLeader.unregisterSupport(obj, false); @ship.Leader = newLeader; if(newLeader !is null) { leaderId = newLeader.id; @leader = newLeader; } else { leaderId = 0; @leader = null; } if(newOffset.lengthSQ > 0.0001) obj.setFleetOffset(newOffset); ship.triggerLeaderChange(prevLeader, newLeader); leaderDelta = true; } void clearLeader(Object& obj, Object@ prevLeader) { Ship@ ship = cast(obj); if(leader is prevLeader) { @ship.Leader = null; @leader = null; leaderId = 0; ship.triggerLeaderChange(prevLeader, null); } } void transferTo(Object& obj, Object@ newLeader) { newOffset = vec3d(); newLeader.registerSupport(obj); Ship@ ship = cast(obj); if(ship !is null && ship.isFree) ship.makeNotFree(); } void transferTo(Object& obj, Object@ newLeader, vec3d offset) { newOffset = offset; newLeader.registerSupport(obj); Ship@ ship = cast(obj); if(ship !is null && ship.isFree) ship.makeNotFree(); } void setFleetOffset(Object& obj, vec3d offset) { Ship@ ship = cast(obj); double fradius = leader.getFormationRadius(); if(offset.length > fradius) return; ship.formationDest.xyz = offset; newOffset = offset; leaderDelta = true; } void supportDestroy(Object& obj) { Ship@ ship = cast(obj); if(leader !is null) { auto@ prevLeader = leader; leader.unregisterSupport(obj, true); @ship.Leader = null; @leader = null; leaderId = 0; ship.triggerLeaderChange(prevLeader, null); } @target = null; @relTarget = null; } void supportScuttle(Object& obj) { Ship@ ship = cast(obj); Object@ prevLeader = leader; if(prevLeader !is null) prevLeader.unregisterSupport(obj, false); findLeader = false; @ship.Leader = null; @leader = null; leaderId = 0; ship.triggerLeaderChange(prevLeader, null); obj.destroy(); leaderDelta = true; } Object@ findNearbyLeader(Object& obj, Object& findFrom, uint depth, int size) { --depth; vec3d pos = obj.position; for(uint i = 0; i < TARGET_COUNT; ++i) { Object@ other = findFrom.targets[i]; if(other.owner is obj.owner) { Object@ check = other; if(check.isShip && check.hasSupportAI && check.LeaderID != 0) @check = cast(check).Leader; if(check !is null && check.hasLeaderAI) { if(pos.distanceToSQ(check.position) < MAX_LEADER_RESCUE_DIST_SQ && check.canTakeSupport(size, pickup=true)) return check; } } if(depth != 0) { @other = findNearbyLeader(obj, other, depth, size); if(other !is null) return other; } } return null; } double evaluateTarget(Object& obj, Object& targ, double maxDistSQ) { Ship@ ship = cast(targ); if(ship is null || !obj.owner.isHostile(targ.owner)) return 0; if(targ.position.distanceToSQ(leader.position) > maxDistSQ) return 0; switch(goal) { case SG_Brawler: return (1.0 + ship.DPS) * (ship.hasSupportAI ? 1.0 : 0.001); case SG_Shield: return (1.0 + ship.DPS); case SG_Cavalry: return (1.0 + ship.DPS) * (ship.hasSupportAI ? 1.0 : 0.001); case SG_Artillery: return (1.0 + ship.DPS) * (ship.hasSupportAI ? 1.0 : 0.001); case SG_Cannon: return ship.hasSupportAI ? 1.0 : 0.001; case SG_Support: return 1.0; default: return 1.0; } return 0; } uint targCycle = 0; Object@ replaceTarget(Object& obj, Object& prev, double maxDistSQ) { Object@ targ = obj.targets[++targCycle % 3]; double b = evaluateTarget(obj, targ, maxDistSQ); if(b <= 0) return prev; double a = evaluateTarget(obj, prev, maxDistSQ); if(a >= b * 0.9) return prev; else return targ; } void supportTick(Object& obj, double time) { Ship@ ship = cast(obj); double abandonDist = MAX_SUPPORT_ABANDON_DIST_SQ; double raidDist = 0; if(leader !is null) { raidDist = leader.raidRange; if(raidDist < 0) { Region@ reg = obj.region; if(reg !is null) { abandonDist = max(abandonDist, sqr(reg.radius)); raidDist = reg.radius; } else raidDist = 0; } else { abandonDist = max(abandonDist, sqr(raidDist * 1.5)); } } //Tick forward the expiration if(leader is null) { detached = true; if(ship.velocity.lengthSQ > 0.01) ship.stopMoving(false); expire_progress += time; if(expire_progress > SUPPORT_EXPIRE_TIME + (double(uint8(obj.id)) / 128.0) - 1.0) { obj.destroy(); return; } } else if(expire_progress > time) { expire_progress -= time; } else { expire_progress = 0; } if(leader !is null) { if(leader.owner !is obj.owner || (ship.position.distanceToSQ(leader.position) > abandonDist && (ship.region is null || ship.region !is leader.region) && !ship.isFTLing && (!leader.isShip || !cast(leader).isFTLing)) && (!detached || order == SO_Idle) && !obj.inCombat) { //We tell the leader we died, as we are being abandoned and will probably die leader.unregisterSupport(obj, true); auto@ prevLeader = leader; @ship.Leader = null; @leader = null; leaderId = 0; ship.triggerLeaderChange(prevLeader, null); } else { double fleetRad = leader.getFormationRadius(); double engageDist = engageRange + fleetRad; { //If the target is out of range, return to the fleet bool wasDetached = detached; detached = leader.position.distanceToSQ(obj.position) > fleetRad * fleetRad * 1.05; if(detached && !wasDetached) resupplyFromFleet(obj); } switch(order) { case SO_Idle: if(!detached && ship.Supply > 1.0e-4) dumpSupplyToFleet(obj); switch(goal) { case SG_Shield: { Object@ targ = ship.blueprint.getCombatTarget(); double quality = 0; for(uint i = 0; i < TARGET_COUNT; ++i) { Object@ t = obj.targets[i]; if(obj.owner.isHostile(t.owner) && (t.isShip || t.isOrbital)) { double q = evaluateTarget(obj, t, INFINITY); if(q > quality) { @targ = t; quality = q; } break; } } if(targ !is null) supportInterfere(obj, leader, targ); } break; case SG_Support: case SG_Cavalry: break; default: { Object@ targ = ship.blueprint.getCombatTarget(); double quality = 0; double maxDist = max(raidDist*raidDist, engageDist*engageDist); for(uint i = 0; i < TARGET_COUNT; ++i) { Object@ t = obj.targets[i]; if(obj.owner.isHostile(t.owner) && (t.isShip || t.isOrbital)) { double q = evaluateTarget(obj, t, maxDist); if(q > quality) { @targ = t; quality = q; } break; } } if(checkTarget !is null) { double q = evaluateTarget(obj, checkTarget, maxDist); if(q > quality) { @targ = checkTarget; quality = q; } @checkTarget = null; } if(targ !is null && obj.owner.isHostile(targ.owner)) supportAttack(obj, targ); } } if(order == SO_Idle) { if(returnTimer > 0.f) { returnTimer -= time; if(returnTimer <= 0.f || !detached) obj.leaderLock = true; } } else { returnTimer = 0.f; } break; case SO_Attack: if(target is null || !target.valid || target.position.distanceToSQ(leader.position) > engageDist*engageDist || ship.DPS < 1.0e-4 || !obj.owner.isHostile(target.owner)) { supportIdle(obj); } else { @target = replaceTarget(obj, target, engageDist*engageDist); vec3d dest = leader.position + obj.internalDestination; //If the target is out of range, return to the fleet if(detached || leader.position.distanceToSQ(target.position) > sqr(engageRange + fleetRad)) { supportIdle(obj); break; } vec3d fireFrom = obj.leaderLock ? obj.position : dest; bool relocate = fireFrom.distanceToSQ(target.position) > engageRange * engageRange || fireFrom.distanceToSQ(leader.position) > fleetRad * fleetRad || obj.isColliding; if(!relocate) { if(obj.leaderLock || obj.position.distanceToSQ(dest) < obj.radius * obj.radius) { line3dd line = line3dd(obj.position, target.position); Object@ hit = trace(line, obj.owner.hostileMask | 0x1); if(hit !is target) relocate = true; } } if(relocate) { double innerDist = leader.radius + obj.radius; vec3d dest = random3d(innerDist, fleetRad); if((dest + leader.position).distanceToSQ(target.position) < engageRange * engageRange) { line3dd line = line3dd(dest + leader.position, target.position); Object@ hit = trace(line, obj.owner.hostileMask | 0x1); if(hit is target) { int moveId = -1; obj.leaderLock = false; returnTimer = 0.f; obj.moveTo(dest, moveId, doPathing=false, enterOrbit=false); } } } } break; case SO_Interfere: if(target is null || relTarget is null || !target.valid || !relTarget.valid || !obj.owner.isHostile(target.owner) || !obj.inCombat) supportIdle(obj); else { double fleetRad = leader.getFormationRadius(); line3dd path = line3dd(leader.position, target.position); double targDist = path.length; if(targDist > fleetRad * 4.0) { supportIdle(obj); break; } vec3d curDest = leader.position + obj.internalDestination; vec3d pt = path.getClosestPoint(curDest, false); if(obj.leaderLock || pt.distanceToSQ(curDest) > obj.radius * obj.radius) { double outerDist = targDist - obj.radius * 1.5 - target.radius; if(outerDist > fleetRad) outerDist = fleetRad; double innerDist = leader.radius + obj.radius * 1.5; vec3d dest = path.direction * (innerDist + (outerDist - innerDist) * psr) + random3d(obj.radius * 0.5); int moveId = -1; obj.leaderLock = false; returnTimer = 0.f; obj.moveTo(dest, moveId, doPathing=false, enterOrbit=false); } } break; case SO_Raid: if(target is null || !target.valid || target.position.distanceToSQ(leader.position) > raidDist*raidDist || ship.DPS < 1.0e-4 || !obj.owner.isHostile(target.owner)) { supportIdle(obj, immediate=false); } else if(ship.Supply > 0.0 || leader.freeRaiding) { if(!freeToRaid) break; //TODO: Handle very low supply levels better @target = replaceTarget(obj, target, raidDist*raidDist); vec3d dest = leader.position + obj.internalDestination; vec3d fireFrom = obj.leaderLock ? obj.position : dest; bool relocate = obj.position.distanceToSQ(dest) < obj.radius * obj.radius || fireFrom.distanceToSQ(target.position) > engageRange * engageRange || dest.distanceToSQ(leader.position) > abandonDist * 0.95; if(!relocate) { if(obj.leaderLock || obj.position.distanceToSQ(dest) < obj.radius * obj.radius) { line3dd line = line3dd(obj.position, target.position); Object@ hit = trace(line, obj.owner.hostileMask | 0x1); if(hit !is target) relocate = true; } } if(relocate) { double innerDist = target.radius + obj.radius; double rMax = (range == SR_Close ? engageRange * 0.52 : engageRange); vec3d dest = target.position + random3d(rMax * 0.9, rMax * 0.999); if(dest.distanceToSQ(leader.position) < abandonDist * 0.95) { line3dd line = line3dd(dest, target.position); Object@ hit = trace(line, obj.owner.hostileMask | 0x1); if(hit is target) { int moveId = -1; obj.leaderLock = false; returnTimer = 0.f; obj.moveTo(dest - leader.position, moveId, doPathing=false, enterOrbit=false); } } } break; } else if(detached) { supportIdle(obj, immediate=true); break; } else { resupplyFromFleet(obj); } break; case SO_AttackBlind: if(target is null || !target.valid || target.position.distanceToSQ(leader.position) > engageDist*engageDist || ship.DPS < 1.0e-4 || !obj.owner.isHostile(target.owner)) { supportIdle(obj); } else { @target = replaceTarget(obj, target, engageDist*engageDist); vec3d dest = leader.position + obj.internalDestination; double fleetRad = leader.getFormationRadius(); //If the target is out of range, return to the fleet if(detached || leader.position.distanceToSQ(target.position) > sqr(engageRange + fleetRad)) { supportIdle(obj); break; } vec3d fireFrom = obj.leaderLock ? obj.position : dest; bool relocate = fireFrom.distanceToSQ(target.position) > engageRange * engageRange || fireFrom.distanceToSQ(leader.position) > fleetRad * fleetRad || obj.isColliding; if(!relocate) { if(obj.leaderLock || obj.position.distanceToSQ(dest) < obj.radius * obj.radius) { line3dd line = line3dd(obj.position, target.position); Object@ hit = trace(line, obj.owner.hostileMask | 0x1); if(hit !is null && hit !is target && !(hit.owner is target.owner && hit.type == target.type)) relocate = true; } } if(relocate) { double innerDist = leader.radius + obj.radius; vec3d dest = random3d(innerDist, fleetRad); if((dest + leader.position).distanceToSQ(target.position) < engageRange * engageRange) { line3dd line = line3dd(dest + leader.position, target.position); Object@ hit = trace(line, obj.owner.hostileMask | 0x1); if(hit is target || hit is null || (hit.owner is target.owner && hit.type == target.type)) { int moveId = -1; obj.leaderLock = false; returnTimer = 0.f; obj.moveTo(dest, moveId, doPathing=false, enterOrbit=false); } } } } break; case SO_Retreat: supportIdle(obj); break; } } } else if(findLeader && obj.hasMover && obj.maxAcceleration > 0) { //Try to find a new leader Object@ newLeader = findNearbyLeader(obj, obj, 2, ship.blueprint.design.size); if(newLeader !is null) newLeader.registerSupport(obj, true); } } void writeSupportAI(const Object& obj, Message& msg) { const Ship@ ship = cast(obj); msg << ship.Leader; } bool writeSupportAIDelta(const Object& obj, Message& msg) { const Ship@ ship = cast(obj); if(leaderDelta) { msg.write1(); msg << ship.Leader; leaderDelta = false; return true; } return false; } };