var PETRA = function(m) { /* Defines an army * An army is a collection of own entities and enemy entities. * This doesn't use entity collections are they aren't really useful * and it would probably slow the rest of the system down too much. * All entities are therefore lists of ID * Inherited by the defense manager and several of the attack manager's attack plan. */ m.Army = function(gameState, ownEntities, foeEntities) { this.ID = gameState.ai.uniqueIDs.armies++; this.Config = gameState.ai.Config; this.defenseRatio = this.Config.Defense.defenseRatio; this.compactSize = this.Config.Defense.armyCompactSize; this.breakawaySize = this.Config.Defense.armyBreakawaySize; // average this.foePosition = [0,0]; this.positionLastUpdate = gameState.ai.elapsedTime; // Some caching // A list of our defenders that were tasked with attacking a particular unit // This doesn't mean that they actually are since they could move on to something else on their own. this.assignedAgainst = {}; // who we assigned against, for quick removal. this.assignedTo = {}; this.foeEntities = []; this.foeStrength = 0; this.ownEntities = []; this.ownStrength = 0; // actually add units for (let id of foeEntities) this.addFoe(gameState, id, true); for (let id of ownEntities) this.addOwn(gameState, id); this.recalculatePosition(gameState, true); return true; } // if not forced, will only recalculate if on a different turn. m.Army.prototype.recalculatePosition = function(gameState, force) { if (!force && this.positionLastUpdate === gameState.ai.elapsedTime) return; var npos = 0; var pos = [0, 0]; for (var id of this.foeEntities) { var ent = gameState.getEntityById(id); if (!ent || !ent.position()) continue; npos++; var epos = ent.position(); pos[0] += epos[0]; pos[1] += epos[1]; } // if npos = 0, the army must have been destroyed and will be removed next turn. keep previous position if (npos > 0) { this.foePosition[0] = pos[0]/npos; this.foePosition[1] = pos[1]/npos; } this.positionLastUpdate = gameState.ai.elapsedTime; }; // helper m.Army.prototype.recalculateStrengths = function (gameState) { this.ownStrength = 0; this.foeStrength = 0; // todo: deal with specifics. for (var id of this.foeEntities) this.evaluateStrength(gameState.getEntityById(id)); for (var id of this.ownEntities) this.evaluateStrength(gameState.getEntityById(id), true); }; // adds or remove the strength of the entity either to the enemy or to our units. m.Army.prototype.evaluateStrength = function (ent, isOwn, remove) { var entStrength = m.getMaxStrength(ent); if (remove) entStrength *= -1; if (isOwn) this.ownStrength += entStrength; else this.foeStrength += entStrength; // todo: deal with specifics. }; // add an entity to the enemy army // Will return true if the entity was added and false otherwise. // won't recalculate our position but will dirty it. m.Army.prototype.addFoe = function (gameState, enemyId, force) { if (this.foeEntities.indexOf(enemyId) !== -1) return false; var ent = gameState.getEntityById(enemyId); if (ent === undefined || ent.position() === undefined) return false; // check distance if (!force && API3.SquareVectorDistance(ent.position(), this.foePosition) > this.compactSize) return false; this.foeEntities.push(enemyId); this.assignedAgainst[enemyId] = []; this.positionLastUpdate = 0; this.evaluateStrength(ent); ent.setMetadata(PlayerID, "PartOfArmy", this.ID); return true; }; // returns true if the entity was removed and false otherwise. // TODO: when there is a technology update, we should probably recompute the strengths, or weird stuffs will happen. m.Army.prototype.removeFoe = function (gameState, enemyId, enemyEntity) { var idx = this.foeEntities.indexOf(enemyId); if (idx === -1) return false; var ent = enemyEntity === undefined ? gameState.getEntityById(enemyId) : enemyEntity; if (ent === undefined) { warn("Trying to remove a non-existing enemy entity, crashing for stacktrace"); xgzrg(); } this.foeEntities.splice(idx, 1); this.evaluateStrength(ent, false, true); ent.setMetadata(PlayerID, "PartOfArmy", undefined); this.assignedAgainst[enemyId] = undefined; for (var to in this.assignedTo) if (this.assignedTo[to] == enemyId) this.assignedTo[to] = undefined; return true; }; // adds a defender but doesn't assign him yet. // force is true when merging armies, so in this case we should add it even if no position as it can be in a ship m.Army.prototype.addOwn = function (gameState, id, force) { if (this.ownEntities.indexOf(id) !== -1) return false; var ent = gameState.getEntityById(id); if (ent === undefined) return false; if(!ent.position() && !force) return false; this.ownEntities.push(id); this.evaluateStrength(ent, true); ent.setMetadata(PlayerID, "PartOfArmy", this.ID); this.assignedTo[id] = 0; var plan = ent.getMetadata(PlayerID, "plan"); if (plan !== undefined) ent.setMetadata(PlayerID, "plan", -2); else ent.setMetadata(PlayerID, "plan", -3); var subrole = ent.getMetadata(PlayerID, "subrole"); if (subrole === undefined || subrole !== "defender") ent.setMetadata(PlayerID, "formerSubrole", subrole); ent.setMetadata(PlayerID, "subrole", "defender"); return true; }; m.Army.prototype.removeOwn = function (gameState, id, Entity) { var idx = this.ownEntities.indexOf(id); if (idx === -1) return false; var ent = Entity === undefined ? gameState.getEntityById(id) : Entity; if (ent === undefined) { warn( id); warn("Trying to remove a non-existing entity, crashing for stacktrace"); xgzrg(); } this.ownEntities.splice(idx, 1); this.evaluateStrength(ent, true, true); ent.setMetadata(PlayerID, "PartOfArmy", undefined); if (ent.getMetadata(PlayerID, "plan") === -2) ent.setMetadata(PlayerID, "plan", -1); else ent.setMetadata(PlayerID, "plan", undefined); if (this.assignedTo[id] !== 0) { var temp = this.assignedAgainst[this.assignedTo[id]]; if (temp) temp.splice(temp.indexOf(id), 1); } this.assignedTo[id] = undefined; var formerSubrole = ent.getMetadata(PlayerID, "formerSubrole"); if (formerSubrole !== undefined) ent.setMetadata(PlayerID, "subrole", formerSubrole); else ent.setMetadata(PlayerID, "subrole", undefined); ent.setMetadata(PlayerID, "formerSubrole", undefined); // TODO be sure that all units in the transport need the cancelation /* if (!ent.position()) // this unit must still be in a transport plan ... try to cancel it { var planID = ent.getMetadata(PlayerID, "transport"); // no plans must mean that the unit was in a ship which was destroyed, so do nothing if (planID) { if (gameState.ai.Config.debug > 0) warn("ent from army still in transport plan: plan " + planID + " canceled"); var plan = gameState.ai.HQ.navalManager.getPlan(planID); if (plan && !plan.canceled) plan.cancelTransport(gameState); } } */ return true; }; // this one is "undefined entity" proof because it's called at odd times. // Orders a unit to attack an enemy. // overridden by specific army classes. m.Army.prototype.assignUnit = function (gameState, entID) { }; // resets the army properly. // assumes we already cleared dead units. m.Army.prototype.clear = function (gameState) { while(this.foeEntities.length > 0) this.removeFoe(gameState,this.foeEntities[0]); while(this.ownEntities.length > 0) this.removeOwn(gameState,this.ownEntities[0]); this.assignedAgainst = {}; this.assignedTo = {}; this.recalculateStrengths(gameState); this.recalculatePosition(gameState); }; // merge this army with another properly. // assumes units are in only one army. // also assumes that all have been properly cleaned up (no dead units). m.Army.prototype.merge = function (gameState, otherArmy) { // copy over all parameters. for (var i in otherArmy.assignedAgainst) { if (this.assignedAgainst[i] === undefined) this.assignedAgainst[i] = otherArmy.assignedAgainst[i]; else this.assignedAgainst[i] = this.assignedAgainst[i].concat(otherArmy.assignedAgainst[i]); } for (var i in otherArmy.assignedTo) this.assignedTo[i] = otherArmy.assignedTo[i]; for (var id of otherArmy.foeEntities) this.addFoe(gameState, id); // TODO: reassign those ? for (var id of otherArmy.ownEntities) this.addOwn(gameState, id, true); this.recalculatePosition(gameState, true); this.recalculateStrengths(gameState); return true; }; // TODO: when there is a technology update, we should probably recompute the strengths, or weird stuffs might happen. m.Army.prototype.checkEvents = function (gameState, events) { var renameEvents = events["EntityRenamed"]; // take care of promoted and packed units var destroyEvents = events["Destroy"]; var convEvents = events["OwnershipChanged"]; var garriEvents = events["Garrison"]; // Warning the metadata is already cloned in shared.js. Futhermore, changes should be done before destroyEvents // otherwise it would remove the old entity from this army list // TODO we should may-be reevaluate the strength for (var msg of renameEvents) { if (this.foeEntities.indexOf(msg.entity) !== -1) { var idx = this.foeEntities.indexOf(msg.entity); this.foeEntities[idx] = msg.newentity; this.assignedAgainst[msg.newentity] = this.assignedAgainst[msg.entity]; this.assignedAgainst[msg.entity] = undefined; for (var to in this.assignedTo) if (this.assignedTo[to] == msg.entity) this.assignedTo[to] = msg.newentity; } else if (this.ownEntities.indexOf(msg.entity) !== -1) { var idx = this.ownEntities.indexOf(msg.entity); this.ownEntities[idx] = msg.newentity; this.assignedTo[msg.newentity] = this.assignedTo[msg.entity]; this.assignedTo[msg.entity] = undefined; for (var against in this.assignedAgainst) { if (!this.assignedAgainst[against]) continue; if (this.assignedAgainst[against].indexOf(msg.entity) !== -1) this.assignedAgainst[against][this.assignedAgainst[against].indexOf(msg.entity)] = msg.newentity; } } } for (var msg of destroyEvents) { if (msg.entityObj === undefined) continue; if (msg.entityObj._entity.owner === PlayerID) this.removeOwn(gameState, msg.entity, msg.entityObj); else this.removeFoe(gameState, msg.entity, msg.entityObj); } for (var msg of garriEvents) this.removeFoe(gameState, msg.entity); for (var msg of convEvents) { if (msg.to === PlayerID) { // we have converted an enemy, let's assign it as a defender if (this.removeFoe(gameState, msg.entity)) this.addOwn(gameState, msg.entity); } else if (msg.from === PlayerID) this.removeOwn(gameState, msg.entity); // TODO: add allies } }; // assumes cleaned army. // this only checks for breakaways. m.Army.prototype.onUpdate = function (gameState) { var breakaways = []; // TODO: assign unassigned defenders, cleanup of a few things. // perhaps occasional strength recomputation // occasional update or breakaways, positions… if (gameState.ai.elapsedTime - this.positionLastUpdate > 5) { this.recalculatePosition(gameState); this.positionLastUpdate = gameState.ai.elapsedTime; // Check for breakaways. for (var i = 0; i < this.foeEntities.length; ++i) { var id = this.foeEntities[i]; var ent = gameState.getEntityById(id); if (!ent || !ent.position()) continue; if (API3.SquareVectorDistance(ent.position(), this.foePosition) > this.breakawaySize) { breakaways.push(id); if (this.removeFoe(gameState, id)) i--; } } this.recalculatePosition(gameState); } return breakaways; }; return m; }(PETRA);