Index: ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/maps/scripts/CaptureTheRelic.js (revision 20953) @@ -1,180 +1,180 @@ Trigger.prototype.InitCaptureTheRelic = function() { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let catafalqueTemplates = shuffleArray(cmpTemplateManager.FindAllTemplates(false).filter( name => GetIdentityClasses(cmpTemplateManager.GetTemplate(name).Identity || {}).indexOf("Relic") != -1)); let potentialSpawnPoints = TriggerHelper.GetLandSpawnPoints(); if (!potentialSpawnPoints.length) { error("No gaia entities found on this map that could be used as spawn points!"); return; } let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); let numSpawnedRelics = cmpEndGameManager.GetGameTypeSettings().relicCount; this.playerRelicsCount = new Array(TriggerHelper.GetNumberOfPlayers()).fill(0, 1); this.playerRelicsCount[0] = numSpawnedRelics; for (let i = 0; i < numSpawnedRelics; ++i) { this.relics[i] = TriggerHelper.SpawnUnits(pickRandom(potentialSpawnPoints), catafalqueTemplates[i], 1, 0)[0]; let cmpDamageReceiver = Engine.QueryInterface(this.relics[i], IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(true); let cmpPositionRelic = Engine.QueryInterface(this.relics[i], IID_Position); cmpPositionRelic.SetYRotation(randomAngle()); } }; Trigger.prototype.CheckCaptureTheRelicVictory = function(data) { let cmpIdentity = Engine.QueryInterface(data.entity, IID_Identity); - if (!cmpIdentity || !cmpIdentity.HasClass("Relic") || data.from == -1) + if (!cmpIdentity || !cmpIdentity.HasClass("Relic") || data.from == INVALID_PLAYER) return; --this.playerRelicsCount[data.from]; if (data.to == -1) { warn("Relic entity " + data.entity + " has been destroyed"); this.relics.splice(this.relics.indexOf(data.entity), 1); } else ++this.playerRelicsCount[data.to]; this.CheckCaptureTheRelicCountdown(); }; /** * Check if an individual player or team has acquired all relics. * Also check if the countdown needs to be stopped if a player/team no longer has all relics. * Reset the countdown if any of the original allies tries to change their diplomacy with one of these allies. */ Trigger.prototype.CheckCaptureTheRelicCountdown = function(data) { let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID) { let playerAndAllies = cmpEndGameManager.GetAlliedVictory() ? QueryPlayerIDInterface(playerID).GetMutualAllies() : [playerID]; let teamRelicsOwned = 0; for (let ally of playerAndAllies) teamRelicsOwned += this.playerRelicsCount[ally]; if (teamRelicsOwned == this.relics.length) { if (!data || !this.relicsVictoryCountdownPlayers.length || this.relicsVictoryCountdownPlayers.indexOf(data.player) != -1 && this.relicsVictoryCountdownPlayers.indexOf(data.otherPlayer) != -1) { this.relicsVictoryCountdownPlayers = playerAndAllies; this.StartCaptureTheRelicCountdown(playerAndAllies); } return; } } this.DeleteCaptureTheRelicVictoryMessages(); }; Trigger.prototype.DeleteCaptureTheRelicVictoryMessages = function() { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.relicsVictoryTimer); let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.DeleteTimeNotification(this.ownRelicsVictoryMessage); cmpGuiInterface.DeleteTimeNotification(this.othersRelicsVictoryMessage); this.relicsVictoryCountdownPlayers = []; }; Trigger.prototype.StartCaptureTheRelicCountdown = function(playerAndAllies) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (this.relicsVictoryTimer) { cmpTimer.CancelTimer(this.relicsVictoryTimer); cmpGuiInterface.DeleteTimeNotification(this.ownRelicsVictoryMessage); cmpGuiInterface.DeleteTimeNotification(this.othersRelicsVictoryMessage); } if (!this.relics.length) return; let others = [-1]; for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID) { let cmpPlayer = QueryPlayerIDInterface(playerID); if (cmpPlayer.GetState() == "won") return; if (playerAndAllies.indexOf(playerID) == -1) others.push(playerID); } let cmpPlayer = QueryOwnerInterface(this.relics[0], IID_Player); let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); let captureTheRelicDuration = cmpEndGameManager.GetGameTypeSettings().relicDuration; let isTeam = playerAndAllies.length > 1; this.ownRelicsVictoryMessage = cmpGuiInterface.AddTimeNotification({ "message": isTeam ? markForTranslation("%(_player_)s's team has captured all relics and will win in %(time)s.") : markForTranslation("%(_player_)s has captured all relics and will win in %(time)s."), "players": others, "parameters": { "_player_": cmpPlayer.GetPlayerID() }, "translateMessage": true, "translateParameters": [] }, captureTheRelicDuration); this.othersRelicsVictoryMessage = cmpGuiInterface.AddTimeNotification({ "message": isTeam ? markForTranslation("Your team has captured all relics and will win in %(time)s.") : markForTranslation("You have captured all relics and will win in %(time)s."), "players": playerAndAllies, "translateMessage": true }, captureTheRelicDuration); this.relicsVictoryTimer = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, "CaptureTheRelicVictorySetWinner", captureTheRelicDuration, playerAndAllies[0]); }; Trigger.prototype.CaptureTheRelicVictorySetWinner = function(playerID) { let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); cmpEndGameManager.MarkPlayerAsWon( playerID, n => markForPluralTranslation( "%(lastPlayer)s has won (Capture the Relic).", "%(players)s and %(lastPlayer)s have won (Capture the Relic).", n), n => markForPluralTranslation( "%(lastPlayer)s has been defeated (Capture the Relic).", "%(players)s and %(lastPlayer)s have been defeated (Capture the Relic).", n)); }; { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.relics = []; cmpTrigger.playerRelicsCount = []; cmpTrigger.relicsVictoryTimer = undefined; cmpTrigger.ownRelicsVictoryMessage = undefined; cmpTrigger.othersRelicsVictoryMessage = undefined; cmpTrigger.relicsVictoryCountdownPlayers = []; cmpTrigger.DoAfterDelay(0, "InitCaptureTheRelic", {}); cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "CheckCaptureTheRelicCountdown", { "enabled": true }); cmpTrigger.RegisterTrigger("OnOwnershipChanged", "CheckCaptureTheRelicVictory", { "enabled": true }); cmpTrigger.RegisterTrigger("OnPlayerWon", "DeleteCaptureTheRelicVictoryMessages", { "enabled": true }); } Index: ps/trunk/binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.js (revision 20953) @@ -1,415 +1,415 @@ Trigger.prototype.tutorialGoals = [ { "instructions": markForTranslation("Welcome to the 0 A.D. tutorial."), }, { "instructions": markForTranslation("Left-click on a female citizen and then right-click on a berry bush to make that female citizen gather food. Female citizens gather vegetables faster than other units."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "fruit") this.NextGoal(); } }, { "instructions": markForTranslation("Select the citizen-soldier, right-click on a tree near the Civic Center to begin gathering Wood. Citizen Soldiers gather Wood faster than female citizens."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "tree") this.NextGoal(); } }, { "instructions": [ { "text": markForTranslation("Select the Civic Center building and hold %(hotkey)s while clicking on the Hoplite icon once to begin training a batch of Hoplites."), "hotkey": "session.batchtrain" } ], "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart_infantry_spearman_b" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the HOPLITE icon."); this.WarningMessage(txt); return; } this.NextGoal(); } }, { "instructions": markForTranslation("Select the two idle female citizens and build a house nearby by selecting the house icon. Place the house by left-clicking on a piece of land."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "House")) this.NextGoal(); } }, { "instructions": markForTranslation("When they are ready, select the newly trained Hoplites and assign them to build a storehouse beside some nearby trees. They will begin to gather Wood when it's constructed."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Storehouse")) this.NextGoal(); } }, { "instructions": [ { "text": markForTranslation("Build a batch of Skirmishers by holding %(hotkey)s and clicking on the Skirmisher icon in the Civic Center."), "hotkey": "session.batchtrain" } ], "Init": function() { this.trainingDone = false; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart_infantry_javelinist_b" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the Skirmisher icon."); this.WarningMessage(txt); return; } this.NextGoal(); } }, { "instructions": markForTranslation("Build a farmstead in an open space beside the Civic Center using any idle builders."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Farmstead")) this.NextGoal(); }, "OnTrainingFinished": function(msg) { this.trainingDone = true; } }, { "instructions": markForTranslation("Let's wait for the farmstead to be built."), "OnTrainingFinished": function(msg) { this.trainingDone = true; }, "OnStructureBuilt": function(msg) { if (TriggerHelper.EntityMatchesClassList(msg.building, "Farmstead")) this.NextGoal(); } }, { "instructions": markForTranslation("Once the farmstead is constructed, its builders will automatically begin gathering food if there is any nearby. Select the builders and instead make them construct a field beside the farmstead."), "Init": function() { this.farmStarted = false; }, "IsDone": function() { return this.farmStarted && this.trainingDone; }, "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Field")) this.farmStarted = true; if (this.IsDone()) this.NextGoal(); }, "OnTrainingFinished": function(msg) { this.trainingDone = true; if (this.IsDone()) this.NextGoal(); } }, { "instructions": markForTranslation("The field's builders will now automatically begin gathering food from the field. Using the newly created group of skirmishers, get them to build another house nearby."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "House")) this.NextGoal(); } }, { "instructions": markForTranslation("Train a batch of Hoplites at the Civic Center. Select the Civic Center and with it selected right-click on a tree nearby. Units from the Civic Center will now automatically gather Wood."), "Init": function() { this.rallyPointSet = false; this.trainingStarted = false; }, "IsDone": function() { return this.rallyPointSet && this.trainingStarted; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart_infantry_spearman_b" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the Hoplite icon."); this.WarningMessage(txt); return; } this.trainingStarted = true; if (this.IsDone()) this.NextGoal(); }, "OnPlayerCommand": function(msg) { if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data || !msg.cmd.data.command || msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "tree") { this.WarningMessage(markForTranslation("Select the Civic Center, then hover the cursor over the tree and right-click when you see your cursor change into a Wood icon.")); return; } this.rallyPointSet = true; if (this.IsDone()) this.NextGoal(); } }, { "instructions": markForTranslation("Order the idle Skirmishers to build an outpost to the north east at the edge of your territory. This will be the fifth Village Phase structure that you have built, allowing you to advance to the Town Phase."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Outpost")) this.NextGoal(); } }, { "instructions": markForTranslation("Select the Civic Center again and advance to Town Phase by clicking on the 'II' icon (you have to wait for the outpost to be built first). This will allow Town Phase buildings to be constructed."), "IsDone": function() { return TriggerHelper.HasDealtWithTech(this.playerID, "phase_town_generic"); }, "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "CivilCentre")) this.NextGoal(); } }, { "instructions": markForTranslation("While waiting for the phasing up, you may reaffect your idle workers to gathering the resources you are short of."), "IsDone": function() { let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let playerEnt = cmpPlayerManager.GetPlayerByID(this.playerID); let cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); return cmpTechnologyManager && cmpTechnologyManager.IsTechnologyResearched("phase_town_generic"); }, "OnResearchFinished": function(msg) { if (msg.tech == "phase_town_generic") this.NextGoal(); } }, { "instructions": markForTranslation("Start building a batch of female citizens in the Civic Center and set its rally point to the farm (right click on it)."), "Init": function() { this.rallyPointSet = false; this.trainingStarted = false; }, "IsDone": function() { return this.rallyPointSet && this.trainingStarted; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart_support_female_citizen" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the female citizen icon."); this.WarningMessage(txt); return; } this.trainingStarted = true; if (this.IsDone()) this.NextGoal(); }, "OnPlayerCommand": function(msg) { if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data || !msg.cmd.data.command || msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "grain") return; this.rallyPointSet = true; if (this.IsDone()) this.NextGoal(); } }, { "instructions": markForTranslation("Build a Barracks nearby. Whenever your population limit is reached, build an extra house using any available builder units."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Barracks")) this.NextGoal(); } }, { "instructions": markForTranslation("Prepare for an attack by an enemy player. Build more soldiers using the Barracks, and get idle soldiers to build a Defense Tower near your Outpost."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "DefenseTower")) this.NextGoal(); } }, { "instructions": markForTranslation("Build a Blacksmith and research the Infantry Training technology (sword icon) to improve infantry hack attack."), "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "Blacksmith")) this.NextGoal(); } }, { "instructions": markForTranslation("The enemy is coming. Build more soldiers to fight off the enemies."), "OnResearchFinished": function(msg) { this.LaunchAttack(); this.NextGoal(); } }, { "instructions": markForTranslation("Try to repel the attack."), "OnOwnershipChanged": function(msg) { - if (msg.to != -1) + if (msg.to != INVALID_PLAYER) return; if (this.IsAttackRepelled()) this.NextGoal(); } }, { "instructions": markForTranslation("The enemy attack has been thwarted. Now build a market and a temple while you assign new units to gather required resources."), "Init": function() { this.marketStarted = false; this.templeStarted = false; }, "IsDone": function() { return this.marketStarted && this.templeStarted; }, "OnPlayerCommand": function(msg) { if (msg.cmd.type != "repair") return; this.marketStarted = this.marketStarted || TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Market"); this.templeStarted = this.templeStarted || TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Temple"); if (this.IsDone()) this.NextGoal(); } }, { "instructions": markForTranslation("Once you meet the City Phase requirements, select your Civic Center and advance to City Phase."), "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "CivilCentre")) this.NextGoal(); } }, { "instructions": markForTranslation("While waiting for the phase change, you may build more soldiers at the barracks."), "OnResearchFinished": function(msg) { if (msg.tech == "phase_city_generic") this.NextGoal(); } }, { "instructions": markForTranslation("Now that you are in City Phase, build a fortress nearby (gather some stone first if needed) and then use it to build 2 Battering Rams."), "Init": function() { this.ramCount = 0; }, "IsDone": function() { return this.ramCount > 1; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate == "units/spart_mechanical_siege_ram") ++this.ramCount; if (this.IsDone()) { this.RemoveChampions(); this.NextGoal(); } } }, { "instructions": [ markForTranslation("Stop all your soldiers gathering resources and instead task small groups to find the enemy Civic Center on the map. Once The enemy's base has been spotted, send your siege weapons and all remaining soldiers to destroy it.\n"), markForTranslation("Female citizens should continue to gather resources.") ], "OnOwnershipChanged": function(msg) { if (msg.from != this.enemyID) return; if (TriggerHelper.EntityMatchesClassList(msg.entity, "CivilCentre")) this.NextGoal(); } }, { "instructions": markForTranslation("The enemy has been defeated. These tutorial tasks are now completed."), } ]; Trigger.prototype.LaunchAttack = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID); let target = entities.find(e => Engine.QueryInterface(e, IID_Identity) && Engine.QueryInterface(e, IID_Identity).HasClass("DefenseTower")) || entities.find(e => Engine.QueryInterface(e, IID_Identity) && Engine.QueryInterface(e, IID_Identity).HasClass("CivilCentre")); let position = Engine.QueryInterface(target, IID_Position).GetPosition2D(); this.attackers = cmpRangeManager.GetEntitiesByPlayer(this.enemyID).filter(e => Engine.QueryInterface(e, IID_Identity) && Engine.QueryInterface(e, IID_UnitAI) && Engine.QueryInterface(e, IID_Identity).HasClass("CitizenSoldier") ); this.attackers.forEach(e => { Engine.QueryInterface(e, IID_UnitAI).WalkAndFight(position.x, position.y, { "attack": ["Unit"] }, false); }); }; Trigger.prototype.IsAttackRepelled = function() { return !this.attackers.some(e => Engine.QueryInterface(e, IID_Health) && Engine.QueryInterface(e, IID_Health).GetHitpoints() > 0); }; Trigger.prototype.RemoveChampions = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let champions = cmpRangeManager.GetEntitiesByPlayer(this.enemyID).filter(e => Engine.QueryInterface(e, IID_Identity).HasClass("Champion")); let keep = 6; for (let ent of champions) { let cmpHealth = Engine.QueryInterface(ent, IID_Health); if (!cmpHealth) Engine.DestroyEntity(ent); else if (--keep < 0) cmpHealth.Kill(); } }; { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.playerID = 1; cmpTrigger.enemyID = 2; cmpTrigger.RegisterTrigger("OnInitGame", "InitTutorial", { "enabled": true }); } Index: ps/trunk/binaries/data/mods/public/maps/tutorials/starting_economy_walkthrough.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/tutorials/starting_economy_walkthrough.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/maps/tutorials/starting_economy_walkthrough.js (revision 20953) @@ -1,459 +1,459 @@ Trigger.prototype.tutorialGoals = [ { "instructions": [ markForTranslation("This tutorial will teach the basics of developing your economy. Typically, you will start with a Civic Center and a couple units in 'Village Phase' and ultimately, your goal will be to develop and expand your empire, often by evolving to 'Town Phase' and 'City Phase' afterward.\n"), { "text": markForTranslation("\nBefore starting, you can toggle between fullscreen and windowed mode using %(hotkey)s."), "hotkey": ["togglefullscreen"] }, markForTranslation("You can change the level of zoom using the mouse wheel and the camera view using any of your keyboard's arrow keys.\n"), markForTranslation("Adjust the game window to your preferences.\n"), { "text": markForTranslation("\nYou may also toggle between showing and hiding this tutorial panel at any moment using %(hotkey)s.\n"), "hotkey": ["session.gui.tutorial.toggle"] } ] }, { "instructions": [ markForTranslation("To start off, select your building, the Civic Center, by clicking on it. A selection ring in the color of your civilization will be displayed after clicking.") ] }, { "instructions": [ markForTranslation("Now that the Civic Center is selected, you will notice that a production panel will appear on the lower right of your screen detailing the actions that the buildings supports. For the production panel, available actions are not masked in any color, while an icon masked in either grey or red indicates that the action has not been unlocked or you do not have sufficient resources to perform that action, respectively. Additionally, you can hover the cursor over any icon to show a tooltip with more details.\n"), markForTranslation("The top row of buttons contains portraits of units that may be trained at the building while the bottom one or two rows will have researchable technologies. Hover the cursor over the 'II' icon. The tooltip will tell us that advancing to 'Town Phase' requires both more constructed structures as well as more Food and Wood resources.") ] }, { "instructions": [ markForTranslation("You have two main types of starting units: female citizens and citizen soldiers. Female citizens are purely economic units; they have low HP, no armor, and little to no attack. Citizen soldiers are workers by default, but in times of need, can utilize a weapon to fight. You have two categories of citizen soldiers: infantry and cavalry. Female citizens and infantry citizen soldiers can gather any land resources while cavalry citizen soldiers can only gather meat from hunted animals.\n") ] }, { "instructions": [ markForTranslation("As a general rule of thumb, left-clicking represents selection while right-clicking with an entity selected represents an order (gather, build, fight, etc.).\n") ] }, { "instructions": [ markForTranslation("At this point, food and wood are the most important resources for developing your economy, so let's start with gathering food. Female citizens gather vegetables faster than other units.\n"), markForTranslation("There are primarily three ways to select units:\n"), markForTranslation("1) Hold the left mouse button and drag a selection rectangle that encloses the units you want to select.\n"), markForTranslation("2) Click on one of them and then add additional units to your selection by holding Shift and clicking each additional unit (or also via the above selection rectangle).\n"), markForTranslation("3) Double-click on a unit. This will select every unit of the same type as the specified unit in your visible window. Triple-click will select all units of the same type on the entire map.\n"), markForTranslation("You can click on an empty space on the map to reset the selection. Try each of these methods before tasking all of your female citizens to gather the grapes to the southeast of your Civic Center by right-clicking on the grapes when you have all the female citizens selected.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "fruit") this.NextGoal(); } }, { "instructions": [ markForTranslation("Now, let's gather some Wood with your Infantry Citizen Soldiers. Select your Infantry Citizen Soldiers and order them to gather Wood by right-clicking on the nearest tree.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "tree") this.NextGoal(); } }, { "instructions": [ markForTranslation("Cavalry Citizen Soldiers are good for hunting. Select your cavalry and order him to hunt the chickens around your Civic Center in similar fashion.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "meat") this.NextGoal(); } }, { "instructions": [ markForTranslation("All your units are now gathering resources. We should train more units!\n"), markForTranslation("First, let's set a rally-point. Setting a rally point on a building that can train units will automatically designate a task to the new unit upon completion of training. We want to send the newly trained units to gather Wood on the group of trees to the south of the Civic Center. To do so, select the Civic Center by clicking on it and then right-click on one of the trees.\n"), markForTranslation("Rally-Points are indicated by a small flag at the end of the blue line.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data || !msg.cmd.data.command || msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "tree") { this.WarningMessage(markForTranslation("Select the Civic Center, then hover the cursor over a tree and right-click when you see the cursor change into a Wood icon.")); return; } this.NextGoal(); } }, { "instructions": [ markForTranslation("Now that the rally-point is set, we can produce additional units and they will do their assigned task automatically.\n"), markForTranslation("Citizen soldiers gather wood faster than female citizens. Select the Civic Center and, while holding Shift, click on the second unit icon, the hoplites (holding Shift trains a batch of five units). You can also train units individually by simply clicking, but training 5 units together takes less time than training 5 units individually.") ], "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/athen_infantry_spearman_b" || +msg.count == 1) { let entity = msg.trainerEntity; let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to hold Shift while clicking to train several units.") : markForTranslation("Hold Shift and click on the Hoplite icon."); this.WarningMessage(txt); return; } this.NextGoal(); } }, { "instructions": [ markForTranslation("Let's wait for the units to be trained.\n"), markForTranslation("While waiting, direct your attention to the panel at the top of your screen. On the upper left, you will see your current resource supply (Food, Wood, Stone, and Metal). As each worker brings resources back to the Civic Center (or another dropsite), you will see the amount of the corresponding resource increase.\n"), markForTranslation("This is a very important concept to keep in mind: gathered resources have to be brought back to a dropsite to be accounted, and you should always try to minimize the distance between resource and nearest dropsite to improve your gathering efficiency.") ], "OnTrainingFinished": function(msg) { this.NextGoal(); } }, { "instructions": [ markForTranslation("The newly trained units automatically go to the trees and start gathering Wood.\n"), markForTranslation("But as they have to bring it back to the Civic Center to deposit it, their gathering efficiency suffers from the distance. To fix that, we can build a storehouse, a dropsite for Wood, Stone, and Metal, close to the trees. To do so, select your five newly trained Citizen Soldiers and look for the construction panel on the bottom right, click on the storehouse icon, move the mouse as close as possible to the trees you want to gather and click on a valid place to build the dropsite.\n"), markForTranslation("Invalid (obstructed) positions will show the building preview overlay in red.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "construct" && msg.cmd.template == "structures/athen_storehouse") this.NextGoal(); } }, { "instructions": [ markForTranslation("The selected citizens will automatically start constructing the building once you place the foundation.") ], "OnStructureBuilt": function(msg) { let cmpResourceDropsite = Engine.QueryInterface(msg.building, IID_ResourceDropsite); if (cmpResourceDropsite && cmpResourceDropsite.AcceptsType("wood")) this.NextGoal(); }, }, { "instructions": [ markForTranslation("When construction finishes, the builders default to gathering Wood automatically.\n"), markForTranslation("Let's train some female citizens to gather more food. Select the Civic Center, hold Shift and click on the female citizen icon to train 5 female citizens.") ], "Init": function() { this.trainingDone = false; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/athen_support_female_citizen" || +msg.count == 1) { let entity = msg.trainerEntity; let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to hold Shift and click to train several units.") : markForTranslation("Hold shift and click on the Female Citizen icon."); this.WarningMessage(txt); return; } this.NextGoal(); } }, { "instructions": [ markForTranslation("Let's wait for the units to be trained.\n"), markForTranslation("In the meantime, we seem to have enough workers gathering Wood. We should remove the current rally-point of the Civic Center away from gathering Wood. For that purpose, right-click on the Civic Center when it is selected (and the flag icon indicating the rally-point is crossed out).") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "unset-rallypoint") this.NextGoal(); }, "OnTrainingFinished": function(msg) { this.trainingDone = true; } }, { "instructions": [ markForTranslation("The units should be ready soon.\n"), markForTranslation("In the meantime, direct your attention to your population count on the top panel. It is the fifth item from the left, after the resources. It would be prudent to keep an eye on it. It indicates your current population (including those being trained) and the current population limit, which is determined by your built structures.") ], "IsDone": function(msg) { return this.trainingDone; }, "OnTrainingFinished": function(msg) { this.NextGoal(); } }, { "instructions": [ markForTranslation("As you have nearly reached the population limit, you must increase it by building some new structures if you want to train more units. The most cost effective structure to increase your population limit is the house.\n"), markForTranslation("Now that the units are ready, let's see how to build several houses in a row.") ] }, { "instructions": [ markForTranslation("Select two of your newly-trained female citizens and ask them to build these houses in the empty space to the east of the Civic Center. To do so, after selecting the female citizens, click on the house icon in the bottom right panel and, while holding Shift, click first on the position in the map where you want to build the first house, and then click on the position where you want to build the second house (when you give a command while holding Shift, you put the command in a queue; units automatically switch to the next command in their queue when they finish their current command). Press Escape to get rid of the house cursor so you don't spam houses all over the map.\n"), markForTranslation("Reminder: to select only two female citizens, click on the first one and then hold Shift and click on the second one.") ], "Init": function() { this.houseGoal = new Set(); this.houseCount = 0; }, "IsDone": function() { return this.houseCount > 1; }, "OnOwnershipChanged": function(msg) { - if (msg.from != -1 && this.houseGoal.has(+msg.entity)) + if (msg.from != INVALID_PLAYER && this.houseGoal.has(+msg.entity)) { this.houseGoal.delete(+msg.entity); let cmpFoundation = Engine.QueryInterface(+msg.entity, IID_Foundation); if (cmpFoundation && cmpFoundation.GetBuildProgress() < 1) // Destroyed before built --this.houseCount; } - else if (msg.from == -1 && msg.to == this.playerID && + else if (msg.from == INVALID_PLAYER && msg.to == this.playerID && Engine.QueryInterface(+msg.entity, IID_Foundation) && TriggerHelper.EntityMatchesClassList(+msg.entity, "House")) { this.houseGoal.add(+msg.entity); ++this.houseCount; if (this.IsDone()) this.NextGoal(); } } }, { "instructions": [ markForTranslation("You may notice that berries are a finite supply of food. We will need a more lasting food source. Fields produce an unlimited food resource, but are slower to gather than forageable fruits.\n"), markForTranslation("But to minimize the distance between a farm and its corresponding food dropsite, we will first build a farmstead.") ], "delay": -1, "OnOwnershipChanged": function(msg) { if (this.houseGoal.has(+msg.entity)) this.houseGoal.delete(+msg.entity); } }, { "instructions": [ markForTranslation("Select the three remaining (idle) female citizens and order them to build a farmstead in the center of the large open area to the west of the Civic Center.\n"), markForTranslation("We will need a decent chunk of space around the farmstead to build fields. In addition, we can see goats on the west side to further improve our food gathering efficiency should we ever decide to hunt them.\n"), markForTranslation("If you try to select the three idle female citizens by clicking and dragging a selection rectangle over them, you might accidentally select additional units. To avoid that, hold the I key while selecting so that only idle units are selected. If you accidentally select a cavalry unit, hold Ctrl and click on the cavalry unit icon of the selection panel at the bottom of the screen to remove the cavalry unit from the current selection.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "construct" && msg.cmd.template == "structures/athen_farmstead") this.NextGoal(); }, "OnOwnershipChanged": function(msg) { if (this.houseGoal.has(+msg.entity)) this.houseGoal.delete(+msg.entity); } }, { "instructions": [ markForTranslation("When the farmstead construction is finished, its builders will automatically look for food, and in this case, they will go after the nearby goats.\n"), markForTranslation("But your house builders will only look for something else to build and, if nothing found, become idle. Let's wait for them to build the houses.") ], "IsDone": function() { return !this.houseGoal.size; }, "OnOwnershipChanged": function(msg) { if (this.houseGoal.has(+msg.entity)) this.houseGoal.delete(+msg.entity); if (this.IsDone()) this.NextGoal(); } }, { "instructions": [ markForTranslation("When both houses are built, select your two female citizens and order them to build a field as close as possible to the farmstead, which is a dropsite for all types of food.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "construct" && msg.cmd.template == "structures/athen_field") this.NextGoal(); } }, { "instructions": [ markForTranslation("When the field is constructed, the builders will automatically start gathering it.\n"), markForTranslation("The cavalry unit should have slaughtered all chickens by now. Select it and explore the south-west area: there is a lake with some camels around. Move your cavalry by right-clicking on the point you want to go, and when you see a herd of camels, right-click on one of them to start hunting for food.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "meat") this.NextGoal(); } }, { "instructions": [ markForTranslation("A field can have up to five farmers working on it. To add additional gatherers, select the Civic Center and setup a rally-point on a field by right-clicking on it. As long as the field is not yet build, new workers sent by a rally-point will help building it, while they will gather it when built.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data || !msg.cmd.data.command || (msg.cmd.data.command != "build" || !msg.cmd.data.target || !TriggerHelper.EntityMatchesClassList(msg.cmd.data.target, "Field")) && (msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "grain")) { this.WarningMessage(markForTranslation("Select the Civic Center and right-click on the field.")); return; } this.NextGoal(); } }, { "instructions": [ markForTranslation("Now click three times on the female citizen icon in the bottom right panel to train three additional farmers.") ], "Init": function(msg) { this.femaleCount = 0; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/athen_support_female_citizen" || +msg.count != 1) { let entity = msg.trainerEntity; let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count != 1 ? markForTranslation("Click without holding Shift to train a single unit.") : markForTranslation("Click on the Female Citizen icon."); this.WarningMessage(txt); return; } if (++this.femaleCount == 3) this.NextGoal(); } }, { "instructions": [ markForTranslation("You can increase the gather rates of your workers by researching new technologies available in some buildings.\n"), markForTranslation("The farming rate, for example, can be improved with a researchable technology in the farmstead. Select the farmstead and look at its production panel on the bottom right. You will see several researchable technologies. Hover the cursor over them to see their costs and effects and click on the one you want to research.") ], "IsDone": function() { return TriggerHelper.HasDealtWithTech(this.playerID, "gather_wicker_baskets") || TriggerHelper.HasDealtWithTech(this.playerID, "gather_farming_plows"); }, "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "Farmstead")) this.NextGoal(); } }, { "instructions": [ markForTranslation("We should start preparing to phase up into 'Town Phase', which will unlock many more units and buildings. Select the Civic Center and hover the cursor over the 'Town Phase' icon to see what is still needed.\n"), markForTranslation("We now have enough resources, but one structure is missing. Although this is an economic tutorial, it is nonetheless useful to be prepared for defense in case of attack, so let's build barracks.\n"), markForTranslation("Select four of your soldiers and ask them to build a barracks: as before, start selecting the soldiers, click on the barracks icon in the production panel and then lay down a foundation not far from your Civic Center where you want to build.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "construct" && msg.cmd.template == "structures/athen_barracks") this.NextGoal(); } }, { "instructions": [ markForTranslation("Let's wait for the barracks to be built. As this construction is lengthy, you can add two soldiers to build it faster. To do so, select your Civic Center and set up a rally-point on the barracks foundation by right-clicking on it (you should see a hammer icon), and then produce two more builders by clicking on the hoplite icon twice.") ], "OnStructureBuilt": function(msg) { if (TriggerHelper.EntityMatchesClassList(msg.building, "Barracks")) this.NextGoal(); }, }, { "instructions": [ markForTranslation("You should now be able to research 'Town Phase'. Select the Civic Center and click on the technology icon.\n"), markForTranslation("If you still miss some resources (icon with red overlay), wait for them to be gathered by your workers.") ], "IsDone": function() { return TriggerHelper.HasDealtWithTech(this.playerID, "phase_town_athen"); }, "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "CivilCentre")) this.NextGoal(); } }, { "instructions": [ markForTranslation("In later phases, you need usually Stone and Metal to build bigger structures and train better soldiers. Hence, while waiting for the research to be done, you will send half of your idle Citizen Soldiers (who have finished building the Barracks) to gather Stone and the other half to gather Metal.\n"), markForTranslation("To do so, we could select three Citizen Soldiers and right-click on the Stone mine on the west of the Civic Center (the cursor changes when hovering the Stone mine while your soldiers are selected). However, these soldiers were gathering Wood, so they may still carry some Wood which would be lost when starting to gather another resource.") ], }, { "instructions": [ markForTranslation("Thus, we should order them to deposit their Wood in the Civic Center along the way. To do so, we will hold Shift while clicking to queue orders: select your soldiers, hold Shift and right-click on the Civic Center to deposit their Wood and then hold Shift and right-click on the Stone mine to gather it.\n"), markForTranslation("Perform a similar order queue with the remaining soldiers and the Metal mine in the west.") ], "Init": function() { this.stone = false; this.metal = false; }, "IsDone": function() { if (!this.stone || !this.metal) return false; return TriggerHelper.HasDealtWithTech(this.playerID, "phase_town_athen"); }, "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target) { if (TriggerHelper.GetResourceType(msg.cmd.target).generic == "stone") this.stone = true; else if (TriggerHelper.GetResourceType(msg.cmd.target).generic == "metal") this.metal = true; } if (this.IsDone()) this.NextGoal(); }, "OnResearchFinished": function(msg) { if (this.IsDone()) this.NextGoal(); } }, { "instructions": [ markForTranslation("This is the end of the walkthrough. This should give you a good idea of the basics of setting up your economy.") ] } ]; { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.playerID = 1; cmpTrigger.RegisterTrigger("OnInitGame", "InitTutorial", { "enabled": true }); } Index: ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js (revision 20953) @@ -1,383 +1,383 @@ function AIProxy() {} AIProxy.prototype.Schema = ""; /** * AIProxy passes its entity's state data to AI scripts. * * Efficiency is critical: there can be many thousands of entities, * and the data returned by this component is serialized and copied to * the AI thread every turn, so it can be quite expensive. * * We omit all data that can be derived statically from the template XML * files - the AI scripts can parse the templates themselves. * This violates the component interface abstraction and is potentially * fragile if the template formats change (since both the component code * and the AI will have to be updated in sync), but it's not *that* bad * really and it helps performance significantly. * * We also add an optimisation to avoid copying non-changing values. * The first call to GetRepresentation calls GetFullRepresentation, * which constructs the complete entity state representation. * After that, we simply listen to events from the rest of the gameplay code, * and store the changed data in this.changes. * Properties in this.changes will override those previously returned * from GetRepresentation; if a property isn't overridden then the AI scripts * will keep its old value. * * The event handlers should set this.changes.whatever to exactly the * same as GetFullRepresentation would set. */ AIProxy.prototype.Init = function() { this.changes = null; this.needsFullGet = true; this.cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); }; AIProxy.prototype.Serialize = null; // we have no dynamic state to save AIProxy.prototype.Deserialize = function () { this.Init(); }; AIProxy.prototype.GetRepresentation = function() { // Return the full representation the first time we're called let ret; if (this.needsFullGet) ret = this.GetFullRepresentation(); else ret = this.changes; // Initialise changes to null instead of {}, to avoid memory allocations in the // common case where there will be no changes; event handlers should each reset // it to {} if needed this.changes = null; return ret; }; AIProxy.prototype.NotifyChange = function() { if (this.needsFullGet) { // not yet notified, be sure that the owner is set before doing so // as the Create event is sent only on first ownership changed let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() < 0) return false; } if (!this.changes) { this.changes = {}; this.cmpAIInterface.ChangedEntity(this.entity); } return true; }; // AI representation-updating event handlers: AIProxy.prototype.OnPositionChanged = function(msg) { if (!this.NotifyChange()) return; if (msg.inWorld) { this.changes.position = [msg.x, msg.z]; this.changes.angle = msg.a; } else { this.changes.position = undefined; this.changes.angle = undefined; } }; AIProxy.prototype.OnHealthChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.hitpoints = msg.to; }; AIProxy.prototype.OnCapturePointsChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.capturePoints = msg.capturePoints; }; AIProxy.prototype.OnUnitIdleChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.idle = msg.idle; }; AIProxy.prototype.OnUnitStanceChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.stance = msg.to; }; AIProxy.prototype.OnUnitAIStateChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.unitAIState = msg.to; }; AIProxy.prototype.OnUnitAIOrderDataChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.unitAIOrderData = msg.to; }; AIProxy.prototype.OnProductionQueueChanged = function(msg) { if (!this.NotifyChange()) return; let cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue); this.changes.trainingQueue = cmpProductionQueue.GetQueue(); }; AIProxy.prototype.OnGarrisonedUnitsChanged = function(msg) { if (!this.NotifyChange()) return; let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); this.changes.garrisoned = cmpGarrisonHolder.GetEntities(); // Send a message telling a unit garrisoned or ungarrisoned. // I won't check if the unit is still alive so it'll be up to the AI. for (let ent of msg.added) this.cmpAIInterface.PushEvent("Garrison", {"entity" : ent, "holder": this.entity}); for (let ent of msg.removed) this.cmpAIInterface.PushEvent("UnGarrison", {"entity" : ent, "holder": this.entity}); }; AIProxy.prototype.OnResourceSupplyChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.resourceSupplyAmount = msg.to; }; AIProxy.prototype.OnResourceSupplyNumGatherersChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.resourceSupplyNumGatherers = msg.to; }; AIProxy.prototype.OnResourceCarryingChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.resourceCarrying = msg.to; }; AIProxy.prototype.OnFoundationProgressChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.foundationProgress = msg.to; }; AIProxy.prototype.OnFoundationBuildersChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.foundationBuilders = msg.to; }; AIProxy.prototype.OnDropsiteSharingChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.sharedDropsite = msg.shared; }; AIProxy.prototype.OnTerritoryDecayChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.decaying = msg.to; this.cmpAIInterface.PushEvent("TerritoryDecayChanged", msg); }; // TODO: event handlers for all the other things AIProxy.prototype.GetFullRepresentation = function() { this.needsFullGet = false; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let ret = { // These properties are constant and won't need to be updated "id": this.entity, "template": cmpTemplateManager.GetCurrentTemplateName(this.entity) }; let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition) { // Updated by OnPositionChanged if (cmpPosition.IsInWorld()) { let pos = cmpPosition.GetPosition2D(); ret.position = [pos.x, pos.y]; ret.angle = cmpPosition.GetRotation().y; } else { ret.position = undefined; ret.angle = undefined; } } let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (cmpHealth) { // Updated by OnHealthChanged ret.hitpoints = cmpHealth.GetHitpoints(); } let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership) { // Updated by OnOwnershipChanged ret.owner = cmpOwnership.GetOwner(); } let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); if (cmpUnitAI) { // Updated by OnUnitIdleChanged ret.idle = cmpUnitAI.IsIdle(); // Updated by OnUnitStanceChanged ret.stance = cmpUnitAI.GetStanceName(); // Updated by OnUnitAIStateChanged ret.unitAIState = cmpUnitAI.GetCurrentState(); // Updated by OnUnitAIOrderDataChanged ret.unitAIOrderData = cmpUnitAI.GetOrderData(); } let cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue); if (cmpProductionQueue) { // Updated by OnProductionQueueChanged ret.trainingQueue = cmpProductionQueue.GetQueue(); } let cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation); if (cmpFoundation) { // Updated by OnFoundationProgressChanged ret.foundationProgress = cmpFoundation.GetBuildPercentage(); } let cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply); if (cmpResourceSupply) { // Updated by OnResourceSupplyChanged ret.resourceSupplyAmount = cmpResourceSupply.GetCurrentAmount(); ret.resourceSupplyNumGatherers = cmpResourceSupply.GetNumGatherers(); } let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) { // Updated by OnResourceCarryingChanged ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); } let cmpResourceDropsite = Engine.QueryInterface(this.entity, IID_ResourceDropsite); if (cmpResourceDropsite) { // Updated by OnDropsiteSharingChanged ret.sharedDropsite = cmpResourceDropsite.IsShared(); } let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (cmpGarrisonHolder) { // Updated by OnGarrisonedUnitsChanged ret.garrisoned = cmpGarrisonHolder.GetEntities(); } let cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay); if (cmpTerritoryDecay) ret.decaying = cmpTerritoryDecay.IsDecaying(); let cmpCapturable = Engine.QueryInterface(this.entity, IID_Capturable); if (cmpCapturable) ret.capturePoints = cmpCapturable.GetCapturePoints(); return ret; }; // AI event handlers: // (These are passed directly as events to the AI scripts, rather than updating // our proxy representation.) // (This shouldn't include extremely high-frequency events, like PositionChanged, // because that would be very expensive and AI will rarely care about all those // events.) // special case: this changes the state and sends an event. AIProxy.prototype.OnOwnershipChanged = function(msg) { this.NotifyChange(); - if (msg.from === -1) + if (msg.from == INVALID_PLAYER) { this.cmpAIInterface.PushEvent("Create", {"entity" : msg.entity}); return; } - else if (msg.to === -1) + else if (msg.to == INVALID_PLAYER) { this.cmpAIInterface.PushEvent("Destroy", {"entity" : msg.entity}); this.needsFullGet = true; return; } this.changes.owner = msg.to; this.cmpAIInterface.PushEvent("OwnershipChanged", msg); }; AIProxy.prototype.OnAttacked = function(msg) { this.cmpAIInterface.PushEvent("Attacked", msg); }; AIProxy.prototype.OnConstructionFinished = function(msg) { this.cmpAIInterface.PushEvent("ConstructionFinished", msg); }; AIProxy.prototype.OnTrainingStarted = function(msg) { this.cmpAIInterface.PushEvent("TrainingStarted", msg); }; AIProxy.prototype.OnTrainingFinished = function(msg) { this.cmpAIInterface.PushEvent("TrainingFinished", msg); }; AIProxy.prototype.OnAIMetadata = function(msg) { this.cmpAIInterface.PushEvent("AIMetadata", msg); }; Engine.RegisterComponentType(IID_AIProxy, "AIProxy", AIProxy); Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 20953) @@ -1,693 +1,693 @@ function Attack() {} var g_AttackTypes = ["Melee", "Ranged", "Capture"]; 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" + "1000" + "" + "" + "Cavalry" + "2" + "" + "" + "" + "props/units/weapons/rock_flaming.xml" + "props/units/weapons/rock_explosion.xml" + "0.1" + "" + "Champion" + "" + "Circular" + "20" + "false" + "0.0" + "10.0" + "0.0" + "" + "" + "" + "1000.0" + "0.0" + "0.0" + "4.0" + "" + "" + "" + "" + "" + DamageTypes.BuildSchema("damage strength") + "" + "" + "" + "" + "" + // TODO: it shouldn't be stretched "" + "" + Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + DamageTypes.BuildSchema("damage strength") + "" + "" + ""+ "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + DamageTypes.BuildSchema("damage strength") + Attack.prototype.bonusesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: it shouldn't be stretched "" + "" + Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + DamageTypes.BuildSchema("damage strength") + "" + // 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(wantedTypes) { let types = g_AttackTypes.filter(type => !!this.template[type]); if (!wantedTypes) return types; let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0); return types.filter(type => wantedTypes.indexOf("!" + type) == -1 && (!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1)); }; 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, wantedTypes) { 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; let cmpIdentity = QueryMiragedInterface(target, IID_Identity); if (!cmpIdentity) return false; let cmpHealth = QueryMiragedInterface(target, IID_Health); let targetClasses = cmpIdentity.GetClassesList(); if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && cmpHealth && cmpHealth.GetHitpoints() && (!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length)) return true; let cmpEntityPlayer = QueryOwnerInterface(this.entity); let cmpTargetPlayer = QueryOwnerInterface(target); if (!cmpTargetPlayer || !cmpEntityPlayer) return false; let types = this.GetAttackTypes(wantedTypes); let entityOwner = cmpEntityPlayer.GetPlayerID(); let targetOwner = cmpTargetPlayer.GetPlayerID(); let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); // 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()); for (let type of types) { if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints())) continue; if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner))) continue; if (heightDiff > this.GetRange(type).max) continue; let restrictedClasses = this.GetRestrictedClasses(type); if (!restrictedClasses.length) return true; 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) { let cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; let 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 g_AttackTypes.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 types = this.GetAttackTypes().filter(type => !this.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 capturable, so remove this attack types.splice(captureIndex, 1); } let isPreferred = className => this.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") }; let ret = {}; for (let damageType of DamageTypes.GetTypes()) ret[damageType] = applyMods(damageType); return ret; }; 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"; splash.shape = this.template[type].Splash.Shape; 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 }; }; Attack.prototype.GetBonusTemplate = function(type) { let template = this.template[type]; if (!template) template = this.template[type.split(".")[0]].Splash; return template.Bonuses || null; }; /** * 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 let horizSpeed = +this.template[type].ProjectileSpeed; let gravity = +this.template[type].Gravity; //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 previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition(); let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength); let timeToTarget = this.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); let predictedPosition = (timeToTarget !== false) ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition; // Add inaccuracy based on spread. let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", +this.template.Ranged.Spread, this.entity) * predictedPosition.horizDistanceTo(selfPosition) / 100; let randNorm = randomNormal2D(); let offsetX = randNorm[0] * distanceModifiedSpread; let offsetZ = randNorm[1] * distanceModifiedSpread; let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, targetPosition.y, predictedPosition.z + offsetZ); // Recalculate 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 actorName = ""; let impactActorName = ""; let impactAnimationLifetime = 0; if (this.template.Ranged.Projectile) { actorName = this.template.Ranged.Projectile.ActorName || ""; impactActorName = this.template.Ranged.Projectile.ImpactActorName || ""; impactAnimationLifetime = this.template.Ranged.Projectile.ImpactAnimationLifetime || 0; } let launchPoint = selfPosition.clone(); // TODO: remove this when all the ranged unit templates are updated with Projectile/Launchpoint launchPoint.y += 3; let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) { // if the projectile definition is missing from the template // then fallback to the projectile name and launchpoint in the visual actor if (!actorName) actorName = cmpVisual.GetProjectileActor(); let visualActorLaunchPoint = cmpVisual.GetProjectileLaunchPoint(); if (visualActorLaunchPoint.length() > 0) launchPoint = visualActorLaunchPoint; } let id = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, realTargetPosition, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime); let attackImpactSound = ""; let cmpSound = Engine.QueryInterface(this.entity, IID_Sound); if (cmpSound) attackImpactSound = cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase()); let data = { "type": type, "attacker": this.entity, "target": target, "strengths": this.GetAttackStrengths(type), "position": realTargetPosition, "direction": missileDirection, "projectileId": id, "bonus": this.GetBonusTemplate(type), "isSplash": false, "attackerOwner": attackerOwner, "attackImpactSound": attackImpactSound }; if (this.template.Ranged.Splash) { data.friendlyFire = this.template.Ranged.Splash.FriendlyFire != "false"; data.radius = +this.template.Ranged.Splash.Range; data.shape = this.template.Ranged.Splash.Shape; data.isSplash = true; data.splashStrengths = this.GetAttackStrengths(type + ".Splash"); data.splashBonus = this.GetBonusTemplate(type + ".Splash"); } cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", timeToTarget * 1000 + +this.template.Ranged.Delay, data); } else if (type == "Capture") { - if (attackerOwner == -1) + if (attackerOwner == INVALID_PLAYER) return; let multiplier = GetDamageBonus(target, this.GetBonusTemplate(type)); 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) && IsOwnedByEnemyOfPlayer(attackerOwner, target)) 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": GetDamageBonus(target, this.GetBonusTemplate(type)), "type": type, "attackerOwner": attackerOwner }); } }; /** * Get the predicted time of collision between a projectile (or a chaser) * and its target, assuming they both move in straight line at a constant speed. * Vertical component of movement is ignored. * @param {Vector3D} selfPosition - the 3D position of the projectile (or chaser). * @param {number} horizSpeed - the horizontal speed of the projectile (or chaser). * @param {Vector3D} targetPosition - the 3D position of the target. * @param {Vector3D} targetVelocity - the 3D velocity vector of the target. * @return {Vector3D|boolean} - the 3D predicted position or false if the collision will not happen. */ Attack.prototype.PredictTimeToTarget = function(selfPosition, horizSpeed, targetPosition, targetVelocity) { let relativePosition = new Vector3D.sub(targetPosition, selfPosition); let a = targetVelocity.x * targetVelocity.x + targetVelocity.z * targetVelocity.z - horizSpeed * horizSpeed; let b = relativePosition.x * targetVelocity.x + relativePosition.z * targetVelocity.z; let c = relativePosition.x * relativePosition.x + relativePosition.z * relativePosition.z; // The predicted time to reach the target is the smallest non negative solution // (when it exists) of the equation a t^2 + 2 b t + c = 0. // Using c>=0, we can straightly compute the right solution. if (c == 0) return 0; let disc = b * b - a * c; if (a < 0 || b < 0 && disc >= 0) return c / (Math.sqrt(disc) - b); return false; }; 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(); }; Attack.prototype.GetRangeOverlays = function() { if (!this.template.Ranged || !this.template.Ranged.RangeOverlay) return []; let range = this.GetRange("Ranged"); let rangeOverlays = []; for (let i in range) if ((i == "min" || i == "max") && range[i]) rangeOverlays.push({ "radius": range[i], "texture": this.template.Ranged.RangeOverlay.LineTexture, "textureMask": this.template.Ranged.RangeOverlay.LineTextureMask, "thickness": +this.template.Ranged.RangeOverlay.LineThickness, }); return rangeOverlays; }; Engine.RegisterComponentType(IID_Attack, "Attack", Attack); Index: ps/trunk/binaries/data/mods/public/simulation/components/AttackDetection.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AttackDetection.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/AttackDetection.js (revision 20953) @@ -1,160 +1,160 @@ function AttackDetection() {} AttackDetection.prototype.Schema = "Detects incoming attacks." + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; AttackDetection.prototype.Init = function() { this.suppressionTime = +this.template.SuppressionTime; // Use squared distance to avoid sqrts this.suppressionTransferRangeSquared = +this.template.SuppressionTransferRange * +this.template.SuppressionTransferRange; this.suppressionRangeSquared = +this.template.SuppressionRange * +this.template.SuppressionRange; this.suppressedList = []; }; AttackDetection.prototype.ActivateTimer = function() { Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).SetTimeout(this.entity, IID_AttackDetection, "HandleTimeout", this.suppressionTime); }; AttackDetection.prototype.AddSuppression = function(event) { this.suppressedList.push(event); this.ActivateTimer(); }; AttackDetection.prototype.UpdateSuppressionEvent = function(index, event) { this.suppressedList[index] = event; this.ActivateTimer(); }; //// Message handlers //// AttackDetection.prototype.OnGlobalAttacked = function(msg) { var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); var cmpOwnership = Engine.QueryInterface(msg.target, IID_Ownership); if (cmpOwnership.GetOwner() != cmpPlayer.GetPlayerID()) return; Engine.PostMessage(msg.target, MT_MinimapPing); this.AttackAlert(msg.target, msg.attacker, msg.attackerOwner); }; //// External interface //// AttackDetection.prototype.AttackAlert = function(target, attacker, attackerOwner) { let playerID = Engine.QueryInterface(this.entity, IID_Player).GetPlayerID(); // Don't register attacks dealt against other players if (Engine.QueryInterface(target, IID_Ownership).GetOwner() != playerID) return; let cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership); - let atkOwner = cmpAttackerOwnership && cmpAttackerOwnership.GetOwner() != -1 ? cmpAttackerOwnership.GetOwner() : attackerOwner; + let atkOwner = cmpAttackerOwnership && cmpAttackerOwnership.GetOwner() != INVALID_PLAYER ? cmpAttackerOwnership.GetOwner() : attackerOwner; // Don't register attacks dealt by myself if (atkOwner == playerID) return; // Since livestock can be attacked/gathered by other players // and generally are not so valuable as other units/buildings, // we have a lower priority notification for it, which can be // overriden by a regular one. var cmpTargetIdentity = Engine.QueryInterface(target, IID_Identity); var targetIsDomesticAnimal = cmpTargetIdentity && cmpTargetIdentity.HasClass("Animal") && cmpTargetIdentity.HasClass("Domestic"); var cmpPosition = Engine.QueryInterface(target, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var event = { "target": target, "position": cmpPosition.GetPosition(), "time": Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(), "targetIsDomesticAnimal": targetIsDomesticAnimal }; // If we already have a low priority livestock event in suppressed list, // and now a more important target is attacked, we want to upgrade the // suppressed event and send the new notification var isPriorityIncreased = false; for (var i = 0; i < this.suppressedList.length; ++i) { var element = this.suppressedList[i]; // If the new attack is within suppression distance of this element, // then check if the element should be updated and return var dist = event.position.horizDistanceToSquared(element.position); if (dist >= this.suppressionRangeSquared) continue; isPriorityIncreased = element.targetIsDomesticAnimal && !targetIsDomesticAnimal; var isPriorityDescreased = !element.targetIsDomesticAnimal && targetIsDomesticAnimal; if (isPriorityIncreased || (!isPriorityDescreased && dist < this.suppressionTransferRangeSquared)) this.UpdateSuppressionEvent(i, event); // If priority has increased, exit the loop to send the upgraded notification below if (isPriorityIncreased) break; return; } // If priority has increased for an existing event, then we already have it // in the suppression list if (!isPriorityIncreased) this.AddSuppression(event); Engine.PostMessage(this.entity, MT_AttackDetected, { "player": playerID, "event": event }); Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "type": "attack", "target": target, "players": [playerID], "attacker": atkOwner, "targetIsDomesticAnimal": targetIsDomesticAnimal }); PlaySound("attacked", target); }; AttackDetection.prototype.GetSuppressionTime = function() { return this.suppressionTime; }; AttackDetection.prototype.HandleTimeout = function() { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var now = cmpTimer.GetTime(); for (var i = 0; i < this.suppressedList.length; ++i) { var event = this.suppressedList[i]; // Check if this event has timed out if (now - event.time >= this.suppressionTime) { this.suppressedList.splice(i, 1); return; } } }; AttackDetection.prototype.GetIncomingAttacks = function() { return this.suppressedList; }; Engine.RegisterComponentType(IID_AttackDetection, "AttackDetection", AttackDetection); Index: ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/Capturable.js (revision 20953) @@ -1,309 +1,309 @@ function Capturable() {} Capturable.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + ""; Capturable.prototype.Init = function() { // Cache this value this.maxCp = +this.template.CapturePoints; this.cp = []; }; //// Interface functions //// /** * Returns the current capture points array */ Capturable.prototype.GetCapturePoints = function() { return this.cp; }; Capturable.prototype.GetMaxCapturePoints = function() { return this.maxCp; }; Capturable.prototype.GetGarrisonRegenRate = function() { return ApplyValueModificationsToEntity("Capturable/GarrisonRegenRate", +this.template.GarrisonRegenRate, this.entity); }; /** * Set the new capture points, used for cloning entities * The caller should assure that the sum of capture points * matches the max. */ Capturable.prototype.SetCapturePoints = function(capturePointsArray) { this.cp = capturePointsArray; }; /** * Reduces the amount of capture points of an entity, * in favour of the player of the source * Returns the number of capture points actually taken */ Capturable.prototype.Reduce = function(amount, playerID) { if (amount <= 0) return 0; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - if (!cmpOwnership || cmpOwnership.GetOwner() == -1) + if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return 0; var cmpPlayerSource = QueryPlayerIDInterface(playerID); if (!cmpPlayerSource) return 0; // Before changing the value, activate Fogging if necessary to hide changes var cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); var numberOfEnemies = this.cp.filter((v, i) => v > 0 && cmpPlayerSource.IsEnemy(i)).length; if (numberOfEnemies == 0) return 0; // distribute the capture points over all enemies let distributedAmount = amount / numberOfEnemies; let removedAmount = 0; while (distributedAmount > 0.0001) { numberOfEnemies = 0; for (let i in this.cp) { if (!this.cp[i] || !cmpPlayerSource.IsEnemy(i)) continue; if (this.cp[i] > distributedAmount) { removedAmount += distributedAmount; this.cp[i] -= distributedAmount; ++numberOfEnemies; } else { removedAmount += this.cp[i]; this.cp[i] = 0; } } distributedAmount = numberOfEnemies ? (amount - removedAmount) / numberOfEnemies : 0; } // give all cp taken to the player var takenCp = this.maxCp - this.cp.reduce((a, b) => a + b); this.cp[playerID] += takenCp; this.CheckTimer(); this.RegisterCapturePointsChanged(); return takenCp; }; /** * Check if the source can (re)capture points from this building */ Capturable.prototype.CanCapture = function(playerID) { var cmpPlayerSource = QueryPlayerIDInterface(playerID); if (!cmpPlayerSource) warn(playerID + " has no player component defined on its id"); var cp = this.GetCapturePoints(); var sourceEnemyCp = 0; for (let i in this.GetCapturePoints()) if (cmpPlayerSource.IsEnemy(i)) sourceEnemyCp += cp[i]; return sourceEnemyCp > 0; }; //// Private functions //// /** * this has to be called whenever the capture points are changed. * It notifies other components of the change, and switches ownership when needed */ Capturable.prototype.RegisterCapturePointsChanged = function() { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return; Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.cp }); var owner = cmpOwnership.GetOwner(); - if (owner == -1 || this.cp[owner] > 0) + if (owner == INVALID_PLAYER || this.cp[owner] > 0) return; // if all cp has been taken from the owner, convert it to the best player var bestPlayer = 0; for (let i in this.cp) if (this.cp[i] >= this.cp[bestPlayer]) bestPlayer = +i; let cmpLostPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpLostPlayerStatisticsTracker) cmpLostPlayerStatisticsTracker.LostEntity(this.entity); cmpOwnership.SetOwner(bestPlayer); let cmpCapturedPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpCapturedPlayerStatisticsTracker) cmpCapturedPlayerStatisticsTracker.CapturedEntity(this.entity); }; Capturable.prototype.GetRegenRate = function() { var regenRate = +this.template.RegenRate; regenRate = ApplyValueModificationsToEntity("Capturable/RegenRate", regenRate, this.entity); var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (cmpGarrisonHolder) var garrisonRegenRate = this.GetGarrisonRegenRate() * cmpGarrisonHolder.GetEntities().length; else var garrisonRegenRate = 0; return regenRate + garrisonRegenRate; }; Capturable.prototype.TimerTick = function() { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - if (!cmpOwnership || cmpOwnership.GetOwner() == -1) + if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return; var owner = cmpOwnership.GetOwner(); var modifiedCp = 0; // special handle for the territory decay // reduce cp from the owner in favour of all neighbours (also allies) var cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay); if (cmpTerritoryDecay && cmpTerritoryDecay.IsDecaying()) { var neighbours = cmpTerritoryDecay.GetConnectedNeighbours(); var totalNeighbours = neighbours.reduce((a, b) => a + b); var decay = Math.min(cmpTerritoryDecay.GetDecayRate(), this.cp[owner]); this.cp[owner] -= decay; if (totalNeighbours) for (let p in neighbours) this.cp[p] += decay * neighbours[p] / totalNeighbours; else // decay to gaia as default this.cp[0] += decay; modifiedCp += decay; this.RegisterCapturePointsChanged(); } var regenRate = this.GetRegenRate(); if (regenRate < 0) modifiedCp += this.Reduce(-regenRate, 0); else if (regenRate > 0) modifiedCp += this.Reduce(regenRate, owner); if (modifiedCp) return; // nothing changed, stop the timer var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = 0; Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": false, "regenRate": 0, "territoryDecay": 0 }); }; /** * Start the regeneration timer when no timer exists. * When nothing can be modified (f.e. because it is fully regenerated), the * timer stops automatically after one execution. */ Capturable.prototype.CheckTimer = function() { if (this.timer) return; var regenRate = this.GetRegenRate(); var cmpDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay); var decay = cmpDecay && cmpDecay.IsDecaying() ? cmpDecay.GetDecayRate() : 0; if (regenRate == 0 && decay == 0) return; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_Capturable, "TimerTick", 1000, 1000, null); Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": true, "regenRate": regenRate, "territoryDecay": decay }); }; //// Message Listeners //// Capturable.prototype.OnValueModification = function(msg) { if (msg.component != "Capturable") return; var oldMaxCp = this.GetMaxCapturePoints(); this.maxCp = ApplyValueModificationsToEntity("Capturable/CapturePoints", +this.template.CapturePoints, this.entity); if (oldMaxCp == this.maxCp) return; var scale = this.maxCp / oldMaxCp; for (let i in this.cp) this.cp[i] *= scale; Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.cp }); this.CheckTimer(); }; Capturable.prototype.OnGarrisonedUnitsChanged = function(msg) { this.CheckTimer(); }; Capturable.prototype.OnTerritoryDecayChanged = function(msg) { if (msg.to) this.CheckTimer(); }; Capturable.prototype.OnDiplomacyChanged = function(msg) { this.CheckTimer(); }; Capturable.prototype.OnOwnershipChanged = function(msg) { - if (msg.to == -1) + if (msg.to == INVALID_PLAYER) return; // we're dead if (this.cp.length) { if (!this.cp[msg.from]) return; // nothing to change // Was already initialised, this happens on defeat or wololo // transfer the points of the old owner to the new one this.cp[msg.to] += this.cp[msg.from]; this.cp[msg.from] = 0; this.RegisterCapturePointsChanged(); } else { // initialise the capture points when created let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) if (i == msg.to) this.cp[i] = this.maxCp; else this.cp[i] = 0; } this.CheckTimer(); }; Engine.RegisterComponentType(IID_Capturable, "Capturable", Capturable); Index: ps/trunk/binaries/data/mods/public/simulation/components/Cost.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Cost.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/Cost.js (revision 20953) @@ -1,119 +1,119 @@ function Cost() {} Cost.prototype.Schema = "Specifies the construction/training costs of this entity." + "" + "1" + "15" + "20.0" + "" + "50" + "0" + "0" + "25" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Resources.BuildSchema("nonNegativeInteger") + ""; Cost.prototype.Init = function() { this.populationCost = +this.template.Population; this.populationBonus = +this.template.PopulationBonus; }; Cost.prototype.GetPopCost = function() { return this.populationCost; }; Cost.prototype.GetPopBonus = function() { return this.populationBonus; }; Cost.prototype.GetBuildTime = function() { var cmpPlayer = QueryOwnerInterface(this.entity); var buildTime = (+this.template.BuildTime) * cmpPlayer.cheatTimeMultiplier; return ApplyValueModificationsToEntity("Cost/BuildTime", buildTime, this.entity); }; Cost.prototype.GetResourceCosts = function(owner) { if (!owner) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) error("GetResourceCosts called without valid ownership"); else owner = cmpOwnership.GetOwner(); } let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let entityTemplateName = cmpTemplateManager.GetCurrentTemplateName(this.entity); let entityTemplate = cmpTemplateManager.GetTemplate(entityTemplateName); let costs = {}; for (let res in this.template.Resources) costs[res] = ApplyValueModificationsToTemplate("Cost/Resources/"+res, +this.template.Resources[res], owner, entityTemplate); return costs; }; Cost.prototype.OnOwnershipChanged = function(msg) { - if (msg.from != -1) + if (msg.from != INVALID_PLAYER) { let cmpPlayer = QueryPlayerIDInterface(msg.from); if (cmpPlayer) cmpPlayer.AddPopulationBonuses(-this.GetPopBonus()); } - if (msg.to != -1) + if (msg.to != INVALID_PLAYER) { let cmpPlayer = QueryPlayerIDInterface(msg.to); if (cmpPlayer) cmpPlayer.AddPopulationBonuses(this.GetPopBonus()); } }; Cost.prototype.OnValueModification = function(msg) { if (msg.component != "Cost") return; // foundations shouldn't give a pop bonus and a pop cost var cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation); if (cmpFoundation) return; // update the population costs var newPopCost = Math.round(ApplyValueModificationsToEntity("Cost/Population", +this.template.Population, this.entity)); var popCostDifference = newPopCost - this.populationCost; this.populationCost = newPopCost; // update the population bonuses var newPopBonus = Math.round(ApplyValueModificationsToEntity("Cost/PopulationBonus", +this.template.PopulationBonus, this.entity)); var popDifference = newPopBonus - this.populationBonus; this.populationBonus = newPopBonus; var cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return; if (popCostDifference) cmpPlayer.AddPopulation(popCostDifference); if (popDifference) cmpPlayer.AddPopulationBonuses(popDifference); }; Engine.RegisterComponentType(IID_Cost, "Cost", Cost); Index: ps/trunk/binaries/data/mods/public/simulation/components/Damage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Damage.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/Damage.js (revision 20953) @@ -1,306 +1,306 @@ function Damage() {} Damage.prototype.Schema = ""; Damage.prototype.Init = function() { }; /** * Gives the position of the given entity, taking the lateness into account. * @param {number} ent - entity id of the entity we are finding the location for. * @param {number} lateness - the time passed since the expected time to fire the function. * @return {Vector3D} - the location of the entity. */ Damage.prototype.InterpolatedLocation = function(ent, lateness) { let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly return undefined; let curPos = cmpTargetPosition.GetPosition(); let prevPos = cmpTargetPosition.GetPreviousPosition(); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let turnLength = cmpTimer.GetLatestTurnLength(); return new Vector3D( (curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength, 0, (curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength); }; /** * Test if a point is inside of an entity's footprint. * @param {number} ent - id of the entity we are checking with. * @param {Vector3D} point - the point we are checking with. * @param {number} lateness - the time passed since the expected time to fire the function. * @return {boolean} - true if the point is inside of the entity's footprint. */ Damage.prototype.TestCollision = function(ent, point, lateness) { let targetPosition = this.InterpolatedLocation(ent, lateness); if (!targetPosition) return false; let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint); if (!cmpFootprint) return false; let targetShape = cmpFootprint.GetShape(); if (!targetShape) return false; if (targetShape.type == "circle") return targetPosition.horizDistanceToSquared(point) < targetShape.radius * targetShape.radius; if (targetShape.type == "square") { let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y; let distance = Vector2D.from3D(Vector3D.sub(point, targetPosition)).rotate(-angle); return Math.abs(distance.x) < targetShape.width / 2 && Math.abs(distance.y) < targetShape.depth / 2; } warn("TestCollision called with an invalid footprint shape"); return false; }; /** * Get the list of players affected by the damage. * @param {number} attackerOwner - the player id of the attacker. * @param {boolean} friendlyFire - a flag indicating if allied entities are also damaged. * @return {number[]} - the ids of players need to be damaged */ Damage.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire) { if (!friendlyFire) return QueryPlayerIDInterface(attackerOwner).GetEnemies(); return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); }; /** * Handles hit logic after the projectile travel time has passed. * @param {Object} data - the data sent by the caller. * @param {number} data.attacker - the entity id of the attacker. * @param {number} data.target - the entity id of the target. * @param {Vector2D} data.origin - the origin of the projectile hit. * @param {Object} data.strengths - data of the form { 'hack': number, 'pierce': number, 'crush': number }. * @param {string} data.type - the type of damage. * @param {number} data.attackerOwner - the player id of the owner of the attacker. * @param {boolean} data.isSplash - a flag indicating if it's splash damage. * @param {Vector3D} data.position - the expected position of the target. * @param {number} data.projectileId - the id of the projectile. * @param {Vector3D} data.direction - the unit vector defining the direction. * @param {Object} data.bonus - the attack bonus template from the attacker. * @param {string} data.attackImpactSound - the name of the sound emited on impact. * ***When splash damage*** * @param {boolean} data.friendlyFire - a flag indicating if allied entities are also damaged. * @param {number} data.radius - the radius of the splash damage. * @param {string} data.shape - the shape of the splash range. * @param {Object} data.splashBonus - the attack bonus template from the attacker. * @param {Object} data.splashStrengths - data of the form { 'hack': number, 'pierce': number, 'crush': number }. */ Damage.prototype.MissileHit = function(data, lateness) { if (!data.position) return; let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager); if (cmpSoundManager && data.attackImpactSound) cmpSoundManager.PlaySoundGroupAtPosition(data.attackImpactSound, data.position); // Do this first in case the direct hit kills the target if (data.isSplash) { this.CauseSplashDamage({ "attacker": data.attacker, "origin": Vector2D.from3D(data.position), "radius": data.radius, "shape": data.shape, "strengths": data.splashStrengths, "splashBonus": data.splashBonus, "direction": data.direction, "playersToDamage": this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire), "type": data.type, "attackerOwner": data.attackerOwner }); } let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); // Deal direct damage if we hit the main target // and if the target has DamageReceiver (not the case for a mirage for example) let cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); if (cmpDamageReceiver && this.TestCollision(data.target, data.position, lateness)) { data.multiplier = GetDamageBonus(data.target, data.bonus); this.CauseDamage(data); cmpProjectileManager.RemoveProjectile(data.projectileId); return; } let targetPosition = this.InterpolatedLocation(data.target, lateness); if (!targetPosition) return; // If we didn't hit the main target look for nearby units let cmpPlayer = QueryPlayerIDInterface(data.attackerOwner); let ents = this.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies()); for (let ent of ents) { if (!this.TestCollision(ent, data.position, lateness)) continue; this.CauseDamage({ "strengths": data.strengths, "target": ent, "attacker": data.attacker, "multiplier": GetDamageBonus(ent, data.bonus), "type": data.type, "attackerOwner": data.attackerOwner }); cmpProjectileManager.RemoveProjectile(data.projectileId); break; } }; /** * Damages units around a given origin. * @param {Object} data - the data sent by the caller. * @param {number} data.attacker - the entity id of the attacker. * @param {Vector2D} data.origin - the origin of the projectile hit. * @param {number} data.radius - the radius of the splash damage. * @param {string} data.shape - the shape of the radius. * @param {Object} data.strengths - data of the form { 'hack': number, 'pierce': number, 'crush': number }. * @param {string} data.type - the type of damage. * @param {number} data.attackerOwner - the player id of the attacker. * @param {Vector3D} [data.direction] - the unit vector defining the direction. Needed for linear splash damage. * @param {Object} data.splashBonus - the attack bonus template from the attacker. * @param {number[]} data.playersToDamage - the array of player id's to damage. */ Damage.prototype.CauseSplashDamage = function(data) { // Get nearby entities and define variables let nearEnts = this.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage); let damageMultiplier = 1; // Cycle through all the nearby entities and damage it appropriately based on its distance from the origin. for (let ent of nearEnts) { let entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D(); if (data.shape == 'Circular') // circular effect with quadratic falloff in every direction damageMultiplier = 1 - data.origin.distanceToSquared(entityPosition) / (data.radius * data.radius); else if (data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles) { // Get position of entity relative to splash origin. let relativePos = entityPosition.sub(data.origin); // Get the position relative to the missile direction. let direction = Vector2D.from3D(data.direction); let parallelPos = relativePos.dot(direction); let perpPos = relativePos.cross(direction); // The width of linear splash is one fifth of the normal splash radius. let width = data.radius / 5; // Check that the unit is within the distance splash width of the line starting at the missile's // landing point which extends in the direction of the missile for length splash radius. if (parallelPos >= 0 && Math.abs(perpPos) < width) // If in radius, quadratic falloff in both directions damageMultiplier = (1 - parallelPos * parallelPos / (data.radius * data.radius)) * (1 - perpPos * perpPos / (width * width)); else damageMultiplier = 0; } else // In case someone calls this function with an invalid shape. { warn("The " + data.shape + " splash damage shape is not implemented!"); } if (data.splashBonus) damageMultiplier *= GetDamageBonus(ent, data.splashBonus); // Call CauseDamage which reduces the hitpoints, posts network command, plays sounds.... this.CauseDamage({ "strengths": data.strengths, "target": ent, "attacker": data.attacker, "multiplier": damageMultiplier, "type": data.type + ".Splash", "attackerOwner": data.attackerOwner }); } }; /** * Causes damage on a given unit. * @param {Object} data - the data passed by the caller. * @param {Object} data.strengths - data in the form of { 'hack': number, 'pierce': number, 'crush': number }. * @param {number} data.target - the entity id of the target. * @param {number} data.attacker - the entity id og the attacker. * @param {number} data.multiplier - the damage multiplier. * @param {string} data.type - the type of damage. * @param {number} data.attackerOwner - the player id of the attacker. */ Damage.prototype.CauseDamage = function(data) { let cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver); if (!cmpDamageReceiver) return; let targetState = cmpDamageReceiver.TakeDamage(data.strengths, data.multiplier); let cmpPromotion = Engine.QueryInterface(data.attacker, IID_Promotion); let cmpLoot = Engine.QueryInterface(data.target, IID_Loot); let cmpHealth = Engine.QueryInterface(data.target, IID_Health); if (cmpPromotion && cmpLoot && cmpLoot.GetXp() > 0) cmpPromotion.IncreaseXp(cmpLoot.GetXp() * -targetState.change / cmpHealth.GetMaxHitpoints()); if (targetState.killed) this.TargetKilled(data.attacker, data.target, data.attackerOwner); Engine.PostMessage(data.target, MT_Attacked, { "attacker": data.attacker, "target": data.target, "type": data.type, "damage": -targetState.change, "attackerOwner": data.attackerOwner }); }; /** * Gets entities near a give point for given players. * @param {Vector2D} origin - the point to check around. * @param {number} radius - the radius around the point to check. * @param {number[]} players - the players of which we need to check entities. * @return {number[]} - the id's of the entities in range of the given point. */ Damage.prototype.EntitiesNearPoint = function(origin, radius, players) { // If there is insufficient data return an empty array. if (!origin || !radius || !players) return []; return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQueryAroundPos(origin, 0, radius, players, IID_DamageReceiver); }; /** * Called when a unit kills something (another unit, building, animal etc) * @param {number} attacker - the entity id of the killer. * @param {number} target - the entity id of the target. * @param {number} attackerOwner - the player id of the attacker. */ Damage.prototype.TargetKilled = function(attacker, target, attackerOwner) { let cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership); - let atkOwner = cmpAttackerOwnership && cmpAttackerOwnership.GetOwner() != -1 ? cmpAttackerOwnership.GetOwner() : attackerOwner; + let atkOwner = cmpAttackerOwnership && cmpAttackerOwnership.GetOwner() != INVALID_PLAYER ? cmpAttackerOwnership.GetOwner() : attackerOwner; // Add to killer statistics. let cmpKillerPlayerStatisticsTracker = QueryPlayerIDInterface(atkOwner, IID_StatisticsTracker); if (cmpKillerPlayerStatisticsTracker) cmpKillerPlayerStatisticsTracker.KilledEntity(target); // Add to loser statistics. let cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(target, IID_StatisticsTracker); if (cmpTargetPlayerStatisticsTracker) cmpTargetPlayerStatisticsTracker.LostEntity(target); // If killer can collect loot, let's try to collect it. let cmpLooter = Engine.QueryInterface(attacker, IID_Looter); if (cmpLooter) cmpLooter.Collect(target); }; Engine.RegisterSystemComponentType(IID_Damage, "Damage", Damage); Index: ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js (revision 20953) @@ -1,91 +1,91 @@ function DeathDamage() {} DeathDamage.prototype.bonusesSchema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; DeathDamage.prototype.Schema = "When a unit or building is destroyed, it inflicts damage to nearby units." + "" + "Circular" + "20" + "false" + "0.0" + "10.0" + "50.0" + "" + "" + "" + "" + DamageTypes.BuildSchema("damage strength") + DeathDamage.prototype.bonusesSchema; DeathDamage.prototype.Init = function() { }; DeathDamage.prototype.Serialize = null; // we have no dynamic state to save DeathDamage.prototype.GetDeathDamageStrengths = function() { // Work out the damage values with technology effects let applyMods = damageType => ApplyValueModificationsToEntity("DeathDamage/" + damageType, +(this.template[damageType] || 0), this.entity); let ret = {}; for (let damageType of DamageTypes.GetTypes()) ret[damageType] = applyMods(damageType); return ret; }; DeathDamage.prototype.GetBonusTemplate = function() { return this.template.Bonuses || null; }; DeathDamage.prototype.CauseDeathDamage = function() { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; let pos = cmpPosition.GetPosition2D(); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); let owner = cmpOwnership.GetOwner(); - if (owner == -1) + if (owner == INVALID_PLAYER) warn("Unit causing death damage does not have any owner."); let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); let playersToDamage = cmpDamage.GetPlayersToDamage(owner, this.template.FriendlyFire); let radius = ApplyValueModificationsToEntity("DeathDamage/Range", +this.template.Range, this.entity); cmpDamage.CauseSplashDamage({ "attacker": this.entity, "origin": pos, "radius": radius, "shape": this.template.Shape, "strengths": this.GetDeathDamageStrengths(), "splashBonus": this.GetBonusTemplate(), "playersToDamage": playersToDamage, "type": "Death", "attackerOwner": owner }); }; Engine.RegisterComponentType(IID_DeathDamage, "DeathDamage", DeathDamage); Index: ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 20953) @@ -1,710 +1,710 @@ function GarrisonHolder() {} GarrisonHolder.prototype.Schema = "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; /** * Initialize GarrisonHolder Component * Garrisoning when loading a map is set in the script of the map, by setting initGarrison * which should contain the array of garrisoned entities. */ GarrisonHolder.prototype.Init = function() { // Garrisoned Units this.entities = []; this.timer = undefined; this.allowGarrisoning = new Map(); this.visibleGarrisonPoints = []; if (this.template.VisibleGarrisonPoints) { let points = this.template.VisibleGarrisonPoints; for (let point in points) this.visibleGarrisonPoints.push({ "offset": { "x": +points[point].X, "y": +points[point].Y, "z": +points[point].Z }, "entity": null }); } }; /** * @return {Object} max and min range at which entities can garrison the holder. */ GarrisonHolder.prototype.GetLoadingRange = function() { return { "max": +this.template.LoadingRange, "min": 0 }; }; GarrisonHolder.prototype.CanPickup = function(ent) { if (!this.template.Pickup || this.IsFull()) return false; let cmpOwner = Engine.QueryInterface(this.entity, IID_Ownership); return !!cmpOwner && IsOwnedByPlayer(cmpOwner.GetOwner(), ent); }; GarrisonHolder.prototype.GetEntities = function() { return this.entities; }; /** * @return {Array} unit classes which can be garrisoned inside this * particular entity. Obtained from the entity's template. */ GarrisonHolder.prototype.GetAllowedClasses = function() { return this.template.List._string; }; GarrisonHolder.prototype.GetCapacity = function() { return ApplyValueModificationsToEntity("GarrisonHolder/Max", +this.template.Max, this.entity); }; GarrisonHolder.prototype.IsFull = function() { return this.GetGarrisonedEntitiesCount() >= this.GetCapacity(); }; GarrisonHolder.prototype.GetHealRate = function() { return ApplyValueModificationsToEntity("GarrisonHolder/BuffHeal", +this.template.BuffHeal, this.entity); }; /** * Set this entity to allow or disallow garrisoning in the entity. * Every component calling this function should do it with its own ID, and as long as one * component doesn't allow this entity to garrison, it can't be garrisoned * When this entity already contains garrisoned soldiers, * these will not be able to ungarrison until the flag is set to true again. * * This more useful for modern-day features. For example you can't garrison or ungarrison * a driving vehicle or plane. * @param {boolean} allow - Whether the entity should be garrisonable. */ GarrisonHolder.prototype.AllowGarrisoning = function(allow, callerID) { this.allowGarrisoning.set(callerID, allow); }; GarrisonHolder.prototype.IsGarrisoningAllowed = function() { return Array.from(this.allowGarrisoning.values()).every(allow => allow); }; GarrisonHolder.prototype.GetGarrisonedEntitiesCount = function() { let count = this.entities.length; for (let ent of this.entities) { let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) count += cmpGarrisonHolder.GetGarrisonedEntitiesCount(); } return count; }; GarrisonHolder.prototype.IsAllowedToGarrison = function(ent) { if (!this.IsGarrisoningAllowed()) return false; if (!IsOwnedByMutualAllyOfEntity(ent, this.entity)) return false; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity) return false; let entityClasses = cmpIdentity.GetClassesList(); return MatchesClassList(entityClasses, this.template.List._string) && !!Engine.QueryInterface(ent, IID_Garrisonable); }; /** * Garrison a unit inside. The timer for AutoHeal is started here. * @param {number} vgpEntity - The visual garrison point that will be used. * If vgpEntity is given, this visualGarrisonPoint will be used for the entity. * @return {boolean} Whether the entity was garrisonned. */ GarrisonHolder.prototype.Garrison = function(entity, vgpEntity) { let cmpPosition = Engine.QueryInterface(entity, IID_Position); if (!cmpPosition) return false; if (!this.PerformGarrison(entity)) return false; let visibleGarrisonPoint = vgpEntity; if (!visibleGarrisonPoint) for (let vgp of this.visibleGarrisonPoints) { if (vgp.entity) continue; visibleGarrisonPoint = vgp; break; } if (visibleGarrisonPoint) { visibleGarrisonPoint.entity = entity; cmpPosition.SetTurretParent(this.entity, visibleGarrisonPoint.offset); let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.SetTurretStance(); } else cmpPosition.MoveOutOfWorld(); return true; }; /** * @return {boolean} Whether the entity was garrisonned. */ GarrisonHolder.prototype.PerformGarrison = function(entity) { if (!this.HasEnoughHealth()) return false; // Check if the unit is allowed to be garrisoned inside the building if (!this.IsAllowedToGarrison(entity)) return false; // Check the capacity let extraCount = 0; let cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder); if (cmpGarrisonHolder) extraCount += cmpGarrisonHolder.GetGarrisonedEntitiesCount(); if (this.GetGarrisonedEntitiesCount() + extraCount >= this.GetCapacity()) return false; if (!this.timer && this.GetHealRate() > 0) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); } // Actual garrisoning happens here this.entities.push(entity); this.UpdateGarrisonFlag(); let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.PauseProduction(); let cmpAura = Engine.QueryInterface(entity, IID_Auras); if (cmpAura && cmpAura.HasGarrisonAura()) cmpAura.ApplyGarrisonBonus(this.entity); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [entity], "removed": [] }); let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.IsUnderAlert()) Engine.PostMessage(cmpUnitAI.GetAlertRaiser(), MT_UnitGarrisonedAfterAlert, { "holder": this.entity, "unit": entity }); return true; }; /** * Simply eject the unit from the garrisoning entity without moving it * @param {number} entity - Id of the entity to be ejected. * @param {boolean} forced - Whether eject is forced (i.e. if building is destroyed). * @return {boolean} Whether the entity was ejected. */ GarrisonHolder.prototype.Eject = function(entity, forced) { let entityIndex = this.entities.indexOf(entity); // Error: invalid entity ID, usually it's already been ejected if (entityIndex == -1) return false; // Find spawning location let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); // If the garrisonHolder is a sinking ship, restrict the location to the intersection of both passabilities // TODO: should use passability classes to be more generic let pos; if ((!cmpHealth || cmpHealth.GetHitpoints() == 0) && cmpIdentity && cmpIdentity.HasClass("Ship")) pos = cmpFootprint.PickSpawnPointBothPass(entity); else pos = cmpFootprint.PickSpawnPoint(entity); if (pos.y < 0) { // Error: couldn't find space satisfying the unit's passability criteria if (!forced) return false; // If ejection is forced, we need to continue, so use center of the building let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); pos = cmpPosition.GetPosition(); } let cmpNewPosition = Engine.QueryInterface(entity, IID_Position); this.entities.splice(entityIndex, 1); let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); for (let vgp of this.visibleGarrisonPoints) { if (vgp.entity != entity) continue; cmpNewPosition.SetTurretParent(INVALID_ENTITY, new Vector3D()); if (cmpUnitAI) cmpUnitAI.ResetTurretStance(); vgp.entity = null; break; } if (cmpUnitAI) cmpUnitAI.Ungarrison(); let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.UnpauseProduction(); let cmpAura = Engine.QueryInterface(entity, IID_Auras); if (cmpAura && cmpAura.HasGarrisonAura()) cmpAura.RemoveGarrisonBonus(this.entity); cmpNewPosition.JumpTo(pos.x, pos.z); cmpNewPosition.SetHeightOffset(0); let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition) cmpNewPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos)); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": [entity] }); return true; }; /** * Ejects units and orders them to move to the rally point. If an ejection * with a given obstruction radius has failed, we won't try anymore to eject * entities with a bigger obstruction as that is compelled to also fail. * @param {Array} entities - An array containing the ids of the entities to eject. * @param {boolean} forced - Whether eject is forced (ie: if building is destroyed). * @return {boolean} Whether the entities were ejected. */ GarrisonHolder.prototype.PerformEject = function(entities, forced) { if (!this.IsGarrisoningAllowed() && !forced) return false; let ejectedEntities = []; let success = true; let failedRadius; let radius; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); for (let entity of entities) { if (failedRadius !== undefined) { let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction); radius = cmpObstruction ? cmpObstruction.GetUnitRadius() : 0; if (radius >= failedRadius) continue; } if (this.Eject(entity, forced)) { let cmpEntOwnership = Engine.QueryInterface(entity, IID_Ownership); if (cmpOwnership && cmpEntOwnership && cmpOwnership.GetOwner() == cmpEntOwnership.GetOwner()) ejectedEntities.push(entity); } else { success = false; if (failedRadius !== undefined) failedRadius = Math.min(failedRadius, radius); else { let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction); failedRadius = cmpObstruction ? cmpObstruction.GetUnitRadius() : 0; } } } this.OrderWalkToRallyPoint(ejectedEntities); this.UpdateGarrisonFlag(); return success; }; /** * Order entities to walk to the rally point. * @param {Array} entities - An array containing all the ids of the entities. */ GarrisonHolder.prototype.OrderWalkToRallyPoint = function(entities) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); if (!cmpRallyPoint || !cmpRallyPoint.GetPositions()[0]) return; let commands = GetRallyPointCommands(cmpRallyPoint, entities); // Ignore the rally point if it is autogarrison if (commands[0].type == "garrison" && commands[0].target == this.entity) return; for (let command of commands) ProcessCommand(cmpOwnership.GetOwner(), command); }; /** * Unload unit from the garrisoning entity and order them * to move to the rally point. * @return {boolean} Whether the command was successful. */ GarrisonHolder.prototype.Unload = function(entity, forced) { return this.PerformEject([entity], forced); }; /** * Unload one or all units that match a template and owner from * the garrisoning entity and order them to move to the rally point. * @param {string} template - Type of units that should be ejected. * @param {number} owner - Id of the player whose units should be ejected. * @param {boolean} all - Whether all units should be ejected. * @param {boolean} forced - Whether unload is forced. * @return {boolean} Whether the unloading was successful. */ GarrisonHolder.prototype.UnloadTemplate = function(template, owner, all, forced) { let entities = []; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); for (let entity of this.entities) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); // Units with multiple ranks are grouped together. let name = cmpIdentity.GetSelectionGroupName() || cmpTemplateManager.GetCurrentTemplateName(entity); if (name != template || owner != Engine.QueryInterface(entity, IID_Ownership).GetOwner()) continue; entities.push(entity); // If 'all' is false, only ungarrison the first matched unit. if (!all) break; } return this.PerformEject(entities, forced); }; /** * Unload all units, that belong to certain player * and order all own units to move to the rally point. * @param {boolean} forced - Whether unload is forced. * @param {number} owner - Id of the player whose units should be ejected. * @return {boolean} Whether the unloading was successful. */ GarrisonHolder.prototype.UnloadAllByOwner = function(owner, forced) { let entities = this.entities.filter(ent => { let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); return cmpOwnership && cmpOwnership.GetOwner() == owner; }); return this.PerformEject(entities, forced); }; /** * Unload all units from the entity and order them to move to the rally point. * @param {boolean} forced - Whether unload is forced. * @return {boolean} Whether the unloading was successful. */ GarrisonHolder.prototype.UnloadAll = function(forced) { return this.PerformEject(this.entities.slice(), forced); }; /** * Used to check if the garrisoning entity's health has fallen below * a certain limit after which all garrisoned units are unloaded. */ GarrisonHolder.prototype.OnHealthChanged = function(msg) { if (!this.HasEnoughHealth() && this.entities.length) this.EjectOrKill(this.entities.slice()); }; GarrisonHolder.prototype.HasEnoughHealth = function() { let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); return cmpHealth.GetHitpoints() > Math.floor(+this.template.EjectHealth * cmpHealth.GetMaxHitpoints()); }; /** * Called every second. Heals garrisoned units. */ GarrisonHolder.prototype.HealTimeout = function(data) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); if (!this.entities.length) { cmpTimer.CancelTimer(this.timer); this.timer = undefined; return; } for (let entity of this.entities) { let cmpHealth = Engine.QueryInterface(entity, IID_Health); if (cmpHealth && !cmpHealth.IsUnhealable()) cmpHealth.Increase(this.GetHealRate()); } this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); }; /** * Updates the garrison flag depending whether something is garrisoned in the entity. */ GarrisonHolder.prototype.UpdateGarrisonFlag = function() { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SetVariant("garrison", this.entities.length ? "garrisoned" : "ungarrisoned"); }; /** * Cancel timer when destroyed. */ GarrisonHolder.prototype.OnDestroy = function() { if (this.timer) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); } }; /** * If a garrisoned entity is captured, or about to be killed (so its owner changes to '-1'), * remove it from the building so we only ever contain valid entities. */ GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg) { // The ownership change may be on the garrisonholder if (this.entity == msg.entity) { - let entities = this.entities.filter(ent => msg.to == -1 || !IsOwnedByMutualAllyOfEntity(this.entity, ent)); + let entities = this.entities.filter(ent => msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, ent)); if (entities.length) this.EjectOrKill(entities); return; } // or on some of its garrisoned units let entityIndex = this.entities.indexOf(msg.entity); if (entityIndex != -1) { // If the entity is dead, remove it directly instead of ejecting the corpse let cmpHealth = Engine.QueryInterface(msg.entity, IID_Health); if (cmpHealth && cmpHealth.GetHitpoints() == 0) { this.entities.splice(entityIndex, 1); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": [msg.entity] }); this.UpdateGarrisonFlag(); for (let point of this.visibleGarrisonPoints) if (point.entity == msg.entity) point.entity = null; } - else if (msg.to == -1 || !IsOwnedByMutualAllyOfEntity(this.entity, msg.entity)) + else if (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, msg.entity)) this.EjectOrKill([msg.entity]); } }; /** * Update list of garrisoned entities if one gets renamed (e.g. by promotion). */ GarrisonHolder.prototype.OnGlobalEntityRenamed = function(msg) { let entityIndex = this.entities.indexOf(msg.entity); if (entityIndex != -1) { let vgpRenamed; for (let vgp of this.visibleGarrisonPoints) { if (vgp.entity != msg.entity) continue; vgpRenamed = vgp; break; } this.Eject(msg.entity, true); this.Garrison(msg.newentity, vgpRenamed); } if (!this.initGarrison) return; // Update the pre-game garrison because of SkirmishReplacement if (msg.entity == this.entity) { let cmpGarrisonHolder = Engine.QueryInterface(msg.newentity, IID_GarrisonHolder); if (cmpGarrisonHolder) cmpGarrisonHolder.initGarrison = this.initGarrison; } else { entityIndex = this.initGarrison.indexOf(msg.entity); if (entityIndex != -1) this.initGarrison[entityIndex] = msg.newentity; } }; /** * Eject all foreign garrisoned entities which are no more allied. */ GarrisonHolder.prototype.OnDiplomacyChanged = function() { this.EjectOrKill(this.entities.filter(ent => !IsOwnedByMutualAllyOfEntity(this.entity, ent))); }; /** * Eject or kill a garrisoned unit which can no more be garrisoned * (garrisonholder's health too small or ownership changed). */ GarrisonHolder.prototype.EjectOrKill = function(entities) { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); // Eject the units which can be ejected (if not in world, it generally means this holder // is inside a holder which kills its entities, so do not eject) if (cmpPosition && cmpPosition.IsInWorld()) { let ejectables = entities.filter(ent => this.IsEjectable(ent)); if (ejectables.length) this.PerformEject(ejectables, false); } // And destroy all remaining entities let killedEntities = []; for (let entity of entities) { let entityIndex = this.entities.indexOf(entity); if (entityIndex == -1) continue; let cmpHealth = Engine.QueryInterface(entity, IID_Health); if (cmpHealth) cmpHealth.Kill(); this.entities.splice(entityIndex, 1); killedEntities.push(entity); } if (killedEntities.length) Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": killedEntities }); this.UpdateGarrisonFlag(); }; GarrisonHolder.prototype.IsEjectable = function(entity) { if (!this.entities.find(ent => ent == entity)) return false; let ejectableClasses = this.template.EjectClassesOnDestroy._string; ejectableClasses = ejectableClasses ? ejectableClasses.split(/\s+/) : []; let entityClasses = Engine.QueryInterface(entity, IID_Identity).GetClassesList(); return ejectableClasses.some(ejectableClass => entityClasses.indexOf(ejectableClass) != -1); }; /** * Initialise the garrisoned units. */ GarrisonHolder.prototype.OnGlobalInitGame = function(msg) { if (!this.initGarrison) return; for (let ent of this.initGarrison) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.CanGarrison(this.entity) && this.Garrison(ent)) cmpUnitAI.SetGarrisoned(); } this.initGarrison = undefined; }; GarrisonHolder.prototype.OnValueModification = function(msg) { if (msg.component != "GarrisonHolder" || msg.valueNames.indexOf("GarrisonHolder/BuffHeal") == -1) return; if (this.timer && this.GetHealRate() == 0) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } else if (!this.timer && this.GetHealRate() > 0) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); } }; Engine.RegisterComponentType(IID_GarrisonHolder, "GarrisonHolder", GarrisonHolder); Index: ps/trunk/binaries/data/mods/public/simulation/components/Gate.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Gate.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/Gate.js (revision 20953) @@ -1,244 +1,244 @@ function Gate() {} Gate.prototype.Schema = "Controls behavior of wall gates" + "" + "20" + "" + "" + "" + ""; /** * Initialize Gate component */ Gate.prototype.Init = function() { this.allies = []; this.opened = false; this.locked = false; }; Gate.prototype.OnOwnershipChanged = function(msg) { - if (msg.to != -1) + if (msg.to != INVALID_PLAYER) { this.SetupRangeQuery(msg.to); // Set the initial state, but don't play unlocking sound if (!this.locked) this.UnlockGate(true); } }; Gate.prototype.OnDiplomacyChanged = function(msg) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() == msg.player) { this.allies = []; this.SetupRangeQuery(msg.player); } }; /** * Cleanup on destroy */ Gate.prototype.OnDestroy = function() { // Clean up range query var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.unitsQuery) cmpRangeManager.DestroyActiveQuery(this.unitsQuery); // Cancel the closing-blocked timer if it's running. if (this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } }; /** * Setup the range query to detect units coming in & out of range */ Gate.prototype.SetupRangeQuery = function(owner) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.unitsQuery) cmpRangeManager.DestroyActiveQuery(this.unitsQuery); // Only allied units can make the gate open. var players = QueryPlayerIDInterface(owner).GetAllies(); var range = this.GetPassRange(); if (range > 0) { // Only find entities with IID_UnitAI interface this.unitsQuery = cmpRangeManager.CreateActiveQuery(this.entity, 0, range, players, IID_UnitAI, cmpRangeManager.GetEntityFlagMask("normal")); cmpRangeManager.EnableActiveQuery(this.unitsQuery); } }; /** * Called when units enter or leave range */ Gate.prototype.OnRangeUpdate = function(msg) { if (msg.tag != this.unitsQuery) return; if (msg.added.length > 0) for (let entity of msg.added) this.allies.push(entity); if (msg.removed.length > 0) for (let entity of msg.removed) this.allies.splice(this.allies.indexOf(entity), 1); this.OperateGate(); }; /** * Get the range in which units are detected */ Gate.prototype.GetPassRange = function() { return +this.template.PassRange; }; /** * Attempt to open or close the gate. * An ally must be in range to open the gate, but an unlocked gate will only close * if there are no allies in range and no units are inside the gate's obstruction. */ Gate.prototype.OperateGate = function() { // Cancel the closing-blocked timer if it's running. if (this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } if (this.opened && (this.allies.length == 0 || this.locked)) this.CloseGate(); else if (!this.opened && this.allies.length) this.OpenGate(); }; Gate.prototype.IsLocked = function() { return this.locked; }; /** * Lock the gate, with sound. It will close at the next opportunity. */ Gate.prototype.LockGate = function() { this.locked = true; // If the door is closed, enable 'block pathfinding' // Else 'block pathfinding' will be enabled the next time the gate close if (!this.opened) { var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (!cmpObstruction) return; cmpObstruction.SetDisableBlockMovementPathfinding(false, false, 0); } else this.OperateGate(); // TODO: Possibly move the lock/unlock sounds to UI? Needs testing PlaySound("gate_locked", this.entity); }; /** * Unlock the gate, with sound. May open the gate if allied units are within range. * If quiet is true, no sound will be played (used for initial setup). */ Gate.prototype.UnlockGate = function(quiet) { var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (!cmpObstruction) return; // Disable 'block pathfinding' cmpObstruction.SetDisableBlockMovementPathfinding(this.opened, true, 0); this.locked = false; // TODO: Possibly move the lock/unlock sounds to UI? Needs testing if (!quiet) PlaySound("gate_unlocked", this.entity); // If the gate is closed, open it if necessary if (!this.opened) this.OperateGate(); }; /** * Open the gate if unlocked, with sound and animation. */ Gate.prototype.OpenGate = function() { // Do not open the gate if it has been locked if (this.locked) return; var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (!cmpObstruction) return; // Disable 'block movement' cmpObstruction.SetDisableBlockMovementPathfinding(true, true, 0); this.opened = true; PlaySound("gate_opening", this.entity); var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("gate_opening", true, 1.0, ""); }; /** * Close the gate, with sound and animation. * * The gate may fail to close due to unit obstruction. If this occurs, the * gate will start a timer and attempt to close on each simulation update. */ Gate.prototype.CloseGate = function() { var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (!cmpObstruction) return; // The gate can't be closed if there are entities colliding with it. var collisions = cmpObstruction.GetUnitCollisions(); if (collisions.length) { if (!this.timer) { // Set an "instant" timer which will run on the next simulation turn. var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_Gate, "OperateGate", 0, {}); } return; } // If we ordered the gate to be locked, enable 'block movement' and 'block pathfinding' if (this.locked) cmpObstruction.SetDisableBlockMovementPathfinding(false, false, 0); // Else just enable 'block movement' else cmpObstruction.SetDisableBlockMovementPathfinding(false, true, 0); this.opened = false; PlaySound("gate_closing", this.entity); var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("gate_closing", true, 1.0, ""); }; Engine.RegisterComponentType(IID_Gate, "Gate", Gate); Index: ps/trunk/binaries/data/mods/public/simulation/components/Guard.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Guard.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/Guard.js (revision 20953) @@ -1,97 +1,97 @@ function Guard() {} Guard.prototype.Schema = ""; Guard.prototype.Init = function() { this.entities = []; }; Guard.prototype.GetRange = function(entity) { let range = 8; let cmpFootprint = Engine.QueryInterface(entity, IID_Footprint); if (cmpFootprint) { let shape = cmpFootprint.GetShape(); if (shape.type == "square") range += Math.sqrt(shape.depth*shape.depth + shape.width*shape.width)*2/3; else if (shape.type == "circle") range += shape.radius*3/2; } return range; }; Guard.prototype.GetEntities = function() { return this.entities.slice(); }; Guard.prototype.SetEntities = function(entities) { this.entities = entities; }; Guard.prototype.AddGuard = function(ent) { if (this.entities.indexOf(ent) != -1) return; this.entities.push(ent); }; Guard.prototype.RemoveGuard = function(ent) { let index = this.entities.indexOf(ent); if (index != -1) this.entities.splice(index, 1); }; Guard.prototype.RenameGuard = function(oldent, newent) { let index = this.entities.indexOf(oldent); if (index != -1) this.entities[index] = newent; }; Guard.prototype.OnAttacked = function(msg) { for (let ent of this.entities) Engine.PostMessage(ent, MT_GuardedAttacked, { "guarded": this.entity, "data": msg }); }; /** * If an entity is captured, or about to be killed (so its owner * changes to '-1') or if diplomacy changed, update the guards list */ Guard.prototype.OnOwnershipChanged = function(msg) { if (!this.entities.length) return; - this.CheckGuards(msg.to == -1); + this.CheckGuards(msg.to == INVALID_PLAYER); }; Guard.prototype.OnDiplomacyChanged = function(msg) { if (!this.entities.length) return; this.CheckGuards(); }; Guard.prototype.CheckGuards = function(force = false) { let entities = this.GetEntities(); for (let ent of entities) { if (force || !IsOwnedByMutualAllyOfEntity(this.entity, ent)) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.IsGuardOf() && cmpUnitAI.IsGuardOf() == this.entity) cmpUnitAI.RemoveGuard(); else this.RemoveGuard(ent); } } }; Engine.RegisterComponentType(IID_Guard, "Guard", Guard); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 20953) @@ -1,2002 +1,2002 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronised for the biggest part // So most of the attributes shouldn't be serialized // Return an object with a small selection of deterministic data return { "timeNotifications": this.timeNotifications, "timeNotificationID": this.timeNotificationID }; }; GuiInterface.prototype.Deserialize = function(data) { this.Init(); this.timeNotifications = data.timeNotifications; this.timeNotificationID = data.timeNotificationID; }; GuiInterface.prototype.Init = function() { this.placementEntity = undefined; // = undefined or [templateName, entityID] this.placementWallEntities = undefined; this.placementWallLastAngle = 0; this.notifications = []; this.renamedEntities = []; this.miragedEntities = []; this.timeNotificationID = 1; this.timeNotifications = []; this.entsRallyPointsDisplayed = []; this.entsWithAuraAndStatusBars = new Set(); this.enabledVisualRangeOverlayTypes = {}; }; /* * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg) * from GUI scripts, and executed here with arguments (player, arg). * * CAUTION: The input to the functions in this module is not network-synchronised, so it * mustn't affect the simulation state (i.e. the data that is serialised and can affect * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors. */ /** * Returns global information about the current game state. * This is used by the GUI and also by AI scripts. */ GuiInterface.prototype.GetSimulationState = function() { let ret = { "players": [] }; let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i); let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits); // Work out what phase we are in let phase = ""; let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager); if (cmpTechnologyManager) { if (cmpTechnologyManager.IsTechnologyResearched("phase_city")) phase = "city"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_town")) phase = "town"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_village")) phase = "village"; } // store player ally/neutral/enemy data as arrays let allies = []; let mutualAllies = []; let neutrals = []; let enemies = []; for (let j = 0; j < numPlayers; ++j) { allies[j] = cmpPlayer.IsAlly(j); mutualAllies[j] = cmpPlayer.IsMutualAlly(j); neutrals[j] = cmpPlayer.IsNeutral(j); enemies[j] = cmpPlayer.IsEnemy(j); } ret.players.push({ "name": cmpPlayer.GetName(), "civ": cmpPlayer.GetCiv(), "color": cmpPlayer.GetColor(), "controlsAll": cmpPlayer.CanControlAllUnits(), "popCount": cmpPlayer.GetPopulationCount(), "popLimit": cmpPlayer.GetPopulationLimit(), "popMax": cmpPlayer.GetMaxPopulation(), "panelEntities": cmpPlayer.GetPanelEntities(), "resourceCounts": cmpPlayer.GetResourceCounts(), "trainingBlocked": cmpPlayer.IsTrainingBlocked(), "state": cmpPlayer.GetState(), "team": cmpPlayer.GetTeam(), "teamsLocked": cmpPlayer.GetLockTeams(), "cheatsEnabled": cmpPlayer.GetCheatsEnabled(), "disabledTemplates": cmpPlayer.GetDisabledTemplates(), "disabledTechnologies": cmpPlayer.GetDisabledTechnologies(), "hasSharedDropsites": cmpPlayer.HasSharedDropsites(), "hasSharedLos": cmpPlayer.HasSharedLos(), "spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(), "phase": phase, "isAlly": allies, "isMutualAlly": mutualAllies, "isNeutral": neutrals, "isEnemy": enemies, "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null, "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null, "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null, "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null, "researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null, "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null, "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null, "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null, "canBarter": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).PlayerHasMarket(i), "barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(i) }); } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) ret.circularMap = cmpRangeManager.GetLosCircular(); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (cmpTerrain) ret.mapSize = cmpTerrain.GetMapSize(); // Add timeElapsed let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); // Add ceasefire info let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); if (cmpCeasefireManager) { ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive(); ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0; } // Add cinema path info let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager); if (cmpCinemaManager) ret.cinemaPlaying = cmpCinemaManager.IsPlaying(); // Add the game type and allied victory let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); ret.gameType = cmpEndGameManager.GetGameType(); ret.alliedVictory = cmpEndGameManager.GetAlliedVictory(); // Add basic statistics to each player for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics(); } return ret; }; /** * Returns global information about the current game state, plus statistics. * This is used by the GUI at the end of a game, in the summary screen. * Note: Amongst statistics, the team exploration map percentage is computed from * scratch, so the extended simulation state should not be requested too often. */ GuiInterface.prototype.GetExtendedSimulationState = function() { // Get basic simulation info let ret = this.GetSimulationState(); // Add statistics to each player let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences(); } return ret; }; GuiInterface.prototype.GetRenamedEntities = function(player) { if (this.miragedEntities[player]) return this.renamedEntities.concat(this.miragedEntities[player]); return this.renamedEntities; }; GuiInterface.prototype.ClearRenamedEntities = function() { this.renamedEntities = []; this.miragedEntities = []; }; GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage) { if (!this.miragedEntities[player]) this.miragedEntities[player] = []; this.miragedEntities[player].push({ "entity": entity, "newentity": mirage }); }; /** * Get common entity info, often used in the gui */ GuiInterface.prototype.GetEntityState = function(player, ent) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); // All units must have a template; if not then it's a nonexistent entity id let template = cmpTemplateManager.GetCurrentTemplateName(ent); if (!template) return null; let ret = { "id": ent, "template": template, "alertRaiser": null, "armour": null, "attack": null, "builder": null, "buildingAI": null, "buildRate": null, "buildTime": null, "canGarrison": null, "deathDamage": null, "heal": null, "identity": null, "isBarterMarket": null, "fogging": null, "foundation": null, "garrisonHolder": null, "gate": null, "guard": null, "loot": null, "market": null, "mirage": null, "pack": null, "promotion": null, "upgrade" : null, "player": -1, "position": null, "production": null, "rallyPoint": null, "repairRate": null, "resourceCarrying": null, "resourceDropsite": null, "resourceGatherRates": null, "resourceSupply": null, "resourceTrickle": null, "rotation": null, "speed": null, "trader": null, "turretParent":null, "unitAI": null, "visibility": null, }; let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) ret.mirage = true; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) ret.identity = { "rank": cmpIdentity.GetRank(), "classes": cmpIdentity.GetClassesList(), "visibleClasses": cmpIdentity.GetVisibleClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName(), "canDelete": !cmpIdentity.IsUndeletable() }; let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) { ret.position = cmpPosition.GetPosition(); ret.rotation = cmpPosition.GetRotation(); } let cmpHealth = QueryMiragedInterface(ent, IID_Health); if (cmpHealth) { ret.hitpoints = cmpHealth.GetHitpoints(); ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints(); ret.needsHeal = !cmpHealth.IsUnhealable(); } let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable); if (cmpCapturable) { ret.capturePoints = cmpCapturable.GetCapturePoints(); ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); } let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (cmpBuilder) ret.builder = true; let cmpMarket = QueryMiragedInterface(ent, IID_Market); if (cmpMarket) ret.market = { "land": cmpMarket.HasType("land"), "naval": cmpMarket.HasType("naval"), }; let cmpPack = Engine.QueryInterface(ent, IID_Pack); if (cmpPack) ret.pack = { "packed": cmpPack.IsPacked(), "progress": cmpPack.GetProgress(), }; var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) ret.upgrade = { "upgrades" : cmpUpgrade.GetUpgrades(), "progress": cmpUpgrade.GetProgress(), "template": cmpUpgrade.GetUpgradingTo() }; let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) ret.production = { "entities": cmpProductionQueue.GetEntitiesList(), "technologies": cmpProductionQueue.GetTechnologiesList(), "techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(), "queue": cmpProductionQueue.GetQueue() }; let cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) ret.trader = { "goods": cmpTrader.GetGoods() }; let cmpFogging = Engine.QueryInterface(ent, IID_Fogging); if (cmpFogging) ret.fogging = { "mirage": cmpFogging.IsMiraged(player) ? cmpFogging.GetMirage(player) : null }; let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation); if (cmpFoundation) { ret.foundation = { "progress": cmpFoundation.GetBuildPercentage(), "numBuilders": cmpFoundation.GetNumBuilders() }; cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); if (cmpFoundation) { ret.buildRate = cmpFoundation.GetBuildRate(); ret.buildTime = cmpFoundation.GetBuildTime(); } } let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable); if (cmpRepairable) { ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders() }; cmpRepairable = Engine.QueryInterface(ent, IID_Repairable); if (cmpRepairable) ret.repairRate = cmpRepairable.GetRepairRate(); } let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) ret.player = cmpOwnership.GetOwner(); let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) ret.garrisonHolder = { "entities": cmpGarrisonHolder.GetEntities(), "buffHeal": cmpGarrisonHolder.GetHealRate(), "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), "capacity": cmpGarrisonHolder.GetCapacity(), "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount() }; ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable); let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) ret.unitAI = { "state": cmpUnitAI.GetCurrentState(), "orders": cmpUnitAI.GetOrders(), "hasWorkOrders": cmpUnitAI.HasWorkOrders(), "canGuard": cmpUnitAI.CanGuard(), "isGuarding": cmpUnitAI.IsGuardOf(), "canPatrol": cmpUnitAI.CanPatrol(), "possibleStances": cmpUnitAI.GetPossibleStances(), "isIdle":cmpUnitAI.IsIdle(), }; let cmpGuard = Engine.QueryInterface(ent, IID_Guard); if (cmpGuard) ret.guard = { "entities": cmpGuard.GetEntities(), }; let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) { ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); } let cmpGate = Engine.QueryInterface(ent, IID_Gate); if (cmpGate) ret.gate = { "locked": cmpGate.IsLocked(), }; let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) ret.alertRaiser = { "level": cmpAlertRaiser.GetLevel(), "canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(), "hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(), }; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); ret.visibility = cmpRangeManager.GetLosVisibility(ent, player); let cmpAttack = Engine.QueryInterface(ent, IID_Attack); if (cmpAttack) { let types = cmpAttack.GetAttackTypes(); if (types.length) ret.attack = {}; for (let type of types) { ret.attack[type] = cmpAttack.GetAttackStrengths(type); ret.attack[type].splash = cmpAttack.GetSplashDamage(type); let range = cmpAttack.GetRange(type); ret.attack[type].minRange = range.min; ret.attack[type].maxRange = range.max; let timers = cmpAttack.GetTimers(type); ret.attack[type].prepareTime = timers.prepare; ret.attack[type].repeatTime = timers.repeat; if (type != "Ranged") { // not a ranged attack, set some defaults ret.attack[type].elevationBonus = 0; ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; continue; } ret.attack[type].elevationBonus = range.elevationBonus; if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld()) { // For units, take the range in front of it, no spread. So angle = 0 ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0); } else if(cmpPosition && cmpPosition.IsInWorld()) { // For buildings, take the average elevation around it. So angle = 2*pi ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI); } else { // not in world, set a default? ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; } } } let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver); if (cmpArmour) ret.armour = cmpArmour.GetArmourStrengths(); let cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (cmpAuras) ret.auras = cmpAuras.GetDescriptions(); let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI); if (cmpBuildingAI) ret.buildingAI = { "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(), "maxArrowCount": cmpBuildingAI.GetMaxArrowCount(), "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(), "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(), "arrowCount": cmpBuildingAI.GetArrowCount() }; let cmpDeathDamage = Engine.QueryInterface(ent, IID_DeathDamage); if (cmpDeathDamage) ret.deathDamage = cmpDeathDamage.GetDeathDamageStrengths(); if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY) ret.turretParent = cmpPosition.GetTurretParent(); let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply); if (cmpResourceSupply) ret.resourceSupply = { "isInfinite": cmpResourceSupply.IsInfinite(), "max": cmpResourceSupply.GetMaxAmount(), "amount": cmpResourceSupply.GetCurrentAmount(), "type": cmpResourceSupply.GetType(), "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(), "maxGatherers": cmpResourceSupply.GetMaxGatherers(), "numGatherers": cmpResourceSupply.GetNumGatherers() }; let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite) ret.resourceDropsite = { "types": cmpResourceDropsite.GetTypes(), "sharable": cmpResourceDropsite.IsSharable(), "shared": cmpResourceDropsite.IsShared() }; let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) ret.promotion = { "curr": cmpPromotion.GetCurrentXp(), "req": cmpPromotion.GetRequiredXp() }; if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket")) ret.isBarterMarket = true; let cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) ret.heal = { "hp": cmpHeal.GetHP(), "range": cmpHeal.GetRange().max, "rate": cmpHeal.GetRate(), "unhealableClasses": cmpHeal.GetUnhealableClasses(), "healableClasses": cmpHeal.GetHealableClasses(), }; let cmpLoot = Engine.QueryInterface(ent, IID_Loot); if (cmpLoot) { ret.loot = cmpLoot.GetResources(); ret.loot.xp = cmpLoot.GetXp(); } let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle); if (cmpResourceTrickle) ret.resourceTrickle = { "interval": cmpResourceTrickle.GetTimer(), "rates": cmpResourceTrickle.GetRates() }; let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) ret.speed = { "walk": cmpUnitMotion.GetWalkSpeed(), "run": cmpUnitMotion.GetRunSpeed() }; return ret; }; GuiInterface.prototype.GetMultipleEntityStates = function(player, ents) { return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) })); }; GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); let rot = { "x": 0, "y": 0, "z": 0 }; let pos = { "x": cmd.x, "y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z), "z": cmd.z }; let elevationBonus = cmd.elevationBonus || 0; let range = cmd.range; return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI); }; GuiInterface.prototype.GetTemplateData = function(player, templateName) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(templateName); if (!template) return null; let aurasTemplate = {}; if (!template.Auras) return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes); let auraNames = template.Auras._string.split(/\s+/); for (let name of auraNames) aurasTemplate[name] = AuraTemplates.Get(name); return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes); }; GuiInterface.prototype.IsTechnologyResearched = function(player, data) { if (!data.tech) return true; let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.IsTechnologyResearched(data.tech); }; // Checks whether the requirements for this technology have been met GuiInterface.prototype.CheckTechnologyRequirements = function(player, data) { let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.CanResearch(data.tech); }; // Returns technologies that are being actively researched, along with // which entity is researching them and how far along the research is. GuiInterface.prototype.GetStartedResearch = function(player) { let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return {}; let ret = {}; for (let tech of cmpTechnologyManager.GetStartedTechs()) { ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) }; let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue); if (cmpProductionQueue) ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress; else ret[tech].progress = 0; } return ret; }; // Returns the battle state of the player. GuiInterface.prototype.GetBattleState = function(player) { let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection); if (!cmpBattleDetection) return false; return cmpBattleDetection.GetState(); }; // Returns a list of ongoing attacks against the player. GuiInterface.prototype.GetIncomingAttacks = function(player) { return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks(); }; // Used to show a red square over GUI elements you can't yet afford. GuiInterface.prototype.GetNeededResources = function(player, data) { return QueryPlayerIDInterface(data.player || player).GetNeededResources(data.cost); }; /** * Add a timed notification. * Warning: timed notifacations are serialised * (to also display them on saved games or after a rejoin) * so they should allways be added and deleted in a deterministic way. */ GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); notification.endTime = duration + cmpTimer.GetTime(); notification.id = ++this.timeNotificationID; // Let all players and observers receive the notification by default if (!notification.players) { notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); notification.players[0] = -1; } this.timeNotifications.push(notification); this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime); cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID); return this.timeNotificationID; }; GuiInterface.prototype.DeleteTimeNotification = function(notificationID) { this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID); }; GuiInterface.prototype.GetTimeNotifications = function(player) { let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); // filter on players and time, since the delete timer might be executed with a delay return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time); }; GuiInterface.prototype.PushNotification = function(notification) { if (!notification.type || notification.type == "text") this.AddTimeNotification(notification); else this.notifications.push(notification); }; GuiInterface.prototype.GetNotifications = function() { let n = this.notifications; this.notifications = []; return n; }; GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer) { return QueryPlayerIDInterface(wantedPlayer).GetFormations(); }; GuiInterface.prototype.GetFormationRequirements = function(player, data) { return GetFormationRequirements(data.formationTemplate); }; GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data) { return CanMoveEntsIntoFormation(data.ents, data.formationTemplate); }; GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(data.templateName); if (!template || !template.Formation) return {}; return { "name": template.Formation.FormationName, "tooltip": template.Formation.DisabledTooltip || "", "icon": template.Formation.Icon }; }; GuiInterface.prototype.IsFormationSelected = function(player, data) { for (let ent of data.ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); // GetLastFormationName is named in a strange way as it (also) is // the value of the current formation (see Formation.js LoadFormation) if (cmpUnitAI && cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate) return true; } return false; }; GuiInterface.prototype.IsStanceSelected = function(player, data) { for (let ent of data.ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance) return true; } return false; }; GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd) { let buildableEnts = []; for (let ent of cmd.entities) { let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (!cmpBuilder) continue; for (let building of cmpBuilder.GetEntitiesList()) if (buildableEnts.indexOf(building) == -1) buildableEnts.push(building); } return buildableEnts; }; /** * Updates player colors on the minimap. */ GuiInterface.prototype.UpdateDisplayedPlayerColors = function() { for (let ent of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetGaiaAndNonGaiaEntities()) { let cmpMinimap = Engine.QueryInterface(ent, IID_Minimap); if (cmpMinimap) cmpMinimap.UpdateColor(); } }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) { let playerColors = {}; // cache of owner -> color map for (let ent of cmd.entities) { let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); if (!cmpSelectable) continue; // Find the entity's owner's color: - let owner = -1; + let owner = INVALID_PLAYER; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); let color = playerColors[owner]; if (!color) { color = { "r":1, "g":1, "b":1 }; let cmpPlayer = QueryPlayerIDInterface(owner); if (cmpPlayer) color = cmpPlayer.GetColor(); playerColors[owner] = color; } cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); - if (!cmpRangeOverlayManager || player != owner && player != -1) + if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER) continue; cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false); } }; GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data) { this.enabledVisualRangeOverlayTypes[data.type] = data.enabled; }; GuiInterface.prototype.GetEntitiesWithStatusBars = function() { return Array.from(this.entsWithAuraAndStatusBars); }; GuiInterface.prototype.SetStatusBars = function(player, cmd) { let affectedEnts = new Set(); for (let ent of cmd.entities) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (!cmpStatusBars) continue; cmpStatusBars.SetEnabled(cmd.enabled); let cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (!cmpAuras) continue; for (let name of cmpAuras.GetAuraNames()) { if (!cmpAuras.GetOverlayIcon(name)) continue; for (let e of cmpAuras.GetAffectedEntities(name)) affectedEnts.add(e); if (cmd.enabled) this.entsWithAuraAndStatusBars.add(ent); else this.entsWithAuraAndStatusBars.delete(ent); } } for (let ent of affectedEnts) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.RegenerateSprites(); } }; GuiInterface.prototype.SetRangeOverlays = function(player, cmd) { for (let ent of cmd.entities) { let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true); } }; GuiInterface.prototype.GetPlayerEntities = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player); }; GuiInterface.prototype.GetNonGaiaEntities = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities(); }; /** * Displays the rally points of a given list of entities (carried in cmd.entities). * * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should * be rendered, in order to support instantaneously rendering a rally point marker at a specified location * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the * RallyPoint component. */ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) { let cmpPlayer = QueryPlayerIDInterface(player); // If there are some rally points already displayed, first hide them for (let ent of this.entsRallyPointsDisplayed) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (cmpRallyPointRenderer) cmpRallyPointRenderer.SetDisplayed(false); } this.entsRallyPointsDisplayed = []; // Show the rally points for the passed entities for (let ent of cmd.entities) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (!cmpRallyPointRenderer) continue; // entity must have a rally point component to display a rally point marker // (regardless of whether cmd specifies a custom location) let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (!cmpRallyPoint) continue; // Verify the owner let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!(cmpPlayer && cmpPlayer.CanControlAllUnits())) if (!cmpOwnership || cmpOwnership.GetOwner() != player) continue; // If the command was passed an explicit position, use that and // override the real rally point position; otherwise use the real position let pos; if (cmd.x && cmd.z) pos = cmd; else pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set if (pos) { // Only update the position if we changed it (cmd.queued is set) if ("queued" in cmd) if (cmd.queued == true) cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z else cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z // rebuild the renderer when not set (when reading saved game or in case of building update) else if (!cmpRallyPointRenderer.IsSet()) for (let posi of cmpRallyPoint.GetPositions()) cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z }); cmpRallyPointRenderer.SetDisplayed(true); // remember which entities have their rally points displayed so we can hide them again this.entsRallyPointsDisplayed.push(ent); } } }; GuiInterface.prototype.AddTargetMarker = function(player, cmd) { let ent = Engine.AddLocalEntity(cmd.template); if (!ent) return; let cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); }; /** * Display the building placement preview. * cmd.template is the name of the entity template, or "" to disable the preview. * cmd.x, cmd.z, cmd.angle give the location. * * Returns result object from CheckPlacement: * { * "success": true iff the placement is valid, else false * "message": message to display in UI for invalid placement, else "" * "parameters": parameters to use in the message * "translateMessage": localisation info * "translateParameters": localisation info * "pluralMessage": we might return a plural translation instead (optional) * "pluralCount": localisation info (optional) * } */ GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) { let result = { "success": false, "message": "", "parameters": {}, "translateMessage": false, "translateParameters": [], }; // See if we're changing template if (!this.placementEntity || this.placementEntity[0] != cmd.template) { // Destroy the old preview if there was one if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); // Load the new template if (cmd.template == "") this.placementEntity = undefined; else this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } if (this.placementEntity) { let ent = this.placementEntity[1]; // Move the preview into the right location let pos = Engine.QueryInterface(ent, IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether building placement is valid let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) error("cmpBuildRestrictions not defined"); else result = cmpBuildRestrictions.CheckPlacement(); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes); // Set it to a red shade if this is an invalid location let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); if (!result.success) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } } return result; }; /** * Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not * specified. Returns an object with information about the list of entities that need to be newly constructed to complete * at least a part of the wall, or false if there are entities required to build at least part of the wall but none of * them can be validly constructed. * * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one * another depending on things like snapping and whether some of the entities inside them can be validly positioned. * We have: * - The list of entities that previews the wall. This list is usually equal to the entities required to construct the * entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities * to preview the completed tower on top of its foundation. * * - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether * any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing * towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we * snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly * constructed. * * - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same * as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens * e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly * constructed but come after said first invalid entity are also truncated away. * * With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset * argument (see below). Otherwise, it will return an object with the following information: * * result: { * 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers. * 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this * can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side * but the wall construction was truncated before we could reach it, it won't be set here. Currently only * supports towers. * 'pieces': Array with the following data for each of the entities in the third list: * [{ * 'template': Template name of the entity. * 'x': X coordinate of the entity's position. * 'z': Z coordinate of the entity's position. * 'angle': Rotation around the Y axis of the entity (in radians). * }, * ...] * 'cost': { The total cost required for constructing all the pieces as listed above. * 'food': ..., * 'wood': ..., * 'stone': ..., * 'metal': ..., * 'population': ..., * 'populationBonus': ..., * } * } * * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview. * @param cmd.start Starting point of the wall segment being created. * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only * the starting point of the wall is available at this time (e.g. while the player is still in the process * of picking a starting point), and that therefore only the first entity in the wall (a tower) should be * previewed. * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to. */ GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd) { let wallSet = cmd.wallSet; let start = { "pos": cmd.start, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; let end = { "pos": cmd.end, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; // -------------------------------------------------------------------------------- // do some entity cache management and check for snapping if (!this.placementWallEntities) this.placementWallEntities = {}; if (!wallSet) { // we're clearing the preview, clear the entity cache and bail for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) Engine.DestroyEntity(ent); this.placementWallEntities[tpl].numUsed = 0; this.placementWallEntities[tpl].entities = []; // keep template data around } return false; } // Move all existing cached entities outside of the world and reset their use count for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) { let pos = Engine.QueryInterface(ent, IID_Position); if (pos) pos.MoveOutOfWorld(); } this.placementWallEntities[tpl].numUsed = 0; } // Create cache entries for templates we haven't seen before for (let type in wallSet.templates) { if (type == "curves") continue; let tpl = wallSet.templates[type]; if (!(tpl in this.placementWallEntities)) { this.placementWallEntities[tpl] = { "numUsed": 0, "entities": [], "templateData": this.GetTemplateData(player, tpl), }; // ensure that the loaded template data contains a wallPiece component if (!this.placementWallEntities[tpl].templateData.wallPiece) { error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'"); return false; } } } // prevent division by zero errors further on if the start and end positions are the same if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z)) end.pos = undefined; // See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData). if (cmd.snapEntities) { let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error let startSnapData = this.GetFoundationSnapData(player, { "x": start.pos.x, "z": start.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (startSnapData) { start.pos.x = startSnapData.x; start.pos.z = startSnapData.z; start.angle = startSnapData.angle; start.snapped = true; if (startSnapData.ent) start.snappedEnt = startSnapData.ent; } if (end.pos) { let endSnapData = this.GetFoundationSnapData(player, { "x": end.pos.x, "z": end.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (endSnapData) { end.pos.x = endSnapData.x; end.pos.z = endSnapData.z; end.angle = endSnapData.angle; end.snapped = true; if (endSnapData.ent) end.snappedEnt = endSnapData.ent; } } } // clear the single-building preview entity (we'll be rolling our own) this.SetBuildingPlacementPreview(player, { "template": "" }); // -------------------------------------------------------------------------------- // calculate wall placement and position preview entities let result = { "pieces": [], "cost": { "population": 0, "populationBonus": 0, "time": 0 }, }; for (let res of Resources.GetCodes()) result.cost[res] = 0; let previewEntities = []; if (end.pos) previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of // an issue, because all preview entities have their obstruction components deactivated, meaning that their // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces. // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION // flag set), which is what we want. The only exception to this is when snapping to existing towers (or // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this, // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below. // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed // by the foundation it snaps to. if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) { let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction); if (previewEntities.length > 0 && startEntObstruction) previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()]; // if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group let startEntState = this.GetEntityState(player, start.snappedEnt); if (startEntState.foundation) { let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position); if (cmpPosition) previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined], "excludeFromResult": true, // preview only, must not appear in the result }); } } else { // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned // wall piece. // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates, // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to // the foundation's angle. // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice. previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle }); } if (end.pos) { // Analogous to the starting side case above if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY) { let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction); // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time). if (previewEntities.length > 0 && endEntObstruction) { previewEntities[previewEntities.length-1].controlGroups = previewEntities[previewEntities.length-1].controlGroups || []; previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup()); } // if we're snapping to a foundation, add an extra preview tower and also set it to the same control group let endEntState = this.GetEntityState(player, end.snappedEnt); if (endEntState.foundation) { let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position); if (cmpPosition) previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined], "excludeFromResult": true }); } } else previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle }); } let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (!cmpTerrain) { error("[SetWallPlacementPreview] System Terrain component not found"); return false; } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) { error("[SetWallPlacementPreview] System RangeManager component not found"); return false; } // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be, // but cannot validly be, constructed). See method-level documentation for more details. let allPiecesValid = true; let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity for (let i = 0; i < previewEntities.length; ++i) { let entInfo = previewEntities[i]; let ent = null; let tpl = entInfo.template; let tplData = this.placementWallEntities[tpl].templateData; let entPool = this.placementWallEntities[tpl]; if (entPool.numUsed >= entPool.entities.length) { // allocate new entity ent = Engine.AddLocalEntity("preview|" + tpl); entPool.entities.push(ent); } else // reuse an existing one ent = entPool.entities[entPool.numUsed]; if (!ent) { error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'"); continue; } // move piece to right location // TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition) { cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z); cmpPosition.SetYRotation(entInfo.angle); // if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces if (tpl === wallSet.templates.tower) { let terrainGroundPrev = null; let terrainGroundNext = null; if (i > 0) terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z); if (i < previewEntities.length - 1) terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z); if (terrainGroundPrev != null || terrainGroundNext != null) { let targetY = Math.max(terrainGroundPrev, terrainGroundNext); cmpPosition.SetHeightFixed(targetY); } } } let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (!cmpObstruction) { error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component"); continue; } // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a // first-come first-served basis; the first value in the array is always assigned as the primary control group, and // any second value as the secondary control group. // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was // once snapped to. let primaryControlGroup = ent; let secondaryControlGroup = INVALID_ENTITY; if (entInfo.controlGroups && entInfo.controlGroups.length > 0) { if (entInfo.controlGroups.length > 2) { error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups"); break; } primaryControlGroup = entInfo.controlGroups[0]; if (entInfo.controlGroups.length > 1) secondaryControlGroup = entInfo.controlGroups[1]; } cmpObstruction.SetControlGroup(primaryControlGroup); cmpObstruction.SetControlGroup2(secondaryControlGroup); // check whether this wall piece can be validly positioned here let validPlacement = false; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether it's in a visible or fogged region // TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden"; if (visible) { let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) { error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'"); continue; } // TODO: Handle results of CheckPlacement validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success; // If a wall piece has two control groups, it's likely a segment that spans // between two existing towers. To avoid placing a duplicate wall segment, // check for collisions with entities that share both control groups. if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1) validPlacement = cmpObstruction.CheckDuplicateFoundation(); } allPiecesValid = allPiecesValid && validPlacement; // The requirement below that all pieces so far have to have valid positions, rather than only this single one, // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall // through and past an existing building). // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed // on top of foundations of incompleted towers that we snapped to; they must not be part of the result. if (!entInfo.excludeFromResult) ++numRequiredPieces; if (allPiecesValid && !entInfo.excludeFromResult) { result.pieces.push({ "template": tpl, "x": entInfo.pos.x, "z": entInfo.pos.z, "angle": entInfo.angle, }); this.placementWallLastAngle = entInfo.angle; // grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components // copied over, so we need to fetch it from the template instead). // TODO: we should really use a Cost object or at least some utility functions for this, this is mindless // boilerplate that's probably duplicated in tons of places. for (let res of Resources.GetCodes().concat(["population", "populationBonus", "time"])) result.cost[res] += tplData.cost[res]; } let canAfford = true; let cmpPlayer = QueryPlayerIDInterface(player, IID_Player); if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost)) canAfford = false; let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (!allPiecesValid || !canAfford) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } ++entPool.numUsed; } // If any were entities required to build the wall, but none of them could be validly positioned, return failure // (see method-level documentation). if (numRequiredPieces > 0 && result.pieces.length == 0) return false; if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) result.startSnappedEnt = start.snappedEnt; // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed, // i.e. are included in result.pieces (see docs for the result object). if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid) result.endSnappedEnt = end.snappedEnt; return result; }; /** * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap * it to (if necessary/useful). * * @param data.x The X position of the foundation to snap. * @param data.z The Z position of the foundation to snap. * @param data.template The template to get the foundation snapping data for. * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius * around the entity. Only takes effect when used in conjunction with data.snapRadius. * When this option is used and the foundation is found to snap to one of the entities passed in this list * (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent", * holding the ID of the entity that was snapped to. * @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that * {data.x, data.z} must be located within to have it snap to that entity. */ GuiInterface.prototype.GetFoundationSnapData = function(player, data) { let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template); if (!template) { warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'"); return false; } if (data.snapEntities && data.snapRadius && data.snapRadius > 0) { // see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest // (TODO: break unlikely ties by choosing the lowest entity ID) let minDist2 = -1; let minDistEntitySnapData = null; let radius2 = data.snapRadius * data.snapRadius; for (let ent of data.snapEntities) { let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; let pos = cmpPosition.GetPosition(); let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z); if (dist2 > radius2) continue; if (minDist2 < 0 || dist2 < minDist2) { minDist2 = dist2; minDistEntitySnapData = { "x": pos.x, "z": pos.z, "angle": cmpPosition.GetRotation().y, "ent": ent }; } } if (minDistEntitySnapData != null) return minDistEntitySnapData; } if (template.BuildRestrictions.PlacementType == "shore") { let angle = GetDockAngle(template, data.x, data.z); if (angle !== undefined) return { "x": data.x, "z": data.z, "angle": angle }; } return false; }; GuiInterface.prototype.PlaySound = function(player, data) { if (!data.entity) return; PlaySound(data.name, data.entity); }; /** * Find any idle units. * * @param data.idleClasses Array of class names to include. * @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined. * @param data.limit The number of idle units to return. May be left undefined (will return all idle units). * @param data.excludeUnits Array of units to exclude. * * Returns an array of idle units. * If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class. */ GuiInterface.prototype.FindIdleUnits = function(player, data) { let idleUnits = []; // The general case is that only the 'first' idle unit is required; filtering would examine every unit. // This loop imitates a grouping/aggregation on the first matching idle class. let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let entity of cmpRangeManager.GetEntitiesByPlayer(player)) { let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits); if (!filtered.idle) continue; // If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any. // By adding to the 'end', there is no pause if the series of units loops. var bucket = filtered.bucket; if(bucket == 0 && data.prevUnit && entity <= data.prevUnit) bucket = data.idleClasses.length; if (!idleUnits[bucket]) idleUnits[bucket] = []; idleUnits[bucket].push(entity); // If enough units have been collected in the first bucket, go ahead and return them. if (data.limit && bucket == 0 && idleUnits[0].length == data.limit) return idleUnits[0]; } let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []); if (data.limit && reduced.length > data.limit) return reduced.slice(0, data.limit); return reduced; }; /** * Discover if the player has idle units. * * @param data.idleClasses Array of class names to include. * @param data.excludeUnits Array of units to exclude. * * Returns a boolean of whether the player has any idle units */ GuiInterface.prototype.HasIdleUnits = function(player, data) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle); }; /** * Whether to filter an idle unit * * @param unit The unit to filter. * @param idleclasses Array of class names to include. * @param excludeUnits Array of units to exclude. * * Returns an object with the following fields: * - idle - true if the unit is considered idle by the filter, false otherwise. * - bucket - if idle, set to the index of the first matching idle class, undefined otherwise. */ GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits) { let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned()) return { "idle": false }; let cmpIdentity = Engine.QueryInterface(unit, IID_Identity); if(!cmpIdentity) return { "idle": false }; let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem)); if (bucket == -1 || excludeUnits.indexOf(unit) > -1) return { "idle": false }; return { "idle": true, "bucket": bucket }; }; GuiInterface.prototype.GetTradingRouteGain = function(player, data) { if (!data.firstMarket || !data.secondMarket) return null; return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template); }; GuiInterface.prototype.GetTradingDetails = function(player, data) { let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader); if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target)) return null; let firstMarket = cmpEntityTrader.GetFirstMarket(); let secondMarket = cmpEntityTrader.GetSecondMarket(); let result = null; if (data.target === firstMarket) { result = { "type": "is first", "hasBothMarkets": cmpEntityTrader.HasBothMarkets() }; if (cmpEntityTrader.HasBothMarkets()) result.gain = cmpEntityTrader.GetGoods().amount; } else if (data.target === secondMarket) { result = { "type": "is second", "gain": cmpEntityTrader.GetGoods().amount, }; } else if (!firstMarket) { result = { "type": "set first" }; } else if (!secondMarket) { result = { "type": "set second", "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target), }; } else { // Else both markets are not null and target is different from them result = { "type": "set first" }; } return result; }; GuiInterface.prototype.CanAttack = function(player, data) { let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined); }; /* * Returns batch build time. */ GuiInterface.prototype.GetBatchTime = function(player, data) { let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue); if (!cmpProductionQueue) return 0; return cmpProductionQueue.GetBatchTime(data.batchSize); }; GuiInterface.prototype.IsMapRevealed = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player); }; GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled); }; GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) { for (let ent of data.entities) { let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetDebugOverlay(data.enabled); } }; GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.GetTraderNumber = function(player) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader)); let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 }; let shipTrader = { "total": 0, "trading": 0 }; for (let ent of traders) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpIdentity || !cmpUnitAI) continue; if (cmpIdentity.HasClass("Ship")) { ++shipTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++shipTrader.trading; } else { ++landTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++landTrader.trading; if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison") { let holder = cmpUnitAI.order.data.target; let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI); if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade") ++landTrader.garrisoned; } } } return { "landTrader": landTrader, "shipTrader": shipTrader }; }; GuiInterface.prototype.GetTradingGoods = function(player) { return QueryPlayerIDInterface(player).GetTradingGoods(); }; GuiInterface.prototype.OnGlobalEntityRenamed = function(msg) { this.renamedEntities.push(msg); }; // List the GuiInterface functions that can be safely called by GUI scripts. // (GUI scripts are non-deterministic and untrusted, so these functions must be // appropriately careful. They are called with a first argument "player", which is // trusted and indicates the player associated with the current client; no data should // be returned unless this player is meant to be able to see it.) let exposedFunctions = { "GetSimulationState": 1, "GetExtendedSimulationState": 1, "GetRenamedEntities": 1, "ClearRenamedEntities": 1, "GetEntityState": 1, "GetMultipleEntityStates": 1, "GetAverageRangeForBuildings": 1, "GetTemplateData": 1, "IsTechnologyResearched": 1, "CheckTechnologyRequirements": 1, "GetStartedResearch": 1, "GetBattleState": 1, "GetIncomingAttacks": 1, "GetNeededResources": 1, "GetNotifications": 1, "GetTimeNotifications": 1, "GetAvailableFormations": 1, "GetFormationRequirements": 1, "CanMoveEntsIntoFormation": 1, "IsFormationSelected": 1, "GetFormationInfoFromTemplate": 1, "IsStanceSelected": 1, "UpdateDisplayedPlayerColors": 1, "SetSelectionHighlight": 1, "GetAllBuildableEntities": 1, "SetStatusBars": 1, "GetPlayerEntities": 1, "GetNonGaiaEntities": 1, "DisplayRallyPoint": 1, "AddTargetMarker": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, "PlaySound": 1, "FindIdleUnits": 1, "HasIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanAttack": 1, "GetBatchTime": 1, "IsMapRevealed": 1, "SetPathfinderDebugOverlay": 1, "SetPathfinderHierDebugOverlay": 1, "SetObstructionDebugOverlay": 1, "SetMotionDebugOverlay": 1, "SetRangeDebugOverlay": 1, "EnableVisualRangeOverlayType": 1, "SetRangeOverlays": 1, "GetTraderNumber": 1, "GetTradingGoods": 1, }; GuiInterface.prototype.ScriptCall = function(player, name, args) { if (exposedFunctions[name]) return this[name](player, args); throw new Error("Invalid GuiInterface Call name \""+name+"\""); }; Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/Market.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Market.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/Market.js (revision 20953) @@ -1,87 +1,87 @@ function Market() {} Market.prototype.Schema = "" + "" + "" + "" + "land" + "naval" + "" + "" + "" + "" + "" + "" + ""; Market.prototype.Init = function() { this.traders = new Set(); // list of traders with a route on this market this.tradeType = new Set(this.template.TradeType.split(/\s+/)); }; Market.prototype.AddTrader = function(ent) { this.traders.add(ent); }; Market.prototype.RemoveTrader = function(ent) { this.traders.delete(ent); }; Market.prototype.GetInternationalBonus = function() { return ApplyValueModificationsToEntity("Market/InternationalBonus", +this.template.InternationalBonus, this.entity); }; Market.prototype.HasType = function(type) { return this.tradeType.has(type); }; Market.prototype.GetType = function() { return this.tradeType; }; Market.prototype.GetTraders = function() { return this.traders; }; /** * Check if the traders attached to this market can still trade with it * Warning: traders currently trading with a mirage of this market are dealt with in Mirage.js */ Market.prototype.UpdateTraders = function(onDestruction) { for (let trader of this.traders) { let cmpTrader = Engine.QueryInterface(trader, IID_Trader); if (!cmpTrader) { this.RemoveTrader(trader); continue; } if (!cmpTrader.HasMarket(this.entity) || !onDestruction && cmpTrader.CanTrade(this.entity)) continue; // this trader can no more trade this.RemoveTrader(trader); cmpTrader.RemoveMarket(this.entity); } }; Market.prototype.OnDiplomacyChanged = function(msg) { this.UpdateTraders(false); }; Market.prototype.OnOwnershipChanged = function(msg) { - this.UpdateTraders(msg.to == -1); + this.UpdateTraders(msg.to == INVALID_PLAYER); }; Engine.RegisterComponentType(IID_Market, "Market", Market); Index: ps/trunk/binaries/data/mods/public/simulation/components/PlayerManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/PlayerManager.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/PlayerManager.js (revision 20953) @@ -1,122 +1,122 @@ function PlayerManager() {} PlayerManager.prototype.Schema = ""; PlayerManager.prototype.Init = function() { this.playerEntities = []; // list of player entity IDs }; PlayerManager.prototype.AddPlayer = function(ent) { var id = this.playerEntities.length; var cmpPlayer = Engine.QueryInterface(ent, IID_Player); cmpPlayer.SetPlayerID(id); this.playerEntities.push(ent); // initialize / update the diplomacy arrays var newDiplo = []; for (var i = 0; i < id; i++) { var cmpOtherPlayer = Engine.QueryInterface(this.GetPlayerByID(i), IID_Player); cmpOtherPlayer.diplomacy[id] = -1; newDiplo[i] = -1; } newDiplo[id] = 1; cmpPlayer.SetDiplomacy(newDiplo); return id; }; /** * To avoid possible problems with cached quantities (as in TechnologyManager), * we first remove all entities from this player, and add them back after the replacement. * Note: This should only be called during setup/init and not during the game */ PlayerManager.prototype.ReplacePlayer = function(id, ent) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var entities = cmpRangeManager.GetEntitiesByPlayer(id); for (var e of entities) { var cmpOwnership = Engine.QueryInterface(e, IID_Ownership); if (cmpOwnership) - cmpOwnership.SetOwner(-1); + cmpOwnership.SetOwner(INVALID_PLAYER); } var oldent = this.playerEntities[id]; var cmpPlayer = Engine.QueryInterface(oldent, IID_Player); var diplo = cmpPlayer.GetDiplomacy(); var color = cmpPlayer.GetColor(); var cmpPlayer = Engine.QueryInterface(ent, IID_Player); cmpPlayer.SetPlayerID(id); this.playerEntities[id] = ent; cmpPlayer.SetColor(color); cmpPlayer.SetDiplomacy(diplo); Engine.DestroyEntity(oldent); Engine.FlushDestroyedEntities(); for (var e of entities) { var cmpOwnership = Engine.QueryInterface(e, IID_Ownership); if (cmpOwnership) cmpOwnership.SetOwner(id); } }; /** * Returns the player entity ID for the given player ID. * The player ID must be valid (else there will be an error message). */ PlayerManager.prototype.GetPlayerByID = function(id) { if (id in this.playerEntities) return this.playerEntities[id]; // All players at or below ID 0 get gaia-level data. (Observers for example) if (id <= 0) return this.playerEntities[0]; var stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line warn("GetPlayerByID: no player defined for id '"+id+"'\n"+stack); return INVALID_ENTITY; }; PlayerManager.prototype.GetNumPlayers = function() { return this.playerEntities.length; }; /** * Returns IDs of all players including gaia. */ PlayerManager.prototype.GetAllPlayers = function() { let players = []; for (let i = 0; i < this.playerEntities.length; ++i) players.push(i); return players; }; PlayerManager.prototype.RemoveAllPlayers = function() { // Destroy existing player entities for (var id of this.playerEntities) Engine.DestroyEntity(id); this.playerEntities = []; }; PlayerManager.prototype.RemoveLastPlayer = function() { if (this.playerEntities.length == 0) return; var lastId = this.playerEntities.pop(); Engine.DestroyEntity(lastId); }; Engine.RegisterSystemComponentType(IID_PlayerManager, "PlayerManager", PlayerManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 20953) @@ -1,873 +1,873 @@ var g_ProgressInterval = 1000; const MAX_QUEUE_SIZE = 16; function ProductionQueue() {} ProductionQueue.prototype.Schema = "Allows the building to train new units and research technologies" + "" + "0.7" + "" + "\n units/{civ}_support_female_citizen\n units/{native}_support_trader\n units/athen_infantry_spearman_b\n " + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + Resources.BuildSchema("nonNegativeDecimal", ["time"]) + ""; ProductionQueue.prototype.Init = function() { this.nextID = 1; this.queue = []; // Queue items are: // { // "id": 1, // "player": 1, // who paid for this batch; we need this to cope with refunds cleanly // "unitTemplate": "units/example", // "count": 10, // "neededSlots": 3, // number of population slots missing for production to begin // "resources": { "wood": 100, ... }, // resources per unit, multiply by count to get total // "population": 1, // population per unit, multiply by count to get total // "productionStarted": false, // true iff we have reserved population // "timeTotal": 15000, // msecs // "timeRemaining": 10000, // msecs // } // // { // "id": 1, // "player": 1, // who paid for this research; we need this to cope with refunds cleanly // "technologyTemplate": "example_tech", // "resources": { "wood": 100, ... }, // resources needed for research // "productionStarted": false, // true iff production has started // "timeTotal": 15000, // msecs // "timeRemaining": 10000, // msecs // } this.timer = undefined; // g_ProgressInterval msec timer, active while the queue is non-empty this.paused = false; this.entityCache = []; this.spawnNotified = false; this.alertRaiser = undefined; }; ProductionQueue.prototype.PutUnderAlert = function(raiser) { this.alertRaiser = raiser; }; ProductionQueue.prototype.ResetAlert = function() { this.alertRaiser = undefined; }; /* * Returns list of entities that can be trained by this building. */ ProductionQueue.prototype.GetEntitiesList = function() { return this.entitiesList; }; ProductionQueue.prototype.CalculateEntitiesList = function() { this.entitiesList = []; if (!this.template.Entities) return; let string = this.template.Entities._string; if (!string) return; // Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID. let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return; let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); if (cmpIdentity) string = string.replace(/\{native\}/g, cmpIdentity.GetCiv()); let entitiesList = string.replace(/\{civ\}/g, cmpPlayer.GetCiv()).split(/\s+/); // filter out disabled and invalid entities let disabledEntities = cmpPlayer.GetDisabledTemplates(); entitiesList = entitiesList.filter(ent => !disabledEntities[ent] && cmpTemplateManager.TemplateExists(ent)); // check if some templates need to show their advanced or elite version let upgradeTemplate = function(templateName) { let template = cmpTemplateManager.GetTemplate(templateName); while (template && template.Promotion !== undefined) { let requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, cmpPlayer.GetPlayerID(), template); if (requiredXp > 0) break; templateName = template.Promotion.Entity; template = cmpTemplateManager.GetTemplate(templateName); } return templateName; }; for (let templateName of entitiesList) this.entitiesList.push(upgradeTemplate(templateName)); for (let item of this.queue) if (item.unitTemplate) item.unitTemplate = upgradeTemplate(item.unitTemplate); }; /* * Returns list of technologies that can be researched by this building. */ ProductionQueue.prototype.GetTechnologiesList = function() { if (!this.template.Technologies) return []; var string = this.template.Technologies._string; if (!string) return []; var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); if (!cmpTechnologyManager) return []; var cmpPlayer = QueryOwnerInterface(this.entity); var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); if (!cmpPlayer || !cmpIdentity) return []; var techs = string.split(/\s+/); // Replace the civ specific technologies for (let i = 0; i < techs.length; ++i) { let tech = techs[i]; if (tech.indexOf("{civ}") == -1) continue; let civTech = tech.replace("{civ}", cmpPlayer.GetCiv()); techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic"); } // Remove any technologies that can't be researched by this civ techs = techs.filter(tech => cmpTechnologyManager.CheckTechnologyRequirements( DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), cmpPlayer.GetCiv()), true)); var techList = []; var superseded = {}; // Stores the tech which supersedes the key var disabledTechnologies = cmpPlayer.GetDisabledTechnologies(); // Add any top level technologies to an array which corresponds to the displayed icons // Also store what a technology is superceded by in the superceded object {"tech1":"techWhichSupercedesTech1", ...} for (var i in techs) { var tech = techs[i]; if (disabledTechnologies && disabledTechnologies[tech]) continue; let template = TechnologyTemplates.Get(tech); if (!template.supersedes || techs.indexOf(template.supersedes) === -1) techList.push(tech); else superseded[template.supersedes] = tech; } // Now make researched/in progress techs invisible for (var i in techList) { var tech = techList[i]; while (this.IsTechnologyResearchedOrInProgress(tech)) { tech = superseded[tech]; } techList[i] = tech; } var ret = []; // This inserts the techs into the correct positions to line up the technology pairs for (var i = 0; i < techList.length; i++) { var tech = techList[i]; if (!tech) { ret[i] = undefined; continue; } let template = TechnologyTemplates.Get(tech); if (template.top) ret[i] = {"pair": true, "top": template.top, "bottom": template.bottom}; else ret[i] = tech; } return ret; }; ProductionQueue.prototype.GetTechCostMultiplier = function() { let techCostMultiplier = {}; for (let res in this.template.TechCostMultiplier) techCostMultiplier[res] = ApplyValueModificationsToEntity("ProductionQueue/TechCostMultiplier/"+res, +this.template.TechCostMultiplier[res], this.entity); return techCostMultiplier; }; ProductionQueue.prototype.IsTechnologyResearchedOrInProgress = function(tech) { if (!tech) return false; var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); let template = TechnologyTemplates.Get(tech); if (template.top) { return (cmpTechnologyManager.IsTechnologyResearched(template.top) || cmpTechnologyManager.IsInProgress(template.top) || cmpTechnologyManager.IsTechnologyResearched(template.bottom) || cmpTechnologyManager.IsInProgress(template.bottom)); } else { return (cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech)); } }; /* * Adds a new batch of identical units to train or a technology to research to the production queue. */ ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadata) { // TODO: there should probably be a limit on the number of queued batches // TODO: there should be a way for the GUI to determine whether it's going // to be possible to add a batch (based on resource costs and length limits) let cmpPlayer = QueryOwnerInterface(this.entity); if (this.queue.length < MAX_QUEUE_SIZE) { if (type == "unit") { if (!Number.isInteger(count) || count <= 0) { error("Invalid batch count " + count); return; } // Find the template data so we can determine the build costs var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetTemplate(templateName); if (!template) return; if (template.Promotion) { var requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, cmpPlayer.GetPlayerID(), template); if (requiredXp == 0) { this.AddBatch(template.Promotion.Entity, type, count, metadata); return; } } // Apply a time discount to larger batches. var timeMult = this.GetBatchTime(count); // We need the costs after tech modifications // Obviously we don't have the entities yet, so we must use template data var costs = {}; var totalCosts = {}; var buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, cmpPlayer.GetPlayerID(), template); var time = timeMult * buildTime; for (let res in template.Cost.Resources) { costs[res] = ApplyValueModificationsToTemplate("Cost/Resources/"+res, +template.Cost.Resources[res], cmpPlayer.GetPlayerID(), template); totalCosts[res] = Math.floor(count * costs[res]); } var population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, cmpPlayer.GetPlayerID(), template); // TrySubtractResources should report error to player (they ran out of resources) if (!cmpPlayer.TrySubtractResources(totalCosts)) return; // Update entity count in the EntityLimits component if (template.TrainingRestrictions) { var unitCategory = template.TrainingRestrictions.Category; var cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits); cmpPlayerEntityLimits.ChangeCount(unitCategory, count); } this.queue.push({ "id": this.nextID++, "player": cmpPlayer.GetPlayerID(), "unitTemplate": templateName, "count": count, "metadata": metadata, "resources": costs, "population": population, "productionStarted": false, "timeTotal": time*1000, "timeRemaining": time*1000, }); // Call the related trigger event var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("TrainingQueued", {"playerid": cmpPlayer.GetPlayerID(), "unitTemplate": templateName, "count": count, "metadata": metadata, "trainerEntity": this.entity}); } else if (type == "technology") { if (!TechnologyTemplates.Has(templateName)) return; if (!this.GetTechnologiesList().some(tech => tech && (tech == templateName || tech.pair && (tech.top == templateName || tech.bottom == templateName)))) { error("This entity cannot research " + templateName); return; } let template = TechnologyTemplates.Get(templateName); let techCostMultiplier = this.GetTechCostMultiplier(); let time = techCostMultiplier.time * template.researchTime * cmpPlayer.GetCheatTimeMultiplier(); let cost = {}; for (let res in template.cost) cost[res] = Math.floor((techCostMultiplier[res] || 1) * template.cost[res]); // TrySubtractResources should report error to player (they ran out of resources) if (!cmpPlayer.TrySubtractResources(cost)) return; // Tell the technology manager that we have started researching this so that people can't research the same // thing twice. var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); cmpTechnologyManager.QueuedResearch(templateName, this.entity); if (this.queue.length == 0) cmpTechnologyManager.StartedResearch(templateName, false); this.queue.push({ "id": this.nextID++, "player": cmpPlayer.GetPlayerID(), "count": 1, "technologyTemplate": templateName, "resources": cost, "productionStarted": false, "timeTotal": time*1000, "timeRemaining": time*1000, }); // Call the related trigger event var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("ResearchQueued", {"playerid": cmpPlayer.GetPlayerID(), "technologyTemplate": templateName, "researcherEntity": this.entity}); } else { warn("Tried to add invalid item of type \"" + type + "\" and template \"" + templateName + "\" to a production queue"); return; } Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); // If this is the first item in the queue, start the timer if (!this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, {}); } } else { var notification = {"players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("The production queue is full."), "translateMessage": true }; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); } }; /* * Removes an existing batch of units from the production queue. * Refunds resource costs and population reservations. */ ProductionQueue.prototype.RemoveBatch = function(id) { // Destroy any cached entities (those which didn't spawn for some reason) for (var i = 0; i < this.entityCache.length; ++i) { Engine.DestroyEntity(this.entityCache[i]); } this.entityCache = []; for (var i = 0; i < this.queue.length; ++i) { var item = this.queue[i]; if (item.id != id) continue; // Now we've found the item to remove var cmpPlayer = QueryPlayerIDInterface(item.player); // Update entity count in the EntityLimits component if (item.unitTemplate) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetTemplate(item.unitTemplate); if (template.TrainingRestrictions) { var unitCategory = template.TrainingRestrictions.Category; var cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits); cmpPlayerEntityLimits.ChangeCount(unitCategory, -item.count); } } // Refund the resource cost for this batch var totalCosts = {}; var cmpStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker); for (let r in item.resources) { totalCosts[r] = Math.floor(item.count * item.resources[r]); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -totalCosts[r]); } cmpPlayer.AddResources(totalCosts); // Remove reserved population slots if necessary if (item.productionStarted && item.unitTemplate) cmpPlayer.UnReservePopulationSlots(item.population * item.count); // Mark the research as stopped if we cancel it if (item.technologyTemplate) { // item.player is used as this.entity's owner may be invalid (deletion, etc.) var cmpTechnologyManager = QueryPlayerIDInterface(item.player, IID_TechnologyManager); cmpTechnologyManager.StoppedResearch(item.technologyTemplate, true); } // Remove from the queue // (We don't need to remove the timer - it'll expire if it discovers the queue is empty) this.queue.splice(i, 1); Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); return; } }; /* * Returns basic data from all batches in the production queue. */ ProductionQueue.prototype.GetQueue = function() { var out = []; for (var item of this.queue) { out.push({ "id": item.id, "unitTemplate": item.unitTemplate, "technologyTemplate": item.technologyTemplate, "count": item.count, "neededSlots": item.neededSlots, "progress": 1 - ( item.timeRemaining / (item.timeTotal || 1) ), "timeRemaining": item.timeRemaining, "metadata": item.metadata, }); } return out; }; /* * Removes all existing batches from the queue. */ ProductionQueue.prototype.ResetQueue = function() { // Empty the production queue and refund all the resource costs // to the player. (This is to avoid players having to micromanage their // buildings' queues when they're about to be destroyed or captured.) while (this.queue.length) this.RemoveBatch(this.queue[0].id); }; /* * Returns batch build time. */ ProductionQueue.prototype.GetBatchTime = function(batchSize) { var cmpPlayer = QueryOwnerInterface(this.entity); var batchTimeModifier = ApplyValueModificationsToEntity("ProductionQueue/BatchTimeModifier", +this.template.BatchTimeModifier, this.entity); // TODO: work out what equation we should use here. return Math.pow(batchSize, batchTimeModifier) * cmpPlayer.GetCheatTimeMultiplier(); }; ProductionQueue.prototype.OnOwnershipChanged = function(msg) { - if (msg.from != -1) + if (msg.from != INVALID_PLAYER) { // Unset flag that previous owner's training may be blocked var cmpPlayer = QueryPlayerIDInterface(msg.from); if (cmpPlayer && this.queue.length > 0) cmpPlayer.UnBlockTraining(); } - if (msg.to != -1) + if (msg.to != INVALID_PLAYER) this.CalculateEntitiesList(); // Reset the production queue whenever the owner changes. // (This should prevent players getting surprised when they capture // an enemy building, and then loads of the enemy's civ's soldiers get // created from it. Also it means we don't have to worry about // updating the reserved pop slots.) this.ResetQueue(); this.ResetAlert(); }; ProductionQueue.prototype.OnCivChanged = function() { this.CalculateEntitiesList(); }; ProductionQueue.prototype.OnDestroy = function() { // Reset the queue to refund any resources this.ResetQueue(); if (this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); } }; /* * This function creates the entities and places them in world if possible * and returns the number of successfully created entities. * (some of these entities may be garrisoned directly if autogarrison, the others are spawned). */ ProductionQueue.prototype.SpawnUnits = function(templateName, count, metadata) { var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); var createdEnts = []; var spawnedEnts = []; if (this.entityCache.length == 0) { // We need entities to test spawning, but we don't want to waste resources, // so only create them once and use as needed for (var i = 0; i < count; ++i) { var ent = Engine.AddEntity(templateName); this.entityCache.push(ent); // Decrement entity count in the EntityLimits component // since it will be increased by EntityLimits.OnGlobalOwnershipChanged function, // i.e. we replace a 'trained' entity to an 'alive' one var cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions); if (cmpTrainingRestrictions) { var unitCategory = cmpTrainingRestrictions.GetCategory(); var cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits); cmpPlayerEntityLimits.ChangeCount(unitCategory,-1); } } } let cmpAutoGarrison; if (cmpRallyPoint) { let data = cmpRallyPoint.GetData()[0]; if (data && data.target && data.target == this.entity && data.command == "garrison") cmpAutoGarrison = Engine.QueryInterface(this.entity, IID_GarrisonHolder); } for (let i = 0; i < count; ++i) { let ent = this.entityCache[0]; let cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership); let garrisoned = false; if (cmpAutoGarrison) { // Temporary owner affectation needed for GarrisonHolder checks cmpNewOwnership.SetOwnerQuiet(cmpOwnership.GetOwner()); garrisoned = cmpAutoGarrison.PerformGarrison(ent); - cmpNewOwnership.SetOwnerQuiet(-1); + cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER); } if (garrisoned) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.Autogarrison(this.entity); } else { let pos = cmpFootprint.PickSpawnPoint(ent); if (pos.y < 0) break; let cmpNewPosition = Engine.QueryInterface(ent, IID_Position); cmpNewPosition.JumpTo(pos.x, pos.z); let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition) cmpNewPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos)); spawnedEnts.push(ent); } cmpNewOwnership.SetOwner(cmpOwnership.GetOwner()); var cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent); // Play a sound, but only for the first in the batch (to avoid nasty phasing effects) if (createdEnts.length == 0) PlaySound("trained", ent); this.entityCache.shift(); createdEnts.push(ent); } if (spawnedEnts.length > 0 && !cmpAutoGarrison) { // If a rally point is set, walk towards it (in formation) using a suitable command based on where the // rally point is placed. if (cmpRallyPoint) { var rallyPos = cmpRallyPoint.GetPositions()[0]; if (rallyPos) { var commands = GetRallyPointCommands(cmpRallyPoint, spawnedEnts); for (var com of commands) ProcessCommand(cmpOwnership.GetOwner(), com); } } } if (createdEnts.length > 0) { Engine.PostMessage(this.entity, MT_TrainingFinished, { "entities": createdEnts, "owner": cmpOwnership.GetOwner(), "metadata": metadata, }); if (this.alertRaiser && spawnedEnts.length > 0) { var cmpAlertRaiser = Engine.QueryInterface(this.alertRaiser, IID_AlertRaiser); if (cmpAlertRaiser) cmpAlertRaiser.UpdateUnits(spawnedEnts); } } return createdEnts.length; }; /* * Increments progress on the first batch in the production queue, and blocks the * queue if population limit is reached or some units failed to spawn. */ ProductionQueue.prototype.ProgressTimeout = function(data) { // Check if the production is paused (eg the entity is garrisoned) if (this.paused) return; // Allocate the 1000msecs to as many queue items as it takes // until we've used up all the time (so that we work accurately // with items that take fractions of a second) var time = g_ProgressInterval; var cmpPlayer = QueryOwnerInterface(this.entity); while (time > 0 && this.queue.length) { var item = this.queue[0]; if (!item.productionStarted) { // If the item is a unit then do population checks if (item.unitTemplate) { // If something change population cost var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(item.unitTemplate); item.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, item.player, template); // Batch's training hasn't started yet. // Try to reserve the necessary population slots item.neededSlots = cmpPlayer.TryReservePopulationSlots(item.population * item.count); if (item.neededSlots) { // Not enough slots available - don't train this batch now // (we'll try again on the next timeout) // Set flag that training is blocked cmpPlayer.BlockTraining(); break; } // Unset flag that training is blocked cmpPlayer.UnBlockTraining(); } if (item.technologyTemplate) { // Mark the research as started. var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); cmpTechnologyManager.StartedResearch(item.technologyTemplate, true); } item.productionStarted = true; if (item.unitTemplate) Engine.PostMessage(this.entity, MT_TrainingStarted, {"entity": this.entity}); } // If we won't finish the batch now, just update its timer if (item.timeRemaining > time) { item.timeRemaining -= time; // send a message for the AIs. Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); break; } if (item.unitTemplate) { var numSpawned = this.SpawnUnits(item.unitTemplate, item.count, item.metadata); if (numSpawned == item.count) { // All entities spawned, this batch finished cmpPlayer.UnReservePopulationSlots(item.population * numSpawned); time -= item.timeRemaining; this.queue.shift(); // Unset flag that training is blocked cmpPlayer.UnBlockTraining(); this.spawnNotified = false; Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); } else { if (numSpawned > 0) { // Only partially finished cmpPlayer.UnReservePopulationSlots(item.population * numSpawned); item.count -= numSpawned; Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); } // Some entities failed to spawn // Set flag that training is blocked cmpPlayer.BlockTraining(); if (!this.spawnNotified) { var cmpPlayer = QueryOwnerInterface(this.entity); var notification = { "players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("Can't find free space to spawn trained units"), "translateMessage": true }; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); this.spawnNotified = true; } break; } } else if (item.technologyTemplate) { var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); cmpTechnologyManager.ResearchTechnology(item.technologyTemplate); let template = TechnologyTemplates.Get(item.technologyTemplate); if (template && template.soundComplete) { var cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager); if (cmpSoundManager) cmpSoundManager.PlaySoundGroup(template.soundComplete, this.entity); } time -= item.timeRemaining; this.queue.shift(); Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { }); } } // If the queue's empty, delete the timer, else repeat it if (this.queue.length == 0) { this.timer = undefined; // Unset flag that training is blocked // (This might happen when the player unqueues all batches) cmpPlayer.UnBlockTraining(); } else { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, data); } }; ProductionQueue.prototype.PauseProduction = function() { this.timer = undefined; this.paused = true; }; ProductionQueue.prototype.UnpauseProduction = function() { this.paused = false; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, {}); }; ProductionQueue.prototype.OnValueModification = function(msg) { // if the promotion requirements of units is changed, // update the entities list so that automatically promoted units are shown // appropriately in the list if (msg.component == "Promotion") this.CalculateEntitiesList(); }; ProductionQueue.prototype.OnDisabledTemplatesChanged = function(msg) { // if the disabled templates of the player is changed, // update the entities list so that this is reflected there this.CalculateEntitiesList(); }; Engine.RegisterComponentType(IID_ProductionQueue, "ProductionQueue", ProductionQueue); Index: ps/trunk/binaries/data/mods/public/simulation/components/RallyPoint.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/RallyPoint.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/RallyPoint.js (revision 20953) @@ -1,128 +1,128 @@ function RallyPoint() {} RallyPoint.prototype.Schema = ""; RallyPoint.prototype.Init = function() { this.pos = []; this.data = []; }; RallyPoint.prototype.AddPosition = function(x, z) { this.pos.push({ "x": x, "z": z }); }; RallyPoint.prototype.GetPositions = function() { // Update positions for moving target entities var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); // We must not affect the simulation state here (modifications of the // RallyPointRenderer are allowed though), so copy the state var ret = []; for (var i = 0; i < this.pos.length; i++) { ret.push(this.pos[i]); // Update the rallypoint coordinates if the target is alive if (!this.data[i] || !this.data[i].target || !this.TargetIsAlive(this.data[i].target)) continue; // and visible if (cmpRangeManager && cmpOwnership && cmpRangeManager.GetLosVisibility(this.data[i].target, cmpOwnership.GetOwner()) != "visible") continue; // Get the actual position of the target entity var cmpPosition = Engine.QueryInterface(this.data[i].target, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; var targetPosition = cmpPosition.GetPosition2D(); if (!targetPosition) continue; if (this.pos[i].x == targetPosition.x && this.pos[i].z == targetPosition.y) continue; ret[i] = { "x": targetPosition.x, "z": targetPosition.y }; var cmpRallyPointRenderer = Engine.QueryInterface(this.entity, IID_RallyPointRenderer); if (cmpRallyPointRenderer) cmpRallyPointRenderer.UpdatePosition(i, targetPosition); } return ret; }; // Extra data for the rally point, should have a command property and then helpful data for that command // See getActionInfo in gui/input.js RallyPoint.prototype.AddData = function(data) { this.data.push(data); }; // Returns an array with the data associated with this rally point. Each element has the structure: // {"type": "walk/gather/garrison/...", "target": targetEntityId, "resourceType": "tree/fruit/ore/..."} where target // and resourceType (specific resource type) are optional, also target may be an invalid entity, check for existence. RallyPoint.prototype.GetData = function() { return this.data; }; RallyPoint.prototype.Unset = function() { this.pos = []; this.data = []; }; RallyPoint.prototype.Reset = function() { this.Unset(); var cmpRallyPointRenderer = Engine.QueryInterface(this.entity, IID_RallyPointRenderer); if (cmpRallyPointRenderer) cmpRallyPointRenderer.Reset(); }; RallyPoint.prototype.OnGlobalEntityRenamed = function(msg) { for (var data of this.data) { if (!data) continue; if (data.target && data.target == msg.entity) data.target = msg.newentity; if (data.source && data.source == msg.entity) data.source = msg.newentity; } }; RallyPoint.prototype.OnOwnershipChanged = function(msg) { // No need to reset when constructing or destructing the entity - if (msg.from == -1 || msg.to == -1) + if (msg.from == INVALID_PLAYER || msg.to == INVALID_PLAYER) return; this.Reset(); }; /** * Returns true if the target exists and has non-zero hitpoints. */ RallyPoint.prototype.TargetIsAlive = function(ent) { var cmpFormation = Engine.QueryInterface(ent, IID_Formation); if (cmpFormation) return true; var cmpHealth = QueryMiragedInterface(ent, IID_Health); return cmpHealth && cmpHealth.GetHitpoints() != 0; }; Engine.RegisterComponentType(IID_RallyPoint, "RallyPoint", RallyPoint); Index: ps/trunk/binaries/data/mods/public/simulation/components/RangeOverlayManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/RangeOverlayManager.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/RangeOverlayManager.js (revision 20953) @@ -1,92 +1,92 @@ function RangeOverlayManager() {} RangeOverlayManager.prototype.Schema = ""; RangeOverlayManager.prototype.Init = function() { this.enabled = false; this.enabledRangeTypes = { "Attack": false, "Auras": false, "Heal": false }; this.rangeVisualizations = new Map(); }; // The GUI enables visualizations RangeOverlayManager.prototype.Serialize = null; RangeOverlayManager.prototype.Deserialize = function(data) { this.Init(); }; RangeOverlayManager.prototype.UpdateRangeOverlays = function(componentName) { let cmp = Engine.QueryInterface(this.entity, global["IID_" + componentName]); if (cmp) this.rangeVisualizations.set(componentName, cmp.GetRangeOverlays()); }; RangeOverlayManager.prototype.SetEnabled = function(enabled, enabledRangeTypes, forceUpdate) { this.enabled = enabled; this.enabledRangeTypes = enabledRangeTypes; this.RegenerateRangeOverlayManagers(forceUpdate); }; RangeOverlayManager.prototype.RegenerateRangeOverlayManagers = function(forceUpdate) { let cmpRangeOverlayRenderer = Engine.QueryInterface(this.entity, IID_RangeOverlayRenderer); if (!cmpRangeOverlayRenderer) return; cmpRangeOverlayRenderer.ResetRangeOverlays(); if (!this.enabled && !forceUpdate) return; // Only render individual range types that have been enabled for (let rangeOverlayType of this.rangeVisualizations.keys()) if (this.enabledRangeTypes[rangeOverlayType]) for (let rangeOverlay of this.rangeVisualizations.get(rangeOverlayType)) cmpRangeOverlayRenderer.AddRangeOverlay( rangeOverlay.radius, rangeOverlay.texture, rangeOverlay.textureMask, rangeOverlay.thickness); }; RangeOverlayManager.prototype.OnOwnershipChanged = function(msg) { - if (msg.to == -1) + if (msg.to == INVALID_PLAYER) return; for (let type in this.enabledRangeTypes) this.UpdateRangeOverlays(type); this.RegenerateRangeOverlayManagers(false); }; RangeOverlayManager.prototype.OnValueModification = function(msg) { if (msg.valueNames.indexOf("Heal/Range") == -1 && msg.valueNames.indexOf("Attack/Ranged/MinRange") == -1 && msg.valueNames.indexOf("Attack/Ranged/MaxRange") == -1) return; this.UpdateRangeOverlays(msg.component); this.RegenerateRangeOverlayManagers(false); }; /** * RangeOverlayManager component is deserialized before the TechnologyManager, so need to update the ranges here */ RangeOverlayManager.prototype.OnDeserialized = function(msg) { for (let type in this.enabledRangeTypes) this.UpdateRangeOverlays(type); }; Engine.RegisterComponentType(IID_RangeOverlayManager, "RangeOverlayManager", RangeOverlayManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 20953) @@ -1,354 +1,354 @@ function ResourceGatherer() {} ResourceGatherer.prototype.Schema = "Lets the unit gather resources from entities that have the ResourceSupply component." + "" + "2.0" + "1.0" + "" + "1" + "3" + "3" + "2" + "" + "" + "10" + "10" + "10" + "10" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Resources.BuildSchema("positiveDecimal", ["treasure"], true) + "" + "" + Resources.BuildSchema("positiveDecimal") + ""; ResourceGatherer.prototype.Init = function() { this.carrying = {}; // { generic type: integer amount currently carried } // (Note that this component supports carrying multiple types of resources, // each with an independent capacity, but the rest of the game currently // ensures and assumes we'll only be carrying one type at once) // The last exact type gathered, so we can render appropriate props this.lastCarriedType = undefined; // { generic, specific } this.RecalculateGatherRatesAndCapacities(); }; /** * Returns data about what resources the unit is currently carrying, * in the form [ {"type":"wood", "amount":7, "max":10} ] */ ResourceGatherer.prototype.GetCarryingStatus = function() { let ret = []; for (let type in this.carrying) { ret.push({ "type": type, "amount": this.carrying[type], "max": +this.GetCapacity(type) }); } return ret; }; /** * Used to instantly give resources to unit * @param resources The same structure as returned form GetCarryingStatus */ ResourceGatherer.prototype.GiveResources = function(resources) { for (let resource of resources) { this.carrying[resource.type] = +(resource.amount); } Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); }; /** * Returns the generic type of one particular resource this unit is * currently carrying, or undefined if none. */ ResourceGatherer.prototype.GetMainCarryingType = function() { // Return the first key, if any for (let type in this.carrying) return type; return undefined; }; /** * Returns the exact resource type we last picked up, as long as * we're still carrying something similar enough, in the form * { generic, specific } */ ResourceGatherer.prototype.GetLastCarriedType = function() { if (this.lastCarriedType && this.lastCarriedType.generic in this.carrying) return this.lastCarriedType; else return undefined; }; // Since this code is very performancecritical and applying technologies quite slow, cache it. ResourceGatherer.prototype.RecalculateGatherRatesAndCapacities = function() { let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); let multiplier = cmpPlayer ? cmpPlayer.GetGatherRateMultiplier() : 1; this.baseSpeed = multiplier * ApplyValueModificationsToEntity("ResourceGatherer/BaseSpeed", +this.template.BaseSpeed, this.entity); this.rates = {}; for (let r in this.template.Rates) { let type = r.split("."); if (type[0] != "treasure" && type.length > 1 && !Resources.GetResource(type[0]).subtypes[type[1]]) { error("Resource subtype not found: " + type[0] + "." + type[1]); continue; } let rate = ApplyValueModificationsToEntity("ResourceGatherer/Rates/" + r, +this.template.Rates[r], this.entity); this.rates[r] = rate * this.baseSpeed; } this.capacities = {}; for (let r in this.template.Capacities) this.capacities[r] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + r, +this.template.Capacities[r], this.entity); }; ResourceGatherer.prototype.GetGatherRates = function() { return this.rates; }; ResourceGatherer.prototype.GetGatherRate = function(resourceType) { if (!this.template.Rates[resourceType]) return 0; return this.rates[resourceType]; }; ResourceGatherer.prototype.GetCapacity = function(resourceType) { if(!this.template.Capacities[resourceType]) return 0; return this.capacities[resourceType]; }; ResourceGatherer.prototype.GetRange = function() { return { "max": +this.template.MaxDistance, "min": 0 }; // maybe this should depend on the unit or target or something? }; /** * Try to gather treasure * @return 'true' if treasure is successfully gathered, otherwise 'false' */ ResourceGatherer.prototype.TryInstantGather = function(target) { let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); let type = cmpResourceSupply.GetType(); if (type.generic != "treasure") return false; let status = cmpResourceSupply.TakeResources(cmpResourceSupply.GetCurrentAmount()); let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (cmpPlayer) cmpPlayer.AddResource(type.specific, status.amount); let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseTreasuresCollectedCounter(); let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); if (cmpTrigger && cmpPlayer) cmpTrigger.CallEvent("TreasureCollected", {"player": cmpPlayer.GetPlayerID(), "type": type.specific, "amount": status.amount }); return true; }; /** * Gather from the target entity. This should only be called after a successful range check, * and if the target has a compatible ResourceSupply. * Call interval will be determined by gather rate, so always gather 1 amount when called. */ ResourceGatherer.prototype.PerformGather = function(target) { if (!this.GetTargetGatherRate(target)) return { "exhausted": true }; let gatherAmount = 1; let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); let type = cmpResourceSupply.GetType(); // Initialise the carried count if necessary if (!this.carrying[type.generic]) this.carrying[type.generic] = 0; // Find the maximum so we won't exceed our capacity let maxGathered = this.GetCapacity(type.generic) - this.carrying[type.generic]; let status = cmpResourceSupply.TakeResources(Math.min(gatherAmount, maxGathered)); this.carrying[type.generic] += status.amount; this.lastCarriedType = type; // Update stats of how much the player collected. // (We have to do it here rather than at the dropsite, because we // need to know what subtype it was) let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseResourceGatheredCounter(type.generic, status.amount, type.specific); Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); return { "amount": status.amount, "exhausted": status.exhausted, "filled": (this.carrying[type.generic] >= this.GetCapacity(type.generic)) }; }; /** * Compute the amount of resources collected per second from the target. * Returns 0 if resources cannot be collected (e.g. the target doesn't * exist, or is the wrong type). */ ResourceGatherer.prototype.GetTargetGatherRate = function(target) { let cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply); if (!cmpResourceSupply) return 0; let type = cmpResourceSupply.GetType(); let rate = 0; if (type.specific) rate = this.GetGatherRate(type.generic+"."+type.specific); if (rate == 0 && type.generic) rate = this.GetGatherRate(type.generic); let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); let cheatMultiplier = cmpPlayer ? cmpPlayer.GetCheatTimeMultiplier() : 1; rate = rate / cheatMultiplier; if ("Mirages" in cmpResourceSupply) return rate; // Apply diminishing returns with more gatherers, for e.g. infinite farms. For most resources this has no effect // (GetDiminishingReturns will return null). We can assume that for resources that are miraged this is the case // (else just add the diminishing returns data to the mirage data and remove the early return above) let diminishingReturns = cmpResourceSupply.GetDiminishingReturns(); if (diminishingReturns) rate *= diminishingReturns; return rate; }; /** * Returns whether this unit can carry more of the given type of resource. * (This ignores whether the unit is actually able to gather that * resource type or not.) */ ResourceGatherer.prototype.CanCarryMore = function(type) { let amount = (this.carrying[type] || 0); return amount < this.GetCapacity(type); }; ResourceGatherer.prototype.IsCarrying = function(type) { let amount = (this.carrying[type] || 0); return amount > 0; }; /** * Returns whether this unit is carrying any resources of a type that is * not the requested type. (This is to support cases where the unit is * only meant to be able to carry one type at once.) */ ResourceGatherer.prototype.IsCarryingAnythingExcept = function(exceptedType) { for (let type in this.carrying) if (type != exceptedType) return true; return false; }; /** * Transfer our carried resources to our owner immediately. * Only resources of the given types will be transferred. * (This should typically be called after reaching a dropsite). */ ResourceGatherer.prototype.CommitResources = function(types) { let cmpPlayer = QueryOwnerInterface(this.entity); if (cmpPlayer) for (let type of types) if (type in this.carrying) { cmpPlayer.AddResource(type, this.carrying[type]); delete this.carrying[type]; } Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); }; /** * Drop all currently-carried resources. * (Currently they just vanish after being dropped - we don't bother depositing * them onto the ground.) */ ResourceGatherer.prototype.DropResources = function() { this.carrying = {}; Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); }; // Since we cache gather rates, we need to make sure we update them when tech changes. // and when our owner change because owners can had different techs. ResourceGatherer.prototype.OnValueModification = function(msg) { if (msg.component != "ResourceGatherer") return; this.RecalculateGatherRatesAndCapacities(); }; ResourceGatherer.prototype.OnOwnershipChanged = function(msg) { - if (msg.to === -1) + if (msg.to == INVALID_PLAYER) return; this.RecalculateGatherRatesAndCapacities(); }; ResourceGatherer.prototype.OnGlobalInitGame = function(msg) { this.RecalculateGatherRatesAndCapacities(); }; Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer); Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 20953) @@ -1,166 +1,166 @@ function ResourceSupply() {} ResourceSupply.prototype.Schema = "Provides a supply of one particular type of resource." + "" + "1000" + "food.meat" + "false" + "25" + "0.8" + "" + "" + "" + "" + "" + "Infinity" + "" + "" + Resources.BuildChoicesSchema(true, true) + "" + "" + "" + "" + "" + "" + "" + "" + ""; ResourceSupply.prototype.Init = function() { // Current resource amount (non-negative) this.amount = this.GetMaxAmount(); // List of IDs for each player this.gatherers = []; let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers() for (let i = 0; i < numPlayers; ++i) this.gatherers.push([]); this.infinite = !isFinite(+this.template.Amount); let [type, subtype] = this.template.Type.split('.'); this.cachedType = { "generic": type, "specific": subtype }; }; ResourceSupply.prototype.IsInfinite = function() { return this.infinite; }; ResourceSupply.prototype.GetKillBeforeGather = function() { return (this.template.KillBeforeGather == "true"); }; ResourceSupply.prototype.GetMaxAmount = function() { return +this.template.Amount; }; ResourceSupply.prototype.GetCurrentAmount = function() { return this.amount; }; ResourceSupply.prototype.GetMaxGatherers = function() { return +this.template.MaxGatherers; }; ResourceSupply.prototype.GetNumGatherers = function() { return this.gatherers.reduce((a, b) => a + b.length, 0); }; /* The rate of each additionnal gatherer rate follow a geometric sequence, with diminishingReturns as common ratio. */ ResourceSupply.prototype.GetDiminishingReturns = function() { if ("DiminishingReturns" in this.template) { let diminishingReturns = ApplyValueModificationsToEntity("ResourceSupply/DiminishingReturns", +this.template.DiminishingReturns, this.entity); if (diminishingReturns) { let numGatherers = this.GetNumGatherers(); if (numGatherers > 1) return diminishingReturns == 1 ? 1 : (1. - Math.pow(diminishingReturns, numGatherers)) / (1. - diminishingReturns) / numGatherers; } } return null; }; ResourceSupply.prototype.TakeResources = function(rate) { // Before changing the amount, activate Fogging if necessary to hide changes let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); if (this.infinite) return { "amount": rate, "exhausted": false }; // 'rate' should be a non-negative integer var old = this.amount; this.amount = Math.max(0, old - rate); var change = old - this.amount; // Remove entities that have been exhausted if (this.amount === 0) Engine.DestroyEntity(this.entity); Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, { "from": old, "to": this.amount }); return { "amount": change, "exhausted": (this.amount === 0) }; }; ResourceSupply.prototype.GetType = function() { // All resources must have both type and subtype return this.cachedType; }; ResourceSupply.prototype.IsAvailable = function(player, gathererID) { return this.amount > 0 && (this.GetNumGatherers() < this.GetMaxGatherers() || this.gatherers[player].indexOf(gathererID) !== -1); }; ResourceSupply.prototype.AddGatherer = function(player, gathererID) { if (!this.IsAvailable(player, gathererID)) return false; if (this.gatherers[player].indexOf(gathererID) === -1) { this.gatherers[player].push(gathererID); // broadcast message, mainly useful for the AIs. Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); } return true; }; // should this return false if the gatherer didn't gather from said resource? ResourceSupply.prototype.RemoveGatherer = function(gathererID, player) { // this can happen if the unit is dead - if (player === undefined || player === -1) + if (player == undefined || player == INVALID_PLAYER) { for (var i = 0; i < this.gatherers.length; ++i) this.RemoveGatherer(gathererID, i); } else { var index = this.gatherers[player].indexOf(gathererID); if (index !== -1) { this.gatherers[player].splice(index,1); // broadcast message, mainly useful for the AIs. Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); return; } } }; Engine.RegisterComponentType(IID_ResourceSupply, "ResourceSupply", ResourceSupply); Index: ps/trunk/binaries/data/mods/public/simulation/components/StatusBars.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/StatusBars.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/StatusBars.js (revision 20953) @@ -1,272 +1,272 @@ const g_NaturalColor = "255 255 255 255"; // pure white function StatusBars() {} StatusBars.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + ""; /** * For every sprite, the code will call their "Add" method when regenerating * the sprites. Every sprite adder should return the height it needs. * * Modders who need extra sprites can just modify this array, and * provide the right methods. */ StatusBars.prototype.Sprites = [ "PackBar", "ResourceSupplyBar", "CaptureBar", "HealthBar", "AuraIcons", ]; StatusBars.prototype.Init = function() { this.enabled = false; this.auraSources = new Map(); }; /** * Don't serialise this.enabled since it's modified by the GUI. */ StatusBars.prototype.Serialize = function() { return { "auraSources": this.auraSources }; }; StatusBars.prototype.Deserialize = function(data) { this.Init(); this.auraSources = data.auraSources; }; StatusBars.prototype.SetEnabled = function(enabled) { // Quick return if no change if (enabled == this.enabled) return; this.enabled = enabled; // Update the displayed sprites this.RegenerateSprites(); }; StatusBars.prototype.AddAuraSource = function(source, auraName) { if (this.auraSources.has(source)) this.auraSources.get(source).push(auraName); else this.auraSources.set(source, [auraName]); this.RegenerateSprites(); }; StatusBars.prototype.RemoveAuraSource = function(source, auraName) { let names = this.auraSources.get(source); names.splice(names.indexOf(auraName), 1); this.RegenerateSprites(); }; StatusBars.prototype.OnHealthChanged = function(msg) { if (this.enabled) this.RegenerateSprites(); }; StatusBars.prototype.OnCapturePointsChanged = function(msg) { if (this.enabled) this.RegenerateSprites(); }; StatusBars.prototype.OnResourceSupplyChanged = function(msg) { if (this.enabled) this.RegenerateSprites(); }; StatusBars.prototype.OnPackProgressUpdate = function(msg) { if (this.enabled) this.RegenerateSprites(); }; StatusBars.prototype.RegenerateSprites = function() { let cmpOverlayRenderer = Engine.QueryInterface(this.entity, IID_OverlayRenderer); cmpOverlayRenderer.Reset(); let yoffset = 0; for (let sprite of this.Sprites) yoffset += this["Add" + sprite](cmpOverlayRenderer, yoffset); }; // Internal helper functions /** * Generic piece of code to add a bar. */ StatusBars.prototype.AddBar = function(cmpOverlayRenderer, yoffset, type, amount) { // Size of health bar (in world-space units) let width = +this.template.BarWidth; let height = +this.template.BarHeight; // World-space offset from the unit's position let offset = { "x": 0, "y": +this.template.HeightOffset, "z": 0 }; // background cmpOverlayRenderer.AddSprite( "art/textures/ui/session/icons/" + type + "_bg.png", { "x": -width / 2, "y": yoffset }, { "x": width / 2, "y": height + yoffset }, offset, g_NaturalColor ); // foreground cmpOverlayRenderer.AddSprite( "art/textures/ui/session/icons/" + type + "_fg.png", { "x": -width / 2, "y": yoffset }, { "x": width * (amount - 0.5), "y": height + yoffset }, offset, g_NaturalColor ); return height * 1.2; }; StatusBars.prototype.AddPackBar = function(cmpOverlayRenderer, yoffset) { if (!this.enabled) return 0; let cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (!cmpPack || !cmpPack.IsPacking()) return 0; return this.AddBar(cmpOverlayRenderer, yoffset, "pack", cmpPack.GetProgress()); }; StatusBars.prototype.AddHealthBar = function(cmpOverlayRenderer, yoffset) { if (!this.enabled) return 0; let cmpHealth = QueryMiragedInterface(this.entity, IID_Health); if (!cmpHealth || cmpHealth.GetHitpoints() <= 0) return 0; return this.AddBar(cmpOverlayRenderer, yoffset, "health", cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints()); }; StatusBars.prototype.AddResourceSupplyBar = function(cmpOverlayRenderer, yoffset) { if (!this.enabled) return 0; let cmpResourceSupply = QueryMiragedInterface(this.entity, IID_ResourceSupply); if (!cmpResourceSupply) return 0; let value = cmpResourceSupply.IsInfinite() ? 1 : cmpResourceSupply.GetCurrentAmount() / cmpResourceSupply.GetMaxAmount(); return this.AddBar(cmpOverlayRenderer, yoffset, "supply", value); }; StatusBars.prototype.AddCaptureBar = function(cmpOverlayRenderer, yoffset) { if (!this.enabled) return 0; let cmpCapturable = QueryMiragedInterface(this.entity, IID_Capturable); if (!cmpCapturable) return 0; let cmpOwnership = QueryMiragedInterface(this.entity, IID_Ownership); if (!cmpOwnership) return 0; let owner = cmpOwnership.GetOwner(); - if (owner == -1) + if (owner == INVALID_PLAYER) return 0; let cp = cmpCapturable.GetCapturePoints(); // Size of health bar (in world-space units) let width = +this.template.BarWidth; let height = +this.template.BarHeight; // World-space offset from the unit's position let offset = { "x": 0, "y": +this.template.HeightOffset, "z": 0 }; let setCaptureBarPart = function(playerID, startSize) { let c = QueryPlayerIDInterface(playerID).GetColor(); let strColor = (c.r * 255) + " " + (c.g * 255) + " " + (c.b * 255) + " 255"; let size = width * cp[playerID] / cmpCapturable.GetMaxCapturePoints(); cmpOverlayRenderer.AddSprite( "art/textures/ui/session/icons/capture_bar.png", { "x": startSize, "y": yoffset }, { "x": startSize + size, "y": height + yoffset }, offset, strColor ); return size + startSize; }; // First handle the owner's points, to keep those points on the left for clarity let size = setCaptureBarPart(owner, -width / 2); for (let i in cp) if (i != owner && cp[i] > 0) size = setCaptureBarPart(i, size); return height * 1.2; }; StatusBars.prototype.AddAuraIcons = function(cmpOverlayRenderer, yoffset) { let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); let sources = cmpGuiInterface.GetEntitiesWithStatusBars().filter(e => this.auraSources.has(e) && this.auraSources.get(e).length); if (!sources.length) return 0; let iconSet = new Set(); for (let ent of sources) { let cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (!cmpAuras) // probably the ent just died continue; for (let name of this.auraSources.get(ent)) iconSet.add(cmpAuras.GetOverlayIcon(name)); } // World-space offset from the unit's position let offset = { "x": 0, "y": +this.template.HeightOffset + yoffset, "z": 0 }; let iconSize = +this.template.BarWidth / 2; let xoffset = -iconSize * (iconSet.size - 1) * 0.6; for (let icon of iconSet) { cmpOverlayRenderer.AddSprite( icon, { "x": xoffset - iconSize / 2, "y": yoffset }, { "x": xoffset + iconSize / 2, "y": iconSize + yoffset }, offset, g_NaturalColor ); xoffset += iconSize * 1.2; } return iconSize + this.template.BarHeight / 2; }; Engine.RegisterComponentType(IID_StatusBars, "StatusBars", StatusBars); Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 20953) @@ -1,481 +1,481 @@ function TechnologyManager() {} TechnologyManager.prototype.Schema = ""; TechnologyManager.prototype.Serialize = function() { // The modifications cache will be affected by property reads from the GUI and other places so we shouldn't // serialize it. var ret = {}; for (var i in this) { if (this.hasOwnProperty(i)) ret[i] = this[i]; } ret.modificationCache = {}; return ret; }; TechnologyManager.prototype.Init = function() { // Holds names of technologies that have been researched. this.researchedTechs = new Set(); // Maps from technolgy name to the entityID of the researcher. this.researchQueued = new Map(); // Holds technologies which are being researched currently (non-queued). this.researchStarted = new Set(); // This stores the modifications to unit stats from researched technologies // Example data: {"ResourceGatherer/Rates/food.grain": [ // {"multiply": 1.15, "affects": ["FemaleCitizen", "Infantry Sword"]}, // {"add": 2} // ]} this.modifications = {}; this.modificationCache = {}; // Caches the values after technologies have been applied // e.g. { "Attack/Melee/Hack" : {5: {"origValue": 8, "newValue": 10}, 7: {"origValue": 9, "newValue": 12}, ...}, ...} // where 5 and 7 are entity id's this.classCounts = {}; // stores the number of entities of each Class this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e. // {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...} // Some technologies are automatically researched when their conditions are met. They have no cost and are // researched instantly. This allows civ bonuses and more complicated technologies. this.unresearchedAutoResearchTechs = new Set(); let allTechs = TechnologyTemplates.GetAll(); for (let key in allTechs) if (allTechs[key].autoResearch || allTechs[key].top) this.unresearchedAutoResearchTechs.add(key); }; TechnologyManager.prototype.OnUpdate = function() { this.UpdateAutoResearch(); }; // This function checks if the requirements of any autoresearch techs are met and if they are it researches them TechnologyManager.prototype.UpdateAutoResearch = function() { for (let key of this.unresearchedAutoResearchTechs) { let tech = TechnologyTemplates.Get(key); if ((tech.autoResearch && this.CanResearch(key)) || (tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom)))) { this.unresearchedAutoResearchTechs.delete(key); this.ResearchTechnology(key); return; // We will have recursively handled any knock-on effects so can just return } } }; // Checks an entity template to see if its technology requirements have been met TechnologyManager.prototype.CanProduce = function (templateName) { var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempManager.GetTemplate(templateName); if (template.Identity && template.Identity.RequiredTechnology) return this.IsTechnologyResearched(template.Identity.RequiredTechnology); // If there is no required technology then this entity can be produced return true; }; TechnologyManager.prototype.IsTechnologyQueued = function(tech) { return this.researchQueued.has(tech); }; TechnologyManager.prototype.IsTechnologyResearched = function(tech) { return this.researchedTechs.has(tech); }; TechnologyManager.prototype.IsTechnologyStarted = function(tech) { return this.researchStarted.has(tech); }; // Checks the requirements for a technology to see if it can be researched at the current time TechnologyManager.prototype.CanResearch = function(tech) { let template = TechnologyTemplates.Get(tech); if (!template) { warn("Technology \"" + tech + "\" does not exist"); return false; } if (template.top && this.IsInProgress(template.top) || template.bottom && this.IsInProgress(template.bottom)) return false; if (template.pair && !this.CanResearch(template.pair)) return false; if (this.IsInProgress(tech)) return false; if (this.IsTechnologyResearched(tech)) return false; return this.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, Engine.QueryInterface(this.entity, IID_Player).GetCiv())); }; /** * Private function for checking a set of requirements is met * @param {object} reqs - Technology requirements as derived from the technology template by globalscripts * @param {boolean} civonly - True if only the civ requirement is to be checked * * @return true if the requirements pass, false otherwise */ TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly = false) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!reqs) return false; if (civonly || !reqs.length) return true; return reqs.some(req => { return Object.keys(req).every(type => { switch (type) { case "techs": return req[type].every(this.IsTechnologyResearched, this); case "entities": return req[type].every(this.DoesEntitySpecPass, this); } return false; }); }); }; TechnologyManager.prototype.DoesEntitySpecPass = function(entity) { switch (entity.check) { case "count": if (!this.classCounts[entity.class] || this.classCounts[entity.class] < entity.number) return false; break; case "variants": if (!this.typeCountsByClass[entity.class] || Object.keys(this.typeCountsByClass[entity.class]).length < entity.number) return false; break; } return true; }; TechnologyManager.prototype.OnGlobalOwnershipChanged = function(msg) { // This automatically updates classCounts and typeCountsByClass var playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID(); if (msg.to == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; var classes = cmpIdentity.GetClassesList(); // don't use foundations for the class counts but check if techs apply (e.g. health increase) if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { for (let cls of classes) { this.classCounts[cls] = this.classCounts[cls] || 0; this.classCounts[cls] += 1; this.typeCountsByClass[cls] = this.typeCountsByClass[cls] || {}; this.typeCountsByClass[cls][template] = this.typeCountsByClass[cls][template] || 0; this.typeCountsByClass[cls][template] += 1; } } // Newly created entity, check if any researched techs might apply // (only do this for new entities because even if an entity is converted or captured, // we want it to maintain whatever technologies previously applied) - if (msg.from == -1) + if (msg.from == INVALID_PLAYER) { var modifiedComponents = {}; for (var name in this.modifications) { // We only need to find one one tech per component for a match var modifications = this.modifications[name]; var component = name.split("/")[0]; for (let modif of modifications) if (DoesModificationApply(modif, classes)) { if (!modifiedComponents[component]) modifiedComponents[component] = []; modifiedComponents[component].push(name); } } // Send mesage(s) to the entity so it knows about researched techs for (var component in modifiedComponents) Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": modifiedComponents[component] }); } } if (msg.from == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); // don't use foundations for the class counts if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (cmpIdentity) { var classes = cmpIdentity.GetClassesList(); for (let cls of classes) { this.classCounts[cls] -= 1; if (this.classCounts[cls] <= 0) delete this.classCounts[cls]; this.typeCountsByClass[cls][template] -= 1; if (this.typeCountsByClass[cls][template] <= 0) delete this.typeCountsByClass[cls][template]; } } } this.clearModificationCache(msg.entity); } }; // Marks a technology as researched. Note that this does not verify that the requirements are met. TechnologyManager.prototype.ResearchTechnology = function(tech) { this.StoppedResearch(tech, false); var modifiedComponents = {}; this.researchedTechs.add(tech); // store the modifications in an easy to access structure let template = TechnologyTemplates.Get(tech); if (template.modifications) { let derivedModifiers = DeriveModificationsFromTech(template); for (let modifierPath in derivedModifiers) { if (!this.modifications[modifierPath]) this.modifications[modifierPath] = []; this.modifications[modifierPath] = this.modifications[modifierPath].concat(derivedModifiers[modifierPath]); let component = modifierPath.split("/")[0]; if (!modifiedComponents[component]) modifiedComponents[component] = []; modifiedComponents[component].push(modifierPath); this.modificationCache[modifierPath] = {}; } } if (template.replaces && template.replaces.length > 0) { for (var i of template.replaces) { if (!i || this.IsTechnologyResearched(i)) continue; this.researchedTechs.add(i); // Change the EntityLimit if any let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && cmpPlayer.GetPlayerID() !== undefined) { let playerID = cmpPlayer.GetPlayerID(); let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.UpdateLimitsFromTech(i); } } } this.UpdateAutoResearch(); var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer || cmpPlayer.GetPlayerID() === undefined) return; var playerID = cmpPlayer.GetPlayerID(); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var ents = cmpRangeManager.GetEntitiesByPlayer(playerID); ents.push(this.entity); // Change the EntityLimit if any var cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.UpdateLimitsFromTech(tech); // always send research finished message Engine.PostMessage(this.entity, MT_ResearchFinished, {"player": playerID, "tech": tech}); for (var component in modifiedComponents) { Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { "player": playerID, "component": component, "valueNames": modifiedComponents[component]}); Engine.BroadcastMessage(MT_ValueModification, { "entities": ents, "component": component, "valueNames": modifiedComponents[component]}); } if (tech.startsWith("phase") && !template.autoResearch) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "phase", "players": [playerID], "phaseName": tech, "phaseState": "completed" }); } }; // Clears the cached data for an entity from the modifications cache TechnologyManager.prototype.clearModificationCache = function(ent) { for (var valueName in this.modificationCache) delete this.modificationCache[valueName][ent]; }; // Caching layer in front of ApplyModificationsWorker // Note: be careful with the type of curValue, if it should be a numerical // value and is derived from template data, you must convert the string // from the template to a number using the + operator, before calling // this function! TechnologyManager.prototype.ApplyModifications = function(valueName, curValue, ent) { if (!this.modificationCache[valueName]) this.modificationCache[valueName] = {}; if (!this.modificationCache[valueName][ent] || this.modificationCache[valueName][ent].origValue != curValue) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity) return curValue; this.modificationCache[valueName][ent] = { "origValue": curValue, "newValue": GetTechModifiedProperty(this.modifications, cmpIdentity.GetClassesList(), valueName, curValue) }; } return this.modificationCache[valueName][ent].newValue; }; // Alternative version of ApplyModifications, applies to templates instead of entities TechnologyManager.prototype.ApplyModificationsTemplate = function(valueName, curValue, template) { if (!template || !template.Identity) return curValue; return GetTechModifiedProperty(this.modifications, GetIdentityClasses(template.Identity), valueName, curValue); }; /** * Marks a technology as being queued for research at the given entityID. */ TechnologyManager.prototype.QueuedResearch = function(tech, researcher) { this.researchQueued.set(tech, researcher); }; // Marks a technology as actively being researched TechnologyManager.prototype.StartedResearch = function(tech, notification) { this.researchStarted.add(tech); if (notification && tech.startsWith("phase")) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "started" }); } }; /** * Marks a technology as not being currently researched and optionally sends a GUI notification. */ TechnologyManager.prototype.StoppedResearch = function(tech, notification) { if (notification && tech.startsWith("phase") && this.researchStarted.has(tech)) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "aborted" }); } this.researchQueued.delete(tech); this.researchStarted.delete(tech); }; /** * Checks whether a technology is set to be researched. */ TechnologyManager.prototype.IsInProgress = function(tech) { return this.researchQueued.has(tech); }; /** * Returns the names of technologies that are currently being researched (non-queued). */ TechnologyManager.prototype.GetStartedTechs = function() { return this.researchStarted; }; /** * Gets the entity currently researching the technology. */ TechnologyManager.prototype.GetResearcher = function(tech) { return this.researchQueued.get(tech) }; /** * Called by GUIInterface for PlayerData. AI use. */ TechnologyManager.prototype.GetQueuedResearch = function() { return this.researchQueued; }; /** * Returns the names of technologies that have already been researched. */ TechnologyManager.prototype.GetResearchedTechs = function() { return this.researchedTechs; }; TechnologyManager.prototype.GetClassCounts = function() { return this.classCounts; }; TechnologyManager.prototype.GetTypeCountsByClass = function() { return this.typeCountsByClass; }; Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/TerritoryDecay.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TerritoryDecay.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/TerritoryDecay.js (revision 20953) @@ -1,156 +1,156 @@ function TerritoryDecay() {} TerritoryDecay.prototype.Schema = "" + "Infinity" + ""; TerritoryDecay.prototype.Init = function() { this.decaying = false; this.connectedNeighbours = new Array(Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers()).fill(0); this.territoryOwnership = !isFinite(+this.template.DecayRate); }; TerritoryDecay.prototype.IsConnected = function() { this.connectedNeighbours.fill(0); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return false; var cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return true;// something without ownership can't decay var cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); var pos = cmpPosition.GetPosition2D(); var tileOwner = cmpTerritoryManager.GetOwner(pos.x, pos.y); if (tileOwner == 0) { this.connectedNeighbours[0] = 1; return cmpPlayer.GetPlayerID() == 0; // Gaia building on gaia ground -> don't decay } var tileConnected = cmpTerritoryManager.IsConnected(pos.x, pos.y); if (tileConnected && !cmpPlayer.IsMutualAlly(tileOwner)) { this.connectedNeighbours[tileOwner] = 1; return false; } if (tileConnected) return true; if (cmpPlayer.GetPlayerID() != tileOwner) { this.connectedNeighbours[0] = 1; return false; } this.connectedNeighbours = cmpTerritoryManager.GetNeighbours(pos.x, pos.y, true); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (var i = 1; i < numPlayers; ++i) if (this.connectedNeighbours[i] > 0 && cmpPlayer.IsMutualAlly(i)) { // don't decay if connected to a connected ally; disable blinking cmpTerritoryManager.SetTerritoryBlinking(pos.x, pos.y, false); return true; } cmpTerritoryManager.SetTerritoryBlinking(pos.x, pos.y, true); return false; }; TerritoryDecay.prototype.IsDecaying = function() { return this.decaying; }; TerritoryDecay.prototype.GetDecayRate = function() { return ApplyValueModificationsToEntity( "TerritoryDecay/DecayRate", +this.template.DecayRate, this.entity); }; /** * Get the number of connected bordering tiles to this region * Only valid when this.IsDecaying() */ TerritoryDecay.prototype.GetConnectedNeighbours = function() { return this.connectedNeighbours; }; TerritoryDecay.prototype.UpdateDecayState = function() { let decaying = !this.IsConnected() && this.GetDecayRate() > 0; if (decaying === this.decaying) return; this.decaying = decaying; Engine.PostMessage(this.entity, MT_TerritoryDecayChanged, { "entity": this.entity, "to": decaying, "rate": this.GetDecayRate() }); }; TerritoryDecay.prototype.UpdateOwner = function() { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpOwnership || !cmpPosition || !cmpPosition.IsInWorld()) return; let cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); let pos = cmpPosition.GetPosition2D(); let tileOwner = cmpTerritoryManager.GetOwner(pos.x, pos.y); if (tileOwner != cmpOwnership.GetOwner()) cmpOwnership.SetOwner(tileOwner); }; TerritoryDecay.prototype.OnTerritoriesChanged = function(msg) { if (this.territoryOwnership) this.UpdateOwner(); else this.UpdateDecayState(); }; TerritoryDecay.prototype.OnTerritoryPositionChanged = function(msg) { if (this.territoryOwnership) this.UpdateOwner(); else this.UpdateDecayState(); }; TerritoryDecay.prototype.OnDiplomacyChanged = function(msg) { // Can change the connectedness of certain areas if (!this.territoryOwnership) this.UpdateDecayState(); }; TerritoryDecay.prototype.OnOwnershipChanged = function(msg) { // Update the list of TerritoryDecay components in the manager - if (msg.from == -1 || msg.to == -1) + if (msg.from == INVALID_PLAYER || msg.to == INVALID_PLAYER) { let cmpTerritoryDecayManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryDecayManager); - if (msg.from == -1) + if (msg.from == INVALID_PLAYER) cmpTerritoryDecayManager.Add(this.entity); else cmpTerritoryDecayManager.Remove(this.entity); } // if it influences the territory, wait until we get a TerritoriesChanged message if (!this.territoryOwnership && !Engine.QueryInterface(this.entity, IID_TerritoryInfluence)) this.UpdateDecayState(); }; TerritoryDecay.prototype.HasTerritoryOwnership = function() { return this.territoryOwnership; }; Engine.RegisterComponentType(IID_TerritoryDecay, "TerritoryDecay", TerritoryDecay); Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 20953) @@ -1,6084 +1,6084 @@ function UnitAI() {} UnitAI.prototype.Schema = "Controls the unit's movement, attacks, etc, in response to commands from the player." + "" + "" + "" + "" + "" + "" + "violent" + "aggressive" + "defensive" + "passive" + "standground" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "violent" + "aggressive" + "defensive" + "passive" + "skittish" + "domestic" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""+ "" + ""; // Unit stances. // There some targeting options: // targetVisibleEnemies: anything in vision range is a viable target // targetAttackersAlways: anything that hurts us is a viable target, // possibly overriding user orders! // There are some response options, triggered when targets are detected: // respondFlee: run away // respondChase: start chasing after the enemy // respondChaseBeyondVision: start chasing, and don't stop even if it's out // of this unit's vision range (though still visible to the player) // respondStandGround: attack enemy but don't move at all // respondHoldGround: attack enemy but don't move far from current position // TODO: maybe add targetAggressiveEnemies (don't worry about lone scouts, // do worry around armies slaughtering the guy standing next to you), etc. var g_Stances = { "violent": { targetVisibleEnemies: true, targetAttackersAlways: true, respondFlee: false, respondChase: true, respondChaseBeyondVision: true, respondStandGround: false, respondHoldGround: false, }, "aggressive": { targetVisibleEnemies: true, targetAttackersAlways: false, respondFlee: false, respondChase: true, respondChaseBeyondVision: false, respondStandGround: false, respondHoldGround: false, }, "defensive": { targetVisibleEnemies: true, targetAttackersAlways: false, respondFlee: false, respondChase: false, respondChaseBeyondVision: false, respondStandGround: false, respondHoldGround: true, }, "passive": { targetVisibleEnemies: false, targetAttackersAlways: false, respondFlee: true, respondChase: false, respondChaseBeyondVision: false, respondStandGround: false, respondHoldGround: false, }, "standground": { targetVisibleEnemies: true, targetAttackersAlways: false, respondFlee: false, respondChase: false, respondChaseBeyondVision: false, respondStandGround: true, respondHoldGround: false, }, }; // See ../helpers/FSM.js for some documentation of this FSM specification syntax UnitAI.prototype.UnitFsmSpec = { // Default event handlers: "MoveCompleted": function() { // ignore spurious movement messages // (these can happen when stopping moving at the same time // as switching states) }, "MoveStarted": function() { // ignore spurious movement messages }, "ConstructionFinished": function(msg) { // ignore uninteresting construction messages }, "LosRangeUpdate": function(msg) { // ignore newly-seen units by default }, "LosHealRangeUpdate": function(msg) { // ignore newly-seen injured units by default }, "Attacked": function(msg) { // ignore attacker }, "HealthChanged": function(msg) { // ignore }, "PackFinished": function(msg) { // ignore }, "PickupCanceled": function(msg) { // ignore }, "TradingCanceled": function(msg) { // ignore }, "GuardedAttacked": function(msg) { // ignore }, // Formation handlers: "FormationLeave": function(msg) { // ignore when we're not in FORMATIONMEMBER }, // Called when being told to walk as part of a formation "Order.FormationWalk": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) { this.FinishOrder(); return; } // For packable units: // 1. If packed, we can move. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpUnitMotion.MoveToFormationOffset(msg.data.target, msg.data.x, msg.data.z); this.SetNextStateAlwaysEntering("FORMATIONMEMBER.WALKING"); }, // Special orders: // (these will be overridden by various states) "Order.LeaveFoundation": function(msg) { // If foundation is not ally of entity, or if entity is unpacked siege, // ignore the order if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) && !Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() || this.IsPacking() || this.CanPack() || this.IsTurret()) { this.FinishOrder(); return; } // Move a tile outside the building let range = 4; if (this.MoveToTargetRangeExplicit(msg.data.target, range, range)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.WALKING"); } else { // We are already at the target, or can't move at all this.FinishOrder(); } }, // Individual orders: // (these will switch the unit out of formation mode) "Order.Stop": function(msg) { // We have no control over non-domestic animals. if (this.IsAnimal() && !this.IsDomestic()) { this.FinishOrder(); return; } // Stop moving immediately. this.StopMoving(); this.FinishOrder(); // No orders left, we're an individual now if (this.IsAnimal()) this.SetNextState("ANIMAL.IDLE"); else this.SetNextState("INDIVIDUAL.IDLE"); }, "Order.Walk": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) { this.FinishOrder(); return; } // For packable units: // 1. If packed, we can move. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } this.SetHeldPosition(this.order.data.x, this.order.data.z); if (!this.order.data.max) this.MoveToPoint(this.order.data.x, this.order.data.z); else this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max); if (this.IsAnimal()) this.SetNextState("ANIMAL.WALKING"); else this.SetNextState("INDIVIDUAL.WALKING"); }, "Order.WalkAndFight": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) { this.FinishOrder(); return; } // For packable units: // 1. If packed, we can move. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } this.SetHeldPosition(this.order.data.x, this.order.data.z); this.MoveToPoint(this.order.data.x, this.order.data.z); if (this.IsAnimal()) this.SetNextState("ANIMAL.WALKING"); // WalkAndFight not applicable for animals else this.SetNextState("INDIVIDUAL.WALKINGANDFIGHTING"); }, "Order.WalkToTarget": function(msg) { // Let players move captured domestic animals around if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) { this.FinishOrder(); return; } // For packable units: // 1. If packed, we can move. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } var ok = this.MoveToTarget(this.order.data.target); if (ok) { // We've started walking to the given point if (this.IsAnimal()) this.SetNextState("ANIMAL.WALKING"); else this.SetNextState("INDIVIDUAL.WALKING"); } else { // We are already at the target, or can't move at all this.StopMoving(); this.FinishOrder(); } }, "Order.PickupUnit": function(msg) { var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) { this.FinishOrder(); return; } // Check if we need to move TODO implement a better way to know if we are on the shoreline var needToMove = true; var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (this.lastShorelinePosition && cmpPosition && (this.lastShorelinePosition.x == cmpPosition.GetPosition().x) && (this.lastShorelinePosition.z == cmpPosition.GetPosition().z)) { // we were already on the shoreline, and have not moved since if (DistanceBetweenEntities(this.entity, this.order.data.target) < 50) needToMove = false; } // TODO: what if the units are on a cliff ? the ship will go below the cliff // and the units won't be able to garrison. Should go to the nearest (accessible) shore if (needToMove && this.MoveToTarget(this.order.data.target)) { this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING"); } else { // We are already at the target, or can't move at all this.StopMoving(); this.SetNextState("INDIVIDUAL.PICKUP.LOADING"); } }, "Order.Guard": function(msg) { if (!this.AddGuard(this.order.data.target)) { this.FinishOrder(); return; } if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("INDIVIDUAL.GUARD.ESCORTING"); else this.SetNextState("INDIVIDUAL.GUARD.GUARDING"); }, "Order.Flee": function(msg) { // We use the distance between the entities to account for ranged attacks var distance = DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion.MoveToTargetRange(this.order.data.target, distance, -1)) { // We've started fleeing from the given target if (this.IsAnimal()) this.SetNextState("ANIMAL.FLEEING"); else this.SetNextState("INDIVIDUAL.FLEEING"); } else { // We are already at the target, or can't move at all this.StopMoving(); this.FinishOrder(); } }, "Order.Attack": function(msg) { // Check the target is alive if (!this.TargetIsAlive(this.order.data.target)) { this.FinishOrder(); return; } // Work out how to attack the given target var type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture); if (!type) { // Oops, we can't attack at all this.FinishOrder(); return; } this.order.data.attackType = type; // If we are already at the target, try attacking it from here if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) { this.StopMoving(); // For packable units within attack range: // 1. If unpacked, we can attack the target. // 2. If packed, we first need to unpack, then follow case 1. if (this.CanUnpack()) { // Ignore unforced attacks // TODO: use special stances instead? if (!this.order.data.force) { this.FinishOrder(); return; } // Case 2: unpack this.PushOrderFront("Unpack", { "force": true }); return; } if (this.order.data.attackType == this.oldAttackType) { if (this.IsAnimal()) this.SetNextState("ANIMAL.COMBAT.ATTACKING"); else this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING"); } else { if (this.IsAnimal()) this.SetNextStateAlwaysEntering("ANIMAL.COMBAT.ATTACKING"); else this.SetNextStateAlwaysEntering("INDIVIDUAL.COMBAT.ATTACKING"); } return; } // For packable units out of attack range: // 1. If packed, we need to move to attack range and then unpack. // 2. If unpacked, we first need to pack, then follow case 1. var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack) { // Ignore unforced attacks // TODO: use special stances instead? if (!this.order.data.force) { this.FinishOrder(); return; } if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } } // If we can't reach the target, but are standing ground, then abandon this attack order. // Unless we're hunting, that's a special case where we should continue attacking our target. if (this.GetStance().respondStandGround && !this.order.data.force && !this.order.data.hunting || this.IsTurret()) { this.FinishOrder(); return; } // Try to move within attack range if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType)) { // We've started walking to the given point if (this.IsAnimal()) this.SetNextState("ANIMAL.COMBAT.APPROACHING"); else this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING"); return; } // We can't reach the target, and can't move towards it, // so abandon this attack order this.FinishOrder(); }, "Order.Patrol": function(msg) { if (this.IsAnimal() || this.IsTurret()) { this.FinishOrder(); return; } if (this.CanPack()) { this.PushOrderFront("Pack", { "force": true }); return; } this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("INDIVIDUAL.PATROL"); }, "Order.Heal": function(msg) { // Check the target is alive if (!this.TargetIsAlive(this.order.data.target)) { this.FinishOrder(); return; } // Healers can't heal themselves. if (this.order.data.target == this.entity) { this.FinishOrder(); return; } // Check if the target is in range if (this.CheckTargetRange(this.order.data.target, IID_Heal)) { this.StopMoving(); this.SetNextState("INDIVIDUAL.HEAL.HEALING"); return; } // If we can't reach the target, but are standing ground, // then abandon this heal order if (this.GetStance().respondStandGround && !this.order.data.force) { this.FinishOrder(); return; } // Try to move within heal range if (this.MoveToTargetRange(this.order.data.target, IID_Heal)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.HEAL.APPROACHING"); return; } // We can't reach the target, and can't move towards it, // so abandon this heal order this.FinishOrder(); }, "Order.Gather": function(msg) { // If the target is still alive, we need to kill it first if (this.MustKillGatherTarget(this.order.data.target)) { // Make sure we can attack the target, else we'll get very stuck if (!this.GetBestAttackAgainst(this.order.data.target, false)) { // Oops, we can't attack at all - give up // TODO: should do something so the player knows why this failed this.FinishOrder(); return; } // The target was visible when this order was issued, // but could now be invisible again. if (!this.CheckTargetVisible(this.order.data.target)) { if (this.order.data.secondTry === undefined) { this.order.data.secondTry = true; this.PushOrderFront("Walk", this.order.data.lastPos); } else { // We couldn't move there, or the target moved away this.FinishOrder(); } return; } this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false, "hunting": true, "allowCapture": false }); return; } // Try to move within range if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.GATHER.APPROACHING"); } else { // We are already at the target, or can't move at all, // so try gathering it from here. // TODO: need better handling of the can't-reach-target case this.StopMoving(); this.SetNextStateAlwaysEntering("INDIVIDUAL.GATHER.GATHERING"); } }, "Order.GatherNearPosition": function(msg) { // Move the unit to the position to gather from. this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("INDIVIDUAL.GATHER.WALKING"); }, "Order.ReturnResource": function(msg) { // Check if the dropsite is already in range if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true)) { var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite); if (cmpResourceDropsite) { // Dump any resources we can var dropsiteTypes = cmpResourceDropsite.GetTypes(); Engine.QueryInterface(this.entity, IID_ResourceGatherer).CommitResources(dropsiteTypes); // Stop showing the carried resource animation. this.SetDefaultAnimationVariant(); // Our next order should always be a Gather, // so just switch back to that order this.FinishOrder(); return; } } // Try to move to the dropsite if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer)) { // We've started walking to the target this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING"); return; } // Oops, we can't reach the dropsite. // Maybe we should try to pick another dropsite, to find an // accessible one? // For now, just give up. this.StopMoving(); this.FinishOrder(); return; }, "Order.Trade": function(msg) { // We must check if this trader has both markets in case it was a back-to-work order var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (!cmpTrader || !cmpTrader.HasBothMarkets()) { this.FinishOrder(); return; } // TODO find the nearest way-point from our position, and start with it this.waypoints = undefined; if (this.MoveToMarket(this.order.data.target)) // We've started walking to the next market this.SetNextState("TRADE.APPROACHINGMARKET"); else this.FinishOrder(); }, "Order.Repair": function(msg) { // Try to move within range if (this.MoveToTargetRange(this.order.data.target, IID_Builder)) { // We've started walking to the given point this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING"); } else { // We are already at the target, or can't move at all, // so try repairing it from here. // TODO: need better handling of the can't-reach-target case this.StopMoving(); this.SetNextStateAlwaysEntering("INDIVIDUAL.REPAIR.REPAIRING"); } }, "Order.Garrison": function(msg) { if (this.IsTurret()) { this.SetNextState("IDLE"); return; } else if (this.IsGarrisoned()) { this.SetNextState("INDIVIDUAL.AUTOGARRISON"); return; } // For packable units: // 1. If packed, we can move to the garrison target. // 2. If unpacked, we first need to pack, then follow case 1. if (this.CanPack()) { // Case 2: pack this.PushOrderFront("Pack", { "force": true }); return; } if (this.MoveToGarrisonRange(this.order.data.target)) { this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING"); } else { // We do a range check before actually garrisoning this.StopMoving(); this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED"); } }, "Order.Autogarrison": function(msg) { if (this.IsTurret()) { this.SetNextState("IDLE"); return; } this.SetNextState("INDIVIDUAL.AUTOGARRISON"); }, "Order.Ungarrison": function() { this.FinishOrder(); this.isGarrisoned = false; }, "Order.Alert": function(msg) { this.alertRaiser = this.order.data.raiser; // Find a target to garrison into, if we don't already have one if (!this.alertGarrisoningTarget) this.alertGarrisoningTarget = this.FindNearbyGarrisonHolder(); if (this.alertGarrisoningTarget) this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget}); else { this.StopMoving(); this.FinishOrder(); } }, "Order.Cheering": function(msg) { this.SetNextState("INDIVIDUAL.CHEERING"); }, "Order.Pack": function(msg) { if (this.CanPack()) { this.StopMoving(); this.SetNextState("INDIVIDUAL.PACKING"); } }, "Order.Unpack": function(msg) { if (this.CanUnpack()) { this.StopMoving(); this.SetNextState("INDIVIDUAL.UNPACKING"); } }, "Order.CancelPack": function(msg) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked()) cmpPack.CancelPack(); this.FinishOrder(); }, "Order.CancelUnpack": function(msg) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked()) cmpPack.CancelPack(); this.FinishOrder(); }, // States for the special entity representing a group of units moving in formation: "FORMATIONCONTROLLER": { "Order.Walk": function(msg) { this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("WALKING"); }, "Order.WalkAndFight": function(msg) { this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("WALKINGANDFIGHTING"); }, "Order.MoveIntoFormation": function(msg) { this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("FORMING"); }, // Only used by other orders to walk there in formation "Order.WalkToTargetRange": function(msg) { if (this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max)) this.SetNextState("WALKING"); else this.FinishOrder(); }, "Order.WalkToTarget": function(msg) { if (this.MoveToTarget(this.order.data.target)) this.SetNextState("WALKING"); else this.FinishOrder(); }, "Order.WalkToPointRange": function(msg) { if (this.MoveToPointRange(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max)) this.SetNextState("WALKING"); else this.FinishOrder(); }, "Order.Patrol": function(msg) { this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("PATROL"); }, "Order.Guard": function(msg) { this.CallMemberFunction("Guard", [msg.data.target, false]); var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.Disband(); }, "Order.Stop": function(msg) { if (!this.IsAttackingAsFormation()) this.CallMemberFunction("Stop", [false]); this.StopMoving(); this.FinishOrder(); }, "Order.Attack": function(msg) { var target = msg.data.target; var allowCapture = msg.data.allowCapture; var cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember()) target = cmpTargetUnitAI.GetFormationController(); var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); // Check if we are already in range, otherwise walk there if (!this.CheckTargetAttackRange(target, target)) { if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) { if (this.MoveToTargetAttackRange(target, target)) { this.SetNextState("COMBAT.APPROACHING"); return; } } this.FinishOrder(); return; } this.CallMemberFunction("Attack", [target, allowCapture, false]); if (cmpAttack.CanAttackAsFormation()) this.SetNextState("COMBAT.ATTACKING"); else this.SetNextState("MEMBER"); }, "Order.Garrison": function(msg) { if (!Engine.QueryInterface(msg.data.target, IID_GarrisonHolder)) { this.FinishOrder(); return; } // Check if we are already in range, otherwise walk there if (!this.CheckGarrisonRange(msg.data.target)) { if (!this.CheckTargetVisible(msg.data.target)) { this.FinishOrder(); return; } else { // Out of range; move there in formation if (this.MoveToGarrisonRange(msg.data.target)) { this.SetNextState("GARRISON.APPROACHING"); return; } } } this.SetNextState("GARRISON.GARRISONING"); }, "Order.Gather": function(msg) { if (this.MustKillGatherTarget(msg.data.target)) { // The target was visible when this order was given, // but could now be invisible. if (!this.CheckTargetVisible(msg.data.target)) { if (msg.data.secondTry === undefined) { msg.data.secondTry = true; this.PushOrderFront("Walk", msg.data.lastPos); } else { // We couldn't move there, or the target moved away this.FinishOrder(); } return; } this.PushOrderFront("Attack", { "target": msg.data.target, "hunting": true, "allowCapture": false }); return; } // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The target isn't gatherable or not visible any more. this.FinishOrder(); // TODO: Should we issue a gather-near-position order // if the target isn't gatherable/doesn't exist anymore? else // Out of range; move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return; } this.CallMemberFunction("Gather", [msg.data.target, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.GatherNearPosition": function(msg) { // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20)) { // Out of range; move there in formation this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 }); return; } this.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Heal": function(msg) { // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The target was destroyed this.FinishOrder(); else // Out of range; move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return; } this.CallMemberFunction("Heal", [msg.data.target, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Repair": function(msg) { // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The building was finished or destroyed this.FinishOrder(); else // Out of range move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return; } this.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.ReturnResource": function(msg) { // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target)) // The target was destroyed this.FinishOrder(); else // Out of range; move there in formation this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); return; } this.CallMemberFunction("ReturnResource", [msg.data.target, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Pack": function(msg) { this.CallMemberFunction("Pack", [false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Unpack": function(msg) { this.CallMemberFunction("Unpack", [false]); this.SetNextStateAlwaysEntering("MEMBER"); }, "IDLE": { "enter": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(false); }, "MoveStarted": function() { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); } }, "WALKING": { "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { if (this.FinishOrder()) this.CallMemberFunction("ResetFinishOrder", []); }, }, "WALKINGANDFIGHTING": { "enter": function(msg) { this.StartTimer(0, 1000); }, "Timer": function(msg) { // check if there are no enemies to attack this.FindWalkAndFightTargets(); }, "leave": function(msg) { this.StopTimer(); }, "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { if (this.FinishOrder()) this.CallMemberFunction("ResetFinishOrder", []); }, }, "PATROL": { "enter": function(msg) { // Memorize the origin position in case that we want to go back let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) { this.FinishOrder(); return; } if (!this.patrolStartPosOrder) { this.patrolStartPosOrder = cmpPosition.GetPosition(); this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses; this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture; } this.StartTimer(0, 1000); }, "Timer": function(msg) { // Check if there are no enemies to attack this.FindWalkAndFightTargets(); }, "leave": function(msg) { this.StopTimer(); delete this.patrolStartPosOrder; }, "MoveStarted": function(msg) { let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function() { /** * A-B-A-B-..: * if the user only commands one patrol order, the patrol will be between * the last position and the defined waypoint * A-B-C-..-A-B-..: * otherwise, the patrol is only between the given patrol commands and the * last position is not included (last position = the position where the unit * is located at the time of the first patrol order) */ if (this.orderQueue.length == 1) this.PushOrder("Patrol", this.patrolStartPosOrder); this.PushOrder(this.order.type, this.order.data); this.FinishOrder(); }, }, "GARRISON":{ "enter": function() { // If the garrisonholder should pickup, warn it so it can take needed action var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder); if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity)) { this.pickup = this.order.data.target; // temporary, deleted in "leave" Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity }); } }, "leave": function() { // If a pickup has been requested and not yet canceled, cancel it if (this.pickup) { Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } }, "APPROACHING": { "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { this.SetNextState("GARRISONING"); }, }, "GARRISONING": { "enter": function() { // If a pickup has been requested, cancel it as it will be requested by members if (this.pickup) { Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } this.CallMemberFunction("Garrison", [this.order.data.target, false]); this.SetNextStateAlwaysEntering("MEMBER"); }, }, }, "FORMING": { "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, false); }, "MoveCompleted": function(msg) { if (this.FinishOrder()) { this.CallMemberFunction("ResetFinishOrder", []); return; } var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.FindInPosition(); } }, "COMBAT": { "APPROACHING": { "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); this.CallMemberFunction("Attack", [this.order.data.target, this.order.data.allowCapture, false]); if (cmpAttack.CanAttackAsFormation()) this.SetNextState("COMBAT.ATTACKING"); else this.SetNextState("MEMBER"); }, }, "ATTACKING": { // Wait for individual members to finish "enter": function(msg) { var target = this.order.data.target; var allowCapture = this.order.data.allowCapture; // Check if we are already in range, otherwise walk there if (!this.CheckTargetAttackRange(target, target)) { if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) { this.FinishOrder(); this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture }); return true; } this.FinishOrder(); return true; } var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); // TODO fix the rearranging while attacking as formation cmpFormation.SetRearrange(!this.IsAttackingAsFormation()); cmpFormation.MoveMembersIntoFormation(false, false); this.StartTimer(200, 200); return false; }, "Timer": function(msg) { var target = this.order.data.target; var allowCapture = this.order.data.allowCapture; // Check if we are already in range, otherwise walk there if (!this.CheckTargetAttackRange(target, target)) { if (this.TargetIsAlive(target) && this.CheckTargetVisible(target)) { this.FinishOrder(); this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture }); return; } this.FinishOrder(); return; } }, "leave": function(msg) { this.StopTimer(); var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (cmpFormation) cmpFormation.SetRearrange(true); }, }, }, "MEMBER": { // Wait for individual members to finish "enter": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(false); this.StartTimer(1000, 1000); }, "Timer": function(msg) { // Have all members finished the task? if (!this.TestAllMemberFunction("HasFinishedOrder", [])) return; this.CallMemberFunction("ResetFinishOrder", []); // Execute the next order if (this.FinishOrder()) { // if WalkAndFight order, look for new target before moving again if (this.IsWalkingAndFighting()) this.FindWalkAndFightTargets(); return; } }, "leave": function(msg) { this.StopTimer(); }, }, }, // States for entities moving as part of a formation: "FORMATIONMEMBER": { "FormationLeave": function(msg) { // We're not in a formation anymore, so no need to track this. this.finishedOrder = false; // Stop moving as soon as the formation disbands this.StopMoving(); // If the controller handled an order but some members rejected it, // they will have no orders and be in the FORMATIONMEMBER.IDLE state. if (this.orderQueue.length) { // We're leaving the formation, so stop our FormationWalk order if (this.FinishOrder()) return; } // No orders left, we're an individual now if (this.IsAnimal()) this.SetNextState("ANIMAL.IDLE"); else this.SetNextState("INDIVIDUAL.IDLE"); }, // Override the LeaveFoundation order since we're not doing // anything more important (and we might be stuck in the WALKING // state forever and need to get out of foundations in that case) "Order.LeaveFoundation": function(msg) { // If foundation is not ally of entity, or if entity is unpacked siege, // ignore the order if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) && !Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() || this.IsPacking() || this.CanPack() || this.IsTurret()) { this.FinishOrder(); return; } // Move a tile outside the building let range = 4; if (this.MoveToTargetRangeExplicit(msg.data.target, range, range)) { // We've started walking to the given point this.SetNextState("WALKINGTOPOINT"); } else { // We are already at the target, or can't move at all this.FinishOrder(); } }, "IDLE": { "enter": function() { if (this.IsAnimal()) this.SetNextState("ANIMAL.IDLE"); else this.SetNextState("INDIVIDUAL.IDLE"); return true; }, }, "WALKING": { "enter": function () { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpFormation && cmpVisual) { cmpVisual.ReplaceMoveAnimation("walk", cmpFormation.GetFormationAnimation(this.entity, "walk")); cmpVisual.ReplaceMoveAnimation("run", cmpFormation.GetFormationAnimation(this.entity, "run")); } this.SelectAnimation("move"); }, // Occurs when the unit has reached its destination and the controller // is done moving. The controller is notified. "MoveCompleted": function(msg) { // We can only finish this order if the move was really completed. if (!msg.data.error && this.FinishOrder()) return; var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) { cmpVisual.ResetMoveAnimation("walk"); cmpVisual.ResetMoveAnimation("run"); } var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.SetInPosition(this.entity); }, }, // Special case used by Order.LeaveFoundation "WALKINGTOPOINT": { "enter": function() { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.UnsetInPosition(this.entity); this.SelectAnimation("move"); }, "MoveCompleted": function() { this.FinishOrder(); }, }, }, // States for entities not part of a formation: "INDIVIDUAL": { "enter": function() { // Sanity-checking if (this.IsAnimal()) error("Animal got moved into INDIVIDUAL.* state"); }, "Attacked": function(msg) { // Respond to attack if we always target attackers or during unforced orders if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force) this.RespondToTargetedEntities([msg.data.attacker]); }, "GuardedAttacked": function(msg) { // do nothing if we have a forced order in queue before the guard order for (var i = 0; i < this.orderQueue.length; ++i) { if (this.orderQueue[i].type == "Guard") break; if (this.orderQueue[i].data && this.orderQueue[i].data.force) return; } // if we already are targeting another unit still alive, finish with it first if (this.order && (this.order.type == "WalkAndFight" || this.order.type == "Attack")) if (this.order.data.target != msg.data.attacker && this.TargetIsAlive(msg.data.attacker)) return; var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health); if (cmpIdentity && cmpIdentity.HasClass("Support") && cmpHealth && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()) { if (this.CanHeal(this.isGuardOf)) this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false }); else if (this.CanRepair(this.isGuardOf)) this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false }); return; } // if the attacker is a building and we can repair the guarded, repair it rather than attacking var cmpBuildingAI = Engine.QueryInterface(msg.data.attacker, IID_BuildingAI); if (cmpBuildingAI && this.CanRepair(this.isGuardOf)) { this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false }); return; } // target the unit if (this.CheckTargetVisible(msg.data.attacker)) this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "allowCapture": true }); else { var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); this.PushOrderFront("WalkAndFight", { "x": pos.x, "z": pos.z, "target": msg.data.attacker, "force": false }); // if we already had a WalkAndFight, keep only the most recent one in case the target has moved if (this.orderQueue[1] && this.orderQueue[1].type == "WalkAndFight") { this.orderQueue.splice(1, 1); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); } } }, "IDLE": { "enter": function() { // Switch back to idle animation to guarantee we won't // get stuck with an incorrect animation var animationName = "idle"; if (this.IsFormationMember()) { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) animationName = cmpFormation.GetFormationAnimation(this.entity, animationName); } this.SelectAnimation(animationName); // If we have some orders, it is because we are in an intermediary state // from FinishOrder (SetNextState("IDLE") is only executed when we get // a ProcessMessage), and thus we should not start another order which could // put us in a weird state if (this.orderQueue.length > 0 && !this.IsGarrisoned()) return false; // If the unit is guarding/escorting, go back to its duty if (this.isGuardOf) { this.Guard(this.isGuardOf, false); return true; } // The GUI and AI want to know when a unit is idle, but we don't // want to send frequent spurious messages if the unit's only // idle for an instant and will quickly go off and do something else. // So we'll set a timer here and only report the idle event if we // remain idle this.StartTimer(1000); // If a unit can heal and attack we first want to heal wounded units, // so check if we are a healer and find whether there's anybody nearby to heal. // (If anyone approaches later it'll be handled via LosHealRangeUpdate.) // If anyone in sight gets hurt that will be handled via LosHealRangeUpdate. if (this.IsHealer() && this.FindNewHealTargets()) return true; // (abort the FSM transition since we may have already switched state) // If we entered the idle state we must have nothing better to do, // so immediately check whether there's anybody nearby to attack. // (If anyone approaches later, it'll be handled via LosRangeUpdate.) if (this.FindNewTargets()) return true; // (abort the FSM transition since we may have already switched state) // Nobody to attack - stay in idle return false; }, "leave": function() { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) cmpRangeManager.DisableActiveQuery(this.losRangeQuery); if (this.losHealRangeQuery) cmpRangeManager.DisableActiveQuery(this.losHealRangeQuery); this.StopTimer(); if (this.isIdle) { this.isIdle = false; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); } }, "LosRangeUpdate": function(msg) { if (this.GetStance().targetVisibleEnemies) { // Start attacking one of the newly-seen enemy (if any) this.AttackEntitiesByPreference(msg.data.added); } }, "LosHealRangeUpdate": function(msg) { this.RespondToHealableEntities(msg.data.added); }, "MoveStarted": function() { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.SelectAnimation("idle"); }, "Timer": function(msg) { if (!this.isIdle) { this.isIdle = true; Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); } }, }, "WALKING": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.FinishOrder(); }, }, "WALKINGANDFIGHTING": { "enter": function () { // Show weapons rather than carried resources. this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); this.SelectAnimation("move"); }, "Timer": function(msg) { this.FindWalkAndFightTargets(); }, "leave": function(msg) { this.StopTimer(); }, "MoveCompleted": function() { this.FinishOrder(); }, }, "PATROL": { "enter": function () { // Memorize the origin position in case that we want to go back let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) { this.FinishOrder(); return; } if (!this.patrolStartPosOrder) { this.patrolStartPosOrder = cmpPosition.GetPosition(); this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses; this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture; } this.StartTimer(0, 1000); this.SelectAnimation("move"); }, "leave": function() { this.StopTimer(); delete this.patrolStartPosOrder; }, "Timer": function(msg) { this.FindWalkAndFightTargets(); }, "MoveCompleted": function() { if (this.orderQueue.length == 1) this.PushOrder("Patrol", this.patrolStartPosOrder); this.PushOrder(this.order.type, this.order.data); this.FinishOrder(); }, }, "GUARD": { "RemoveGuard": function() { this.StopMoving(); this.FinishOrder(); }, "ESCORTING": { "enter": function () { // Show weapons rather than carried resources. this.SetAnimationVariant("combat"); this.StartTimer(0, 1000); this.SelectAnimation("move"); this.SetHeldPositionOnEntity(this.isGuardOf); return false; }, "Timer": function(msg) { // Check the target is alive if (!this.TargetIsAlive(this.isGuardOf)) { this.StopMoving(); this.FinishOrder(); return; } this.SetHeldPositionOnEntity(this.isGuardOf); }, "leave": function(msg) { this.SetMoveSpeed(this.GetWalkSpeed()); this.StopTimer(); }, "MoveStarted": function(msg) { // Adapt the speed to the one of the target if needed var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion.IsInTargetRange(this.isGuardOf, 0, 3*this.guardRange)) { var cmpUnitAI = Engine.QueryInterface(this.isGuardOf, IID_UnitAI); if (cmpUnitAI) { var speed = cmpUnitAI.GetWalkSpeed(); if (speed < this.GetWalkSpeed()) this.SetMoveSpeed(speed); } } }, "MoveCompleted": function() { this.SetMoveSpeed(this.GetWalkSpeed()); if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("GUARDING"); }, }, "GUARDING": { "enter": function () { this.StartTimer(1000, 1000); this.SetHeldPositionOnEntity(this.entity); this.SelectAnimation("idle"); return false; }, "LosRangeUpdate": function(msg) { // Start attacking one of the newly-seen enemy (if any) if (this.GetStance().targetVisibleEnemies) this.AttackEntitiesByPreference(msg.data.added); }, "Timer": function(msg) { // Check the target is alive if (!this.TargetIsAlive(this.isGuardOf)) { this.FinishOrder(); return; } // then check is the target has moved if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange)) this.SetNextState("ESCORTING"); else { // if nothing better to do, check if the guarded needs to be healed or repaired var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health); if (cmpHealth && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints())) { if (this.CanHeal(this.isGuardOf)) this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false }); else if (this.CanRepair(this.isGuardOf)) this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false }); } } }, "leave": function(msg) { this.StopTimer(); }, }, }, "FLEEING": { "enter": function() { this.PlaySound("panic"); // Run quickly var speed = this.GetRunSpeed(); this.SelectAnimation("move"); this.SetMoveSpeed(speed); }, "HealthChanged": function() { var speed = this.GetRunSpeed(); this.SetMoveSpeed(speed); }, "leave": function() { // Reset normal speed this.SetMoveSpeed(this.GetWalkSpeed()); }, "MoveCompleted": function() { // When we've run far enough, stop fleeing this.FinishOrder(); }, // TODO: what if we run into more enemies while fleeing? }, "COMBAT": { "Order.LeaveFoundation": function(msg) { // Ignore the order as we're busy. return { "discardOrder": true }; }, "Attacked": function(msg) { // If we're already in combat mode, ignore anyone else who's attacking us // unless it's a melee attack since they may be blocking our way to the target if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || !this.order.data.force)) this.RespondToTargetedEntities([msg.data.attacker]); }, "APPROACHING": { "enter": function () { // Show weapons rather than carried resources. this.SetAnimationVariant("combat"); this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, "leave": function() { // Show carried resources when walking. this.SetDefaultAnimationVariant(); this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } }, "MoveCompleted": function() { if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType)) { // If the unit needs to unpack, do so if (this.CanUnpack()) { this.PushOrderFront("Unpack", { "force": true }); return; } else this.SetNextState("ATTACKING"); } else { if (this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType)) { this.SetNextState("APPROACHING"); } else { // Give up this.FinishOrder(); } } }, }, "ATTACKING": { "enter": function() { var target = this.order.data.target; var cmpFormation = Engine.QueryInterface(target, IID_Formation); // if the target is a formation, save the attacking formation, and pick a member if (cmpFormation) { this.order.data.formationTarget = target; target = cmpFormation.GetClosestMember(this.entity); this.order.data.target = target; } // Check the target is still alive and attackable if (this.CanAttack(target) && !this.CheckTargetAttackRange(target, this.order.data.attackType)) { // Can't reach it - try to chase after it if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { if (this.MoveToTargetAttackRange(target, this.order.data.attackType)) { this.SetNextState("COMBAT.CHASING"); return; } } } var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); this.attackTimers = cmpAttack.GetTimers(this.order.data.attackType); // If the repeat time since the last attack hasn't elapsed, // delay this attack to avoid attacking too fast. var prepare = this.attackTimers.prepare; if (this.lastAttacked) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime(); prepare = Math.max(prepare, repeatLeft); } this.oldAttackType = this.order.data.attackType; // add prefix + no capital first letter for attackType var animationName = "attack_" + this.order.data.attackType.toLowerCase(); if (this.IsFormationMember()) { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) animationName = cmpFormation.GetFormationAnimation(this.entity, animationName); } this.SelectAnimation(animationName, false, 1.0, animationName); this.SetAnimationSync(prepare, this.attackTimers.repeat); this.StartTimer(prepare, this.attackTimers.repeat); // TODO: we should probably only bother syncing projectile attacks, not melee // If using a non-default prepare time, re-sync the animation when the timer runs. this.resyncAnimation = (prepare != this.attackTimers.prepare) ? true : false; this.FaceTowardsTarget(this.order.data.target); var cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); if (cmpBuildingAI) cmpBuildingAI.SetUnitAITarget(this.order.data.target); }, "leave": function() { var cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); if (cmpBuildingAI) cmpBuildingAI.SetUnitAITarget(0); this.StopTimer(); }, "Timer": function(msg) { var target = this.order.data.target; var cmpFormation = Engine.QueryInterface(target, IID_Formation); // if the target is a formation, save the attacking formation, and pick a member if (cmpFormation) { var thisObject = this; var filter = function(t) { return thisObject.CanAttack(t); }; this.order.data.formationTarget = target; target = cmpFormation.GetClosestMember(this.entity, filter); this.order.data.target = target; } // Check the target is still alive and attackable if (this.CanAttack(target)) { // If we are hunting, first update the target position of the gather order so we know where will be the killed animal if (this.order.data.hunting && this.orderQueue[1] && this.orderQueue[1].data.lastPos) { var cmpPosition = Engine.QueryInterface(this.order.data.target, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) { // Store the initial position, so that we can find the rest of the herd later if (!this.orderQueue[1].data.initPos) this.orderQueue[1].data.initPos = this.orderQueue[1].data.lastPos; this.orderQueue[1].data.lastPos = cmpPosition.GetPosition(); // We still know where the animal is, so we shouldn't give up before going there this.orderQueue[1].data.secondTry = undefined; } } var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.lastAttacked = cmpTimer.GetTime() - msg.lateness; this.FaceTowardsTarget(target); // BuildingAI has it's own attack-routine var cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI); if (!cmpBuildingAI) { let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); cmpAttack.PerformAttack(this.order.data.attackType, target); } // Check we can still reach the target for the next attack if (this.CheckTargetAttackRange(target, this.order.data.attackType)) { if (this.resyncAnimation) { this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat); this.resyncAnimation = false; } return; } // Can't reach it - try to chase after it if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { if (this.MoveToTargetRange(target, IID_Attack, this.order.data.attackType)) { this.SetNextState("COMBAT.CHASING"); return; } } } // if we're targetting a formation, find a new member of that formation var cmpTargetFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation); // if there is no target, it means previously searching for the target inside the target formation failed, so don't repeat the search if (target && cmpTargetFormation) { this.order.data.target = this.order.data.formationTarget; this.TimerHandler(msg.data, msg.lateness); return; } // Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up // Except if in WalkAndFight mode where we look for more ennemies around before moving again if (this.FinishOrder()) { if (this.IsWalkingAndFighting()) this.FindWalkAndFightTargets(); return; } // See if we can switch to a new nearby enemy if (this.FindNewTargets()) { // Attempt to immediately re-enter the timer function, to avoid wasting the attack. if (this.orderQueue.length > 0 && this.orderQueue[0].data.attackType == this.oldAttackType) this.TimerHandler(msg.data, msg.lateness); return; } // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); }, // TODO: respond to target deaths immediately, rather than waiting // until the next Timer event "Attacked": function(msg) { // If we are capturing and are attacked by something that we would not capture, attack that entity instead if (this.order.data.attackType == "Capture" && (this.GetStance().targetAttackersAlways || !this.order.data.force) && this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, true) != "Capture") this.RespondToTargetedEntities([msg.data.attacker]); }, }, "CHASING": { "enter": function () { // Show weapons rather than carried resources. this.SetAnimationVariant("combat"); this.SelectAnimation("move"); var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.IsFleeing()) { // Run after a fleeing target var speed = this.GetRunSpeed(); this.SetMoveSpeed(speed); } this.StartTimer(1000, 1000); }, "HealthChanged": function() { var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsFleeing()) return; var speed = this.GetRunSpeed(); this.SetMoveSpeed(speed); }, "leave": function() { // Reset normal speed in case it was changed this.SetMoveSpeed(this.GetWalkSpeed()); // Show carried resources when walking. this.SetDefaultAnimationVariant(); this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } }, "MoveCompleted": function() { this.SetNextState("ATTACKING"); }, }, }, "GATHER": { "APPROACHING": { "enter": function() { this.SelectAnimation("move"); this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave". // check that we can gather from the resource we're supposed to gather from. var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); var cmpMirage = Engine.QueryInterface(this.gatheringTarget, IID_Mirage); if ((!cmpMirage || !cmpMirage.Mirages(IID_ResourceSupply)) && (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity))) { // Save the current order's data in case we need it later var oldType = this.order.data.type; var oldTarget = this.order.data.target; var oldTemplate = this.order.data.template; // Try the next queued order if there is any if (this.FinishOrder()) return true; // Try to find another nearby target of the same specific type // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource(function (ent, type, template) { return ( ent != oldTarget && ((type.generic == "treasure" && oldType.generic == "treasure") || (type.specific == oldType.specific && (type.specific != "meat" || oldTemplate == template))) ); }, oldTarget); if (nearby) { this.PerformGather(nearby, false, false); return true; } else { // It's probably better in this case, to avoid units getting stuck around a dropsite // in a "Target is far away, full, nearby are no good resources, return to dropsite" loop // to order it to GatherNear the resource position. var cmpPosition = Engine.QueryInterface(oldTarget, IID_Position); if (cmpPosition) { var pos = cmpPosition.GetPosition(); this.GatherNearPosition(pos.x, pos.z, oldType, oldTemplate); return true; } else { // we're kind of stuck here. Return resource. var nearby = this.FindNearestDropsite(oldType.generic); if (nearby) { this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return true; } } } return true; } return false; }, "MoveCompleted": function(msg) { if (msg.data.error) { // We failed to reach the target // remove us from the list of entities gathering from Resource. var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply && cmpOwnership) cmpSupply.RemoveGatherer(this.entity, cmpOwnership.GetOwner()); else if (cmpSupply) cmpSupply.RemoveGatherer(this.entity); // Save the current order's data in case we need it later var oldType = this.order.data.type; var oldTarget = this.order.data.target; var oldTemplate = this.order.data.template; // Try the next queued order if there is any if (this.FinishOrder()) return; // Try to find another nearby target of the same specific type // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource(function (ent, type, template) { return ( ent != oldTarget && ((type.generic == "treasure" && oldType.generic == "treasure") || (type.specific == oldType.specific && (type.specific != "meat" || oldTemplate == template))) ); }); if (nearby) { this.PerformGather(nearby, false, false); return; } // Couldn't find anything else. Just try this one again, // maybe we'll succeed next time this.PerformGather(oldTarget, false, false); return; } // We reached the target - start gathering from it now this.SetNextState("GATHERING"); }, "leave": function() { // don't use ownership because this is called after a conversion/resignation // and the ownership would be invalid then. var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply) cmpSupply.RemoveGatherer(this.entity); delete this.gatheringTarget; }, }, // Walking to a good place to gather resources near, used by GatherNearPosition "WALKING": { "enter": function() { this.SelectAnimation("move"); }, "MoveCompleted": function(msg) { var resourceType = this.order.data.type; var resourceTemplate = this.order.data.template; // Try to find another nearby target of the same specific type // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource(function (ent, type, template) { return ( (type.generic == "treasure" && resourceType.generic == "treasure") || (type.specific == resourceType.specific && (type.specific != "meat" || resourceTemplate == template)) ); }); // If there is a nearby resource start gathering if (nearby) { this.PerformGather(nearby, false, false); return; } // Couldn't find nearby resources, so give up if (this.FinishOrder()) return; // Nothing better to do: go back to dropsite var nearby = this.FindNearestDropsite(resourceType.generic); if (nearby) { this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } // No dropsites, just give up }, }, "GATHERING": { "enter": function() { this.gatheringTarget = this.order.data.target; // deleted in "leave". // Check if the resource is full. if (this.gatheringTarget) { // Check that we can gather from the resource we're supposed to gather from. // Will only be added if we're not already in. var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity)) { this.gatheringTarget = INVALID_ENTITY; this.StartTimer(0); return false; } } // If this order was forced, the player probably gave it, but now we've reached the target // switch to an unforced order (can be interrupted by attacks) this.order.data.force = false; this.order.data.autoharvest = true; // Calculate timing based on gather rates // This allows the gather rate to control how often we gather, instead of how much. var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); var rate = cmpResourceGatherer.GetTargetGatherRate(this.gatheringTarget); if (!rate) { // Try to find another target if the current one stopped existing if (!Engine.QueryInterface(this.gatheringTarget, IID_Identity)) { // Let the Timer logic handle this this.StartTimer(0); return false; } // No rate, give up on gathering this.FinishOrder(); return true; } // Scale timing interval based on rate, and start timer // The offset should be at least as long as the repeat time so we use the same value for both. var offset = 1000/rate; var repeat = offset; this.StartTimer(offset, repeat); // We want to start the gather animation as soon as possible, // but only if we're actually at the target and it's still alive // (else it'll look like we're chopping empty air). // (If it's not alive, the Timer handler will deal with sending us // off to a different target.) if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer)) { this.SetDefaultAnimationVariant(); var typename = "gather_" + this.order.data.type.specific; this.SelectAnimation(typename, false, 1.0, typename); } return false; }, "leave": function() { this.StopTimer(); // don't use ownership because this is called after a conversion/resignation // and the ownership would be invalid then. var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply) cmpSupply.RemoveGatherer(this.entity); delete this.gatheringTarget; // Show the carried resource, if we've gathered anything. this.SetDefaultAnimationVariant(); }, "Timer": function(msg) { var resourceTemplate = this.order.data.template; var resourceType = this.order.data.type; var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return; var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply); if (cmpSupply && cmpSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity)) { // Check we can still reach and gather from the target if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer) && this.CanGather(this.gatheringTarget)) { // Gather the resources: var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); // Try to gather treasure if (cmpResourceGatherer.TryInstantGather(this.gatheringTarget)) return; // If we've already got some resources but they're the wrong type, // drop them first to ensure we're only ever carrying one type if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic)) cmpResourceGatherer.DropResources(); // Collect from the target var status = cmpResourceGatherer.PerformGather(this.gatheringTarget); // If we've collected as many resources as possible, // return to the nearest dropsite if (status.filled) { var nearby = this.FindNearestDropsite(resourceType.generic); if (nearby) { // (Keep this Gather order on the stack so we'll // continue gathering after returning) this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } // Oh no, couldn't find any drop sites. Give up on gathering. this.FinishOrder(); return; } // We can gather more from this target, do so in the next timer if (!status.exhausted) return; } else { // Try to follow the target if (this.MoveToTargetRange(this.gatheringTarget, IID_ResourceGatherer)) { this.SetNextState("APPROACHING"); return; } // Can't reach the target, or it doesn't exist any more // We want to carry on gathering resources in the same area as // the old one. So try to get close to the old resource's // last known position var maxRange = 8; // get close but not too close if (this.order.data.lastPos && this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z, 0, maxRange)) { this.SetNextState("APPROACHING"); return; } } } // We're already in range, can't get anywhere near it or the target is exhausted. var herdPos = this.order.data.initPos; // Give up on this order and try our next queued order // but first check what is our next order and, if needed, insert a returnResource order var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer.IsCarrying(resourceType.generic) && this.orderQueue.length > 1 && this.orderQueue[1] !== "ReturnResource" && (this.orderQueue[1].type !== "Gather" || this.orderQueue[1].data.type.generic !== resourceType.generic)) { let nearby = this.FindNearestDropsite(resourceType.generic); if (nearby) this.orderQueue.splice(1, 0, { "type": "ReturnResource", "data": { "target": nearby, "force": false } }); } if (this.FinishOrder()) return; // No remaining orders - pick a useful default behaviour // Try to find a new resource of the same specific type near our current position: // Also don't switch to a different type of huntable animal var nearby = this.FindNearbyResource(function (ent, type, template) { return ( (type.generic == "treasure" && resourceType.generic == "treasure") || (type.specific == resourceType.specific && (type.specific != "meat" || resourceTemplate == template)) ); }); if (nearby) { this.PerformGather(nearby, false, false); return; } // If hunting, try to go to the initial herd position to see if we are more lucky if (herdPos) { this.GatherNearPosition(herdPos.x, herdPos.z, resourceType, resourceTemplate); return; } // Nothing else to gather - if we're carrying anything then we should // drop it off, and if not then we might as well head to the dropsite // anyway because that's a nice enough place to congregate and idle var nearby = this.FindNearestDropsite(resourceType.generic); if (nearby) { this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } // No dropsites - just give up }, }, }, "HEAL": { "Attacked": function(msg) { // If we stand ground we will rather die than flee if (!this.GetStance().respondStandGround && !this.order.data.force) this.Flee(msg.data.attacker, false); }, "APPROACHING": { "enter": function () { this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, "leave": function() { this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } }, "MoveCompleted": function() { this.SetNextState("HEALING"); }, }, "HEALING": { "enter": function() { var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); this.healTimers = cmpHeal.GetTimers(); // If the repeat time since the last heal hasn't elapsed, // delay the action to avoid healing too fast. var prepare = this.healTimers.prepare; if (this.lastHealed) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime(); prepare = Math.max(prepare, repeatLeft); } this.SelectAnimation("heal", false, 1.0, "heal"); this.SetAnimationSync(prepare, this.healTimers.repeat); this.StartTimer(prepare, this.healTimers.repeat); // If using a non-default prepare time, re-sync the animation when the timer runs. this.resyncAnimation = (prepare != this.healTimers.prepare) ? true : false; this.FaceTowardsTarget(this.order.data.target); }, "leave": function() { this.StopTimer(); }, "Timer": function(msg) { var target = this.order.data.target; // Check the target is still alive and healable if (this.TargetIsAlive(target) && this.CanHeal(target)) { // Check if we can still reach the target if (this.CheckTargetRange(target, IID_Heal)) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.lastHealed = cmpTimer.GetTime() - msg.lateness; this.FaceTowardsTarget(target); var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); cmpHeal.PerformHeal(target); if (this.resyncAnimation) { this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat); this.resyncAnimation = false; } return; } // Can't reach it - try to chase after it if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { if (this.MoveToTargetRange(target, IID_Heal)) { this.SetNextState("HEAL.CHASING"); return; } } } // Can't reach it, healed to max hp or doesn't exist any more - give up if (this.FinishOrder()) return; // Heal another one if (this.FindNewHealTargets()) return; // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); }, }, "CHASING": { "enter": function () { this.SelectAnimation("move"); this.StartTimer(1000, 1000); }, "leave": function () { this.StopTimer(); }, "Timer": function(msg) { if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null)) { this.StopMoving(); this.FinishOrder(); // Return to our original position if (this.GetStance().respondHoldGround) this.WalkToHeldPosition(); } }, "MoveCompleted": function () { this.SetNextState("HEALING"); }, }, }, // Returning to dropsite "RETURNRESOURCE": { "APPROACHING": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { // Switch back to idle animation to guarantee we won't // get stuck with the carry animation after stopping moving this.SelectAnimation("idle"); // Check the dropsite is in range and we can return our resource there // (we didn't get stopped before reaching it) if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true)) { var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite); if (cmpResourceDropsite) { // Dump any resources we can var dropsiteTypes = cmpResourceDropsite.GetTypes(); var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); cmpResourceGatherer.CommitResources(dropsiteTypes); // Stop showing the carried resource animation. this.SetDefaultAnimationVariant(); // Our next order should always be a Gather, // so just switch back to that order this.FinishOrder(); return; } } // The dropsite was destroyed, or we couldn't reach it, or ownership changed // Look for a new one. var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); var genericType = cmpResourceGatherer.GetMainCarryingType(); var nearby = this.FindNearestDropsite(genericType); if (nearby) { this.FinishOrder(); this.PushOrderFront("ReturnResource", { "target": nearby, "force": false }); return; } // Oh no, couldn't find any drop sites. Give up on returning. this.FinishOrder(); }, }, }, "TRADE": { "Attacked": function(msg) { // Ignore attack // TODO: Inform player }, "APPROACHINGMARKET": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { if (this.waypoints && this.waypoints.length) { if (!this.MoveToMarket(this.order.data.target)) this.StopTrading(); } else this.PerformTradeAndMoveToNextMarket(this.order.data.target); }, }, "TradingCanceled": function(msg) { if (msg.market != this.order.data.target) return; let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); let otherMarket = cmpTrader && cmpTrader.GetFirstMarket(); this.StopTrading(); if (otherMarket) this.WalkToTarget(otherMarket); }, }, "REPAIR": { "APPROACHING": { "enter": function () { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.SetNextState("REPAIRING"); }, }, "REPAIRING": { "enter": function() { // If this order was forced, the player probably gave it, but now we've reached the target // switch to an unforced order (can be interrupted by attacks) if (this.order.data.force) this.order.data.autoharvest = true; this.order.data.force = false; this.repairTarget = this.order.data.target; // temporary, deleted in "leave". // Check we can still reach and repair the target if (!this.CanRepair(this.repairTarget)) { // Can't reach it, no longer owned by ally, or it doesn't exist any more this.FinishOrder(); return true; } if (!this.CheckTargetRange(this.repairTarget, IID_Builder)) { if (this.MoveToTargetRange(this.repairTarget, IID_Builder)) this.SetNextState("APPROACHING"); else this.FinishOrder(); return true; } // Check if the target is still repairable var cmpHealth = Engine.QueryInterface(this.repairTarget, IID_Health); if (cmpHealth && cmpHealth.GetHitpoints() >= cmpHealth.GetMaxHitpoints()) { // The building was already finished/fully repaired before we arrived; // let the ConstructionFinished handler handle this. this.OnGlobalConstructionFinished({"entity": this.repairTarget, "newentity": this.repairTarget}); return true; } let cmpBuilderList = QueryBuilderListInterface(this.repairTarget); if (cmpBuilderList) cmpBuilderList.AddBuilder(this.entity); this.SelectAnimation("build", false, 1.0, "build"); this.StartTimer(1000, 1000); return false; }, "leave": function() { let cmpBuilderList = QueryBuilderListInterface(this.repairTarget); if (cmpBuilderList) cmpBuilderList.RemoveBuilder(this.entity); delete this.repairTarget; this.StopTimer(); }, "Timer": function(msg) { // Check we can still reach and repair the target if (!this.CanRepair(this.repairTarget)) { // No longer owned by ally, or it doesn't exist any more this.FinishOrder(); return; } var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); cmpBuilder.PerformBuilding(this.repairTarget); // if the building is completed, the leave() function will be called // by the ConstructionFinished message // in that case, the repairTarget is deleted, and we can just return if (!this.repairTarget) return; if (this.MoveToTargetRange(this.repairTarget, IID_Builder)) this.SetNextState("APPROACHING"); else if (!this.CheckTargetRange(this.repairTarget, IID_Builder)) this.FinishOrder(); //can't approach and isn't in reach }, }, "ConstructionFinished": function(msg) { if (msg.data.entity != this.order.data.target) return; // ignore other buildings // Save the current order's data in case we need it later var oldData = this.order.data; // Save the current state so we can continue walking if necessary // FinishOrder() below will switch to IDLE if there's no order, which sets the idle animation. // Idle animation while moving towards finished construction looks weird (ghosty). var oldState = this.GetCurrentState(); // Drop any resource we can if we are in range when the construction finishes var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite); if (cmpResourceGatherer && cmpResourceDropsite && this.CheckTargetRange(msg.data.newentity, IID_Builder) && this.CanReturnResource(msg.data.newentity, true)) { let dropsiteTypes = cmpResourceDropsite.GetTypes(); cmpResourceGatherer.CommitResources(dropsiteTypes); this.SetDefaultAnimationVariant(); } // We finished building it. // Switch to the next order (if any) if (this.FinishOrder()) { if (this.CanReturnResource(msg.data.newentity, true)) { this.SetDefaultAnimationVariant(); this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false }); } return; } // No remaining orders - pick a useful default behaviour // If autocontinue explicitly disabled (e.g. by AI) then // do nothing automatically if (!oldData.autocontinue) return; // If this building was e.g. a farm of ours, the entities that recieved // the build command should start gathering from it if ((oldData.force || oldData.autoharvest) && this.CanGather(msg.data.newentity)) { if (this.CanReturnResource(msg.data.newentity, true)) { this.SetDefaultAnimationVariant(); this.PushOrder("ReturnResource", { "target": msg.data.newentity, "force": false }); } this.PerformGather(msg.data.newentity, true, false); return; } // If this building was e.g. a farmstead of ours, entities that received // the build command should look for nearby resources to gather if ((oldData.force || oldData.autoharvest) && this.CanReturnResource(msg.data.newentity, false)) { var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite); var types = cmpResourceDropsite.GetTypes(); // TODO: Slightly undefined behavior here, we don't know what type of resource will be collected, // may cause problems for AIs (especially hunting fast animals), but avoid ugly hacks to fix that! var nearby = this.FindNearbyResource(function (ent, type, template) { return (types.indexOf(type.generic) != -1); }, msg.data.newentity); if (nearby) { this.PerformGather(nearby, true, false); return; } } // Look for a nearby foundation to help with var nearbyFoundation = this.FindNearbyFoundation(); if (nearbyFoundation) { this.AddOrder("Repair", { "target": nearbyFoundation, "autocontinue": oldData.autocontinue, "force": false }, true); return; } // Unit was approaching and there's nothing to do now, so switch to walking if (oldState === "INDIVIDUAL.REPAIR.APPROACHING") { // We're already walking to the given point, so add this as a order. this.WalkToTarget(msg.data.newentity, true); } }, }, "GARRISON": { "enter": function() { // If the garrisonholder should pickup, warn it so it can take needed action var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder); if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity)) { this.pickup = this.order.data.target; // temporary, deleted in "leave" Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity }); } }, "leave": function() { // If a pickup has been requested and not yet canceled, cancel it if (this.pickup) { Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } }, "APPROACHING": { "enter": function() { this.SelectAnimation("move"); }, "MoveCompleted": function() { if (this.IsUnderAlert() && this.alertGarrisoningTarget) { // check that we can garrison in the building we're supposed to garrison in var cmpGarrisonHolder = Engine.QueryInterface(this.alertGarrisoningTarget, IID_GarrisonHolder); if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) { // Try to find another nearby building var nearby = this.FindNearbyGarrisonHolder(); if (nearby) { this.alertGarrisoningTarget = nearby; this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget}); } else this.FinishOrder(); } else this.SetNextState("GARRISONED"); } else this.SetNextState("GARRISONED"); }, }, "GARRISONED": { "enter": function() { // Target is not handled the same way with Alert and direct garrisoning if (this.order.data.target) var target = this.order.data.target; else { if (!this.alertGarrisoningTarget) { // We've been unable to find a target nearby, so give up this.FinishOrder(); return true; } var target = this.alertGarrisoningTarget; } // Check that we can garrison here if (this.CanGarrison(target)) { // Check that we're in range of the garrison target if (this.CheckGarrisonRange(target)) { var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); // Check that garrisoning succeeds if (cmpGarrisonHolder.Garrison(this.entity)) { this.isGarrisoned = true; if (this.formationController) { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) { // disable rearrange for this removal, // but enable it again for the next // move command var rearrange = cmpFormation.rearrange; cmpFormation.SetRearrange(false); cmpFormation.RemoveMembers([this.entity]); cmpFormation.SetRearrange(rearrange); } } // Check if we are garrisoned in a dropsite var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite); if (cmpResourceDropsite && this.CanReturnResource(target, true)) { // Dump any resources we can var dropsiteTypes = cmpResourceDropsite.GetTypes(); var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) { cmpResourceGatherer.CommitResources(dropsiteTypes); this.SetDefaultAnimationVariant(); } } // If a pickup has been requested, remove it if (this.pickup) { var cmpHolderPosition = Engine.QueryInterface(target, IID_Position); var cmpHolderUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (cmpHolderUnitAI && cmpHolderPosition) cmpHolderUnitAI.lastShorelinePosition = cmpHolderPosition.GetPosition(); Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity }); delete this.pickup; } if (this.IsTurret()) this.SetNextState("IDLE"); return false; } } else { // Unable to reach the target, try again (or follow if it is a moving target) // except if the does not exits anymore or its orders have changed if (this.pickup) { var cmpUnitAI = Engine.QueryInterface(this.pickup, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.HasPickupOrder(this.entity)) { this.FinishOrder(); return true; } } if (this.MoveToTarget(target)) { this.SetNextState("APPROACHING"); return false; } } } // Garrisoning failed for some reason, so finish the order this.FinishOrder(); return true; }, "leave": function() { } }, }, "AUTOGARRISON": { "enter": function() { this.isGarrisoned = true; return false; }, "leave": function() { } }, "CHEERING": { "enter": function() { // Unit is invulnerable while cheering var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(true); this.SelectAnimation("promotion"); this.StartTimer(2800, 2800); return false; }, "leave": function() { this.StopTimer(); var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(false); }, "Timer": function(msg) { this.FinishOrder(); }, }, "PACKING": { "enter": function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.Pack(); }, "PackFinished": function(msg) { this.FinishOrder(); }, "leave": function() { }, "Attacked": function(msg) { // Ignore attacks while packing }, }, "UNPACKING": { "enter": function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); cmpPack.Unpack(); }, "PackFinished": function(msg) { this.FinishOrder(); }, "leave": function() { }, "Attacked": function(msg) { // Ignore attacks while unpacking }, }, "PICKUP": { "APPROACHING": { "enter": function() { this.SelectAnimation("move"); }, "MoveCompleted": function() { this.SetNextState("LOADING"); }, "PickupCanceled": function() { this.StopMoving(); this.FinishOrder(); }, }, "LOADING": { "enter": function() { this.SelectAnimation("idle"); var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) { this.FinishOrder(); return true; } return false; }, "PickupCanceled": function() { this.FinishOrder(); }, }, }, }, "ANIMAL": { "Attacked": function(msg) { if (this.template.NaturalBehaviour == "skittish" || this.template.NaturalBehaviour == "passive") { this.Flee(msg.data.attacker, false); } else if (this.IsDangerousAnimal() || this.template.NaturalBehaviour == "defensive") { if (this.CanAttack(msg.data.attacker)) this.Attack(msg.data.attacker, false); } else if (this.template.NaturalBehaviour == "domestic") { // Never flee, stop what we were doing this.SetNextState("IDLE"); } }, "Order.LeaveFoundation": function(msg) { // Move a tile outside the building var range = 4; if (this.MoveToTargetRangeExplicit(msg.data.target, range, range)) { // We've started walking to the given point this.SetNextState("WALKING"); } else { // We are already at the target, or can't move at all this.FinishOrder(); } }, "IDLE": { // (We need an IDLE state so that FinishOrder works) "enter": function() { // Start feeding immediately this.SetNextState("FEEDING"); return true; }, }, "ROAMING": { "enter": function() { // Walk in a random direction this.SelectAnimation("walk", false, this.GetWalkSpeed()); this.SetFacePointAfterMove(false); this.MoveRandomly(+this.template.RoamDistance); // Set a random timer to switch to feeding state this.StartTimer(randIntInclusive(+this.template.RoamTimeMin, +this.template.RoamTimeMax)); }, "leave": function() { this.StopTimer(); this.SetFacePointAfterMove(true); }, "LosRangeUpdate": function(msg) { if (this.template.NaturalBehaviour == "skittish") { if (msg.data.added.length > 0) { this.Flee(msg.data.added[0], false); return; } } // Start attacking one of the newly-seen enemy (if any) else if (this.IsDangerousAnimal()) { this.AttackVisibleEntity(msg.data.added); } // TODO: if two units enter our range together, we'll attack the // first and then the second won't trigger another LosRangeUpdate // so we won't notice it. Probably we should do something with // ResetActiveQuery in ROAMING.enter/FEEDING.enter in order to // find any units that are already in range. }, "Timer": function(msg) { this.SetNextState("FEEDING"); }, "MoveCompleted": function() { this.MoveRandomly(+this.template.RoamDistance); }, }, "FEEDING": { "enter": function() { // Stop and eat for a while this.SelectAnimation("feeding"); this.StopMoving(); this.StartTimer(randIntInclusive(+this.template.FeedTimeMin, +this.template.FeedTimeMax)); }, "leave": function() { this.StopTimer(); }, "LosRangeUpdate": function(msg) { if (this.template.NaturalBehaviour == "skittish") { if (msg.data.added.length > 0) { this.Flee(msg.data.added[0], false); return; } } // Start attacking one of the newly-seen enemy (if any) else if (this.template.NaturalBehaviour == "violent") { this.AttackVisibleEntity(msg.data.added); } }, "MoveCompleted": function() { }, "Timer": function(msg) { this.SetNextState("ROAMING"); }, }, "FLEEING": "INDIVIDUAL.FLEEING", // reuse the same fleeing behaviour for animals "COMBAT": "INDIVIDUAL.COMBAT", // reuse the same combat behaviour for animals "WALKING": "INDIVIDUAL.WALKING", // reuse the same walking behaviour for animals // only used for domestic animals }, }; UnitAI.prototype.Init = function() { this.orderQueue = []; // current order is at the front of the list this.order = undefined; // always == this.orderQueue[0] this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to this.isGarrisoned = false; this.isIdle = false; // For A19, keep no formations as a default to help pathfinding. this.lastFormationTemplate = "special/formations/null"; this.finishedOrder = false; // used to find if all formation members finished the order this.heldPosition = undefined; // Queue of remembered works this.workOrders = []; this.isGuardOf = undefined; // "Town Bell" behaviour this.alertRaiser = undefined; this.alertGarrisoningTarget = undefined; // For preventing increased action rate due to Stop orders or target death. this.lastAttacked = undefined; this.lastHealed = undefined; this.SetStance(this.template.DefaultStance); }; UnitAI.prototype.IsTurret = function() { if (!this.IsGarrisoned()) return false; var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); return cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY; }; UnitAI.prototype.ReactsToAlert = function(level) { return this.template.AlertReactiveLevel <= level; }; UnitAI.prototype.IsUnderAlert = function() { return this.alertRaiser != undefined; }; UnitAI.prototype.ResetAlert = function() { this.alertGarrisoningTarget = undefined; this.alertRaiser = undefined; }; UnitAI.prototype.GetAlertRaiser = function() { return this.alertRaiser; }; UnitAI.prototype.IsFormationController = function() { return (this.template.FormationController == "true"); }; UnitAI.prototype.IsFormationMember = function() { return (this.formationController != INVALID_ENTITY); }; UnitAI.prototype.HasFinishedOrder = function() { return this.finishedOrder; }; UnitAI.prototype.ResetFinishOrder = function() { this.finishedOrder = false; }; UnitAI.prototype.IsAnimal = function() { return (this.template.NaturalBehaviour ? true : false); }; UnitAI.prototype.IsDangerousAnimal = function() { return (this.IsAnimal() && (this.template.NaturalBehaviour == "violent" || this.template.NaturalBehaviour == "aggressive")); }; UnitAI.prototype.IsDomestic = function() { var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); return cmpIdentity && cmpIdentity.HasClass("Domestic"); }; UnitAI.prototype.IsHealer = function() { return Engine.QueryInterface(this.entity, IID_Heal); }; UnitAI.prototype.IsIdle = function() { return this.isIdle; }; UnitAI.prototype.IsGarrisoned = function() { return this.isGarrisoned; }; UnitAI.prototype.SetGarrisoned = function() { this.isGarrisoned = true; }; UnitAI.prototype.GetGarrisonHolder = function() { if (this.IsGarrisoned()) { for (let order of this.orderQueue) if (order.type == "Garrison" || order.type == "Autogarrison") return order.data.target; } return INVALID_ENTITY; }; UnitAI.prototype.IsFleeing = function() { var state = this.GetCurrentState().split(".").pop(); return (state == "FLEEING"); }; UnitAI.prototype.IsWalking = function() { var state = this.GetCurrentState().split(".").pop(); return (state == "WALKING"); }; /** * return true if in WalkAndFight looking for new targets */ UnitAI.prototype.IsWalkingAndFighting = function() { if (this.IsFormationMember()) { var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); return (cmpUnitAI && cmpUnitAI.IsWalkingAndFighting()); } return (this.orderQueue.length > 0 && this.orderQueue[0].type == "WalkAndFight"); }; UnitAI.prototype.OnCreate = function() { if (this.IsAnimal()) this.UnitFsm.Init(this, "ANIMAL.FEEDING"); else if (this.IsFormationController()) this.UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE"); else this.UnitFsm.Init(this, "INDIVIDUAL.IDLE"); this.isIdle = true; }; UnitAI.prototype.OnDiplomacyChanged = function(msg) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() == msg.player) this.SetupRangeQueries(); if (this.isGuardOf && !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf)) this.RemoveGuard(); }; UnitAI.prototype.OnOwnershipChanged = function(msg) { this.SetupRangeQueries(); - if (this.isGuardOf && (msg.to == -1 || !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf))) + if (this.isGuardOf && (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf))) this.RemoveGuard(); // If the unit isn't being created or dying, reset stance and clear orders - if (msg.to != -1 && msg.from != -1) + if (msg.to != INVALID_PLAYER && msg.from != INVALID_PLAYER) { // Switch to a virgin state to let states execute their leave handlers. // except if garrisoned or cheering or (un)packing, in which case we only clear the order queue if (this.isGarrisoned || this.IsPacking() || this.orderQueue[0] && this.orderQueue[0].type == "Cheering") { this.orderQueue.length = Math.min(this.orderQueue.length, 1); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); } else { let index = this.GetCurrentState().indexOf("."); if (index != -1) this.UnitFsm.SwitchToNextState(this, this.GetCurrentState().slice(0,index)); this.Stop(false); } this.workOrders = []; let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (cmpTrader) cmpTrader.StopTrading(); this.SetStance(this.template.DefaultStance); if (this.IsTurret()) this.SetTurretStance(); } }; UnitAI.prototype.OnDestroy = function() { // Switch to an empty state to let states execute their leave handlers. this.UnitFsm.SwitchToNextState(this, ""); // Clean up range queries var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) cmpRangeManager.DestroyActiveQuery(this.losRangeQuery); if (this.losHealRangeQuery) cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery); }; UnitAI.prototype.OnVisionRangeChanged = function(msg) { // Update range queries if (this.entity == msg.entity) this.SetupRangeQueries(); }; UnitAI.prototype.HasPickupOrder = function(entity) { return this.orderQueue.some(order => order.type == "PickupUnit" && order.data.target == entity); }; UnitAI.prototype.OnPickupRequested = function(msg) { // First check if we already have such a request if (this.HasPickupOrder(msg.entity)) return; // Otherwise, insert the PickUp order after the last forced order this.PushOrderAfterForced("PickupUnit", { "target": msg.entity }); }; UnitAI.prototype.OnPickupCanceled = function(msg) { for (let i = 0; i < this.orderQueue.length; ++i) { if (this.orderQueue[i].type != "PickupUnit" || this.orderQueue[i].data.target != msg.entity) continue; if (i == 0) this.UnitFsm.ProcessMessage(this, {"type": "PickupCanceled", "data": msg}); else this.orderQueue.splice(i, 1); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); break; } }; // Wrapper function that sets up the normal and healer range queries. UnitAI.prototype.SetupRangeQueries = function() { this.SetupRangeQuery(); if (this.IsHealer()) this.SetupHealRangeQuery(); }; UnitAI.prototype.UpdateRangeQueries = function() { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) this.SetupRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losRangeQuery)); if (this.IsHealer() && this.losHealRangeQuery) this.SetupHealRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losHealRangeQuery)); }; // Set up a range query for all enemy and gaia units within LOS range // which can be attacked. // This should be called whenever our ownership changes. UnitAI.prototype.SetupRangeQuery = function(enable = true) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losRangeQuery) { cmpRangeManager.DestroyActiveQuery(this.losRangeQuery); this.losRangeQuery = undefined; } var cmpPlayer = QueryOwnerInterface(this.entity); // If we are being destructed (owner -1), creating a range query is pointless if (!cmpPlayer) return; // Exclude allies, and self // TODO: How to handle neutral players - Special query to attack military only? var players = cmpPlayer.GetEnemies(); var range = this.GetQueryRange(IID_Attack); this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal")); if (enable) cmpRangeManager.EnableActiveQuery(this.losRangeQuery); }; // Set up a range query for all own or ally units within LOS range // which can be healed. // This should be called whenever our ownership changes. UnitAI.prototype.SetupHealRangeQuery = function(enable = true) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.losHealRangeQuery) { cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery); this.losHealRangeQuery = undefined; } var cmpPlayer = QueryOwnerInterface(this.entity); // If we are being destructed (owner -1), creating a range query is pointless if (!cmpPlayer) return; var players = cmpPlayer.GetAllies(); var range = this.GetQueryRange(IID_Heal); this.losHealRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health, cmpRangeManager.GetEntityFlagMask("injured")); if (enable) cmpRangeManager.EnableActiveQuery(this.losHealRangeQuery); }; //// FSM linkage functions //// UnitAI.prototype.SetNextState = function(state) { this.UnitFsm.SetNextState(this, state); }; // This will make sure that the state is always entered even if this means leaving it and reentering it // This is so that a state can be reinitialized with new order data without having to switch to an intermediate state UnitAI.prototype.SetNextStateAlwaysEntering = function(state) { this.UnitFsm.SetNextStateAlwaysEntering(this, state); }; UnitAI.prototype.DeferMessage = function(msg) { this.UnitFsm.DeferMessage(this, msg); }; UnitAI.prototype.GetCurrentState = function() { return this.UnitFsm.GetCurrentState(this); }; UnitAI.prototype.FsmStateNameChanged = function(state) { Engine.PostMessage(this.entity, MT_UnitAIStateChanged, { "to": state }); }; /** * Call when the current order has been completed (or failed). * Removes the current order from the queue, and processes the * next one (if any). Returns false and defaults to IDLE * if there are no remaining orders. */ UnitAI.prototype.FinishOrder = function() { if (!this.orderQueue.length) { var stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(this.entity); error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack); } this.orderQueue.shift(); this.order = this.orderQueue[0]; if (this.orderQueue.length) { let ret = this.UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data} ); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // If the order was rejected then immediately take it off // and process the remaining queue if (ret && ret.discardOrder) return this.FinishOrder(); // Otherwise we've successfully processed a new order return true; } else { this.SetNextState("IDLE"); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // Check if there are queued formation orders if (this.IsFormationMember()) { let cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpUnitAI) { // Inform the formation controller that we finished this task this.finishedOrder = true; // We don't want to carry out the default order // if there are still queued formation orders left if (cmpUnitAI.GetOrders().length > 1) return true; } } return false; } }; /** * Add an order onto the back of the queue, * and execute it if we didn't already have an order. */ UnitAI.prototype.PushOrder = function(type, data) { var order = { "type": type, "data": data }; this.orderQueue.push(order); // If we didn't already have an order, then process this new one if (this.orderQueue.length == 1) { this.order = order; let ret = this.UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data} ); // If the order was rejected then immediately take it off // and process the remaining queue if (ret && ret.discardOrder) this.FinishOrder(); } Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; /** * Add an order onto the front of the queue, * and execute it immediately. */ UnitAI.prototype.PushOrderFront = function(type, data) { var order = { "type": type, "data": data }; // If current order is cheering then add new order after it // same thing if current order if packing/unpacking if (this.order && this.order.type == "Cheering") { var cheeringOrder = this.orderQueue.shift(); this.orderQueue.unshift(cheeringOrder, order); } else if (this.order && this.IsPacking()) { var packingOrder = this.orderQueue.shift(); this.orderQueue.unshift(packingOrder, order); } else { this.orderQueue.unshift(order); this.order = order; let ret = this.UnitFsm.ProcessMessage(this, {"type": "Order."+this.order.type, "data": this.order.data} ); // If the order was rejected then immediately take it off again; // assume the previous active order is still valid (the short-lived // new order hasn't changed state or anything) so we can carry on // as if nothing had happened if (ret && ret.discardOrder) { this.orderQueue.shift(); this.order = this.orderQueue[0]; } } Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; /** * Insert an order after the last forced order onto the queue * and after the other orders of the same type */ UnitAI.prototype.PushOrderAfterForced = function(type, data) { if (!this.order || ((!this.order.data || !this.order.data.force) && this.order.type != type)) this.PushOrderFront(type, data); else { for (let i = 1; i < this.orderQueue.length; ++i) { if (this.orderQueue[i].data && this.orderQueue[i].data.force) continue; if (this.orderQueue[i].type == type) continue; this.orderQueue.splice(i, 0, {"type": type, "data": data}); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); return; } this.PushOrder(type, data); } Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; UnitAI.prototype.ReplaceOrder = function(type, data) { // Remember the previous work orders to be able to go back to them later if required if (data && data.force) { if (this.IsFormationController()) this.CallMemberFunction("UpdateWorkOrders", [type]); else this.UpdateWorkOrders(type); } let garrisonHolder = this.IsGarrisoned() && type != "Ungarrison" ? this.GetGarrisonHolder() : null; // Special cases of orders that shouldn't be replaced: // 1. Cheering - we're invulnerable, add order after we finish // 2. Packing/unpacking - we're immobile, add order after we finish (unless it's cancel) // TODO: maybe a better way of doing this would be to use priority levels if (this.order && this.order.type == "Cheering") { var order = { "type": type, "data": data }; var cheeringOrder = this.orderQueue.shift(); this.orderQueue = [cheeringOrder, order]; } else if (this.IsPacking() && type != "CancelPack" && type != "CancelUnpack") { var order = { "type": type, "data": data }; var packingOrder = this.orderQueue.shift(); this.orderQueue = [packingOrder, order]; } else { this.orderQueue = []; this.PushOrder(type, data); } if (garrisonHolder) this.PushOrder("Garrison", { "target": garrisonHolder }); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; UnitAI.prototype.GetOrders = function() { return this.orderQueue.slice(); }; UnitAI.prototype.AddOrders = function(orders) { orders.forEach(order => this.PushOrder(order.type, order.data)); }; UnitAI.prototype.GetOrderData = function() { var orders = []; for (let order of this.orderQueue) if (order.data) orders.push(clone(order.data)); return orders; }; UnitAI.prototype.UpdateWorkOrders = function(type) { // Under alert, remembered work orders won't be forgotten if (this.IsUnderAlert()) return; var isWorkType = type => type == "Gather" || type == "Trade" || type == "Repair" || type == "ReturnResource"; // If we are being re-affected to a work order, forget the previous ones if (isWorkType(type)) { this.workOrders = []; return; } // Then if we already have work orders, keep them if (this.workOrders.length) return; // First if the unit is in a formation, get its workOrders from it if (this.IsFormationMember()) { var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpUnitAI) { for (var i = 0; i < cmpUnitAI.orderQueue.length; ++i) { if (isWorkType(cmpUnitAI.orderQueue[i].type)) { this.workOrders = cmpUnitAI.orderQueue.slice(i); return; } } } } // If nothing found, take the unit orders for (var i = 0; i < this.orderQueue.length; ++i) { if (isWorkType(this.orderQueue[i].type)) { this.workOrders = this.orderQueue.slice(i); return; } } }; UnitAI.prototype.BackToWork = function() { if (this.workOrders.length == 0) return false; if (this.IsGarrisoned()) { let cmpGarrisonHolder = Engine.QueryInterface(this.GetGarrisonHolder(), IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.PerformEject([this.entity], false)) return false; } // Clear the order queue considering special orders not to avoid if (this.order && this.order.type == "Cheering") { var cheeringOrder = this.orderQueue.shift(); this.orderQueue = [cheeringOrder]; } else this.orderQueue = []; this.AddOrders(this.workOrders); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); // And if the unit is in a formation, remove it from the formation if (this.IsFormationMember()) { var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers([this.entity]); } this.workOrders = []; return true; }; UnitAI.prototype.HasWorkOrders = function() { return this.workOrders.length > 0; }; UnitAI.prototype.GetWorkOrders = function() { return this.workOrders; }; UnitAI.prototype.SetWorkOrders = function(orders) { this.workOrders = orders; }; UnitAI.prototype.TimerHandler = function(data, lateness) { // Reset the timer if (data.timerRepeat === undefined) this.timer = undefined; this.UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness}); }; /** * Set up the UnitAI timer to run after 'offset' msecs, and then * every 'repeat' msecs until StopTimer is called. A "Timer" message * will be sent each time the timer runs. */ UnitAI.prototype.StartTimer = function(offset, repeat) { if (this.timer) error("Called StartTimer when there's already an active timer"); var data = { "timerRepeat": repeat }; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); if (repeat === undefined) this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data); else this.timer = cmpTimer.SetInterval(this.entity, IID_UnitAI, "TimerHandler", offset, repeat, data); }; /** * Stop the current UnitAI timer. */ UnitAI.prototype.StopTimer = function() { if (!this.timer) return; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; }; //// Message handlers ///// UnitAI.prototype.OnMotionChanged = function(msg) { if (msg.starting && !msg.error) this.UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg}); else if (!msg.starting || msg.error) this.UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg}); }; UnitAI.prototype.OnGlobalConstructionFinished = function(msg) { // TODO: This is a bit inefficient since every unit listens to every // construction message - ideally we could scope it to only the one we're building this.UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg}); }; UnitAI.prototype.OnGlobalEntityRenamed = function(msg) { let changed = false; for (let order of this.orderQueue) { if (order.data && order.data.target && order.data.target == msg.entity) { changed = true; order.data.target = msg.newentity; } if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity) { changed = true; order.data.formationTarget = msg.newentity; } } if (changed) Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; UnitAI.prototype.OnAttacked = function(msg) { this.UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg}); }; UnitAI.prototype.OnGuardedAttacked = function(msg) { this.UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data}); }; UnitAI.prototype.OnHealthChanged = function(msg) { this.UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to}); }; UnitAI.prototype.OnRangeUpdate = function(msg) { if (msg.tag == this.losRangeQuery) this.UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg}); else if (msg.tag == this.losHealRangeQuery) this.UnitFsm.ProcessMessage(this, {"type": "LosHealRangeUpdate", "data": msg}); }; UnitAI.prototype.OnPackFinished = function(msg) { this.UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed}); }; //// Helper functions to be called by the FSM //// UnitAI.prototype.GetWalkSpeed = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.GetWalkSpeed(); }; UnitAI.prototype.GetRunSpeed = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); var runSpeed = cmpUnitMotion.GetRunSpeed(); var walkSpeed = cmpUnitMotion.GetWalkSpeed(); if (runSpeed <= walkSpeed) return runSpeed; var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); var health = cmpHealth.GetHitpoints()/cmpHealth.GetMaxHitpoints(); return (health*runSpeed + (1-health)*walkSpeed); }; /** * Returns true if the target exists and has non-zero hitpoints. */ UnitAI.prototype.TargetIsAlive = function(ent) { var cmpFormation = Engine.QueryInterface(ent, IID_Formation); if (cmpFormation) return true; var cmpHealth = QueryMiragedInterface(ent, IID_Health); return cmpHealth && cmpHealth.GetHitpoints() != 0; }; /** * Returns true if the target exists and needs to be killed before * beginning to gather resources from it. */ UnitAI.prototype.MustKillGatherTarget = function(ent) { var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); if (!cmpResourceSupply) return false; if (!cmpResourceSupply.GetKillBeforeGather()) return false; return this.TargetIsAlive(ent); }; /** * Returns the entity ID of the nearest resource supply where the given * filter returns true, or undefined if none can be found. * if target if given, the nearest is computed versus this target position. * TODO: extend this to exclude resources that already have lots of * gatherers. */ UnitAI.prototype.FindNearbyResource = function(filter, target) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - if (!cmpOwnership || cmpOwnership.GetOwner() == -1) + if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return undefined; var owner = cmpOwnership.GetOwner(); // We accept resources owned by Gaia or any player var players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); var range = 64; // TODO: what's a sensible number? var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let entity = this.entity; if (target) { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) entity = target; } var nearby = cmpRangeManager.ExecuteQuery(entity, 0, range, players, IID_ResourceSupply); return nearby.find(ent => { if (!this.CanGather(ent)) return false; var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); var type = cmpResourceSupply.GetType(); var amount = cmpResourceSupply.GetCurrentAmount(); var template = cmpTemplateManager.GetCurrentTemplateName(ent); // Remove "resource|" prefix from template names, if present. if (template.indexOf("resource|") != -1) template = template.slice(9); return amount > 0 && cmpResourceSupply.IsAvailable(owner, this.entity) && filter(ent, type, template); }); }; /** * Returns the entity ID of the nearest resource dropsite that accepts * the given type, or undefined if none can be found. */ UnitAI.prototype.FindNearestDropsite = function(genericType) { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - if (!cmpOwnership || cmpOwnership.GetOwner() == -1) + if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return undefined; let cmpPosition = Engine.QueryInterface(this.entity, IID_Position) if (!cmpPosition) return undefined; let pos = cmpPosition.GetPosition2D(); let bestDropsite; let bestDist = Infinity; // Maximum distance a point on an obstruction can be from the center of the obstruction. let maxDifference = 40; // Find dropsites owned by this unit's player or allied ones if allowed. let owner = cmpOwnership.GetOwner(); let cmpPlayer = QueryOwnerInterface(this.entity); let players = cmpPlayer && cmpPlayer.HasSharedDropsites() ? cmpPlayer.GetMutualAllies() : [owner]; let nearbyDropsites = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite); let isShip = Engine.QueryInterface(this.entity, IID_Identity).HasClass("Ship"); let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); for (let dropsite of nearbyDropsites) { // Ships are unable to reach land dropsites and shouldn't attempt to do so. if (isShip && !Engine.QueryInterface(dropsite, IID_Identity).HasClass("Naval")) continue; let cmpResourceDropsite = Engine.QueryInterface(dropsite, IID_ResourceDropsite); if (!cmpResourceDropsite.AcceptsType(genericType)) continue; if (Engine.QueryInterface(dropsite, IID_Ownership).GetOwner() != owner && !cmpResourceDropsite.IsShared()) continue; // The range manager sorts entities by the distance to their center, // but we want the distance to the point where resources will be dropped off. let dist = cmpObstructionManager.DistanceToPoint(dropsite, pos.x, pos.y); if (dist == -1) continue; if (dist < bestDist) { bestDropsite = dropsite; bestDist = dist; } else if (dist > bestDist + maxDifference) break; } return bestDropsite; }; /** * Returns the entity ID of the nearest building that needs to be constructed, * or undefined if none can be found close enough. */ UnitAI.prototype.FindNearbyFoundation = function() { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - if (!cmpOwnership || cmpOwnership.GetOwner() == -1) + if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return undefined; // Find buildings owned by this unit's player var players = [cmpOwnership.GetOwner()]; var range = 64; // TODO: what's a sensible number? var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, IID_Foundation); // Skip foundations that are already complete. (This matters since // we process the ConstructionFinished message before the foundation // we're working on has been deleted.) return nearby.find(ent => !Engine.QueryInterface(ent, IID_Foundation).IsFinished()); }; /** * Returns the entity ID of the nearest building in which the unit can garrison, * or undefined if none can be found close enough. */ UnitAI.prototype.FindNearbyGarrisonHolder = function() { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - if (!cmpOwnership || cmpOwnership.GetOwner() == -1) + if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) return undefined; // Find buildings owned by this unit's player var players = [cmpOwnership.GetOwner()]; var range = 128; // TODO: what's a sensible number? var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, IID_GarrisonHolder); return nearby.find(ent => { // We only want to garrison in buildings, not in moving units like ships,... if (Engine.QueryInterface(ent, IID_UnitAI)) return false; var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); return cmpGarrisonHolder.IsAllowedToGarrison(this.entity) && !cmpGarrisonHolder.IsFull(); }); }; /** * Play a sound appropriate to the current entity. */ UnitAI.prototype.PlaySound = function(name) { // If we're a formation controller, use the sounds from our first member if (this.IsFormationController()) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); var member = cmpFormation.GetPrimaryMember(); if (member) PlaySound(name, member); } else { // Otherwise use our own sounds PlaySound(name, this.entity); } }; /* * Set a visualActor animation variant. * By changing the animation variant, you can change animations based on unitAI state. * If there are no specific variants or the variant doesn't exist in the actor, * the actor fallbacks to any existing animation. * @param type if present, switch to a specific animation variant. */ UnitAI.prototype.SetAnimationVariant = function(type) { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SetVariant("animationVariant", type); return; }; /* * Reset the animation variant to default behavior * Default behavior is to pick a resource-carrying variant if resources are being carried. * Otherwise pick nothing in particular. */ UnitAI.prototype.SetDefaultAnimationVariant = function() { let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) { this.SetAnimationVariant(""); return; } let type = cmpResourceGatherer.GetLastCarriedType(); if (type) { let typename = "carry_" + type.generic; // Special case for meat if (type.specific == "meat") typename = "carry_" + type.specific; this.SetAnimationVariant(typename); return; } this.SetAnimationVariant(""); }; UnitAI.prototype.SelectAnimation = function(name, once, speed, sound) { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; // Special case: the "move" animation gets turned into a special // movement mode that deals with speeds and walk/run automatically if (name == "move") { // Speed to switch from walking to running animations var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2; cmpVisual.SelectMovementAnimation(runThreshold); return; } var soundgroup; if (sound) { var cmpSound = Engine.QueryInterface(this.entity, IID_Sound); if (cmpSound) soundgroup = cmpSound.GetSoundGroup(sound); } // Set default values if unspecified if (once === undefined) once = false; if (speed === undefined) speed = 1.0; if (soundgroup === undefined) soundgroup = ""; cmpVisual.SelectAnimation(name, once, speed, soundgroup); }; UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime) { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SetAnimationSyncRepeat(repeattime); cmpVisual.SetAnimationSyncOffset(actiontime); }; UnitAI.prototype.StopMoving = function() { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpUnitMotion.StopMoving(); }; UnitAI.prototype.MoveToPoint = function(x, z) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToPointRange(x, z, 0, 0); }; UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax); }; UnitAI.prototype.MoveToTarget = function(target) { if (!this.CheckTargetVisible(target)) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, 0, 0); }; UnitAI.prototype.MoveToTargetRange = function(target, iid, type) { if (!this.CheckTargetVisible(target) || this.IsTurret()) return false; var cmpRanged = Engine.QueryInterface(this.entity, iid); if (!cmpRanged) return false; var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; /** * Move unit so we hope the target is in the attack range * for melee attacks, this goes straight to the default range checks * for ranged attacks, the parabolic range is used */ UnitAI.prototype.MoveToTargetAttackRange = function(target, type) { // for formation members, the formation will take care of the range check if (this.IsFormationMember()) { var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation()) return false; } var cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) target = cmpFormation.GetClosestMember(this.entity); if (type != "Ranged") return this.MoveToTargetRange(target, IID_Attack, type); if (!this.CheckTargetVisible(target)) return false; var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); var range = cmpAttack.GetRange(type); var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!thisCmpPosition.IsInWorld()) return false; var s = thisCmpPosition.GetPosition(); var targetCmpPosition = Engine.QueryInterface(target, IID_Position); if (!targetCmpPosition.IsInWorld()) return false; var t = targetCmpPosition.GetPosition(); // h is positive when I'm higher than the target var h = s.y-t.y+range.elevationBonus; // No negative roots please if (h>-range.max/2) var parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h); else // return false? Or hope you come close enough? var parabolicMaxRange = 0; //return false; // the parabole changes while walking, take something in the middle var guessedMaxRange = (range.max + parabolicMaxRange)/2; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange)) return true; // if that failed, try closer return cmpUnitMotion.MoveToTargetRange(target, range.min, Math.min(range.max, parabolicMaxRange)); }; UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max) { if (!this.CheckTargetVisible(target)) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, min, max); }; UnitAI.prototype.MoveToGarrisonRange = function(target) { if (!this.CheckTargetVisible(target)) return false; var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); if (!cmpGarrisonHolder) return false; var range = cmpGarrisonHolder.GetLoadingRange(); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max); }; UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInPointRange(x, z, min, max); }; UnitAI.prototype.CheckTargetRange = function(target, iid, type) { var cmpRanged = Engine.QueryInterface(this.entity, iid); if (!cmpRanged) return false; var range = cmpRanged.GetRange(type); var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInTargetRange(target, range.min, range.max); }; /** * Check if the target is inside the attack range * For melee attacks, this goes straigt to the regular range calculation * For ranged attacks, the parabolic formula is used to accout for bigger ranges * when the target is lower, and smaller ranges when the target is higher */ UnitAI.prototype.CheckTargetAttackRange = function(target, type) { // for formation members, the formation will take care of the range check if (this.IsFormationMember()) { var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation() && cmpFormationUnitAI.order.data.target == target) return true; } var cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) target = cmpFormation.GetClosestMember(this.entity); if (type != "Ranged") return this.CheckTargetRange(target, IID_Attack, type); var targetCmpPosition = Engine.QueryInterface(target, IID_Position); if (!targetCmpPosition || !targetCmpPosition.IsInWorld()) return false; var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); var range = cmpAttack.GetRange(type); var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!thisCmpPosition.IsInWorld()) return false; var s = thisCmpPosition.GetPosition(); var t = targetCmpPosition.GetPosition(); var h = s.y-t.y+range.elevationBonus; var maxRangeSq = 2*range.max*(h + range.max/2); if (maxRangeSq < 0) return false; var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInTargetRange(target, range.min, Math.sqrt(maxRangeSq)); }; UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInTargetRange(target, min, max); }; UnitAI.prototype.CheckGarrisonRange = function(target) { var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); if (!cmpGarrisonHolder) return false; var range = cmpGarrisonHolder.GetLoadingRange(); var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (cmpObstruction) range.max += cmpObstruction.GetUnitRadius()*1.5; // multiply by something larger than sqrt(2) var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); return cmpUnitMotion.IsInTargetRange(target, range.min, range.max); }; /** * Returns true if the target entity is visible through the FoW/SoD. */ UnitAI.prototype.CheckTargetVisible = function(target) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return false; var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) return false; // Entities that are hidden and miraged are considered visible var cmpFogging = Engine.QueryInterface(target, IID_Fogging); if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner())) return true; if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) == "hidden") return false; // Either visible directly, or visible in fog return true; }; UnitAI.prototype.FaceTowardsTarget = function(target) { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; var targetpos = cmpTargetPosition.GetPosition2D(); var angle = cmpPosition.GetPosition2D().angleTo(targetpos); var rot = cmpPosition.GetRotation(); var delta = (rot.y - angle + Math.PI) % (2 * Math.PI) - Math.PI; if (Math.abs(delta) > 0.2) { var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.FaceTowardsPoint(targetpos.x, targetpos.y); } }; UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type) { var cmpRanged = Engine.QueryInterface(this.entity, iid); var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(type); var cmpPosition = Engine.QueryInterface(target, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return false; var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return false; var halfvision = cmpVision.GetRange() / 2; var pos = cmpPosition.GetPosition(); var heldPosition = this.heldPosition; if (heldPosition === undefined) heldPosition = {"x": pos.x, "z": pos.z}; return Math.euclidDistance2D(pos.x, pos.z, heldPosition.x, heldPosition.z) < halfvision + range.max; }; UnitAI.prototype.CheckTargetIsInVisionRange = function(target) { var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return false; var range = cmpVision.GetRange(); var distance = DistanceBetweenEntities(this.entity, target); return distance < range; }; UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return undefined; return cmpAttack.GetBestAttackAgainst(target, allowCapture); }; /** * Try to find one of the given entities which can be attacked, * and start attacking it. * Returns true if it found something to attack. */ UnitAI.prototype.AttackVisibleEntity = function(ents) { var target = ents.find(target => this.CanAttack(target)); if (!target) return false; this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true }); return true; }; /** * Try to find one of the given entities which can be attacked * and which is close to the hold position, and start attacking it. * Returns true if it found something to attack. */ UnitAI.prototype.AttackEntityInZone = function(ents) { var target = ents.find(target => this.CanAttack(target) && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true)) && (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target)) ); if (!target) return false; this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true }); return true; }; /** * Try to respond appropriately given our current stance, * given a list of entities that match our stance's target criteria. * Returns true if it responded. */ UnitAI.prototype.RespondToTargetedEntities = function(ents) { if (!ents.length) return false; if (this.GetStance().respondChase) return this.AttackVisibleEntity(ents); if (this.GetStance().respondStandGround) return this.AttackVisibleEntity(ents); if (this.GetStance().respondHoldGround) return this.AttackEntityInZone(ents); if (this.GetStance().respondFlee) { this.PushOrderFront("Flee", { "target": ents[0], "force": false }); return true; } return false; }; /** * Try to respond to healable entities. * Returns true if it responded. */ UnitAI.prototype.RespondToHealableEntities = function(ents) { var ent = ents.find(ent => this.CanHeal(ent)); if (!ent) return false; this.PushOrderFront("Heal", { "target": ent, "force": false }); return true; }; /** * Returns true if we should stop following the target entity. */ UnitAI.prototype.ShouldAbandonChase = function(target, force, iid, type) { // Forced orders shouldn't be interrupted. if (force) return false; // If we are guarding/escorting, don't abandon as long as the guarded unit is in target range of the attacker if (this.isGuardOf) { var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI); var cmpAttack = Engine.QueryInterface(target, IID_Attack); if (cmpUnitAI && cmpAttack && cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type))) return false; } // Stop if we're in hold-ground mode and it's too far from the holding point if (this.GetStance().respondHoldGround) { if (!this.CheckTargetDistanceFromHeldPosition(target, iid, type)) return true; } // Stop if it's left our vision range, unless we're especially persistent if (!this.GetStance().respondChaseBeyondVision) { if (!this.CheckTargetIsInVisionRange(target)) return true; } // (Note that CCmpUnitMotion will detect if the target is lost in FoW, // and will continue moving to its last seen position and then stop) return false; }; /* * Returns whether we should chase the targeted entity, * given our current stance. */ UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force) { if (this.IsTurret()) return false; // TODO: use special stances instead? var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack) return false; if (this.GetStance().respondChase) return true; // If we are guarding/escorting, chase at least as long as the guarded unit is in target range of the attacker if (this.isGuardOf) { var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI); var cmpAttack = Engine.QueryInterface(target, IID_Attack); if (cmpUnitAI && cmpAttack && cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type))) return true; } if (force) return true; return false; }; //// External interface functions //// UnitAI.prototype.SetFormationController = function(ent) { this.formationController = ent; // Set obstruction group, so we can walk through members // of our own formation (or ourself if not in formation) var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (cmpObstruction) { if (ent == INVALID_ENTITY) cmpObstruction.SetControlGroup(this.entity); else cmpObstruction.SetControlGroup(ent); } // If we were removed from a formation, let the FSM switch back to INDIVIDUAL if (ent == INVALID_ENTITY) this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" }); }; UnitAI.prototype.GetFormationController = function() { return this.formationController; }; UnitAI.prototype.SetLastFormationTemplate = function(template) { this.lastFormationTemplate = template; }; UnitAI.prototype.GetLastFormationTemplate = function() { return this.lastFormationTemplate; }; UnitAI.prototype.MoveIntoFormation = function(cmd) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) return; var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); // Add new order to move into formation at the current position this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true }); }; UnitAI.prototype.GetTargetPositions = function() { var targetPositions = []; for (var i = 0; i < this.orderQueue.length; ++i) { var order = this.orderQueue[i]; switch (order.type) { case "Walk": case "WalkAndFight": case "WalkToPointRange": case "MoveIntoFormation": case "GatherNearPosition": case "Patrol": targetPositions.push(new Vector2D(order.data.x, order.data.z)); break; // and continue the loop case "WalkToTarget": case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will. case "Guard": case "Flee": case "LeaveFoundation": case "Attack": case "Heal": case "Gather": case "ReturnResource": case "Repair": case "Garrison": // Find the target unit's position var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return targetPositions; targetPositions.push(cmpTargetPosition.GetPosition2D()); return targetPositions; case "Stop": return []; default: error("GetTargetPositions: Unrecognised order type '"+order.type+"'"); return []; } } return targetPositions; }; /** * Returns the estimated distance that this unit will travel before either * finishing all of its orders, or reaching a non-walk target (attack, gather, etc). * Intended for Formation to switch to column layout on long walks. */ UnitAI.prototype.ComputeWalkingDistance = function() { var distance = 0; var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return 0; // Keep track of the position at the start of each order var pos = cmpPosition.GetPosition2D(); var targetPositions = this.GetTargetPositions(); for (var i = 0; i < targetPositions.length; ++i) { distance += pos.distanceTo(targetPositions[i]); // Remember this as the start position for the next order pos = targetPositions[i]; } // Return the total distance to the end of the order queue return distance; }; UnitAI.prototype.AddOrder = function(type, data, queued) { if (this.expectedRoute) this.expectedRoute = undefined; if (queued) this.PushOrder(type, data); else { // May happen if an order arrives on the same turn the unit is garrisoned // in that case, just forget the order as this will lead to an infinite loop if (this.IsGarrisoned() && !this.IsTurret() && type != "Ungarrison") return; this.ReplaceOrder(type, data); } }; /** * Adds guard/escort order to the queue, forced by the player. */ UnitAI.prototype.Guard = function(target, queued) { if (!this.CanGuard()) { this.WalkToTarget(target, queued); return; } // if we already had an old guard order, do nothing if the target is the same // and the order is running, otherwise remove the previous order if (this.isGuardOf) { if (this.isGuardOf == target && this.order && this.order.type == "Guard") return; else this.RemoveGuard(); } this.AddOrder("Guard", { "target": target, "force": false }, queued); }; UnitAI.prototype.AddGuard = function(target) { if (!this.CanGuard()) return false; var cmpGuard = Engine.QueryInterface(target, IID_Guard); if (!cmpGuard) return false; // Do not allow to guard a unit already guarding var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.IsGuardOf()) return false; this.isGuardOf = target; this.guardRange = cmpGuard.GetRange(this.entity); cmpGuard.AddGuard(this.entity); return true; }; UnitAI.prototype.RemoveGuard = function() { if (!this.isGuardOf) return; let cmpGuard = Engine.QueryInterface(this.isGuardOf, IID_Guard); if (cmpGuard) cmpGuard.RemoveGuard(this.entity); this.guardRange = undefined; this.isGuardOf = undefined; if (!this.order) return; if (this.order.type == "Guard") this.UnitFsm.ProcessMessage(this, {"type": "RemoveGuard"}); else for (let i = 1; i < this.orderQueue.length; ++i) if (this.orderQueue[i].type == "Guard") this.orderQueue.splice(i, 1); Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; UnitAI.prototype.IsGuardOf = function() { return this.isGuardOf; }; UnitAI.prototype.SetGuardOf = function(entity) { // entity may be undefined this.isGuardOf = entity; }; UnitAI.prototype.CanGuard = function() { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Do not let a unit already guarded to guard. This would work in principle, // but would clutter the gui with too much buttons to take all cases into account var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard); if (cmpGuard && cmpGuard.GetEntities().length) return false; return this.template.CanGuard == "true"; }; UnitAI.prototype.CanPatrol = function() { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) return this.IsFormationController() || this.template.CanPatrol == "true"; }; /** * Adds walk order to queue, forced by the player. */ UnitAI.prototype.Walk = function(x, z, queued) { if (this.expectedRoute && queued) this.expectedRoute.push({ "x": x, "z": z }); else this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued); }; /** * Adds walk to point range order to queue, forced by the player. */ UnitAI.prototype.WalkToPointRange = function(x, z, min, max, queued) { this.AddOrder("Walk", { "x": x, "z": z, "min": min, "max": max, "force": true }, queued); }; /** * Adds stop order to queue, forced by the player. */ UnitAI.prototype.Stop = function(queued) { this.AddOrder("Stop", { "force": true }, queued); }; /** * Adds walk-to-target order to queue, this only occurs in response * to a player order, and so is forced. */ UnitAI.prototype.WalkToTarget = function(target, queued) { this.AddOrder("WalkToTarget", { "target": target, "force": true }, queued); }; /** * Adds walk-and-fight order to queue, this only occurs in response * to a player order, and so is forced. * If targetClasses is given, only entities matching the targetClasses can be attacked. */ UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, allowCapture = true, queued = false) { this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued); }; UnitAI.prototype.Patrol = function(x, z, targetClasses, allowCapture = true, queued = false) { if (!this.CanPatrol()) { this.Walk(x, z, queued); return; } this.AddOrder("Patrol", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued); }; /** * Adds leave foundation order to queue, treated as forced. */ UnitAI.prototype.LeaveFoundation = function(target) { // If we're already being told to leave a foundation, then // ignore this new request so we don't end up being too indecisive // to ever actually move anywhere // Ignore also the request if we are packing if (this.order && (this.order.type == "LeaveFoundation" || (this.order.type == "Flee" && this.order.data.target == target) || this.IsPacking())) return; this.PushOrderFront("LeaveFoundation", { "target": target, "force": true }); }; /** * Adds attack order to the queue, forced by the player. */ UnitAI.prototype.Attack = function(target, allowCapture = true, queued = false) { if (!this.CanAttack(target)) { // We don't want to let healers walk to the target unit so they can be easily killed. // Instead we just let them get into healing range. if (this.IsHealer()) this.MoveToTargetRange(target, IID_Heal); else this.WalkToTarget(target, queued); return; } this.AddOrder("Attack", { "target": target, "force": true, "allowCapture": allowCapture}, queued); }; /** * Adds garrison order to the queue, forced by the player. */ UnitAI.prototype.Garrison = function(target, queued) { if (target == this.entity) return; if (!this.CanGarrison(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Garrison", { "target": target, "force": true }, queued); }; /** * Adds ungarrison order to the queue. */ UnitAI.prototype.Ungarrison = function() { if (this.IsGarrisoned()) this.AddOrder("Ungarrison", null, false); }; /** * Adds autogarrison order to the queue (only used by ProductionQueue for auto-garrisoning * and Promotion when promoting already garrisoned entities). */ UnitAI.prototype.Autogarrison = function(target) { this.AddOrder("Autogarrison", { "target": target }, false); }; /** * Adds gather order to the queue, forced by the player * until the target is reached */ UnitAI.prototype.Gather = function(target, queued) { this.PerformGather(target, queued, true); }; /** * Internal function to abstract the force parameter. */ UnitAI.prototype.PerformGather = function(target, queued, force) { if (!this.CanGather(target)) { this.WalkToTarget(target, queued); return; } // Save the resource type now, so if the resource gets destroyed // before we process the order then we still know what resource // type to look for more of var type; var cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply); if (cmpResourceSupply) type = cmpResourceSupply.GetType(); else error("CanGather allowed gathering from invalid entity"); // Also save the target entity's template, so that if it's an animal, // we won't go from hunting slow safe animals to dangerous fast ones var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(target); // Remove "resource|" prefix from template name, if present. if (template.indexOf("resource|") != -1) template = template.slice(9); // Remember the position of our target, if any, in case it disappears // later and we want to head to its last known position var lastPos = undefined; var cmpPosition = Engine.QueryInterface(target, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) lastPos = cmpPosition.GetPosition(); this.AddOrder("Gather", { "target": target, "type": type, "template": template, "lastPos": lastPos, "force": force }, queued); }; /** * Adds gather-near-position order to the queue, not forced, so it can be * interrupted by attacks. */ UnitAI.prototype.GatherNearPosition = function(x, z, type, template, queued) { // Remove "resource|" prefix from template name, if present. if (template.indexOf("resource|") != -1) template = template.slice(9); if (this.IsFormationController() || Engine.QueryInterface(this.entity, IID_ResourceGatherer)) this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued); else this.AddOrder("Walk", { "x": x, "z": z, "force": false }, queued); }; /** * Adds heal order to the queue, forced by the player. */ UnitAI.prototype.Heal = function(target, queued) { if (!this.CanHeal(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Heal", { "target": target, "force": true }, queued); }; /** * Adds return resource order to the queue, forced by the player. */ UnitAI.prototype.ReturnResource = function(target, queued) { if (!this.CanReturnResource(target, true)) { this.WalkToTarget(target, queued); return; } this.AddOrder("ReturnResource", { "target": target, "force": true }, queued); }; /** * Adds trade order to the queue. Either walk to the first market, or * start a new route. Not forced, so it can be interrupted by attacks. * The possible route may be given directly as a SetupTradeRoute argument * if coming from a RallyPoint, or through this.expectedRoute if a user command. */ UnitAI.prototype.SetupTradeRoute = function(target, source, route, queued) { if (!this.CanTrade(target)) { this.WalkToTarget(target, queued); return; } var marketsChanged = this.SetTargetMarket(target, source); if (!marketsChanged) return; var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (cmpTrader.HasBothMarkets()) { let data = { "target": cmpTrader.GetFirstMarket(), "route": route, "force": false }; if (this.expectedRoute) { if (!route && this.expectedRoute.length) data.route = this.expectedRoute.slice(); this.expectedRoute = undefined; } if (this.IsFormationController()) { this.CallMemberFunction("AddOrder", ["Trade", data, queued]); var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (cmpFormation) cmpFormation.Disband(); } else this.AddOrder("Trade", data, queued); } else { if (this.IsFormationController()) this.CallMemberFunction("WalkToTarget", [cmpTrader.GetFirstMarket(), queued]); else this.WalkToTarget(cmpTrader.GetFirstMarket(), queued); this.expectedRoute = []; } }; UnitAI.prototype.SetTargetMarket = function(target, source) { var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); if (!cmpTrader) return false; var marketsChanged = cmpTrader.SetTargetMarket(target, source); if (this.IsFormationController()) this.CallMemberFunction("SetTargetMarket", [target, source]); return marketsChanged; }; UnitAI.prototype.SwitchMarketOrder = function(oldMarket, newMarket) { if (this.order && this.order.data && this.order.data.target && this.order.data.target == oldMarket) this.order.data.target = newMarket; }; UnitAI.prototype.MoveToMarket = function(targetMarket) { if (this.waypoints && this.waypoints.length > 1) { let point = this.waypoints.pop(); return this.MoveToPoint(point.x, point.z) || this.MoveToMarket(targetMarket); } this.waypoints = undefined; return this.MoveToTarget(targetMarket); }; UnitAI.prototype.PerformTradeAndMoveToNextMarket = function(currentMarket) { if (!this.CanTrade(currentMarket)) { this.StopTrading(); return; } if (!this.CheckTargetRange(currentMarket, IID_Trader)) { if (!this.MoveToMarket(currentMarket)) // If the current market is not reached try again this.StopTrading(); return; } let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); let nextMarket = cmpTrader.PerformTrade(currentMarket); let amount = cmpTrader.GetGoods().amount; if (!nextMarket || !amount || !amount.traderGain) { this.StopTrading(); return; } this.order.data.target = nextMarket; if (this.order.data.route && this.order.data.route.length) { this.waypoints = this.order.data.route.slice(); if (this.order.data.target == cmpTrader.GetSecondMarket()) this.waypoints.reverse(); this.waypoints.unshift(null); // additionnal dummy point for the market } if (this.MoveToMarket(nextMarket)) // We've started walking to the next market this.SetNextState("APPROACHINGMARKET"); else this.StopTrading(); }; UnitAI.prototype.MarketRemoved = function(market) { if (this.order && this.order.data && this.order.data.target && this.order.data.target == market) this.UnitFsm.ProcessMessage(this, { "type": "TradingCanceled", "market": market }); }; UnitAI.prototype.StopTrading = function() { this.StopMoving(); this.FinishOrder(); var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); cmpTrader.StopTrading(); }; /** * Adds repair/build order to the queue, forced by the player * until the target is reached */ UnitAI.prototype.Repair = function(target, autocontinue, queued) { if (!this.CanRepair(target)) { this.WalkToTarget(target, queued); return; } this.AddOrder("Repair", { "target": target, "autocontinue": autocontinue, "force": true }, queued); }; /** * Adds flee order to the queue, not forced, so it can be * interrupted by attacks. */ UnitAI.prototype.Flee = function(target, queued) { this.AddOrder("Flee", { "target": target, "force": false }, queued); }; /** * Adds cheer order to the queue. Forced so it won't be interrupted by attacks. */ UnitAI.prototype.Cheer = function() { this.AddOrder("Cheering", { "force": true }, false); }; UnitAI.prototype.Pack = function(queued) { // Check that we can pack if (this.CanPack()) this.AddOrder("Pack", { "force": true }, queued); }; UnitAI.prototype.Unpack = function(queued) { // Check that we can unpack if (this.CanUnpack()) this.AddOrder("Unpack", { "force": true }, queued); }; UnitAI.prototype.CancelPack = function(queued) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked()) this.AddOrder("CancelPack", { "force": true }, queued); }; UnitAI.prototype.CancelUnpack = function(queued) { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked()) this.AddOrder("CancelUnpack", { "force": true }, queued); }; UnitAI.prototype.SetStance = function(stance) { if (g_Stances[stance]) { this.stance = stance; Engine.PostMessage(this.entity, MT_UnitStanceChanged, { "to": this.stance }); } else error("UnitAI: Setting to invalid stance '"+stance+"'"); }; UnitAI.prototype.SwitchToStance = function(stance) { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); this.SetHeldPosition(pos.x, pos.z); this.SetStance(stance); // Stop moving if switching to stand ground // TODO: Also stop existing orders in a sensible way if (stance == "standground") this.StopMoving(); // Reset the range queries, since the range depends on stance. this.SetupRangeQueries(); }; UnitAI.prototype.SetTurretStance = function() { this.previousStance = undefined; if (this.GetStance().respondStandGround) return; for (let stance in g_Stances) { if (!g_Stances[stance].respondStandGround) continue; this.previousStance = this.GetStanceName(); this.SwitchToStance(stance); return; } }; UnitAI.prototype.ResetTurretStance = function() { if (!this.previousStance) return; this.SwitchToStance(this.previousStance); this.previousStance = undefined; }; /** * Resets losRangeQuery, and if there are some targets in range that we can * attack then we start attacking and this returns true; otherwise, returns false. */ UnitAI.prototype.FindNewTargets = function() { if (!this.losRangeQuery) return false; if (!this.GetStance().targetVisibleEnemies) return false; var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return this.AttackEntitiesByPreference(cmpRangeManager.ResetActiveQuery(this.losRangeQuery)); }; UnitAI.prototype.FindWalkAndFightTargets = function() { if (this.IsFormationController()) { var cmpUnitAI; var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); for (var ent of cmpFormation.members) { if (!(cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI))) continue; var targets = cmpUnitAI.GetTargetsFromUnit(); for (var targ of targets) { if (!cmpUnitAI.CanAttack(targ)) continue; if (this.order.data.targetClasses) { var cmpIdentity = Engine.QueryInterface(targ, IID_Identity); var targetClasses = this.order.data.targetClasses; if (targetClasses.attack && cmpIdentity && !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack)) continue; if (targetClasses.avoid && cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid)) continue; // Only used by the AIs to prevent some choices of targets if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) continue; } this.PushOrderFront("Attack", { "target": targ, "force": true, "allowCapture": this.order.data.allowCapture }); return true; } } return false; } var targets = this.GetTargetsFromUnit(); for (var targ of targets) { if (!this.CanAttack(targ)) continue; if (this.order.data.targetClasses) { var cmpIdentity = Engine.QueryInterface(targ, IID_Identity); var targetClasses = this.order.data.targetClasses; if (cmpIdentity && targetClasses.attack && !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack)) continue; if (cmpIdentity && targetClasses.avoid && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid)) continue; // Only used by the AIs to prevent some choices of targets if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) continue; } this.PushOrderFront("Attack", { "target": targ, "force": true, "allowCapture": this.order.data.allowCapture }); return true; } // healers on a walk-and-fight order should heal injured units if (this.IsHealer()) return this.FindNewHealTargets(); return false; }; UnitAI.prototype.GetTargetsFromUnit = function() { if (!this.losRangeQuery) return []; if (!this.GetStance().targetVisibleEnemies) return []; var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return []; var attackfilter = function(e) { var cmpOwnership = Engine.QueryInterface(e, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() > 0) return true; var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); }; var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var entities = cmpRangeManager.ResetActiveQuery(this.losRangeQuery); var targets = entities.filter(function (v) { return cmpAttack.CanAttack(v) && attackfilter(v); }) .sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); }); return targets; }; /** * Resets losHealRangeQuery, and if there are some targets in range that we can heal * then we start healing and this returns true; otherwise, returns false. */ UnitAI.prototype.FindNewHealTargets = function() { if (!this.losHealRangeQuery) return false; var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return this.RespondToHealableEntities(cmpRangeManager.ResetActiveQuery(this.losHealRangeQuery)); }; UnitAI.prototype.GetQueryRange = function(iid) { var ret = { "min": 0, "max": 0 }; if (this.GetStance().respondStandGround) { var cmpRanged = Engine.QueryInterface(this.entity, iid); if (!cmpRanged) return ret; var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetFullAttackRange(); ret.min = range.min; ret.max = range.max; } else if (this.GetStance().respondChase) { var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return ret; var range = cmpVision.GetRange(); ret.max = range; } else if (this.GetStance().respondHoldGround) { var cmpRanged = Engine.QueryInterface(this.entity, iid); if (!cmpRanged) return ret; var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetFullAttackRange(); var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return ret; var halfvision = cmpVision.GetRange() / 2; ret.max = range.max + halfvision; } // We probably have stance 'passive' and we wouldn't have a range, // but as it is the default for healers we need to set it to something sane. else if (iid === IID_Heal) { var cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (!cmpVision) return ret; var range = cmpVision.GetRange(); ret.max = range; } return ret; }; UnitAI.prototype.GetStance = function() { return g_Stances[this.stance]; }; UnitAI.prototype.GetPossibleStances = function() { if (this.IsTurret()) return []; return Object.keys(g_Stances); }; UnitAI.prototype.GetStanceName = function() { return this.stance; }; UnitAI.prototype.SetMoveSpeed = function(speed) { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); cmpMotion.SetSpeed(speed); }; UnitAI.prototype.SetHeldPosition = function(x, z) { this.heldPosition = {"x": x, "z": z}; }; UnitAI.prototype.SetHeldPositionOnEntity = function(entity) { var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; var pos = cmpPosition.GetPosition(); this.SetHeldPosition(pos.x, pos.z); }; UnitAI.prototype.GetHeldPosition = function() { return this.heldPosition; }; UnitAI.prototype.WalkToHeldPosition = function() { if (this.heldPosition) { this.AddOrder("Walk", { "x": this.heldPosition.x, "z": this.heldPosition.z, "force": false }, false); return true; } return false; }; //// Helper functions //// UnitAI.prototype.CanAttack = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttack(target); }; UnitAI.prototype.CanGarrison = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); if (!cmpGarrisonHolder) return false; // Verify that the target is owned by this entity's player or a mutual ally of this player var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target))) return false; // Don't let animals garrison for now // (If we want to support that, we'll need to change Order.Garrison so it // doesn't move the animal into an INVIDIDUAL.* state) if (this.IsAnimal()) return false; return true; }; UnitAI.prototype.CanGather = function(target) { if (this.IsTurret()) return false; // The target must be a valid resource supply, or the mirage of one. var cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply); if (!cmpResourceSupply) return false; // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Gather commands var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return false; // Verify that we can gather from this target if (!cmpResourceGatherer.GetTargetGatherRate(target)) return false; // No need to verify ownership as we should be able to gather from // a target regardless of ownership. // No need to call "cmpResourceSupply.IsAvailable()" either because that // would cause units to walk to full entities instead of choosing another one // nearby to gather from, which is undesirable. return true; }; UnitAI.prototype.CanHeal = function(target) { // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Heal commands var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); if (!cmpHeal) return false; // Verify that the target is alive if (!this.TargetIsAlive(target)) return false; // Verify that the target is owned by the same player as the entity or of an ally var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target))) return false; // Verify that the target is not unhealable (or at max health) var cmpHealth = Engine.QueryInterface(target, IID_Health); if (!cmpHealth || cmpHealth.IsUnhealable()) return false; // Verify that the target has no unhealable class var cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return false; if (MatchesClassList(cmpIdentity.GetClassesList(), cmpHeal.GetUnhealableClasses())) return false; // Verify that the target is a healable class if (MatchesClassList(cmpIdentity.GetClassesList(), cmpHeal.GetHealableClasses())) return true; return false; }; UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource) { if (this.IsTurret()) return false; // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to ReturnResource commands var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (!cmpResourceGatherer) return false; // Verify that the target is a dropsite var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite); if (!cmpResourceDropsite) return false; if (checkCarriedResource) { // Verify that we are carrying some resources, // and can return our current resource to this target var type = cmpResourceGatherer.GetMainCarryingType(); if (!type || !cmpResourceDropsite.AcceptsType(type)) return false; } // Verify that the dropsite is owned by this entity's player (or a mutual ally's if allowed) var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && IsOwnedByPlayer(cmpOwnership.GetOwner(), target)) return true; var cmpPlayer = QueryOwnerInterface(this.entity); return cmpPlayer && cmpPlayer.HasSharedDropsites() && cmpResourceDropsite.IsShared() && cmpOwnership && IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target); }; UnitAI.prototype.CanTrade = function(target) { if (this.IsTurret()) return false; // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Trade commands var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader); return cmpTrader && cmpTrader.CanTrade(target); }; UnitAI.prototype.CanRepair = function(target) { if (this.IsTurret()) return false; // Formation controllers should always respond to commands // (then the individual units can make up their own minds) if (this.IsFormationController()) return true; // Verify that we're able to respond to Repair (Builder) commands var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder); if (!cmpBuilder) return false; // Verify that the target can be either built or repaired var cmpFoundation = QueryMiragedInterface(target, IID_Foundation); var cmpRepairable = Engine.QueryInterface(target, IID_Repairable); if (!cmpFoundation && !cmpRepairable) return false; // Verify that the target is owned by an ally of this entity's player var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); return cmpOwnership && IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target); }; UnitAI.prototype.CanPack = function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); return (cmpPack && !cmpPack.IsPacking() && !cmpPack.IsPacked()); }; UnitAI.prototype.CanUnpack = function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); return (cmpPack && !cmpPack.IsPacking() && cmpPack.IsPacked()); }; UnitAI.prototype.IsPacking = function() { var cmpPack = Engine.QueryInterface(this.entity, IID_Pack); return (cmpPack && cmpPack.IsPacking()); }; //// Formation specific functions //// UnitAI.prototype.IsAttackingAsFormation = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttackAsFormation() && this.GetCurrentState() == "FORMATIONCONTROLLER.COMBAT.ATTACKING"; }; //// Animal specific functions //// UnitAI.prototype.MoveRandomly = function(distance) { // To minimize drift all across the map, animals describe circles // approximated by polygons. // And to avoid getting stuck in obstacles or narrow spaces, each side // of the polygon is obtained by trying to go away from a point situated // half a meter backwards of the current position, after rotation. // We also add a fluctuation on the length of each side of the polygon (dist) // which, in addition to making the move more random, helps escaping narrow spaces // with bigger values of dist. let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (!cmpPosition || !cmpPosition.IsInWorld() || !cmpUnitMotion) return; let pos = cmpPosition.GetPosition(); let ang = cmpPosition.GetRotation().y; if (!this.roamAngle) { this.roamAngle = (randBool() ? 1 : -1) * Math.PI / 6; ang -= this.roamAngle / 2; this.startAngle = ang; } else if (Math.abs((ang - this.startAngle + Math.PI) % (2 * Math.PI) - Math.PI) < Math.abs(this.roamAngle / 2)) this.roamAngle *= randBool() ? 1 : -1; let halfDelta = randFloat(this.roamAngle / 4, this.roamAngle * 3 / 4); // First half rotation to decrease the impression of immediate rotation ang += halfDelta; cmpUnitMotion.FaceTowardsPoint(pos.x + 0.5 * Math.sin(ang), pos.z + 0.5 * Math.cos(ang)); // Then second half of the rotation ang += halfDelta; let dist = randFloat(0.5, 1.5) * distance; cmpUnitMotion.MoveToPointRange(pos.x - 0.5 * Math.sin(ang), pos.z - 0.5 * Math.cos(ang), dist, dist); }; UnitAI.prototype.SetFacePointAfterMove = function(val) { var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); if (cmpMotion) cmpMotion.SetFacePointAfterMove(val); }; UnitAI.prototype.AttackEntitiesByPreference = function(ents) { if (!ents.length) return false; var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return false; var attackfilter = function(e) { var cmpOwnership = Engine.QueryInterface(e, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() > 0) return true; var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); }; let entsByPreferences = {}; let preferences = []; let entsWithoutPref = []; for (let ent of ents) { if (!attackfilter(ent)) continue; let pref = cmpAttack.GetPreference(ent); if (pref === null || pref === undefined) entsWithoutPref.push(ent); else if (!entsByPreferences[pref]) { preferences.push(pref); entsByPreferences[pref] = [ent]; } else entsByPreferences[pref].push(ent); } if (preferences.length) { preferences.sort((a, b) => a - b); for (let pref of preferences) if (this.RespondToTargetedEntities(entsByPreferences[pref])) return true; } return this.RespondToTargetedEntities(entsWithoutPref); }; /** * Call obj.funcname(args) on UnitAI components of all formation members. */ UnitAI.prototype.CallMemberFunction = function(funcname, args) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) return; cmpFormation.GetMembers().forEach(ent => { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI[funcname].apply(cmpUnitAI, args); }); }; /** * Call obj.functname(args) on UnitAI components of all formation members, * and return true if all calls return true. */ UnitAI.prototype.TestAllMemberFunction = function(funcname, args) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) return false; return cmpFormation.GetMembers().every(ent => { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); return cmpUnitAI[funcname].apply(cmpUnitAI, args); }); }; UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec); Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI); Index: ps/trunk/binaries/data/mods/public/simulation/components/VisionSharing.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/VisionSharing.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/VisionSharing.js (revision 20953) @@ -1,178 +1,178 @@ function VisionSharing() {} VisionSharing.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; VisionSharing.prototype.Init = function() { this.activated = false; this.shared = undefined; this.spyId = 0; this.spies = undefined; }; /** * As entities have not necessarily the VisionSharing component, it has to be activated * before use so that the rangeManager can register it */ VisionSharing.prototype.Activate = function() { if (this.activated) return; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() <= 0) return; this.shared = new Set([cmpOwnership.GetOwner()]); Engine.PostMessage(this.entity, MT_VisionSharingChanged, { "entity": this.entity, "player": cmpOwnership.GetOwner(), "add": true }); this.activated = true; }; VisionSharing.prototype.CheckVisionSharings = function() { let shared = new Set(); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - let owner = cmpOwnership ? cmpOwnership.GetOwner() : -1; + let owner = cmpOwnership ? cmpOwnership.GetOwner() : INVALID_PLAYER; if (owner >= 0) { // The owner has vision if (owner > 0) shared.add(owner); // Vision sharing due to garrisoned units let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (cmpGarrisonHolder) { for (let ent of cmpGarrisonHolder.GetEntities()) { let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!cmpEntOwnership) continue; let entOwner = cmpEntOwnership.GetOwner(); if (entOwner > 0 && entOwner != owner) { shared.add(entOwner); // if shared by another player than the owner and not yet activated, do it this.Activate(); } } } // vision sharing due to spies if (this.spies) for (let spy of this.spies.values()) if (spy > 0 && spy != owner) shared.add(spy); } if (!this.activated) return; // compare with previous vision sharing, and update if needed for (let player of shared) if (!this.shared.has(player)) Engine.PostMessage(this.entity, MT_VisionSharingChanged, { "entity": this.entity, "player": player, "add": true }); for (let player of this.shared) if (!shared.has(player)) Engine.PostMessage(this.entity, MT_VisionSharingChanged, { "entity": this.entity, "player": player, "add": false }); this.shared = shared; }; VisionSharing.prototype.IsBribable = function() { return this.template.Bribable == "true"; }; VisionSharing.prototype.OnGarrisonedUnitsChanged = function(msg) { this.CheckVisionSharings(); }; VisionSharing.prototype.OnOwnershipChanged = function(msg) { if (this.activated) this.CheckVisionSharings(); }; VisionSharing.prototype.AddSpy = function(player, timeLength) { if (!this.IsBribable()) return 0; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() == player || player <= 0) return 0; let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager || !cmpTechnologyManager.CanProduce("special/spy")) return 0; let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate("special/spy"); if (!IncurBribeCost(template, player, cmpOwnership.GetOwner(), false)) return 0; // If no duration given, take it from the spy template and scale it with the ent vision // When no duration argument nor in spy template, it is a permanent spy let duration = timeLength; if (!duration && template.VisionSharing && template.VisionSharing.Duration) { duration = ApplyValueModificationsToTemplate("VisionSharing/Duration", +template.VisionSharing.Duration, player, template); let cmpVision = Engine.QueryInterface(this.entity, IID_Vision); if (cmpVision) duration *= 60 / Math.max(30, cmpVision.GetRange()); } if (!this.spies) this.spies = new Map(); this.spies.set(++this.spyId, player); if (duration) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.SetTimeout(this.entity, IID_VisionSharing, "RemoveSpy", duration * 1000, { "id": this.spyId }); } this.Activate(); this.CheckVisionSharings(); // update statistics for successful bribes let cmpBribesStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker); if (cmpBribesStatisticsTracker) cmpBribesStatisticsTracker.IncreaseSuccessfulBribesCounter(); return this.spyId; }; VisionSharing.prototype.RemoveSpy = function(data) { this.spies.delete(data.id); this.CheckVisionSharings(); }; /** * Returns true if this entity share its vision with player */ VisionSharing.prototype.ShareVisionWith = function(player) { if (this.activated) return this.shared.has(player); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); return cmpOwnership && cmpOwnership.GetOwner() == player; }; Engine.RegisterComponentType(IID_VisionSharing, "VisionSharing", VisionSharing); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_EntityLimits.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_EntityLimits.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_EntityLimits.js (revision 20953) @@ -1,203 +1,203 @@ Engine.LoadComponentScript("interfaces/BuildRestrictions.js"); Engine.LoadComponentScript("interfaces/EntityLimits.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/TrainingRestrictions.js"); Engine.LoadComponentScript("interfaces/Player.js"); Engine.LoadComponentScript("EntityLimits.js"); let template ={ "Limits": { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 }, "LimitChangers": { "Tower": { "Monument": 1 } }, "LimitRemovers": { "Tower": { "RequiredTechs": { "_string": "TechA" } }, "Hero": { "RequiredClasses": { "_string": "Aegis" } } } }; AddMock(10, IID_Player, { "GetPlayerID": id => 1 }); AddMock(SYSTEM_ENTITY, IID_GuiInterface, { "PushNotification": () => {} }); let cmpEntityLimits = ConstructComponent(10, "EntityLimits", template); // Test getters TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimitChangers(), { "Tower": { "Monument": 1 } }); // Test training restrictions TS_ASSERT(cmpEntityLimits.AllowedToTrain("Hero")); TS_ASSERT(cmpEntityLimits.AllowedToTrain("Hero", 1)); TS_ASSERT(cmpEntityLimits.AllowedToTrain("Hero", 2)); for (let ent = 60; ent < 63; ++ent) { AddMock(ent, IID_TrainingRestrictions, { "GetCategory": () => "Hero" }); } -cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 60, "from": -1, "to": 1 }); +cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 60, "from": INVALID_PLAYER, "to": 1 }); cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 61, "from": 2, "to": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 2, "Champion": 0 }); TS_ASSERT(cmpEntityLimits.AllowedToTrain("Hero")); TS_ASSERT(!cmpEntityLimits.AllowedToTrain("Hero", 1)); // Restrictions can be enforced -cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 62, "from": -1, "to": 1 }); +cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 62, "from": INVALID_PLAYER, "to": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 3, "Champion": 0 }); for (let ent = 60; ent < 63; ++ent) - cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": ent, "from": 1, "to": -1 }); + cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": ent, "from": 1, "to": INVALID_PLAYER }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 }); // Test building restrictions AddMock(70, IID_BuildRestrictions, { "GetCategory": () => "Wonder" }); cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 70, "from": 3, "to": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 1, "Hero": 0, "Champion": 0 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 }); // AllowedToBuild is used after foundation placement, which are meant to be replaced TS_ASSERT(cmpEntityLimits.AllowedToBuild("Wonder")); -cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 70, "from": 1, "to": -1 }); +cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 70, "from": 1, "to": INVALID_PLAYER }); // Test limit changers AddMock(80, IID_Identity, { "GetClassesList": () => ["Monument"] }); AddMock(81, IID_Identity, { "GetClassesList": () => ["Monument"] }); -cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 80, "from": -1, "to": 1 }); +cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 80, "from": INVALID_PLAYER, "to": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5 + 1, "Wonder": 1, "Hero": 2, "Champion": 1 }); -cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 81, "from": 1, "to": -1 }); +cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 81, "from": 1, "to": INVALID_PLAYER }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 }); // Foundations don't change limits AddMock(81, IID_Foundation, {}); -cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 81, "from": -1, "to": 1 }); +cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 81, "from": INVALID_PLAYER, "to": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 }); -cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 81, "from": 1, "to": -1 }); +cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 81, "from": 1, "to": INVALID_PLAYER }); // Test limit removers by classes AddMock(90, IID_Identity, { "GetClassesList": () => ["Aegis"] }); -cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 90, "from": -1, "to": 1 }); +cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 90, "from": INVALID_PLAYER, "to": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": undefined, "Champion": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 }); AddMock(91, IID_TrainingRestrictions, { "GetCategory": () => "Hero" }); -cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 91, "from": -1, "to": 1 }); +cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 91, "from": INVALID_PLAYER, "to": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": undefined, "Champion": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 1, "Champion": 0 }); -cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 90, "from": 1, "to": -1 }); +cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 90, "from": 1, "to": INVALID_PLAYER }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 1, "Champion": 0 }); // Edge case AddMock(92, IID_TrainingRestrictions, { "GetCategory": () => "Hero" }); AddMock(92, IID_Identity, { "GetClassesList": () => ["Aegis"] }); -cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 92, "from": -1, "to": 1 }); +cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 92, "from": INVALID_PLAYER, "to": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": undefined, "Champion": 1 }); TS_ASSERT(cmpEntityLimits.AllowedToTrain("Hero", 157)); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 2, "Champion": 0 }); -cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 91, "from": 1, "to": -1 }); -cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 92, "from": 1, "to": -1 }); +cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 91, "from": 1, "to": INVALID_PLAYER }); +cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 92, "from": 1, "to": INVALID_PLAYER }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 }); // Test AllowedToReplace AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "GetTemplate": name => { switch (name) { case "templateA": return { "TrainingRestrictions": { "Category": "Champion" } }; case "templateB": return { "TrainingRestrictions": { "Category": "Hero" } }; case "templateC": return { "BuildRestrictions": { "Category": "Wonder" } }; case "templateD": return { "BuildRestrictions": { "Category": "Tower" } }; default: return null; } }, "GetCurrentTemplateName": id => { switch (id) { case 100: return "templateA"; case 101: return "templateB"; case 102: return "templateC"; case 103: return "templateD"; default: return null; } } }); cmpEntityLimits.ChangeCount("Champion", 1) TS_ASSERT(cmpEntityLimits.AllowedToReplace(100, "templateA")) TS_ASSERT(!cmpEntityLimits.AllowedToReplace(101, "templateA")) cmpEntityLimits.ChangeCount("Champion", -1) cmpEntityLimits.ChangeCount("Tower", 5) TS_ASSERT(!cmpEntityLimits.AllowedToReplace(102, "templateD")) TS_ASSERT(cmpEntityLimits.AllowedToReplace(103, "templateD")) cmpEntityLimits.ChangeCount("Tower", -5) TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 }); // Test limit removers by tech cmpEntityLimits.UpdateLimitsFromTech("TechB"); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 }); cmpEntityLimits.UpdateLimitsFromTech("TechA"); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": undefined, "Wonder": 1, "Hero": 2, "Champion": 1 }); cmpEntityLimits.UpdateLimitsFromTech("TechA"); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": undefined, "Wonder": 1, "Hero": 2, "Champion": 1 }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_RallyPoint.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_RallyPoint.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_RallyPoint.js (revision 20953) @@ -1,80 +1,80 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/RallyPoint.js"); Engine.LoadComponentScript("RallyPoint.js"); function initialRallyPointTest(test_function) { ResetState(); let entityID = 123; let cmpRallyPoint = ConstructComponent(entityID, "RallyPoint", {}); TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(), []); TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), []); cmpRallyPoint.AddPosition(3, 1415); TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), [{ "x": 3, "z": 1415 }]); cmpRallyPoint.AddPosition(926, 535); TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), [{ "x": 3, "z": 1415 }, { "x": 926, "z": 535 }]); let targetID = 456; let myData = { "command": "write a unit test", "target": targetID }; cmpRallyPoint.AddData(myData); TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), [{ "x": 3, "z": 1415 }, { "x": 926, "z": 535 }]); TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(), [myData]); let targetID2 = 789; let myData2 = { "command": "this time really", "target": targetID2 }; cmpRallyPoint.AddData(myData2); TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(), [myData, myData2]); if (test_function(cmpRallyPoint)) { TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(), []); TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), []); } else { TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(), [myData, myData2]); TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), [{ "x": 3, "z": 1415 }, { "x": 926, "z": 535 }]); } } initialRallyPointTest((cmpRallyPoint) => {}); initialRallyPointTest((cmpRallyPoint) => { cmpRallyPoint.Unset() return true; }); initialRallyPointTest((cmpRallyPoint) => { cmpRallyPoint.Reset() return true; }); // Construction initialRallyPointTest((cmpRallyPoint) => { - cmpRallyPoint.OnOwnershipChanged({ "from": -1, "to": 1 }); + cmpRallyPoint.OnOwnershipChanged({ "from": INVALID_PLAYER, "to": 1 }); return false; }); // Capturing initialRallyPointTest((cmpRallyPoint) => { cmpRallyPoint.OnOwnershipChanged({ "from": 1, "to": 2 }); return true; }); // Destruction initialRallyPointTest((cmpRallyPoint) => { - cmpRallyPoint.OnOwnershipChanged({ "from": 2, "to": -1 }); + cmpRallyPoint.OnOwnershipChanged({ "from": 2, "to": INVALID_PLAYER }); return false; }); // Gaia initialRallyPointTest((cmpRallyPoint) => { cmpRallyPoint.OnOwnershipChanged({ "from": 2, "to": 0 }); return true; }); Index: ps/trunk/source/simulation2/system/ComponentManager.cpp =================================================================== --- ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 20952) +++ ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 20953) @@ -1,1187 +1,1188 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ComponentManager.h" #include "DynamicSubscription.h" #include "IComponent.h" #include "ParamNode.h" #include "SimContext.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpTemplateManager.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/scripting/JSInterface_VFS.h" /** * Used for script-only message types. */ class CMessageScripted : public CMessage { public: virtual int GetType() const { return mtid; } virtual const char* GetScriptHandlerName() const { return handlerName.c_str(); } virtual const char* GetScriptGlobalHandlerName() const { return globalHandlerName.c_str(); } virtual JS::Value ToJSVal(const ScriptInterface& UNUSED(scriptInterface)) const { return msg.get(); } CMessageScripted(const ScriptInterface& scriptInterface, int mtid, const std::string& name, JS::HandleValue msg) : mtid(mtid), handlerName("On" + name), globalHandlerName("OnGlobal" + name), msg(scriptInterface.GetJSRuntime(), msg) { } int mtid; std::string handlerName; std::string globalHandlerName; JS::PersistentRootedValue msg; }; CComponentManager::CComponentManager(CSimContext& context, shared_ptr rt, bool skipScriptFunctions) : m_NextScriptComponentTypeId(CID__LastNative), m_ScriptInterface("Engine", "Simulation", rt), m_SimContext(context), m_CurrentlyHotloading(false) { context.SetComponentManager(this); m_ScriptInterface.SetCallbackData(static_cast (this)); m_ScriptInterface.ReplaceNondeterministicRNG(m_RNG); // For component script tests, the test system sets up its own scripted implementation of // these functions, so we skip registering them here in those cases if (!skipScriptFunctions) { JSI_VFS::RegisterScriptFunctions_Simulation(m_ScriptInterface); m_ScriptInterface.RegisterFunction ("RegisterComponentType"); m_ScriptInterface.RegisterFunction ("RegisterSystemComponentType"); m_ScriptInterface.RegisterFunction ("ReRegisterComponentType"); m_ScriptInterface.RegisterFunction ("RegisterInterface"); m_ScriptInterface.RegisterFunction ("RegisterMessageType"); m_ScriptInterface.RegisterFunction ("RegisterGlobal"); m_ScriptInterface.RegisterFunction ("QueryInterface"); m_ScriptInterface.RegisterFunction, int, CComponentManager::Script_GetEntitiesWithInterface> ("GetEntitiesWithInterface"); m_ScriptInterface.RegisterFunction, int, CComponentManager::Script_GetComponentsWithInterface> ("GetComponentsWithInterface"); m_ScriptInterface.RegisterFunction ("PostMessage"); m_ScriptInterface.RegisterFunction ("BroadcastMessage"); m_ScriptInterface.RegisterFunction ("AddEntity"); m_ScriptInterface.RegisterFunction ("AddLocalEntity"); m_ScriptInterface.RegisterFunction ("DestroyEntity"); m_ScriptInterface.RegisterFunction ("FlushDestroyedEntities"); } // Globalscripts may use VFS script functions m_ScriptInterface.LoadGlobalScripts(); // Define MT_*, IID_* as script globals, and store their names #define MESSAGE(name) m_ScriptInterface.SetGlobal("MT_" #name, (int)MT_##name); #define INTERFACE(name) \ m_ScriptInterface.SetGlobal("IID_" #name, (int)IID_##name); \ m_InterfaceIdsByName[#name] = IID_##name; #define COMPONENT(name) #include "simulation2/TypeList.h" #undef MESSAGE #undef INTERFACE #undef COMPONENT m_ScriptInterface.SetGlobal("INVALID_ENTITY", (int)INVALID_ENTITY); + m_ScriptInterface.SetGlobal("INVALID_PLAYER", (int)INVALID_PLAYER); m_ScriptInterface.SetGlobal("SYSTEM_ENTITY", (int)SYSTEM_ENTITY); m_ComponentsByInterface.resize(IID__LastNative); ResetState(); } CComponentManager::~CComponentManager() { ResetState(); } void CComponentManager::LoadComponentTypes() { #define MESSAGE(name) \ RegisterMessageType(MT_##name, #name); #define INTERFACE(name) \ extern void RegisterComponentInterface_##name(ScriptInterface&); \ RegisterComponentInterface_##name(m_ScriptInterface); #define COMPONENT(name) \ extern void RegisterComponentType_##name(CComponentManager&); \ m_CurrentComponent = CID_##name; \ RegisterComponentType_##name(*this); #include "simulation2/TypeList.h" m_CurrentComponent = CID__Invalid; #undef MESSAGE #undef INTERFACE #undef COMPONENT } bool CComponentManager::LoadScript(const VfsPath& filename, bool hotload) { m_CurrentlyHotloading = hotload; CVFSFile file; PSRETURN loadOk = file.Load(g_VFS, filename); if (loadOk != PSRETURN_OK) // VFS will log the failed file and the reason return false; std::string content = file.DecodeUTF8(); // assume it's UTF-8 bool ok = m_ScriptInterface.LoadScript(filename, content); return ok; } void CComponentManager::Script_RegisterComponentType_Common(ScriptInterface::CxPrivate* pCxPrivate, int iid, const std::string& cname, JS::HandleValue ctor, bool reRegister, bool systemComponent) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); JSContext* cx = componentManager->m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); // Find the C++ component that wraps the interface int cidWrapper = componentManager->GetScriptWrapper(iid); if (cidWrapper == CID__Invalid) { componentManager->m_ScriptInterface.ReportError("Invalid interface id"); return; } const ComponentType& ctWrapper = componentManager->m_ComponentTypesById[cidWrapper]; bool mustReloadComponents = false; // for hotloading ComponentTypeId cid = componentManager->LookupCID(cname); if (cid == CID__Invalid) { if (reRegister) { std::string msg("ReRegistering component type that was not registered before '"+cname+"'"); componentManager->m_ScriptInterface.ReportError(msg.c_str()); return; } // Allocate a new cid number cid = componentManager->m_NextScriptComponentTypeId++; componentManager->m_ComponentTypeIdsByName[cname] = cid; if (systemComponent) componentManager->MarkScriptedComponentForSystemEntity(cid); } else { // Component type is already loaded, so do hotloading: if (!componentManager->m_CurrentlyHotloading && !reRegister) { std::string msg("Registering component type with already-registered name '"+cname+"'"); componentManager->m_ScriptInterface.ReportError(msg.c_str()); return; } const ComponentType& ctPrevious = componentManager->m_ComponentTypesById[cid]; // We can only replace scripted component types, not native ones if (ctPrevious.type != CT_Script) { std::string msg("Loading script component type with same name '"+cname+"' as native component"); componentManager->m_ScriptInterface.ReportError(msg.c_str()); return; } // We don't support changing the IID of a component type (it would require fiddling // around with m_ComponentsByInterface and being careful to guarantee uniqueness per entity) if (ctPrevious.iid != iid) { // ...though it only matters if any components exist with this type if (!componentManager->m_ComponentsByTypeId[cid].empty()) { componentManager->m_ScriptInterface.ReportError("Hotloading script component type mustn't change interface ID"); return; } } // Remove the old component type's message subscriptions std::map >::iterator it; for (it = componentManager->m_LocalMessageSubscriptions.begin(); it != componentManager->m_LocalMessageSubscriptions.end(); ++it) { std::vector& types = it->second; std::vector::iterator ctit = find(types.begin(), types.end(), cid); if (ctit != types.end()) types.erase(ctit); } for (it = componentManager->m_GlobalMessageSubscriptions.begin(); it != componentManager->m_GlobalMessageSubscriptions.end(); ++it) { std::vector& types = it->second; std::vector::iterator ctit = find(types.begin(), types.end(), cid); if (ctit != types.end()) types.erase(ctit); } mustReloadComponents = true; } std::string schema = ""; { JS::RootedValue prototype(cx); if (componentManager->m_ScriptInterface.GetProperty(ctor, "prototype", &prototype) && componentManager->m_ScriptInterface.HasProperty(prototype, "Schema")) { componentManager->m_ScriptInterface.GetProperty(prototype, "Schema", schema); } } // Construct a new ComponentType, using the wrapper's alloc functions ComponentType ct( CT_Script, iid, ctWrapper.alloc, ctWrapper.dealloc, cname, schema, DefPersistentRooted(cx, ctor) ); componentManager->m_ComponentTypesById[cid] = std::move(ct); componentManager->m_CurrentComponent = cid; // needed by Subscribe // Find all the ctor prototype's On* methods, and subscribe to the appropriate messages: JS::RootedValue protoVal(cx); if (!componentManager->m_ScriptInterface.GetProperty(ctor, "prototype", &protoVal)) return; // error std::vector methods; JS::RootedObject proto(cx); if (!protoVal.isObjectOrNull()) return; // error proto = protoVal.toObjectOrNull(); if (!componentManager->m_ScriptInterface.EnumeratePropertyNamesWithPrefix(protoVal, "On", methods)) return; // error for (std::vector::const_iterator it = methods.begin(); it != methods.end(); ++it) { std::string name = (*it).substr(2); // strip the "On" prefix // Handle "OnGlobalFoo" functions specially bool isGlobal = false; if (name.substr(0, 6) == "Global") { isGlobal = true; name = name.substr(6); } std::map::const_iterator mit = componentManager->m_MessageTypeIdsByName.find(name); if (mit == componentManager->m_MessageTypeIdsByName.end()) { std::string msg("Registered component has unrecognised '" + *it + "' message handler method"); componentManager->m_ScriptInterface.ReportError(msg.c_str()); return; } if (isGlobal) componentManager->SubscribeGloballyToMessageType(mit->second); else componentManager->SubscribeToMessageType(mit->second); } componentManager->m_CurrentComponent = CID__Invalid; if (mustReloadComponents) { // For every script component with this cid, we need to switch its // prototype from the old constructor's prototype property to the new one's const std::map& comps = componentManager->m_ComponentsByTypeId[cid]; std::map::const_iterator eit = comps.begin(); for (; eit != comps.end(); ++eit) { JS::RootedValue instance(cx, eit->second->GetJSInstance()); if (!instance.isNull()) { componentManager->m_ScriptInterface.SetPrototype(instance, protoVal); } } } } void CComponentManager::Script_RegisterComponentType(ScriptInterface::CxPrivate* pCxPrivate, int iid, const std::string& cname, JS::HandleValue ctor) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); componentManager->Script_RegisterComponentType_Common(pCxPrivate, iid, cname, ctor, false, false); componentManager->m_ScriptInterface.SetGlobal(cname.c_str(), ctor, componentManager->m_CurrentlyHotloading); } void CComponentManager::Script_RegisterSystemComponentType(ScriptInterface::CxPrivate* pCxPrivate, int iid, const std::string& cname, JS::HandleValue ctor) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); componentManager->Script_RegisterComponentType_Common(pCxPrivate, iid, cname, ctor, false, true); componentManager->m_ScriptInterface.SetGlobal(cname.c_str(), ctor, componentManager->m_CurrentlyHotloading); } void CComponentManager::Script_ReRegisterComponentType(ScriptInterface::CxPrivate* pCxPrivate, int iid, const std::string& cname, JS::HandleValue ctor) { Script_RegisterComponentType_Common(pCxPrivate, iid, cname, ctor, true, false); } void CComponentManager::Script_RegisterInterface(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); std::map::iterator it = componentManager->m_InterfaceIdsByName.find(name); if (it != componentManager->m_InterfaceIdsByName.end()) { // Redefinitions are fine (and just get ignored) when hotloading; otherwise // they're probably unintentional and should be reported if (!componentManager->m_CurrentlyHotloading) { std::string msg("Registering interface with already-registered name '"+name+"'"); componentManager->m_ScriptInterface.ReportError(msg.c_str()); } return; } // IIDs start at 1, so size+1 is the next unused one size_t id = componentManager->m_InterfaceIdsByName.size() + 1; componentManager->m_InterfaceIdsByName[name] = (InterfaceId)id; componentManager->m_ComponentsByInterface.resize(id+1); // add one so we can index by InterfaceId componentManager->m_ScriptInterface.SetGlobal(("IID_" + name).c_str(), (int)id); } void CComponentManager::Script_RegisterMessageType(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); std::map::iterator it = componentManager->m_MessageTypeIdsByName.find(name); if (it != componentManager->m_MessageTypeIdsByName.end()) { // Redefinitions are fine (and just get ignored) when hotloading; otherwise // they're probably unintentional and should be reported if (!componentManager->m_CurrentlyHotloading) { std::string msg("Registering message type with already-registered name '"+name+"'"); componentManager->m_ScriptInterface.ReportError(msg.c_str()); } return; } // MTIDs start at 1, so size+1 is the next unused one size_t id = componentManager->m_MessageTypeIdsByName.size() + 1; componentManager->RegisterMessageType((MessageTypeId)id, name.c_str()); componentManager->m_ScriptInterface.SetGlobal(("MT_" + name).c_str(), (int)id); } void CComponentManager::Script_RegisterGlobal(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name, JS::HandleValue value) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); // Set the value, and accept duplicates only if hotloading (otherwise it's an error, // in order to detect accidental duplicate definitions of globals) componentManager->m_ScriptInterface.SetGlobal(name.c_str(), value, componentManager->m_CurrentlyHotloading); } IComponent* CComponentManager::Script_QueryInterface(ScriptInterface::CxPrivate* pCxPrivate, int ent, int iid) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); IComponent* component = componentManager->QueryInterface((entity_id_t)ent, iid); return component; } std::vector CComponentManager::Script_GetEntitiesWithInterface(ScriptInterface::CxPrivate* pCxPrivate, int iid) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); std::vector ret; const InterfaceListUnordered& ents = componentManager->GetEntitiesWithInterfaceUnordered(iid); for (InterfaceListUnordered::const_iterator it = ents.begin(); it != ents.end(); ++it) if (!ENTITY_IS_LOCAL(it->first)) ret.push_back(it->first); std::sort(ret.begin(), ret.end()); return ret; } std::vector CComponentManager::Script_GetComponentsWithInterface(ScriptInterface::CxPrivate* pCxPrivate, int iid) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); std::vector ret; InterfaceList ents = componentManager->GetEntitiesWithInterface(iid); for (InterfaceList::const_iterator it = ents.begin(); it != ents.end(); ++it) ret.push_back(it->second); // TODO: maybe we should exclude local entities return ret; } CMessage* CComponentManager::ConstructMessage(int mtid, JS::HandleValue data) { if (mtid == MT__Invalid || mtid > (int)m_MessageTypeIdsByName.size()) // (IDs start at 1 so use '>' here) LOGERROR("PostMessage with invalid message type ID '%d'", mtid); if (mtid < MT__LastNative) { return CMessageFromJSVal(mtid, m_ScriptInterface, data); } else { return new CMessageScripted(m_ScriptInterface, mtid, m_MessageTypeNamesById[mtid], data); } } void CComponentManager::Script_PostMessage(ScriptInterface::CxPrivate* pCxPrivate, int ent, int mtid, JS::HandleValue data) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); CMessage* msg = componentManager->ConstructMessage(mtid, data); if (!msg) return; // error componentManager->PostMessage(ent, *msg); delete msg; } void CComponentManager::Script_BroadcastMessage(ScriptInterface::CxPrivate* pCxPrivate, int mtid, JS::HandleValue data) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); CMessage* msg = componentManager->ConstructMessage(mtid, data); if (!msg) return; // error componentManager->BroadcastMessage(*msg); delete msg; } int CComponentManager::Script_AddEntity(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); std::wstring name(templateName.begin(), templateName.end()); // TODO: should validate the string to make sure it doesn't contain scary characters // that will let it access non-component-template files entity_id_t ent = componentManager->AddEntity(name, componentManager->AllocateNewEntity()); return (int)ent; } int CComponentManager::Script_AddLocalEntity(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); std::wstring name(templateName.begin(), templateName.end()); // TODO: should validate the string to make sure it doesn't contain scary characters // that will let it access non-component-template files entity_id_t ent = componentManager->AddEntity(name, componentManager->AllocateNewLocalEntity()); return (int)ent; } void CComponentManager::Script_DestroyEntity(ScriptInterface::CxPrivate* pCxPrivate, int ent) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); componentManager->DestroyComponentsSoon(ent); } void CComponentManager::Script_FlushDestroyedEntities(ScriptInterface::CxPrivate *pCxPrivate) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); componentManager->FlushDestroyedComponents(); } void CComponentManager::ResetState() { // Delete all dynamic message subscriptions m_DynamicMessageSubscriptionsNonsync.clear(); m_DynamicMessageSubscriptionsNonsyncByComponent.clear(); // Delete all IComponents std::map >::iterator iit = m_ComponentsByTypeId.begin(); for (; iit != m_ComponentsByTypeId.end(); ++iit) { std::map::iterator eit = iit->second.begin(); for (; eit != iit->second.end(); ++eit) { eit->second->Deinit(); m_ComponentTypesById[iit->first].dealloc(eit->second); } } std::vector >::iterator ifcit = m_ComponentsByInterface.begin(); for (; ifcit != m_ComponentsByInterface.end(); ++ifcit) ifcit->clear(); m_ComponentsByTypeId.clear(); // Delete all SEntityComponentCaches std::unordered_map::iterator ccit = m_ComponentCaches.begin(); for (; ccit != m_ComponentCaches.end(); ++ccit) free(ccit->second); m_ComponentCaches.clear(); m_SystemEntity = CEntityHandle(); m_DestructionQueue.clear(); // Reset IDs m_NextEntityId = SYSTEM_ENTITY + 1; m_NextLocalEntityId = FIRST_LOCAL_ENTITY; } void CComponentManager::SetRNGSeed(u32 seed) { m_RNG.seed(seed); } void CComponentManager::RegisterComponentType(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc, const char* name, const std::string& schema) { ComponentType c(CT_Native, iid, alloc, dealloc, name, schema, DefPersistentRooted()); m_ComponentTypesById.insert(std::make_pair(cid, std::move(c))); m_ComponentTypeIdsByName[name] = cid; } void CComponentManager::RegisterComponentTypeScriptWrapper(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc, const char* name, const std::string& schema) { ComponentType c(CT_ScriptWrapper, iid, alloc, dealloc, name, schema, DefPersistentRooted()); m_ComponentTypesById.insert(std::make_pair(cid, std::move(c))); m_ComponentTypeIdsByName[name] = cid; // TODO: merge with RegisterComponentType } void CComponentManager::MarkScriptedComponentForSystemEntity(CComponentManager::ComponentTypeId cid) { m_ScriptedSystemComponents.push_back(cid); } void CComponentManager::RegisterMessageType(MessageTypeId mtid, const char* name) { m_MessageTypeIdsByName[name] = mtid; m_MessageTypeNamesById[mtid] = name; } void CComponentManager::SubscribeToMessageType(MessageTypeId mtid) { // TODO: verify mtid ENSURE(m_CurrentComponent != CID__Invalid); std::vector& types = m_LocalMessageSubscriptions[mtid]; types.push_back(m_CurrentComponent); std::sort(types.begin(), types.end()); // TODO: just sort once at the end of LoadComponents } void CComponentManager::SubscribeGloballyToMessageType(MessageTypeId mtid) { // TODO: verify mtid ENSURE(m_CurrentComponent != CID__Invalid); std::vector& types = m_GlobalMessageSubscriptions[mtid]; types.push_back(m_CurrentComponent); std::sort(types.begin(), types.end()); // TODO: just sort once at the end of LoadComponents } void CComponentManager::FlattenDynamicSubscriptions() { std::map::iterator it; for (it = m_DynamicMessageSubscriptionsNonsync.begin(); it != m_DynamicMessageSubscriptionsNonsync.end(); ++it) { it->second.Flatten(); } } void CComponentManager::DynamicSubscriptionNonsync(MessageTypeId mtid, IComponent* component, bool enable) { if (enable) { bool newlyInserted = m_DynamicMessageSubscriptionsNonsyncByComponent[component].insert(mtid).second; if (newlyInserted) m_DynamicMessageSubscriptionsNonsync[mtid].Add(component); } else { size_t numRemoved = m_DynamicMessageSubscriptionsNonsyncByComponent[component].erase(mtid); if (numRemoved) m_DynamicMessageSubscriptionsNonsync[mtid].Remove(component); } } void CComponentManager::RemoveComponentDynamicSubscriptions(IComponent* component) { std::map >::iterator it = m_DynamicMessageSubscriptionsNonsyncByComponent.find(component); if (it == m_DynamicMessageSubscriptionsNonsyncByComponent.end()) return; std::set::iterator mtit; for (mtit = it->second.begin(); mtit != it->second.end(); ++mtit) { m_DynamicMessageSubscriptionsNonsync[*mtit].Remove(component); // Need to flatten the subscription lists immediately to avoid dangling IComponent* references m_DynamicMessageSubscriptionsNonsync[*mtit].Flatten(); } m_DynamicMessageSubscriptionsNonsyncByComponent.erase(it); } CComponentManager::ComponentTypeId CComponentManager::LookupCID(const std::string& cname) const { std::map::const_iterator it = m_ComponentTypeIdsByName.find(cname); if (it == m_ComponentTypeIdsByName.end()) return CID__Invalid; return it->second; } std::string CComponentManager::LookupComponentTypeName(ComponentTypeId cid) const { std::map::const_iterator it = m_ComponentTypesById.find(cid); if (it == m_ComponentTypesById.end()) return ""; return it->second.name; } CComponentManager::ComponentTypeId CComponentManager::GetScriptWrapper(InterfaceId iid) { if (iid >= IID__LastNative && iid <= (int)m_InterfaceIdsByName.size()) // use <= since IDs start at 1 return CID_UnknownScript; std::map::const_iterator it = m_ComponentTypesById.begin(); for (; it != m_ComponentTypesById.end(); ++it) if (it->second.iid == iid && it->second.type == CT_ScriptWrapper) return it->first; std::map::const_iterator iiit = m_InterfaceIdsByName.begin(); for (; iiit != m_InterfaceIdsByName.end(); ++iiit) if (iiit->second == iid) { LOGERROR("No script wrapper found for interface id %d '%s'", iid, iiit->first.c_str()); return CID__Invalid; } LOGERROR("No script wrapper found for interface id %d", iid); return CID__Invalid; } entity_id_t CComponentManager::AllocateNewEntity() { entity_id_t id = m_NextEntityId++; // TODO: check for overflow return id; } entity_id_t CComponentManager::AllocateNewLocalEntity() { entity_id_t id = m_NextLocalEntityId++; // TODO: check for overflow return id; } entity_id_t CComponentManager::AllocateNewEntity(entity_id_t preferredId) { // TODO: ensure this ID hasn't been allocated before // (this might occur with broken map files) // Trying to actually add two entities with the same id will fail in AddEntitiy entity_id_t id = preferredId; // Ensure this ID won't be allocated again if (id >= m_NextEntityId) m_NextEntityId = id+1; // TODO: check for overflow return id; } bool CComponentManager::AddComponent(CEntityHandle ent, ComponentTypeId cid, const CParamNode& paramNode) { IComponent* component = ConstructComponent(ent, cid); if (!component) return false; component->Init(paramNode); return true; } void CComponentManager::AddSystemComponents(bool skipScriptedComponents, bool skipAI) { CParamNode noParam; AddComponent(m_SystemEntity, CID_TemplateManager, noParam); AddComponent(m_SystemEntity, CID_CinemaManager, noParam); AddComponent(m_SystemEntity, CID_CommandQueue, noParam); AddComponent(m_SystemEntity, CID_ObstructionManager, noParam); AddComponent(m_SystemEntity, CID_ParticleManager, noParam); AddComponent(m_SystemEntity, CID_Pathfinder, noParam); AddComponent(m_SystemEntity, CID_ProjectileManager, noParam); AddComponent(m_SystemEntity, CID_RangeManager, noParam); AddComponent(m_SystemEntity, CID_SoundManager, noParam); AddComponent(m_SystemEntity, CID_Terrain, noParam); AddComponent(m_SystemEntity, CID_TerritoryManager, noParam); AddComponent(m_SystemEntity, CID_UnitRenderer, noParam); AddComponent(m_SystemEntity, CID_WaterManager, noParam); // Add scripted system components: if (!skipScriptedComponents) { for (uint32_t i = 0; i < m_ScriptedSystemComponents.size(); ++i) AddComponent(m_SystemEntity, m_ScriptedSystemComponents[i], noParam); if (!skipAI) AddComponent(m_SystemEntity, CID_AIManager, noParam); } } IComponent* CComponentManager::ConstructComponent(CEntityHandle ent, ComponentTypeId cid) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); std::map::const_iterator it = m_ComponentTypesById.find(cid); if (it == m_ComponentTypesById.end()) { LOGERROR("Invalid component id %d", cid); return NULL; } const ComponentType& ct = it->second; ENSURE((size_t)ct.iid < m_ComponentsByInterface.size()); boost::unordered_map& emap1 = m_ComponentsByInterface[ct.iid]; if (emap1.find(ent.GetId()) != emap1.end()) { LOGERROR("Multiple components for interface %d", ct.iid); return NULL; } std::map& emap2 = m_ComponentsByTypeId[cid]; // If this is a scripted component, construct the appropriate JS object first JS::RootedValue obj(cx); if (ct.type == CT_Script) { m_ScriptInterface.CallConstructor(ct.ctor.get(), JS::HandleValueArray::empty(), &obj); if (obj.isNull()) { LOGERROR("Script component constructor failed"); return NULL; } } // Construct the new component IComponent* component = ct.alloc(m_ScriptInterface, obj); ENSURE(component); component->SetEntityHandle(ent); component->SetSimContext(m_SimContext); // Store a reference to the new component emap1.insert(std::make_pair(ent.GetId(), component)); emap2.insert(std::make_pair(ent.GetId(), component)); // TODO: We need to more careful about this - if an entity is constructed by a component // while we're iterating over all components, this will invalidate the iterators and everything // will break. // We probably need some kind of delayed addition, so they get pushed onto a queue and then // inserted into the world later on. (Be careful about immediation deletion in that case, too.) SEntityComponentCache* cache = ent.GetComponentCache(); ENSURE(cache != NULL && ct.iid < (int)cache->numInterfaces && cache->interfaces[ct.iid] == NULL); cache->interfaces[ct.iid] = component; return component; } void CComponentManager::AddMockComponent(CEntityHandle ent, InterfaceId iid, IComponent& component) { // Just add it into the by-interface map, not the by-component-type map, // so it won't be considered for messages or deletion etc boost::unordered_map& emap1 = m_ComponentsByInterface.at(iid); if (emap1.find(ent.GetId()) != emap1.end()) debug_warn(L"Multiple components for interface"); emap1.insert(std::make_pair(ent.GetId(), &component)); SEntityComponentCache* cache = ent.GetComponentCache(); ENSURE(cache != NULL && iid < (int)cache->numInterfaces && cache->interfaces[iid] == NULL); cache->interfaces[iid] = &component; } CEntityHandle CComponentManager::AllocateEntityHandle(entity_id_t ent) { // Interface IDs start at 1, and SEntityComponentCache is defined with a 1-sized array, // so we need space for an extra m_InterfaceIdsByName.size() items SEntityComponentCache* cache = (SEntityComponentCache*)calloc(1, sizeof(SEntityComponentCache) + sizeof(IComponent*) * m_InterfaceIdsByName.size()); ENSURE(cache != NULL); cache->numInterfaces = m_InterfaceIdsByName.size() + 1; ENSURE(m_ComponentCaches.find(ent) == m_ComponentCaches.end()); m_ComponentCaches[ent] = cache; return CEntityHandle(ent, cache); } CEntityHandle CComponentManager::LookupEntityHandle(entity_id_t ent, bool allowCreate) { std::unordered_map::iterator it; it = m_ComponentCaches.find(ent); if (it == m_ComponentCaches.end()) { if (allowCreate) return AllocateEntityHandle(ent); else return CEntityHandle(ent, NULL); } else return CEntityHandle(ent, it->second); } void CComponentManager::InitSystemEntity() { ENSURE(m_SystemEntity.GetId() == INVALID_ENTITY); m_SystemEntity = AllocateEntityHandle(SYSTEM_ENTITY); m_SimContext.SetSystemEntity(m_SystemEntity); } entity_id_t CComponentManager::AddEntity(const std::wstring& templateName, entity_id_t ent) { ICmpTemplateManager *cmpTemplateManager = static_cast (QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)); if (!cmpTemplateManager) { debug_warn(L"No ICmpTemplateManager loaded"); return INVALID_ENTITY; } const CParamNode* tmpl = cmpTemplateManager->LoadTemplate(ent, utf8_from_wstring(templateName)); if (!tmpl) return INVALID_ENTITY; // LoadTemplate will have reported the error // This also ensures that ent does not exist CEntityHandle handle = AllocateEntityHandle(ent); // Construct a component for each child of the root element const CParamNode::ChildrenMap& tmplChilds = tmpl->GetChildren(); for (CParamNode::ChildrenMap::const_iterator it = tmplChilds.begin(); it != tmplChilds.end(); ++it) { // Ignore attributes on the root element if (it->first.length() && it->first[0] == '@') continue; CComponentManager::ComponentTypeId cid = LookupCID(it->first); if (cid == CID__Invalid) { LOGERROR("Unrecognised component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); return INVALID_ENTITY; } if (!AddComponent(handle, cid, it->second)) { LOGERROR("Failed to construct component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); return INVALID_ENTITY; } // TODO: maybe we should delete already-constructed components if one of them fails? } CMessageCreate msg(ent); PostMessage(ent, msg); return ent; } void CComponentManager::DestroyComponentsSoon(entity_id_t ent) { m_DestructionQueue.push_back(ent); } void CComponentManager::FlushDestroyedComponents() { PROFILE2("Flush Destroyed Components"); while (!m_DestructionQueue.empty()) { // Make a copy of the destruction queue, so that the iterators won't be invalidated if the // CMessageDestroy handlers try to destroy more entities themselves std::vector queue; queue.swap(m_DestructionQueue); for (std::vector::iterator it = queue.begin(); it != queue.end(); ++it) { entity_id_t ent = *it; CEntityHandle handle = LookupEntityHandle(ent); CMessageDestroy msg(ent); PostMessage(ent, msg); // Flatten all the dynamic subscriptions to ensure there are no dangling // references in the 'removed' lists to components we're going to delete // Some components may have dynamically unsubscribed following the Destroy message FlattenDynamicSubscriptions(); // Destroy the components, and remove from m_ComponentsByTypeId: std::map >::iterator iit = m_ComponentsByTypeId.begin(); for (; iit != m_ComponentsByTypeId.end(); ++iit) { std::map::iterator eit = iit->second.find(ent); if (eit != iit->second.end()) { eit->second->Deinit(); RemoveComponentDynamicSubscriptions(eit->second); m_ComponentTypesById[iit->first].dealloc(eit->second); iit->second.erase(ent); handle.GetComponentCache()->interfaces[m_ComponentTypesById[iit->first].iid] = NULL; } } free(handle.GetComponentCache()); m_ComponentCaches.erase(ent); // Remove from m_ComponentsByInterface std::vector >::iterator ifcit = m_ComponentsByInterface.begin(); for (; ifcit != m_ComponentsByInterface.end(); ++ifcit) { ifcit->erase(ent); } } } } IComponent* CComponentManager::QueryInterface(entity_id_t ent, InterfaceId iid) const { if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return NULL; } boost::unordered_map::const_iterator eit = m_ComponentsByInterface[iid].find(ent); if (eit == m_ComponentsByInterface[iid].end()) { // This entity doesn't implement this interface return NULL; } return eit->second; } CComponentManager::InterfaceList CComponentManager::GetEntitiesWithInterface(InterfaceId iid) const { std::vector > ret; if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return ret; } ret.reserve(m_ComponentsByInterface[iid].size()); boost::unordered_map::const_iterator it = m_ComponentsByInterface[iid].begin(); for (; it != m_ComponentsByInterface[iid].end(); ++it) ret.push_back(*it); std::sort(ret.begin(), ret.end()); // lexicographic pair comparison means this'll sort by entity ID return ret; } static CComponentManager::InterfaceListUnordered g_EmptyEntityMap; const CComponentManager::InterfaceListUnordered& CComponentManager::GetEntitiesWithInterfaceUnordered(InterfaceId iid) const { if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return g_EmptyEntityMap; } return m_ComponentsByInterface[iid]; } void CComponentManager::PostMessage(entity_id_t ent, const CMessage& msg) { PROFILE2_IFSPIKE("Post Message", 0.0005); PROFILE2_ATTR("%s", msg.GetScriptHandlerName()); // Send the message to components of ent, that subscribed locally to this message std::map >::const_iterator it; it = m_LocalMessageSubscriptions.find(msg.GetType()); if (it != m_LocalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.find(ent); if (eit != emap->second.end()) eit->second->HandleMessage(msg, false); } } SendGlobalMessage(ent, msg); } void CComponentManager::BroadcastMessage(const CMessage& msg) { // Send the message to components of all entities that subscribed locally to this message std::map >::const_iterator it; it = m_LocalMessageSubscriptions.find(msg.GetType()); if (it != m_LocalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.begin(); for (; eit != emap->second.end(); ++eit) eit->second->HandleMessage(msg, false); } } SendGlobalMessage(INVALID_ENTITY, msg); } void CComponentManager::SendGlobalMessage(entity_id_t ent, const CMessage& msg) { PROFILE2_IFSPIKE("SendGlobalMessage", 0.001); PROFILE2_ATTR("%s", msg.GetScriptHandlerName()); // (Common functionality for PostMessage and BroadcastMessage) // Send the message to components of all entities that subscribed globally to this message std::map >::const_iterator it; it = m_GlobalMessageSubscriptions.find(msg.GetType()); if (it != m_GlobalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Special case: Messages for local entities shouldn't be sent to script // components that subscribed globally, so that we don't have to worry about // them accidentally picking up non-network-synchronised data. if (ENTITY_IS_LOCAL(ent)) { std::map::const_iterator it = m_ComponentTypesById.find(*ctit); if (it != m_ComponentTypesById.end() && it->second.type == CT_Script) continue; } // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.begin(); for (; eit != emap->second.end(); ++eit) eit->second->HandleMessage(msg, true); } } // Send the message to component instances that dynamically subscribed to this message std::map::iterator dit = m_DynamicMessageSubscriptionsNonsync.find(msg.GetType()); if (dit != m_DynamicMessageSubscriptionsNonsync.end()) { dit->second.Flatten(); const std::vector& dynamic = dit->second.GetComponents(); for (size_t i = 0; i < dynamic.size(); i++) dynamic[i]->HandleMessage(msg, false); } } std::string CComponentManager::GenerateSchema() const { std::string numericOperation = "" "" "" "add" "mul" "" "" ""; std::string schema = "" "" "" + numericOperation + "" "" "0" + numericOperation + "" "" "0" + numericOperation + "" "" "" "" "" "" "" "" "" "" "" "" ""; std::map > interfaceComponentTypes; std::vector componentTypes; for (std::map::const_iterator it = m_ComponentTypesById.begin(); it != m_ComponentTypesById.end(); ++it) { schema += "" "" "" + it->second.schema + "" "" ""; interfaceComponentTypes[it->second.iid].push_back(it->second.name); componentTypes.push_back(it->second.name); } // Declare the implementation of each interface, for documentation for (std::map::const_iterator it = m_InterfaceIdsByName.begin(); it != m_InterfaceIdsByName.end(); ++it) { schema += ""; std::vector& cts = interfaceComponentTypes[it->second]; for (size_t i = 0; i < cts.size(); ++i) schema += ""; schema += ""; } // List all the component types, in alphabetical order (to match the reordering performed by CParamNode). // (We do it this way, rather than ing all the interface definitions (which would additionally perform // a check that we don't use multiple component types of the same interface in one file), because libxml2 gives // useless error messages in the latter case; this way lets it report the real error.) std::sort(componentTypes.begin(), componentTypes.end()); schema += "" "" ""; for (std::vector::const_iterator it = componentTypes.begin(); it != componentTypes.end(); ++it) schema += ""; schema += "" ""; schema += ""; return schema; } Index: ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js (revision 20953) @@ -1,319 +1,320 @@ function Upgrade() {} const UPGRADING_PROGRESS_INTERVAL = 250; Upgrade.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Resources.BuildSchema("nonNegativeInteger") + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; Upgrade.prototype.Init = function() { this.upgrading = false; this.completed = false; this.elapsedTime = 0; this.timer = undefined; this.expendedResources = {}; this.upgradeTemplates = {}; for (let choice in this.template) { let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); let name = this.template[choice].Entity; if (cmpIdentity) name = name.replace(/\{civ\}/g, cmpIdentity.GetCiv()); if (this.upgradeTemplates.name) warn("Upgrade Component: entity " + this.entity + " has two upgrades to the same entity, only the last will be used."); this.upgradeTemplates[name] = choice; } }; // This will also deal with the "OnDestroy" case. Upgrade.prototype.OnOwnershipChanged = function(msg) { if (!this.completed) this.CancelUpgrade(msg.from); - if (msg.to !== -1) + + if (msg.to != INVALID_PLAYER) this.owner = msg.to; }; Upgrade.prototype.ChangeUpgradedEntityCount = function(amount) { if (!this.IsUpgrading()) return; let cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTempMan.GetTemplate(this.upgrading); let categoryTo; if (template.TrainingRestrictions) categoryTo = template.TrainingRestrictions.Category; else if (template.BuildRestrictions) categoryTo = template.BuildRestrictions.Category; if (!categoryTo) return; let categoryFrom; let cmpTrainingRestrictions = Engine.QueryInterface(this.entity, IID_TrainingRestrictions); let cmpBuildRestrictions = Engine.QueryInterface(this.entity, IID_BuildRestrictions); if (cmpTrainingRestrictions) categoryFrom = cmpTrainingRestrictions.GetCategory(); else if (cmpBuildRestrictions) categoryFrom = cmpBuildRestrictions.GetCategory(); if (categoryTo == categoryFrom) return; let cmpEntityLimits = QueryPlayerIDInterface(this.owner, IID_EntityLimits); cmpEntityLimits.ChangeCount(categoryTo, amount); }; Upgrade.prototype.CanUpgradeTo = function(template) { return this.upgradeTemplates[template] !== undefined; }; Upgrade.prototype.GetUpgrades = function() { let ret = []; let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); for (let option in this.template) { let choice = this.template[option]; let templateName = cmpIdentity ? choice.Entity.replace(/\{civ\}/g, cmpIdentity.GetCiv()) : choice.Entity; let cost = {}; if (choice.Cost) cost = this.GetResourceCosts(templateName); if (choice.Time) cost.time = this.GetUpgradeTime(templateName); let hasCost = choice.Cost || choice.Time; ret.push({ "entity": templateName, "icon": choice.Icon || undefined, "cost": hasCost ? cost : undefined, "tooltip": choice.Tooltip || undefined, "requiredTechnology": this.GetRequiredTechnology(option), }); } return ret; }; Upgrade.prototype.CancelTimer = function() { if (!this.timer) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; }; Upgrade.prototype.IsUpgrading = function() { return !!this.upgrading; }; Upgrade.prototype.GetUpgradingTo = function() { return this.upgrading; }; Upgrade.prototype.WillCheckPlacementRestrictions = function(template) { if (!this.upgradeTemplates[template]) return undefined; // is undefined by default so use X in Y return "CheckPlacementRestrictions" in this.template[this.upgradeTemplates[template]]; }; Upgrade.prototype.GetRequiredTechnology = function(templateArg) { let choice = this.upgradeTemplates[templateArg] || templateArg; if (this.template[choice].RequiredTechnology) return this.template[choice].RequiredTechnology; if (!("RequiredTechnology" in this.template[choice])) return undefined; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); let entType = this.template[choice].Entity; if (cmpIdentity) entType = entType.replace(/\{civ\}/g, cmpIdentity.GetCiv()); let template = cmpTemplateManager.GetTemplate(entType); return template.Identity.RequiredTechnology || undefined; }; Upgrade.prototype.GetResourceCosts = function(template) { if (!this.upgradeTemplates[template]) return undefined; if (this.IsUpgrading() && template == this.GetUpgradingTo()) return clone(this.expendedResources); let choice = this.upgradeTemplates[template]; if (!this.template[choice].Cost) return {}; let costs = {}; for (let r in this.template[choice].Cost) costs[r] = ApplyValueModificationsToEntity("Upgrade/Cost/"+r, +this.template[choice].Cost[r], this.entity); return costs; }; Upgrade.prototype.Upgrade = function(template) { if (this.IsUpgrading() || !this.upgradeTemplates[template]) return false; let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); this.expendedResources = this.GetResourceCosts(template); if (!cmpPlayer.TrySubtractResources(this.expendedResources)) { this.expendedResources = {}; return false; } this.upgrading = template; // Prevent cheating this.ChangeUpgradedEntityCount(1); if (this.GetUpgradeTime(template) !== 0) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_Upgrade, "UpgradeProgress", 0, UPGRADING_PROGRESS_INTERVAL, { "upgrading": template }); } else this.UpgradeProgress(); return true; }; Upgrade.prototype.CancelUpgrade = function(owner) { if (!this.IsUpgrading()) return; let cmpPlayer = QueryPlayerIDInterface(owner, IID_Player); if (cmpPlayer) cmpPlayer.AddResources(this.expendedResources); this.expendedResources = {}; this.ChangeUpgradedEntityCount(-1); this.upgrading = false; this.CancelTimer(); this.SetElapsedTime(0); }; Upgrade.prototype.GetUpgradeTime = function(templateArg) { let template = this.upgrading || templateArg; let choice = this.upgradeTemplates[template]; if (!choice) return undefined; if (!this.template[choice].Time) return 0; let cmpPlayer = QueryPlayerIDInterface(this.owner, IID_Player); return ApplyValueModificationsToEntity("Upgrade/Time", +this.template[choice].Time, this.entity) * cmpPlayer.GetCheatTimeMultiplier(); }; Upgrade.prototype.GetElapsedTime = function() { return this.elapsedTime; }; Upgrade.prototype.GetProgress = function() { if (!this.IsUpgrading()) return undefined; return this.GetUpgradeTime() == 0 ? 1 : Math.min(this.elapsedTime / 1000.0 / this.GetUpgradeTime(), 1.0); }; Upgrade.prototype.SetElapsedTime = function(time) { this.elapsedTime = time; }; Upgrade.prototype.UpgradeProgress = function(data, lateness) { if (this.elapsedTime/1000.0 < this.GetUpgradeTime()) { this.SetElapsedTime(this.GetElapsedTime() + UPGRADING_PROGRESS_INTERVAL + lateness); return; } this.CancelTimer(); this.completed = true; this.ChangeUpgradedEntityCount(-1); this.expendedResources = {}; let newEntity = ChangeEntityTemplate(this.entity, this.upgrading); if (newEntity) PlaySound("upgraded", newEntity); }; Engine.RegisterComponentType(IID_Upgrade, "Upgrade", Upgrade); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Capturable.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Capturable.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Capturable.js (revision 20953) @@ -1,193 +1,193 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/TerritoryDecay.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Capturable.js"); let testData = { "structure": 20, "playerID": 1, "regenRate": 2, "garrisonedEntities": [30,31,32,33], "garrisonRegenRate": 5, "decay": false, "decayRate": 30, "maxCp": 3000, "neighbours": [20, 0, 20, 10] }; function testCapturable(testData, test_function) { ResetState(); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": (ent, iid, funcname, time, repeattime, data) => {}, "CancelTimer": timer => {} }); AddMock(testData.structure, IID_Ownership, { "GetOwner": () => testData.playerID, "SetOwner": id => {} }); AddMock(testData.structure, IID_GarrisonHolder, { "GetEntities": () => testData.garrisonedEntities }); AddMock(testData.structure, IID_Fogging, { "Activate": () => {} }); AddMock(10, IID_Player, { "IsEnemy": id => id != 0 }); AddMock(11, IID_Player, { "IsEnemy": id => id != 1 && id != 2 }); AddMock(12, IID_Player, { "IsEnemy": id => id != 1 && id != 2 }); AddMock(13, IID_Player, { "IsEnemy": id => id != 3 }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetNumPlayers": () => 4, "GetPlayerByID": id => 10 + id }); AddMock(testData.structure, IID_StatisticsTracker, { "LostEntity" : () => {}, "CapturedBuilding": () => {} }); let cmpCapturable = ConstructComponent(testData.structure, "Capturable", { "CapturePoints" : testData.maxCp, "RegenRate" : testData.regenRate, "GarrisonRegenRate" : testData.garrisonRegenRate }); AddMock(testData.structure, IID_TerritoryDecay, { "IsDecaying": () => testData.decay, "GetDecayRate": () => testData.decayRate, "GetConnectedNeighbours": () => testData.neighbours }); TS_ASSERT_EQUALS(cmpCapturable.GetRegenRate(), testData.regenRate + testData.garrisonRegenRate * testData.garrisonedEntities.length); test_function(cmpCapturable); Engine.PostMessage = (ent, iid, message) => {}; } // Tests initialisation of the capture points when the entity is created testCapturable(testData, cmpCapturable => { Engine.PostMessage = function(ent, iid, message) { TS_ASSERT_UNEVAL_EQUALS(message, { "regenerating": true, "regenRate": cmpCapturable.GetRegenRate() , "territoryDecay": 0 }); }; - cmpCapturable.OnOwnershipChanged({ "from": -1, "to": testData.playerID }); + cmpCapturable.OnOwnershipChanged({ "from": INVALID_PLAYER, "to": testData.playerID }); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 3000, 0, 0]); }); // Tests if the message is sent when capture points change testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0 , 1000]); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 2000, 0 , 1000]); Engine.PostMessage = function(ent, iid, message) { TS_ASSERT_UNEVAL_EQUALS(message, { "capturePoints": [0, 2000, 0 , 1000] }); }; cmpCapturable.RegisterCapturePointsChanged(); }); // Tests reducing capture points (after a capture attack or a decay) testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0 , 1000]); cmpCapturable.CheckTimer(); Engine.PostMessage = function(ent, iid, message) { if (iid == MT_CapturePointsChanged) TS_ASSERT_UNEVAL_EQUALS(message, { "capturePoints": [0, 2000 - 100, 0, 1000 + 100] }); if (iid == MT_CaptureRegenStateChanged) TS_ASSERT_UNEVAL_EQUALS(message, { "regenerating": true, "regenRate": cmpCapturable.GetRegenRate(), "territoryDecay": 0 }); }; TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.Reduce(100, 3), 100); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 2000 - 100, 0, 1000 + 100]); }); // Tests reducing capture points (after a capture attack or a decay) testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0 , 1000]); cmpCapturable.CheckTimer(); TS_ASSERT_EQUALS(cmpCapturable.Reduce(2500, 3),2000); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), [0, 0, 0, 3000]); }); function testRegen(testData, cpIn, cpOut, regenerating) { testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints(cpIn); cmpCapturable.CheckTimer(); Engine.PostMessage = function(ent, iid, message) { if (iid == MT_CaptureRegenStateChanged) TS_ASSERT_UNEVAL_EQUALS(message.regenerating, regenerating); }; cmpCapturable.TimerTick(cpIn); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), cpOut); }); } // With our testData, the total regen rate is 22. That should be taken from the ennemies testRegen(testData, [12, 2950, 2 , 36], [1, 2972, 2, 25], true); testRegen(testData, [0, 2994, 2, 4], [0, 2998, 2, 0], true); testRegen(testData, [0, 2998, 2, 0], [0, 2998, 2, 0], false); // If the regeneration rate becomes negative, capture points are given in favour of gaia testData.regenRate = -32; // With our testData, the total regen rate is -12. That should be taken from all players to gaia testRegen(testData, [100, 2800, 50, 50], [112, 2796, 46, 46], true); testData.regenRate = 2; function testDecay(testData, cpIn, cpOut) { testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints(cpIn); cmpCapturable.CheckTimer(); Engine.PostMessage = function(ent, iid, message) { if (iid == MT_CaptureRegenStateChanged) TS_ASSERT_UNEVAL_EQUALS(message.territoryDecay, testData.decayRate); }; cmpCapturable.TimerTick(); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.GetCapturePoints(), cpOut); }); } testData.decay = true; // With our testData, the decay rate is 30, that should be given to all neighbours with weights [20/50, 0, 20/50, 10/50], then it regens. testDecay(testData, [2900, 35, 10, 55], [2901, 27, 22, 50]); testData.decay = false; // Tests Reduce function testReduce(testData, amount, player, taken) { testCapturable(testData, cmpCapturable => { cmpCapturable.SetCapturePoints([0, 2000, 0 , 1000]); cmpCapturable.CheckTimer(); TS_ASSERT_UNEVAL_EQUALS(cmpCapturable.Reduce(amount, player), taken); }); } testReduce(testData, 50, 3, 50); testReduce(testData, 50, 2, 50); testReduce(testData, 50, 1, 50); testReduce(testData, -50, 3, 0); testReduce(testData, 50, 0, 50); testReduce(testData, 0, 3, 0); testReduce(testData, 1500, 3, 1500); testReduce(testData, 2000, 3, 2000); testReduce(testData, 3000, 3, 2000); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 20953) @@ -1,625 +1,625 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/AlertRaiser.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Barter.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/CeasefireManager.js"); Engine.LoadComponentScript("interfaces/DamageReceiver.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); Engine.LoadComponentScript("interfaces/EndGameManager.js"); Engine.LoadComponentScript("interfaces/EntityLimits.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Gate.js"); Engine.LoadComponentScript("interfaces/Guard.js"); Engine.LoadComponentScript("interfaces/Heal.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Market.js"); Engine.LoadComponentScript("interfaces/Pack.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/RallyPoint.js"); Engine.LoadComponentScript("interfaces/Repairable.js"); Engine.LoadComponentScript("interfaces/ResourceDropsite.js"); Engine.LoadComponentScript("interfaces/ResourceGatherer.js"); Engine.LoadComponentScript("interfaces/ResourceTrickle.js"); Engine.LoadComponentScript("interfaces/ResourceSupply.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/Trader.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("interfaces/Upgrade.js"); Engine.LoadComponentScript("interfaces/BuildingAI.js"); Engine.LoadComponentScript("GuiInterface.js"); Resources = { "GetCodes": () => ["food", "metal", "stone", "wood"], "GetNames": () => ({ "food": "Food", "metal": "Metal", "stone": "Stone", "wood": "Wood" }), "GetResource": resource => ({ "aiAnalysisInfluenceGroup": resource == "food" ? "ignore" : resource == "wood" ? "abundant" : "sparse" }) }; var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface"); AddMock(SYSTEM_ENTITY, IID_Barter, { GetPrices: function() { return { "buy": { "food": 150 }, "sell": { "food": 25 } }; }, PlayerHasMarket: function () { return false; } }); AddMock(SYSTEM_ENTITY, IID_EndGameManager, { GetGameType: function() { return "conquest"; }, GetAlliedVictory: function() { return false; } }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { GetNumPlayers: function() { return 2; }, GetPlayerByID: function(id) { TS_ASSERT(id === 0 || id === 1); return 100+id; } }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { GetLosVisibility: function(ent, player) { return "visible"; }, GetLosCircular: function() { return false; } }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { GetCurrentTemplateName: function(ent) { return "example"; }, GetTemplate: function(name) { return ""; } }); AddMock(SYSTEM_ENTITY, IID_Timer, { GetTime: function() { return 0; }, SetTimeout: function(ent, iid, funcname, time, data) { return 0; } }); AddMock(100, IID_Player, { GetName: function() { return "Player 1"; }, GetCiv: function() { return "gaia"; }, GetColor: function() { return { r: 1, g: 1, b: 1, a: 1}; }, CanControlAllUnits: function() { return false; }, GetPopulationCount: function() { return 10; }, GetPopulationLimit: function() { return 20; }, GetMaxPopulation: function() { return 200; }, GetResourceCounts: function() { return { food: 100 }; }, GetPanelEntities: function() { return []; }, IsTrainingBlocked: function() { return false; }, GetState: function() { return "active"; }, GetTeam: function() { return -1; }, GetLockTeams: function() { return false; }, GetCheatsEnabled: function() { return false; }, GetDiplomacy: function() { return [-1, 1]; }, IsAlly: function() { return false; }, IsMutualAlly: function() { return false; }, IsNeutral: function() { return false; }, IsEnemy: function() { return true; }, GetDisabledTemplates: function() { return {}; }, GetDisabledTechnologies: function() { return {}; }, GetSpyCostMultiplier: function() { return 1; }, HasSharedDropsites: function() { return false; }, HasSharedLos: function() { return false; } }); AddMock(100, IID_EntityLimits, { GetLimits: function() { return {"Foo": 10}; }, GetCounts: function() { return {"Foo": 5}; }, GetLimitChangers: function() {return {"Foo": {}}; } }); AddMock(100, IID_TechnologyManager, { "IsTechnologyResearched": tech => tech == "phase_village", "GetQueuedResearch": () => new Map(), "GetStartedTechs": () => new Set(), "GetResearchedTechs": () => new Set(), "GetClassCounts": () => ({}), "GetTypeCountsByClass": () => ({}) }); AddMock(100, IID_StatisticsTracker, { GetBasicStatistics: function() { return { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }; }, GetSequences: function() { return { "unitsTrained": [0, 10], "unitsLost": [0, 42], "buildingsConstructed": [1, 3], "buildingsCaptured": [3, 7], "buildingsLost": [3, 10], "civCentresBuilt": [4, 10], "resourcesGathered": { "food": [5, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [1, 20], "lootCollected": [0, 2], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] }; }, IncreaseTrainedUnitsCounter: function() { return 1; }, IncreaseConstructedBuildingsCounter: function() { return 1; }, IncreaseBuiltCivCentresCounter: function() { return 1; } }); AddMock(101, IID_Player, { GetName: function() { return "Player 2"; }, GetCiv: function() { return "mace"; }, GetColor: function() { return { r: 1, g: 0, b: 0, a: 1}; }, CanControlAllUnits: function() { return true; }, GetPopulationCount: function() { return 40; }, GetPopulationLimit: function() { return 30; }, GetMaxPopulation: function() { return 300; }, GetResourceCounts: function() { return { food: 200 }; }, GetPanelEntities: function() { return []; }, IsTrainingBlocked: function() { return false; }, GetState: function() { return "active"; }, GetTeam: function() { return -1; }, GetLockTeams: function() {return false; }, GetCheatsEnabled: function() { return false; }, GetDiplomacy: function() { return [-1, 1]; }, IsAlly: function() { return true; }, IsMutualAlly: function() {return false; }, IsNeutral: function() { return false; }, IsEnemy: function() { return false; }, GetDisabledTemplates: function() { return {}; }, GetDisabledTechnologies: function() { return {}; }, GetSpyCostMultiplier: function() { return 1; }, HasSharedDropsites: function() { return false; }, HasSharedLos: function() { return false; } }); AddMock(101, IID_EntityLimits, { GetLimits: function() { return {"Bar": 20}; }, GetCounts: function() { return {"Bar": 0}; }, GetLimitChangers: function() {return {"Bar": {}}; } }); AddMock(101, IID_TechnologyManager, { "IsTechnologyResearched": tech => tech == "phase_village", "GetQueuedResearch": () => new Map(), "GetStartedTechs": () => new Set(), "GetResearchedTechs": () => new Set(), "GetClassCounts": () => ({}), "GetTypeCountsByClass": () => ({}) }); AddMock(101, IID_StatisticsTracker, { GetBasicStatistics: function() { return { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }; }, GetSequences: function() { return { "unitsTrained": [0, 10], "unitsLost": [0, 9], "buildingsConstructed": [0, 5], "buildingsCaptured": [0, 7], "buildingsLost": [0, 4], "civCentresBuilt": [0, 1], "resourcesGathered": { "food": [0, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [0, 0], "lootCollected": [0, 0], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] }; }, IncreaseTrainedUnitsCounter: function() { return 1; }, IncreaseConstructedBuildingsCounter: function() { return 1; }, IncreaseBuiltCivCentresCounter: function() { return 1; } }); // Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS, // because uneval preserves property order. So make sure this object // matches the ordering in GuiInterface. TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), { players: [ { name: "Player 1", civ: "gaia", color: { r:1, g:1, b:1, a:1 }, controlsAll: false, popCount: 10, popLimit: 20, popMax: 200, panelEntities: [], resourceCounts: { food: 100 }, trainingBlocked: false, state: "active", team: -1, teamsLocked: false, cheatsEnabled: false, disabledTemplates: {}, disabledTechnologies: {}, hasSharedDropsites: false, hasSharedLos: false, spyCostMultiplier: 1, phase: "village", isAlly: [false, false], isMutualAlly: [false, false], isNeutral: [false, false], isEnemy: [true, true], entityLimits: {"Foo": 10}, entityCounts: {"Foo": 5}, entityLimitChangers: {"Foo": {}}, researchQueued: new Map(), researchStarted: new Set(), researchedTechs: new Set(), classCounts: {}, typeCountsByClass: {}, canBarter: false, barterPrices: { "buy": { "food": 150 }, "sell": { "food": 25 } }, statistics: { resourcesGathered: { food: 100, wood: 0, metal: 0, stone: 0, vegetarianFood: 0 }, percentMapExplored: 10 } }, { name: "Player 2", civ: "mace", color: { r:1, g:0, b:0, a:1 }, controlsAll: true, popCount: 40, popLimit: 30, popMax: 300, panelEntities: [], resourceCounts: { food: 200 }, trainingBlocked: false, state: "active", team: -1, teamsLocked: false, cheatsEnabled: false, disabledTemplates: {}, disabledTechnologies: {}, hasSharedDropsites: false, hasSharedLos: false, spyCostMultiplier: 1, phase: "village", isAlly: [true, true], isMutualAlly: [false, false], isNeutral: [false, false], isEnemy: [false, false], entityLimits: {"Bar": 20}, entityCounts: {"Bar": 0}, entityLimitChangers: {"Bar": {}}, researchQueued: new Map(), researchStarted: new Set(), researchedTechs: new Set(), classCounts: {}, typeCountsByClass: {}, canBarter: false, barterPrices: { "buy": { "food": 150 }, "sell": { "food": 25 } }, statistics: { resourcesGathered: { food: 100, wood: 0, metal: 0, stone: 0, vegetarianFood: 0 }, percentMapExplored: 10 } } ], circularMap: false, timeElapsed: 0, gameType: "conquest", alliedVictory: false }); TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), { "players": [ { "name": "Player 1", "civ": "gaia", "color": { "r":1, "g":1, "b":1, "a":1 }, "controlsAll": false, "popCount": 10, "popLimit": 20, "popMax": 200, "panelEntities": [], "resourceCounts": { "food": 100 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [false, false], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [true, true], "entityLimits": {"Foo": 10}, "entityCounts": {"Foo": 5}, "entityLimitChangers": {"Foo": {}}, "researchQueued": new Map(), "researchStarted": new Set(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }, "sequences": { "unitsTrained": [0, 10], "unitsLost": [0, 42], "buildingsConstructed": [1, 3], "buildingsCaptured": [3, 7], "buildingsLost": [3, 10], "civCentresBuilt": [4, 10], "resourcesGathered": { "food": [5, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [1, 20], "lootCollected": [0, 2], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] } }, { "name": "Player 2", "civ": "mace", "color": { "r":1, "g":0, "b":0, "a":1 }, "controlsAll": true, "popCount": 40, "popLimit": 30, "popMax": 300, "panelEntities": [], "resourceCounts": { "food": 200 }, "trainingBlocked": false, "state": "active", "team": -1, "teamsLocked": false, "cheatsEnabled": false, "disabledTemplates": {}, "disabledTechnologies": {}, "hasSharedDropsites": false, "hasSharedLos": false, "spyCostMultiplier": 1, "phase": "village", "isAlly": [true, true], "isMutualAlly": [false, false], "isNeutral": [false, false], "isEnemy": [false, false], "entityLimits": {"Bar": 20}, "entityCounts": {"Bar": 0}, "entityLimitChangers": {"Bar": {}}, "researchQueued": new Map(), "researchStarted": new Set(), "researchedTechs": new Set(), "classCounts": {}, "typeCountsByClass": {}, "canBarter": false, "barterPrices": { "buy": { "food": 150 }, "sell": { "food": 25 } }, "statistics": { "resourcesGathered": { "food": 100, "wood": 0, "metal": 0, "stone": 0, "vegetarianFood": 0 }, "percentMapExplored": 10 }, "sequences": { "unitsTrained": [0, 10], "unitsLost": [0, 9], "buildingsConstructed": [0, 5], "buildingsCaptured": [0, 7], "buildingsLost": [0, 4], "civCentresBuilt": [0, 1], "resourcesGathered": { "food": [0, 100], "wood": [0, 0], "metal": [0, 0], "stone": [0, 0], "vegetarianFood": [0, 0] }, "treasuresCollected": [0, 0], "lootCollected": [0, 0], "percentMapExplored": [0, 10], "teamPercentMapExplored": [0, 10], "percentMapControlled": [0, 10], "teamPercentMapControlled": [0, 10], "peakPercentOfMapControlled": [0, 10], "teamPeakPercentOfMapControlled": [0, 10] } } ], "circularMap": false, "timeElapsed": 0, "gameType": "conquest", "alliedVictory": false }); AddMock(10, IID_Builder, { GetEntitiesList: function() { return ["test1", "test2"]; }, }); AddMock(10, IID_Health, { GetHitpoints: function() { return 50; }, GetMaxHitpoints: function() { return 60; }, IsRepairable: function() { return false; }, IsUnhealable: function() { return false; } }); AddMock(10, IID_Identity, { GetClassesList: function() { return ["class1", "class2"]; }, GetVisibleClassesList: function() { return ["class3", "class4"]; }, GetRank: function() { return "foo"; }, GetSelectionGroupName: function() { return "Selection Group Name"; }, HasClass: function() { return true; }, IsUndeletable: function() { return false; } }); AddMock(10, IID_Position, { GetTurretParent: function() {return INVALID_ENTITY;}, GetPosition: function() { return {x:1, y:2, z:3}; }, GetRotation: function() { return {x:4, y:5, z:6}; }, IsInWorld: function() { return true; } }); AddMock(10, IID_ResourceTrickle, { "GetTimer": () => 1250, "GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 }) }); // Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS, // because uneval preserves property order. So make sure this object // matches the ordering in GuiInterface. TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), { "id": 10, "template": "example", "alertRaiser": null, "armour": null, "attack": null, "builder": true, "buildingAI": null, "buildRate": null, "buildTime": null, "canGarrison": false, "deathDamage": null, "heal": null, "identity": { "rank": "foo", "classes": ["class1", "class2"], "visibleClasses": ["class3", "class4"], "selectionGroupName": "Selection Group Name", "canDelete": true }, "isBarterMarket": true, "fogging": null, "foundation": null, "garrisonHolder": null, "gate": null, "guard": null, "loot": null, "market": null, "mirage": null, "pack": null, "promotion": null, "upgrade" : null, - "player": -1, + "player": INVALID_PLAYER, "position": {x:1, y:2, z:3}, "production": null, "rallyPoint": null, "repairRate": null, "resourceCarrying": null, "resourceDropsite": null, "resourceGatherRates": null, "resourceSupply": null, "resourceTrickle": { "interval": 1250, "rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 } }, "rotation": {x:4, y:5, z:6}, "speed": null, "trader": null, "turretParent":null, "unitAI": null, "visibility": "visible", "hitpoints": 50, "maxHitpoints": 60, "needsRepair": false, "needsHeal": true }); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 20952) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 20953) @@ -1,338 +1,338 @@ /** * Used to create player entities prior to reading the rest of a map, * all other initialization must be done after loading map (terrain/entities). * DO NOT use other components here, as they may fail unpredictably. * settings is the object containing settings for this map. * newPlayers if true will remove old player entities or add new ones until * the new number of player entities is obtained * (used when loading a map or when Atlas changes the number of players). */ function LoadPlayerSettings(settings, newPlayers) { var playerDefaults = Engine.ReadJSONFile("simulation/data/settings/player_defaults.json").PlayerData; // Default settings if (!settings) settings = {}; // Add gaia to simplify iteration if (settings.PlayerData && settings.PlayerData[0]) settings.PlayerData.unshift(null); var playerData = settings.PlayerData; var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var numPlayers = cmpPlayerManager.GetNumPlayers(); var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); // Remove existing players or add new ones if (newPlayers) { var settingsNumPlayers = 9; // default 8 players + gaia if (playerData) settingsNumPlayers = playerData.length; // includes gaia (see above) else warn("Player.js: Setup has no player data - using defaults"); while (settingsNumPlayers > numPlayers) { // Add player entity to engine var entID = Engine.AddEntity(GetPlayerTemplateName(getSetting(playerData, playerDefaults, numPlayers, "Civ"))); var cmpPlayer = Engine.QueryInterface(entID, IID_Player); if (!cmpPlayer) throw new Error("Player.js: Error creating player entity " + numPlayers); cmpPlayerManager.AddPlayer(entID); ++numPlayers; } while (settingsNumPlayers < numPlayers) { cmpPlayerManager.RemoveLastPlayer(); --numPlayers; } } // Even when no new player, we must check the template compatibility as player template may be civ dependent for (var i = 0; i < numPlayers; ++i) { var template = GetPlayerTemplateName(getSetting(playerData, playerDefaults, i, "Civ")); var entID = cmpPlayerManager.GetPlayerByID(i); if (cmpTemplateManager.GetCurrentTemplateName(entID) === template) continue; // We need to recreate this player to have the right template entID = Engine.AddEntity(template); cmpPlayerManager.ReplacePlayer(i, entID); } // Initialize the player data for (var i = 0; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i); cmpPlayer.SetName(getSetting(playerData, playerDefaults, i, "Name")); cmpPlayer.SetCiv(getSetting(playerData, playerDefaults, i, "Civ")); var color = getSetting(playerData, playerDefaults, i, "Color"); cmpPlayer.SetColor(color.r, color.g, color.b); // Special case for gaia if (i == 0) { // Gaia should be its own ally. cmpPlayer.SetAlly(0); // Gaia is everyone's enemy for (var j = 1; j < numPlayers; ++j) cmpPlayer.SetEnemy(j); continue; } // Note: this is not yet implemented but I leave it commented to highlight it's easy // If anyone ever adds handicap. //if (getSetting(playerData, playerDefaults, i, "GatherRateMultiplier") !== undefined) // cmpPlayer.SetGatherRateMultiplier(getSetting(playerData, playerDefaults, i, "GatherRateMultiplier")); if (getSetting(playerData, playerDefaults, i, "PopulationLimit") !== undefined) cmpPlayer.SetMaxPopulation(getSetting(playerData, playerDefaults, i, "PopulationLimit")); if (getSetting(playerData, playerDefaults, i, "Resources") !== undefined) cmpPlayer.SetResourceCounts(getSetting(playerData, playerDefaults, i, "Resources")); if (getSetting(playerData, playerDefaults, i, "StartingTechnologies") !== undefined) cmpPlayer.SetStartingTechnologies(getSetting(playerData, playerDefaults, i, "StartingTechnologies")); if (getSetting(playerData, playerDefaults, i, "DisabledTechnologies") !== undefined) cmpPlayer.SetDisabledTechnologies(getSetting(playerData, playerDefaults, i, "DisabledTechnologies")); let disabledTemplates = []; if (settings.DisabledTemplates !== undefined) disabledTemplates = settings.DisabledTemplates; if (getSetting(playerData, playerDefaults, i, "DisabledTemplates") !== undefined) disabledTemplates = disabledTemplates.concat(getSetting(playerData, playerDefaults, i, "DisabledTemplates")); if (disabledTemplates.length) cmpPlayer.SetDisabledTemplates(disabledTemplates); if (settings.DisableSpies) { cmpPlayer.AddDisabledTechnology("unlock_spies"); cmpPlayer.AddDisabledTemplate("special/spy"); } // If diplomacy explicitly defined, use that; otherwise use teams if (getSetting(playerData, playerDefaults, i, "Diplomacy") !== undefined) cmpPlayer.SetDiplomacy(getSetting(playerData, playerDefaults, i, "Diplomacy")); else { // Init diplomacy var myTeam = getSetting(playerData, playerDefaults, i, "Team"); // Set all but self as enemies as SetTeam takes care of allies for (var j = 0; j < numPlayers; ++j) { if (i == j) cmpPlayer.SetAlly(j); else cmpPlayer.SetEnemy(j); } cmpPlayer.SetTeam(myTeam === undefined ? -1 : myTeam); } cmpPlayer.SetFormations( getSetting(playerData, playerDefaults, i, "Formations") || Engine.ReadJSONFile("simulation/data/civs/" + cmpPlayer.GetCiv() + ".json").Formations); var startCam = getSetting(playerData, playerDefaults, i, "StartingCamera"); if (startCam !== undefined) cmpPlayer.SetStartingCamera(startCam.Position, startCam.Rotation); } // NOTE: We need to do the team locking here, as otherwise // SetTeam can't ally the players. if (settings.LockTeams) for (let i = 0; i < numPlayers; ++i) QueryPlayerIDInterface(i).SetLockTeams(true); // Disable the AIIinterface when no AI players are present if (playerData && !playerData.some(v => v && !!v.AI)) Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface).Disable(); } // Get a setting if it exists or return default function getSetting(settings, defaults, idx, property) { if (settings && settings[idx] && (property in settings[idx])) return settings[idx][property]; // Use defaults if (defaults && defaults[idx] && (property in defaults[idx])) return defaults[idx][property]; return undefined; } function GetPlayerTemplateName(civ) { let path = "special/player/player"; if (Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).TemplateExists(path + "_" + civ)) return path + "_" + civ; return path; } /** * Similar to Engine.QueryInterface but applies to the player entity * that owns the given entity. * iid is typically IID_Player. */ function QueryOwnerInterface(ent, iid = IID_Player) { var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!cmpOwnership) return null; var owner = cmpOwnership.GetOwner(); - if (owner == -1) + if (owner == INVALID_PLAYER) return null; return QueryPlayerIDInterface(owner, iid); } /** * Similar to Engine.QueryInterface but applies to the player entity * with the given ID number. * iid is typically IID_Player. */ function QueryPlayerIDInterface(id, iid = IID_Player) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var playerEnt = cmpPlayerManager.GetPlayerByID(id); if (!playerEnt) return null; return Engine.QueryInterface(playerEnt, iid); } /** * Similar to Engine.QueryInterface but first checks if the entity * mirages the interface. */ function QueryMiragedInterface(ent, iid) { var cmp = Engine.QueryInterface(ent, IID_Mirage); if (cmp && !cmp.Mirages(iid)) return null; else if (!cmp) cmp = Engine.QueryInterface(ent, iid); return cmp; } /** * Similar to Engine.QueryInterface, but checks for all interfaces * implementing a builder list (currently Foundation and Repairable) * TODO Foundation and Repairable could both implement a BuilderList component */ function QueryBuilderListInterface(ent) { return Engine.QueryInterface(ent, IID_Foundation) || Engine.QueryInterface(ent, IID_Repairable); } /** * Returns true if the entity 'target' is owned by an ally of * the owner of 'entity'. */ function IsOwnedByAllyOfEntity(entity, target) { return IsOwnedByEntityHelper(entity, target, "IsAlly"); } function IsOwnedByMutualAllyOfEntity(entity, target) { return IsOwnedByEntityHelper(entity, target, "IsMutualAlly"); } function IsOwnedByEntityHelper(entity, target, check) { // Figure out which player controls us let owner = 0; let cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); // Figure out which player controls the target entity let targetOwner = 0; let cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); if (cmpOwnershipTarget) targetOwner = cmpOwnershipTarget.GetOwner(); let cmpPlayer = QueryPlayerIDInterface(owner); return cmpPlayer && cmpPlayer[check](targetOwner); } /** * Returns true if the entity 'target' is owned by player */ function IsOwnedByPlayer(player, target) { var cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); return cmpOwnershipTarget && player == cmpOwnershipTarget.GetOwner(); } function IsOwnedByGaia(target) { return IsOwnedByPlayer(0, target); } /** * Returns true if the entity 'target' is owned by an ally of player */ function IsOwnedByAllyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsAlly"); } function IsOwnedByMutualAllyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsMutualAlly"); } function IsOwnedByNeutralOfPlayer(player,target) { return IsOwnedByHelper(player, target, "IsNeutral"); } function IsOwnedByEnemyOfPlayer(player, target) { return IsOwnedByHelper(player, target, "IsEnemy"); } function IsOwnedByHelper(player, target, check) { let targetOwner = 0; let cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); if (cmpOwnershipTarget) targetOwner = cmpOwnershipTarget.GetOwner(); let cmpPlayer = QueryPlayerIDInterface(player); return cmpPlayer && cmpPlayer[check](targetOwner); } Engine.RegisterGlobal("LoadPlayerSettings", LoadPlayerSettings); Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface); Engine.RegisterGlobal("QueryPlayerIDInterface", QueryPlayerIDInterface); Engine.RegisterGlobal("QueryMiragedInterface", QueryMiragedInterface); Engine.RegisterGlobal("QueryBuilderListInterface", QueryBuilderListInterface); Engine.RegisterGlobal("IsOwnedByAllyOfEntity", IsOwnedByAllyOfEntity); Engine.RegisterGlobal("IsOwnedByMutualAllyOfEntity", IsOwnedByMutualAllyOfEntity); Engine.RegisterGlobal("IsOwnedByPlayer", IsOwnedByPlayer); Engine.RegisterGlobal("IsOwnedByGaia", IsOwnedByGaia); Engine.RegisterGlobal("IsOwnedByAllyOfPlayer", IsOwnedByAllyOfPlayer); Engine.RegisterGlobal("IsOwnedByMutualAllyOfPlayer", IsOwnedByMutualAllyOfPlayer); Engine.RegisterGlobal("IsOwnedByNeutralOfPlayer", IsOwnedByNeutralOfPlayer); Engine.RegisterGlobal("IsOwnedByEnemyOfPlayer", IsOwnedByEnemyOfPlayer);