Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 18740) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 18741) @@ -1,938 +1,937 @@ var API3 = function(m) { // defines a template. // It's completely raw data, except it's slightly cleverer now and then. m.Template = m.Class({ _init: function(template) { this._template = template; this._tpCache = new Map(); }, // helper function to return a template value, optionally adjusting for tech. // TODO: there's no support for "_string" values here. get: function(string) { let value = this._template; if (this._entityModif && this._entityModif.has(string)) return this._entityModif.get(string); else if (this._templateModif && this._templateModif.has(string)) return this._templateModif.get(string); if (!this._tpCache.has(string)) { let args = string.split("/"); for (let arg of args) { if (value[arg]) value = value[arg]; else { value = undefined; break; } } this._tpCache.set(string, value); } return this._tpCache.get(string); }, genericName: function() { return this.get("Identity/GenericName"); }, rank: function() { return this.get("Identity/Rank"); }, classes: function() { let template = this.get("Identity"); if (!template) return undefined; return GetIdentityClasses(template); }, requiredTech: function() { return this.get("Identity/RequiredTechnology"); }, available: function(gameState) { let techRequired = this.requiredTech(); if (!techRequired) return true; return gameState.isResearched(techRequired); }, // specifically phase: function() { let techRequired = this.requiredTech(); if (!techRequired) return 0; if (techRequired === "phase_village") return 1; if (techRequired === "phase_town") return 2; if (techRequired === "phase_city") return 3; return 0; }, hasClass: function(name) { if (!this._classes) this._classes = this.classes(); let classes = this._classes; return classes && classes.indexOf(name) !== -1; }, hasClasses: function(array) { if (!this._classes) this._classes = this.classes(); let classes = this._classes; if (!classes) return false; for (let cls of array) if (classes.indexOf(cls) === -1) return false; return true; }, civ: function() { return this.get("Identity/Civ"); }, "cost": function(productionQueue) { if (!this.get("Cost")) return undefined; let ret = {}; for (let type in this.get("Cost/Resources")) ret[type] = +this.get("Cost/Resources/" + type); return ret; }, "costSum": function(productionQueue) { let cost = this.cost(productionQueue); if (!cost) return undefined; let ret = 0; for (let type in cost) ret += cost[type]; return ret; }, "techCostMultiplier": function(type) { return +(this.get("ProductionQueue/TechCostMultiplier/"+type) || 1); }, /** * Returns the radius of a circle surrounding this entity's * obstruction shape, or undefined if no obstruction. */ obstructionRadius: function() { if (!this.get("Obstruction")) return undefined; if (this.get("Obstruction/Static")) { let w = +this.get("Obstruction/Static/@width"); let h = +this.get("Obstruction/Static/@depth"); return Math.sqrt(w*w + h*h) / 2; } if (this.get("Obstruction/Unit")) return +this.get("Obstruction/Unit/@radius"); return 0; // this should never happen }, /** * Returns the radius of a circle surrounding this entity's * footprint. */ footprintRadius: function() { if (!this.get("Footprint")) return undefined; if (this.get("Footprint/Square")) { let w = +this.get("Footprint/Square/@width"); let h = +this.get("Footprint/Square/@depth"); return Math.sqrt(w*w + h*h) / 2; } if (this.get("Footprint/Circle")) return +this.get("Footprint/Circle/@radius"); return 0; // this should never happen }, maxHitpoints: function() { return +(this.get("Health/Max") || 0); }, isHealable: function() { if (this.get("Health") !== undefined) return this.get("Health/Unhealable") !== "true"; return false; }, isRepairable: function() { return this.get("Repairable") !== undefined; }, getPopulationBonus: function() { return +this.get("Cost/PopulationBonus"); }, armourStrengths: function() { if (!this.get("Armour")) return undefined; return { hack: +this.get("Armour/Hack"), pierce: +this.get("Armour/Pierce"), crush: +this.get("Armour/Crush") }; }, attackTypes: function() { if (!this.get("Attack")) return undefined; let ret = []; for (let type in this.get("Attack")) ret.push(type); return ret; }, attackRange: function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { max: +this.get("Attack/" + type +"/MaxRange"), min: +(this.get("Attack/" + type +"/MinRange") || 0) }; }, attackStrengths: function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { hack: +(this.get("Attack/" + type + "/Hack") || 0), pierce: +(this.get("Attack/" + type + "/Pierce") || 0), crush: +(this.get("Attack/" + type + "/Crush") || 0) }; }, captureStrength: function() { if (!this.get("Attack/Capture")) return undefined; return +this.get("Attack/Capture/Value") || 0; }, attackTimes: function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { prepare: +(this.get("Attack/" + type + "/PrepareTime") || 0), repeat: +(this.get("Attack/" + type + "/RepeatTime") || 1000) }; }, // returns the classes this templates counters: // Return type is [ [-neededClasses- , multiplier], … ]. getCounteredClasses: function() { if (!this.get("Attack")) return undefined; let Classes = []; for (let type in this.get("Attack")) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses" + b +"/Multiplier")]); } } return Classes; }, // returns true if the entity counters those classes. // TODO: refine using the multiplier countersClasses: function(classes) { if (!this.get("Attack")) return false; let mcounter = []; for (let type in this.get("Attack")) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) mcounter.concat(bonusClasses.split(" ")); } } for (let i in classes) { if (mcounter.indexOf(classes[i]) !== -1) return true; } return false; }, // returns, if it exists, the multiplier from each attack against a given class getMultiplierAgainst: function(type, againstClass) { if (!this.get("Attack/" + type +"")) return undefined; if (this.get("Attack/" + type + "/Bonuses")) { for (let b in this.get("Attack/" + type + "/Bonuses")) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (!bonusClasses) continue; for (let bcl of bonusesClasses.split(" ")) if (bcl === againstClass) return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier"); } } return 1; }, // returns true if the entity can attack the given class canAttackClass: function(saidClass) { if (!this.get("Attack")) return false; for (let type in this.get("Attack")) { let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); - if (!restrictedClasses) - continue; - if (restrictedClasses.split(" ").indexOf(saidClass) !== -1) + if (restrictedClasses && !MatchesClassList([saidClass], restrictedClasses)) return false; } + return true; }, "buildableEntities": function() { let templates = this.get("Builder/Entities/_string"); if (!templates) return []; let civ = this.civ(); return templates.replace(/\{civ\}/g, civ).split(/\s+/); }, "trainableEntities": function(civ) { let templates = this.get("ProductionQueue/Entities/_string"); if (!templates) return undefined; if (civ) templates = templates.replace(/\{civ\}/g, civ); return templates.split(/\s+/); }, "researchableTechs": function(civ) { if (this.civ() !== civ) // techs can only be researched in structures from the player civ TODO no more true return undefined; let templates = this.get("ProductionQueue/Technologies/_string"); if (!templates) return undefined; return templates.split(/\s+/); }, resourceSupplyType: function() { if (!this.get("ResourceSupply")) return undefined; let [type, subtype] = this.get("ResourceSupply/Type").split('.'); return { "generic": type, "specific": subtype }; }, // will return either "food", "wood", "stone", "metal" and not treasure. getResourceType: function() { if (!this.get("ResourceSupply")) return undefined; let [type, subtype] = this.get("ResourceSupply/Type").split('.'); if (type == "treasure") return subtype; return type; }, resourceSupplyMax: function() { return +this.get("ResourceSupply/Amount"); }, maxGatherers: function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); }, resourceGatherRates: function() { if (!this.get("ResourceGatherer")) return undefined; let ret = {}; let baseSpeed = +this.get("ResourceGatherer/BaseSpeed"); for (let r in this.get("ResourceGatherer/Rates")) ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed; return ret; }, resourceDropsiteTypes: function() { if (!this.get("ResourceDropsite")) return undefined; let types = this.get("ResourceDropsite/Types"); return types ? types.split(/\s+/) : []; }, garrisonableClasses: function() { return this.get("GarrisonHolder/List/_string"); }, garrisonMax: function() { return this.get("GarrisonHolder/Max"); }, garrisonEjectHealth: function() { return +this.get("GarrisonHolder/EjectHealth"); }, getDefaultArrow: function() { return +this.get("BuildingAI/DefaultArrowCount"); }, getArrowMultiplier: function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); }, getGarrisonArrowClasses: function() { if (!this.get("BuildingAI")) return undefined; return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/); }, buffHeal: function() { return +this.get("GarrisonHolder/BuffHeal"); }, promotion: function() { return this.get("Promotion/Entity"); }, /** * Returns whether this is an animal that is too difficult to hunt. * (Any non domestic currently.) */ isHuntable: function() { if(!this.get("ResourceSupply/KillBeforeGather")) return false; // special case: rabbits too difficult to hunt for such a small food amount let specificName = this.get("Identity/SpecificName"); if (specificName && specificName === "Rabbit") return false; // do not hunt retaliating animals (animals without UnitAI are dead animals) let behaviour = this.get("UnitAI/NaturalBehaviour"); return !this.get("UnitAI") || !(behaviour === "violent" || behaviour === "aggressive" || behaviour === "defensive"); }, walkSpeed: function() { return +this.get("UnitMotion/WalkSpeed"); }, trainingCategory: function() { return this.get("TrainingRestrictions/Category"); }, buildCategory: function() { return this.get("BuildRestrictions/Category"); }, "buildTime": function(productionQueue) { let time = +this.get("Cost/BuildTime"); if (productionQueue) time *= productionQueue.techCostMultiplier("time"); return time; }, buildDistance: function() { return this.get("BuildRestrictions/Distance"); }, buildPlacementType: function() { return this.get("BuildRestrictions/PlacementType"); }, buildTerritories: function() { if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Territory")) return undefined; return this.get("BuildRestrictions/Territory").split(/\s+/); }, hasBuildTerritory: function(territory) { let territories = this.buildTerritories(); return territories && territories.indexOf(territory) !== -1; }, hasTerritoryInfluence: function() { return this.get("TerritoryInfluence") !== undefined; }, hasDefensiveFire: function() { if (!this.get("Attack/Ranged")) return false; return this.getDefaultArrow() || this.getArrowMultiplier(); }, territoryInfluenceRadius: function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Radius"); return -1; }, territoryInfluenceWeight: function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Weight"); return -1; }, territoryDecayRate: function() { return +(this.get("TerritoryDecay/DecayRate") || 0); }, defaultRegenRate: function() { return +(this.get("Capturable/RegenRate") || 0); }, garrisonRegenRate: function() { return +(this.get("Capturable/GarrisonRegenRate") || 0); }, visionRange: function() { return +this.get("Vision/Range"); }, gainMultiplier: function() { return +this.get("Trader/GainMultiplier"); } }); // defines an entity, with a super Template. // also redefines several of the template functions where the only change is applying aura and tech modifications. m.Entity = m.Class({ _super: m.Template, _init: function(sharedAI, entity) { this._super.call(this, sharedAI.GetTemplate(entity.template)); this._templateName = entity.template; this._entity = entity; this._ai = sharedAI; // save a reference to the template tech modifications if (!sharedAI._templatesModifications[entity.owner][this._templateName]) sharedAI._templatesModifications[entity.owner][this._templateName] = new Map(); this._templateModif = sharedAI._templatesModifications[entity.owner][this._templateName]; // save a reference to the entity tech/aura modifications if (!sharedAI._entitiesModifications.has(entity.id)) sharedAI._entitiesModifications.set(entity.id, new Map()); this._entityModif = sharedAI._entitiesModifications.get(entity.id); }, toString: function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; }, id: function() { return this._entity.id; }, templateName: function() { return this._templateName; }, /** * Returns extra data that the AI scripts have associated with this entity, * for arbitrary local annotations. * (This data should not be shared with any other AI scripts.) */ getMetadata: function(player, key) { return this._ai.getMetadata(player, this, key); }, /** * Sets extra data to be associated with this entity. */ setMetadata: function(player, key, value) { this._ai.setMetadata(player, this, key, value); }, deleteAllMetadata: function(player) { delete this._ai._entityMetadata[player][this.id()]; }, deleteMetadata: function(player, key) { this._ai.deleteMetadata(player, this, key); }, position: function() { return this._entity.position; }, angle: function() { return this._entity.angle; }, isIdle: function() { if (typeof this._entity.idle === "undefined") return undefined; return this._entity.idle; }, unitAIState: function() { return this._entity.unitAIState !== undefined ? this._entity.unitAIState : undefined; }, unitAIOrderData: function() { return this._entity.unitAIOrderData !== undefined ? this._entity.unitAIOrderData : undefined; }, hitpoints: function() { return this._entity.hitpoints !== undefined ? this._entity.hitpoints : undefined; }, isHurt: function() { return this.hitpoints() < this.maxHitpoints(); }, healthLevel: function() { return this.hitpoints() / this.maxHitpoints(); }, needsHeal: function() { return this.isHurt() && this.isHealable(); }, needsRepair: function() { return this.isHurt() && this.isRepairable(); }, decaying: function() { return this._entity.decaying !== undefined ? this._entity.decaying : undefined; }, capturePoints: function() {return this._entity.capturePoints !== undefined ? this._entity.capturePoints : undefined; }, "isSharedDropsite": function() { return this._entity.sharedDropsite === true; }, /** * Returns the current training queue state, of the form * [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ] */ trainingQueue: function() { let queue = this._entity.trainingQueue; return queue; }, trainingQueueTime: function() { let queue = this._entity.trainingQueue; if (!queue) return undefined; let time = 0; for (let item of queue) time += item.timeRemaining; return time/1000; }, foundationProgress: function() { if (this._entity.foundationProgress === undefined) return undefined; return this._entity.foundationProgress; }, getBuilders: function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return []; return this._entity.foundationBuilders; }, getBuildersNb: function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return 0; return this._entity.foundationBuilders.length; }, owner: function() { return this._entity.owner; }, isOwn: function(player) { if (typeof this._entity.owner === "undefined") return false; return this._entity.owner === player; }, resourceSupplyAmount: function() { if (this._entity.resourceSupplyAmount === undefined) return undefined; return this._entity.resourceSupplyAmount; }, resourceSupplyNumGatherers: function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this._entity.resourceSupplyNumGatherers; return undefined; }, isFull: function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this.maxGatherers() === this._entity.resourceSupplyNumGatherers; return undefined; }, resourceCarrying: function() { if (this._entity.resourceCarrying === undefined) return undefined; return this._entity.resourceCarrying; }, currentGatherRate: function() { // returns the gather rate for the current target if applicable. if (!this.get("ResourceGatherer")) return undefined; if (this.unitAIOrderData().length && (this.unitAIState().split(".")[1] === "GATHER" || this.unitAIState().split(".")[1] === "RETURNRESOURCE")) { let res; // this is an abuse of "_ai" but it works. if (this.unitAIState().split(".")[1] === "GATHER" && this.unitAIOrderData()[0].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[0].target); else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[1].target); if (!res) return 0; let type = res.resourceSupplyType(); if (!type) return 0; if (type.generic === "treasure") return 1000; let tstring = type.generic + "." + type.specific; let rate = +this.get("ResourceGatherer/BaseSpeed"); rate *= +this.get("ResourceGatherer/Rates/" +tstring); if (rate) return rate; return 0; } return undefined; }, isBuilder: function() { return this.get("Builder") !== undefined; }, isGatherer: function() { return this.get("ResourceGatherer") !== undefined; }, canGather: function(type) { let gatherRates = this.get("ResourceGatherer/Rates"); if (!gatherRates) return false; for (let r in gatherRates) if (r.split('.')[0] === type) return true; return false; }, isGarrisonHolder: function() { return this.get("GarrisonHolder") !== undefined; }, garrisoned: function() { return this._entity.garrisoned; }, canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); }, move: function(x, z, queued = false) { Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); return this; }, moveToRange: function(x, z, min, max, queued = false) { Engine.PostCommand(PlayerID,{"type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued }); return this; }, attackMove: function(x, z, targetClasses, queued = false) { Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "queued": queued }); return this; }, // violent, aggressive, defensive, passive, standground setStance: function(stance, queued = false) { Engine.PostCommand(PlayerID,{"type": "stance", "entities": [this.id()], "name" : stance, "queued": queued }); return this; }, stopMoving: function() { Engine.PostCommand(PlayerID,{"type": "stop", "entities": [this.id()], "queued": false}); }, unload: function(id) { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID,{"type": "unload", "garrisonHolder": this.id(), "entities": [id]}); return this; }, // Unloads all owned units, don't unload allies unloadAll: function() { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID,{"type": "unload-all-by-owner", "garrisonHolders": [this.id()]}); return this; }, garrison: function(target, queued = false) { Engine.PostCommand(PlayerID,{"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": queued}); return this; }, attack: function(unitId, allowCapture = true, queued = false) { Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued}); return this; }, // moveApart from a point in the opposite direction with a distance dist moveApart: function(point, dist) { if (this.position() !== undefined) { let direction = [this.position()[0] - point[0], this.position()[1] - point[1]]; let norm = m.VectorDistance(point, this.position()); if (norm === 0) direction = [1, 0]; else { direction[0] /= norm; direction[1] /= norm; } Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false}); } return this; }, // Flees from a unit in the opposite direction. flee: function(unitToFleeFrom) { if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) { let FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0], this.position()[1] - unitToFleeFrom.position()[1]]; let dist = m.VectorDistance(unitToFleeFrom.position(), this.position() ); FleeDirection[0] = 40 * FleeDirection[0]/dist; FleeDirection[1] = 40 * FleeDirection[1]/dist; Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false}); } return this; }, gather: function(target, queued = false) { Engine.PostCommand(PlayerID,{"type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued}); return this; }, repair: function(target, autocontinue = false, queued = false) { Engine.PostCommand(PlayerID,{"type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued}); return this; }, returnResources: function(target, queued = false) { Engine.PostCommand(PlayerID,{"type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued}); return this; }, destroy: function() { Engine.PostCommand(PlayerID,{"type": "delete-entities", "entities": [this.id()] }); return this; }, barter: function(buyType, sellType, amount) { Engine.PostCommand(PlayerID,{"type": "barter", "sell" : sellType, "buy" : buyType, "amount" : amount }); return this; }, tradeRoute: function(target, source) { Engine.PostCommand(PlayerID,{"type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false }); return this; }, setRallyPoint: function(target, command) { let data = {"command": command, "target": target.id()}; Engine.PostCommand(PlayerID, {"type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data}); return this; }, unsetRallyPoint: function() { Engine.PostCommand(PlayerID, {"type": "unset-rallypoint", "entities": [this.id()]}); return this; }, train: function(civ, type, count, metadata, promotedTypes) { let trainable = this.trainableEntities(civ); if (!trainable) { error("Called train("+type+", "+count+") on non-training entity "+this); return this; } if (trainable.indexOf(type) === -1) { error("Called train("+type+", "+count+") on entity "+this+" which can't train that"); return this; } Engine.PostCommand(PlayerID,{ "type": "train", "entities": [this.id()], "template": type, "count": count, "metadata": metadata, "promoted": promotedTypes }); return this; }, construct: function(template, x, z, angle, metadata) { // TODO: verify this unit can construct this, just for internal // sanity-checking and error reporting Engine.PostCommand(PlayerID,{ "type": "construct", "entities": [this.id()], "template": template, "x": x, "z": z, "angle": angle, "autorepair": false, "autocontinue": false, "queued": false, "metadata" : metadata // can be undefined }); return this; }, research: function(template) { Engine.PostCommand(PlayerID,{ "type": "research", "entity": this.id(), "template": template }); return this; }, stopProduction: function(id) { Engine.PostCommand(PlayerID,{ "type": "stop-production", "entity": this.id(), "id": id }); return this; }, stopAllProduction: function(percentToStopAt) { let queue = this._entity.trainingQueue; if (!queue) return true; // no queue, so technically we stopped all production. for (let item of queue) if (item.progress < percentToStopAt) Engine.PostCommand(PlayerID,{ "type": "stop-production", "entity": this.id(), "id": item.id }); return this; } }); return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 18740) +++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 18741) @@ -1,608 +1,608 @@ function Attack() {} Attack.prototype.bonusesSchema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; Attack.prototype.preferredClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; Attack.prototype.restrictedClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; Attack.prototype.Schema = "Controls the attack abilities and strengths of the unit." + "" + "" + "10.0" + "0.0" + "5.0" + "4.0" + "1000" + "" + "" + "pers" + "Infantry" + "1.5" + "" + "" + "Cavalry Melee" + "1.5" + "" + "" + "Champion" + "Cavalry Infantry" + "" + "" + "0.0" + "10.0" + "0.0" + "44.0" + "20.0" + "15.0" + "800" + "1600" + "50.0" + "2.5" + "" + "" + "Cavalry" + "2" + "" + "" + "Champion" + "" + "Circular" + "20" + "false" + "0.0" + "10.0" + "0.0" + "" + "" + "" + "1000.0" + "0.0" + "0.0" + "4.0" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: it shouldn't be stretched "" + "" + Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""+ "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + Attack.prototype.bonusesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: it shouldn't be stretched "" + "" + Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: how do these work? Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + ""; Attack.prototype.Init = function() { }; Attack.prototype.Serialize = null; // we have no dynamic state to save Attack.prototype.GetAttackTypes = function() { return ["Melee", "Ranged", "Capture"].filter(type => !!this.template[type]); }; Attack.prototype.GetPreferredClasses = function(type) { if (this.template[type] && this.template[type].PreferredClasses && this.template[type].PreferredClasses._string) return this.template[type].PreferredClasses._string.split(/\s+/); return []; }; Attack.prototype.GetRestrictedClasses = function(type) { if (this.template[type] && this.template[type].RestrictedClasses && this.template[type].RestrictedClasses._string) return this.template[type].RestrictedClasses._string.split(/\s+/); return []; }; Attack.prototype.CanAttack = function(target) { let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) return true; let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) return false; // Check if the relative height difference is larger than the attack range // If the relative height is bigger, it means they will never be able to // reach each other, no matter how close they come. let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset()); const cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; const targetClasses = cmpIdentity.GetClassesList(); for (let type of this.GetAttackTypes()) { if (type == "Capture" && !QueryMiragedInterface(target, IID_Capturable)) continue; if (heightDiff > this.GetRange(type).max) continue; let restrictedClasses = this.GetRestrictedClasses(type); if (!restrictedClasses.length) return true; - if (targetClasses.every(c => restrictedClasses.indexOf(c) == -1)) + if (!MatchesClassList(targetClasses, restrictedClasses)) return true; } return false; }; /** * Returns null if we have no preference or the lowest index of a preferred class. */ Attack.prototype.GetPreference = function(target) { const cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; const targetClasses = cmpIdentity.GetClassesList(); let minPref = null; for (let type of this.GetAttackTypes()) { let preferredClasses = this.GetPreferredClasses(type); for (let targetClass of targetClasses) { let pref = preferredClasses.indexOf(targetClass); if (pref === 0) return pref; if (pref != -1 && (minPref === null || minPref > pref)) minPref = pref; } } return minPref; }; /** * Get the full range of attack using all available attack types. */ Attack.prototype.GetFullAttackRange = function() { let ret = { "min": Infinity, "max": 0 }; for (let type of this.GetAttackTypes()) { let range = this.GetRange(type); ret.min = Math.min(ret.min, range.min); ret.max = Math.max(ret.max, range.max); } return ret; }; Attack.prototype.GetBestAttackAgainst = function(target, allowCapture) { let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) { // TODO: Formation against formation needs review let types = this.GetAttackTypes(); return ["Ranged", "Melee", "Capture"].find(attack => types.indexOf(attack) != -1); } let cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; let targetClasses = cmpIdentity.GetClassesList(); let isTargetClass = className => targetClasses.indexOf(className) != -1; // Always slaughter domestic animals instead of using a normal attack if (isTargetClass("Domestic") && this.template.Slaughter) return "Slaughter"; let attack = this; let types = this.GetAttackTypes().filter(type => !attack.GetRestrictedClasses(type).some(isTargetClass)); // check if the target is capturable let captureIndex = types.indexOf("Capture"); if (captureIndex != -1) { let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); let cmpPlayer = QueryOwnerInterface(this.entity); if (allowCapture && cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID())) return "Capture"; // not captureable, so remove this attack types.splice(captureIndex, 1); } let isPreferred = className => attack.GetPreferredClasses(className).some(isTargetClass); return types.sort((a, b) => (types.indexOf(a) + (isPreferred(a) ? types.length : 0)) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop(); }; Attack.prototype.CompareEntitiesByPreference = function(a, b) { let aPreference = this.GetPreference(a); let bPreference = this.GetPreference(b); if (aPreference === null && bPreference === null) return 0; if (aPreference === null) return 1; if (bPreference === null) return -1; return aPreference - bPreference; }; Attack.prototype.GetTimers = function(type) { let prepare = +(this.template[type].PrepareTime || 0); prepare = ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", prepare, this.entity); let repeat = +(this.template[type].RepeatTime || 1000); repeat = ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeat, this.entity); return { "prepare": prepare, "repeat": repeat }; }; Attack.prototype.GetAttackStrengths = function(type) { // Work out the attack values with technology effects let template = this.template[type]; let splash = ""; if (!template) { template = this.template[type.split(".")[0]].Splash; splash = "/Splash"; } let applyMods = damageType => ApplyValueModificationsToEntity("Attack/" + type + splash + "/" + damageType, +(template[damageType] || 0), this.entity); if (type == "Capture") return { "value": applyMods("Value") }; return { "hack": applyMods("Hack"), "pierce": applyMods("Pierce"), "crush": applyMods("Crush") }; }; Attack.prototype.GetSplashDamage = function(type) { if (!this.template[type].Splash) return false; let splash = this.GetAttackStrengths(type + ".Splash"); splash.friendlyFire = this.template[type].Splash.FriendlyFire != "false"; return splash; }; Attack.prototype.GetRange = function(type) { let max = +this.template[type].MaxRange; max = ApplyValueModificationsToEntity("Attack/" + type + "/MaxRange", max, this.entity); let min = +(this.template[type].MinRange || 0); min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity); let elevationBonus = +(this.template[type].ElevationBonus || 0); elevationBonus = ApplyValueModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity); return { "max": max, "min": min, "elevationBonus": elevationBonus }; }; // Calculate the attack damage multiplier against a target Attack.prototype.GetAttackBonus = function(type, target) { let attackBonus = 1; let template = this.template[type]; if (!template) template = this.template[type.split(".")[0]].Splash; if (template.Bonuses) { let cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return 1; // Multiply the bonuses for all matching classes for (let key in template.Bonuses) { let bonus = template.Bonuses[key]; let hasClasses = true; if (bonus.Classes){ let classes = bonus.Classes.split(/\s+/); for (let key in classes) hasClasses = hasClasses && cmpIdentity.HasClass(classes[key]); } if (hasClasses && (!bonus.Civ || bonus.Civ === cmpIdentity.GetCiv())) attackBonus *= bonus.Multiplier; } } return attackBonus; }; // Returns a 2d random distribution scaled for a spread of scale 1. // The current implementation is a 2d gaussian with sigma = 1 Attack.prototype.GetNormalDistribution = function(){ // Use the Box-Muller transform to get a gaussian distribution let a = Math.random(); let b = Math.random(); let c = Math.sqrt(-2*Math.log(a)) * Math.cos(2*Math.PI*b); let d = Math.sqrt(-2*Math.log(a)) * Math.sin(2*Math.PI*b); return [c, d]; }; /** * Attack the target entity. This should only be called after a successful range check, * and should only be called after GetTimers().repeat msec has passed since the last * call to PerformAttack. */ Attack.prototype.PerformAttack = function(type, target) { let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(); let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); // If this is a ranged attack, then launch a projectile if (type == "Ranged") { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let turnLength = cmpTimer.GetLatestTurnLength()/1000; // In the future this could be extended: // * Obstacles like trees could reduce the probability of the target being hit // * Obstacles like walls should block projectiles entirely // Get some data about the entity let horizSpeed = +this.template[type].ProjectileSpeed; let gravity = 9.81; // this affects the shape of the curve; assume it's constant for now let spread = +this.template.Ranged.Spread; spread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", spread, this.entity); //horizSpeed /= 2; gravity /= 2; // slow it down for testing let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; let selfPosition = cmpPosition.GetPosition(); let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; let targetPosition = cmpTargetPosition.GetPosition(); let relativePosition = Vector3D.sub(targetPosition, selfPosition); let previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition(); let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength); // The component of the targets velocity radially away from the archer let radialSpeed = relativePosition.dot(targetVelocity) / relativePosition.length(); let horizDistance = targetPosition.horizDistanceTo(selfPosition); // This is an approximation of the time ot the target, it assumes that the target has a constant radial // velocity, but since units move in straight lines this is not true. The exact value would be more // difficult to calculate and I think this is sufficiently accurate. (I tested and for cavalry it was // about 5% of the units radius out in the worst case) let timeToTarget = horizDistance / (horizSpeed - radialSpeed); // Predict where the unit is when the missile lands. let predictedPosition = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition); // Compute the real target point (based on spread and target speed) let range = this.GetRange(type); let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let elevationAdaptedMaxRange = cmpRangeManager.GetElevationAdaptedRange(selfPosition, cmpPosition.GetRotation(), range.max, range.elevationBonus, 0); let distanceModifiedSpread = spread * horizDistance/elevationAdaptedMaxRange; let randNorm = this.GetNormalDistribution(); let offsetX = randNorm[0] * distanceModifiedSpread * (1 + targetVelocity.length() / 20); let offsetZ = randNorm[1] * distanceModifiedSpread * (1 + targetVelocity.length() / 20); let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, targetPosition.y, predictedPosition.z + offsetZ); // Calculate when the missile will hit the target position let realHorizDistance = realTargetPosition.horizDistanceTo(selfPosition); timeToTarget = realHorizDistance / horizSpeed; let missileDirection = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance); // Launch the graphical projectile let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); let id = cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity); cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let data = { "type": type, "attacker": this.entity, "target": target, "strengths": this.GetAttackStrengths(type), "position": realTargetPosition, "direction": missileDirection, "projectileId": id, "multiplier": this.GetAttackBonus(type, target), "isSplash": false, "attackerOwner": attackerOwner }; if (this.template.Ranged.Splash) { data.friendlyFire = this.template.Ranged.Splash.FriendlyFire; data.radius = +this.template.Ranged.Splash.Range; data.shape = this.template.Ranged.Splash.Shape; data.isSplash = true; } cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", timeToTarget * 1000, data); } else if (type == "Capture") { if (attackerOwner == -1) return; let multiplier = this.GetAttackBonus(type, target); let cmpHealth = Engine.QueryInterface(target, IID_Health); if (!cmpHealth || cmpHealth.GetHitpoints() == 0) return; multiplier *= cmpHealth.GetMaxHitpoints() / (0.1 * cmpHealth.GetMaxHitpoints() + 0.9 * cmpHealth.GetHitpoints()); let cmpCapturable = Engine.QueryInterface(target, IID_Capturable); if (!cmpCapturable || !cmpCapturable.CanCapture(attackerOwner)) return; let strength = this.GetAttackStrengths("Capture").value * multiplier; if (cmpCapturable.Reduce(strength, attackerOwner)) Engine.PostMessage(target, MT_Attacked, { "attacker": this.entity, "target": target, "type": type, "damage": strength, "attackerOwner": attackerOwner }); } else { // Melee attack - hurt the target immediately cmpDamage.CauseDamage({ "strengths": this.GetAttackStrengths(type), "target": target, "attacker": this.entity, "multiplier": this.GetAttackBonus(type, target), "type": type, "attackerOwner": attackerOwner }); } }; Attack.prototype.OnValueModification = function(msg) { if (msg.component != "Attack") return; let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); if (!cmpUnitAI) return; if (this.GetAttackTypes().some(type => msg.valueNames.indexOf("Attack/" + type + "/MaxRange") != -1)) cmpUnitAI.UpdateRangeQueries(); }; Engine.RegisterComponentType(IID_Attack, "Attack", Attack); Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fishing.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fishing.xml (revision 18740) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fishing.xml (revision 18741) @@ -1,67 +1,67 @@ 2 5 2 10.0 0.0 0.0 5.0 1000 - Ship Structure Human Elephant Domestic + !SeaCreature 6.0 1 0 Female Infantry Support Infantry 1 10 true Fishing Boat Fish the waters for Food. Garrison a support or infantry unit inside to boost fishing rate. FishingBoat 1 10 0 6.0 1.0 1.8 actor/ship/boat_move.xml actor/ship/boat_move.xml passive false ship-small 10 24