From f7e591c9f2bf384187a73d7dfa64b27d0b8d31bb Mon Sep 17 00:00:00 2001 From: Itms Date: Mon, 4 Aug 2014 22:49:19 +0000 Subject: [PATCH] Hide changes to buildings in the fog-of-war. To achieve this, mirage entities are created per player, to replace the real entities when these ones fall into the fog-of-war. These mirage entities are created on-the-fly, and destroyed when they get back in sight. This depends heavily on the VisibilityChanged message added in 2174eaaeee. As a temporary adjustment, territories do not explore the map anymore when their borders change. See #2709. Fixes #599 This was SVN commit r15612. --- .../data/mods/public/gui/session/selection.js | 14 +- .../public/gui/session/selection_details.js | 4 +- .../mods/public/gui/session/unit_actions.js | 9 + .../simulation/components/BuildingAI.js | 5 + .../public/simulation/components/Fogging.js | 165 ++++++++++++++++++ .../simulation/components/GuiInterface.js | 39 +++++ .../public/simulation/components/Mirage.js | 149 ++++++++++++++++ .../simulation/components/ResourceGatherer.js | 15 +- .../public/simulation/components/UnitAI.js | 27 ++- .../components/interfaces/Messages.js | 5 +- .../components/tests/test_GuiInterface.js | 2 + .../public/simulation/helpers/Commands.js | 5 + .../simulation/templates/template_gaia.xml | 1 + .../templates/template_structure.xml | 1 + source/ps/TemplateLoader.cpp | 50 ++++++ source/ps/TemplateLoader.h | 6 + source/simulation2/TypeList.h | 6 + .../components/CCmpRangeManager.cpp | 68 ++++++-- source/simulation2/components/ICmpFogging.cpp | 44 +++++ source/simulation2/components/ICmpFogging.h | 39 +++++ source/simulation2/components/ICmpMirage.cpp | 39 +++++ source/simulation2/components/ICmpMirage.h | 37 ++++ 22 files changed, 705 insertions(+), 25 deletions(-) create mode 100644 binaries/data/mods/public/simulation/components/Fogging.js create mode 100644 binaries/data/mods/public/simulation/components/Mirage.js create mode 100644 source/simulation2/components/ICmpFogging.cpp create mode 100644 source/simulation2/components/ICmpFogging.h create mode 100644 source/simulation2/components/ICmpMirage.cpp create mode 100644 source/simulation2/components/ICmpMirage.h diff --git a/binaries/data/mods/public/gui/session/selection.js b/binaries/data/mods/public/gui/session/selection.js index c678873709..fd19fd6c9e 100644 --- a/binaries/data/mods/public/gui/session/selection.js +++ b/binaries/data/mods/public/gui/session/selection.js @@ -235,8 +235,10 @@ EntitySelection.prototype.getTemplateNames = function() */ EntitySelection.prototype.update = function() { - var changed = false; this.checkRenamedEntities(); + + var miraged = {}; + var changed = false; for each (var ent in this.selected) { var entState = GetEntityState(ent); @@ -250,6 +252,13 @@ EntitySelection.prototype.update = function() continue; } + // Manually replace newly miraged entities by their mirages + if (entState.fogging && entState.fogging.mirage) + { + miraged[ent] = entState.fogging.mirage; + continue; + } + // Remove non-visible units (e.g. moved back into fog-of-war) if (entState.visibility == "hidden") { @@ -264,6 +273,9 @@ EntitySelection.prototype.update = function() continue; } } + + this.rebuildSelection(miraged); + if (changed) this.onChange(); }; diff --git a/binaries/data/mods/public/gui/session/selection_details.js b/binaries/data/mods/public/gui/session/selection_details.js index 4d8a347783..baa4d90909 100644 --- a/binaries/data/mods/public/gui/session/selection_details.js +++ b/binaries/data/mods/public/gui/session/selection_details.js @@ -164,7 +164,7 @@ function displaySingle(entState, template) Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = sprintf(translate("Gain: %(amount)s"), { amount: getTradingTooltip(entState.trader.goods.amount) }); } // And for number of workers - else if (entState.foundation) + else if (entState.foundation && !entState.mirage) { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; @@ -172,7 +172,7 @@ function displaySingle(entState, template) Engine.GetGUIObjectByName("resourceCarryingText").caption = entState.foundation.numBuilders + " "; Engine.GetGUIObjectByName("resourceCarryingIcon").tooltip = sprintf(translate("Number of builders.\nTasking another to this foundation would speed construction up by %(numb)s%%"), { numb : Math.round((Math.pow((entState.foundation.numBuilders+1)/entState.foundation.numBuilders, 0.7) - 1.0)*100) }); } - else if (entState.resourceSupply && (!entState.resourceSupply.killBeforeGather || !entState.hitpoints)) + else if (entState.resourceSupply && (!entState.resourceSupply.killBeforeGather || !entState.hitpoints) && !entState.mirage) { Engine.GetGUIObjectByName("resourceCarryingIcon").hidden = false; Engine.GetGUIObjectByName("resourceCarryingText").hidden = false; diff --git a/binaries/data/mods/public/gui/session/unit_actions.js b/binaries/data/mods/public/gui/session/unit_actions.js index 4c140558b4..1dab22c78e 100644 --- a/binaries/data/mods/public/gui/session/unit_actions.js +++ b/binaries/data/mods/public/gui/session/unit_actions.js @@ -656,6 +656,12 @@ var g_EntityCommands = "delete": { "getInfo": function(entState) { + if (entState.mirage) + return { + "tooltip": translate("You cannot destroy this entity because it is in the fog-of-war"), + "icon": "kill_small.png" + }; + return { "tooltip": translate("Delete"), "icon": "kill_small.png" @@ -663,6 +669,9 @@ var g_EntityCommands = }, "execute": function(entState) { + if (entState.mirage) + return; + var selection = g_Selection.toList(); if (selection.length < 1) return; diff --git a/binaries/data/mods/public/simulation/components/BuildingAI.js b/binaries/data/mods/public/simulation/components/BuildingAI.js index 4eef15b28f..ef124d9270 100644 --- a/binaries/data/mods/public/simulation/components/BuildingAI.js +++ b/binaries/data/mods/public/simulation/components/BuildingAI.js @@ -335,6 +335,11 @@ BuildingAI.prototype.CheckTargetVisible = function(target) var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + // 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(), false) == "hidden") return false; diff --git a/binaries/data/mods/public/simulation/components/Fogging.js b/binaries/data/mods/public/simulation/components/Fogging.js new file mode 100644 index 0000000000..87e6907f03 --- /dev/null +++ b/binaries/data/mods/public/simulation/components/Fogging.js @@ -0,0 +1,165 @@ +const VIS_HIDDEN = 0; +const VIS_FOGGED = 1; +const VIS_VISIBLE = 2; + +function Fogging() {} + +Fogging.prototype.Schema = + "Allows this entity to be replaced by mirages entities in the fog-of-war." + + ""; + +Fogging.prototype.Init = function() +{ + this.mirages = []; + this.seen = []; + + var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + for (var player = 0; player < cmpPlayerManager.GetNumPlayers(); ++player) + { + this.mirages.push(INVALID_ENTITY); + this.seen.push(false); + } +}; + +Fogging.prototype.LoadMirage = function(player) +{ + var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + var templateName = "mirage|" + cmpTemplateManager.GetCurrentTemplateName(this.entity); + + // If this is an entity without visibility (e.g. a foundation), it should be + // marked as seen for its owner + var cmpParentOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (cmpParentOwnership && cmpParentOwnership.GetOwner() == player) + this.seen[player] = true; + + if (!this.seen[player] || this.mirages[player] != INVALID_ENTITY) + return; + + this.mirages[player] = Engine.AddEntity(templateName); + var cmpMirage = Engine.QueryInterface(this.mirages[player], IID_Mirage); + if (!cmpMirage) + { + error("Failed to load mirage entity for template " + templateName); + this.mirages[player] = INVALID_ENTITY; + return; + } + + // Setup basic mirage properties + cmpMirage.SetPlayer(player); + cmpMirage.SetParent(this.entity); + + // Copy cmpOwnership data + var cmpMirageOwnership = Engine.QueryInterface(this.mirages[player], IID_Ownership); + if (!cmpParentOwnership || !cmpMirageOwnership) + { + error("Failed to setup the ownership data of the fogged entity " + templateName); + return; + } + cmpMirageOwnership.SetOwner(cmpParentOwnership.GetOwner()); + + // Copy cmpPosition data + var cmpParentPosition = Engine.QueryInterface(this.entity, IID_Position); + var cmpMiragePosition = Engine.QueryInterface(this.mirages[player], IID_Position); + if (!cmpParentPosition || !cmpMiragePosition) + { + error("Failed to setup the position data of the fogged entity " + templateName); + return; + } + if (!cmpParentPosition.IsInWorld()) + return; + var pos = cmpParentPosition.GetPosition(); + cmpMiragePosition.JumpTo(pos.x, pos.z); + var rot = cmpParentPosition.GetRotation(); + cmpMiragePosition.SetYRotation(rot.y); + cmpMiragePosition.SetXZRotation(rot.x, rot.z); + + // Copy cmpVisualActor data + var cmpParentVisualActor = Engine.QueryInterface(this.entity, IID_Visual); + var cmpMirageVisualActor = Engine.QueryInterface(this.mirages[player], IID_Visual); + if (!cmpParentVisualActor || !cmpMirageVisualActor) + { + error("Failed to setup the visual data of the fogged entity " + templateName); + return; + } + cmpMirageVisualActor.SetActorSeed(cmpParentVisualActor.GetActorSeed()); + + // Store valuable information into the mirage component (especially for the GUI) + var cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation); + if (cmpFoundation) + cmpMirage.AddFoundation(cmpFoundation.GetBuildPercentage()); + + var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); + if (cmpHealth) + cmpMirage.AddHealth( + cmpHealth.GetMaxHitpoints(), + cmpHealth.GetHitpoints(), + cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()) + ); + + var cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply); + if (cmpResourceSupply) + cmpMirage.AddResourceSupply( + cmpResourceSupply.GetMaxAmount(), + cmpResourceSupply.GetCurrentAmount(), + cmpResourceSupply.GetType(), + cmpResourceSupply.IsInfinite() + ); +}; + +Fogging.prototype.IsMiraged = function(player) +{ + if (player >= this.mirages.length) + return false; + + return this.mirages[player] != INVALID_ENTITY; +}; + +Fogging.prototype.GetMirage = function(player) +{ + if (player >= this.mirages.length) + return INVALID_ENTITY; + + return this.mirages[player]; +}; + +Fogging.prototype.WasSeen = function(player) +{ + if (player >= this.seen.length) + return false; + + return this.seen[player]; +}; + +Fogging.prototype.OnVisibilityChanged = function(msg) +{ + if (msg.player >= this.mirages.length) + return; + + if (msg.newVisibility == VIS_VISIBLE) + { + this.seen[msg.player] = true; + + // Destroy mirages when we get back into LoS + if (this.mirages[msg.player] != INVALID_ENTITY) + { + Engine.DestroyEntity(this.mirages[msg.player]); + this.mirages[msg.player] = INVALID_ENTITY; + } + } + + // Intermediate LoS state, meaning we must create a mirage + if (msg.newVisibility == VIS_FOGGED) + this.LoadMirage(msg.player); +}; + +Fogging.prototype.OnDestroy = function(msg) +{ + for (var player = 0; player < this.mirages.length; ++player) + { + var cmpMirage = Engine.QueryInterface(this.mirages[player], IID_Mirage); + if (cmpMirage) + cmpMirage.SetParent(INVALID_ENTITY); + } +}; + +Engine.RegisterComponentType(IID_Fogging, "Fogging", Fogging); diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js index cfca3af02a..06196ab4cb 100644 --- a/binaries/data/mods/public/simulation/components/GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -175,6 +175,7 @@ GuiInterface.prototype.GetEntityState = function(player, ent) "alertRaiser": null, "buildEntities": null, "identity": null, + "fogging": null, "foundation": null, "garrisonHolder": null, "gate": null, @@ -190,6 +191,9 @@ GuiInterface.prototype.GetEntityState = function(player, ent) "visibility": null, }; + // Used for several components + var cmpMirage = Engine.QueryInterface(ent, IID_Mirage); + var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) { @@ -216,6 +220,12 @@ GuiInterface.prototype.GetEntityState = function(player, ent) ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()); ret.needsHeal = !cmpHealth.IsUnhealable(); } + if (cmpMirage && cmpMirage.Health()) + { + ret.hitpoints = cmpMirage.GetHitpoints(); + ret.maxHitpoints = cmpMirage.GetMaxHitpoints(); + ret.needsRepair = cmpMirage.NeedsRepair(); + } var cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (cmpBuilder) @@ -251,6 +261,15 @@ GuiInterface.prototype.GetEntityState = function(player, ent) }; } + var cmpFogging = Engine.QueryInterface(ent, IID_Fogging); + if (cmpFogging) + { + if (cmpFogging.IsMiraged(player)) + ret.fogging = {"mirage": cmpFogging.GetMirage(player)}; + else + ret.fogging = {"mirage": null}; + } + var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); if (cmpFoundation) { @@ -259,6 +278,12 @@ GuiInterface.prototype.GetEntityState = function(player, ent) "numBuilders": cmpFoundation.GetNumBuilders() }; } + if (cmpMirage && cmpMirage.Foundation()) + { + ret.foundation = { + "progress": cmpMirage.GetBuildPercentage() + }; + } var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) @@ -342,6 +367,7 @@ GuiInterface.prototype.GetExtendedEntityState = function(player, ent) "barterMarket": null, "buildingAI": null, "healer": null, + "mirage": null, "obstruction": null, "turretParent":null, "promotion": null, @@ -351,6 +377,10 @@ GuiInterface.prototype.GetExtendedEntityState = function(player, ent) "resourceSupply": null, }; + var cmpMirage = Engine.QueryInterface(ent, IID_Mirage); + if (cmpMirage) + ret.mirage = true; + var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); var cmpAttack = Engine.QueryInterface(ent, IID_Attack); @@ -445,6 +475,15 @@ GuiInterface.prototype.GetExtendedEntityState = function(player, ent) "gatherers": cmpResourceSupply.GetGatherers() }; } + if (cmpMirage && cmpMirage.ResourceSupply()) + { + ret.resourceSupply = { + "max": cmpMirage.GetMaxAmount(), + "amount": cmpMirage.GetAmount(), + "type": cmpMirage.GetType(), + "isInfinite": cmpMirage.IsInfinite() + }; + } var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) diff --git a/binaries/data/mods/public/simulation/components/Mirage.js b/binaries/data/mods/public/simulation/components/Mirage.js new file mode 100644 index 0000000000..9306149e7c --- /dev/null +++ b/binaries/data/mods/public/simulation/components/Mirage.js @@ -0,0 +1,149 @@ +const VIS_HIDDEN = 0; +const VIS_FOGGED = 1; +const VIS_VISIBLE = 2; + +function Mirage() {} + +Mirage.prototype.Schema = + "Mirage entities replace real entities in the fog-of-war." + + ""; + +Mirage.prototype.Init = function() +{ + this.player = null; + this.parent = INVALID_ENTITY; + + this.foundation = false; + this.buildPercentage = null; + + this.health = false; + this.maxHitpoints = null; + this.hitpoints = null; + this.needsRepair = null; + + this.resourceSupply = false; + this.maxAmount = null; + this.amount = null; + this.type = null; + this.isInfinite = null; +}; + +Mirage.prototype.SetParent = function(ent) +{ + this.parent = ent; +}; + +Mirage.prototype.GetPlayer = function() +{ + return this.player; +}; + +Mirage.prototype.SetPlayer = function(player) +{ + this.player = player; +}; + +// ============================ +// Parent entity data + +// Foundation data + +Mirage.prototype.AddFoundation = function(buildPercentage) +{ + this.foundation = true; + this.buildPercentage = buildPercentage; +}; + +Mirage.prototype.Foundation = function() +{ + return this.foundation; +}; + +Mirage.prototype.GetBuildPercentage = function() +{ + return this.buildPercentage; +}; + +// Health data + +Mirage.prototype.AddHealth = function(maxHitpoints, hitpoints, needsRepair) +{ + this.health = true; + this.maxHitpoints = maxHitpoints; + this.hitpoints = Math.ceil(hitpoints); + this.needsRepair = needsRepair; +}; + +Mirage.prototype.Health = function() +{ + return this.health; +}; + +Mirage.prototype.GetMaxHitpoints = function() +{ + return this.maxHitpoints; +}; + +Mirage.prototype.GetHitpoints = function() +{ + return this.hitpoints; +}; + +Mirage.prototype.NeedsRepair = function() +{ + return this.needsRepair; +}; + +// ResourceSupply data + +Mirage.prototype.AddResourceSupply = function(maxAmount, amount, type, isInfinite) +{ + this.resourceSupply = true; + this.maxAmount = maxAmount; + this.amount = amount; + this.type = type; + this.isInfinite = isInfinite; +}; + +Mirage.prototype.ResourceSupply = function() +{ + return this.resourceSupply; +}; + +Mirage.prototype.GetMaxAmount = function() +{ + return this.maxAmount; +}; + +Mirage.prototype.GetAmount = function() +{ + return this.amount; +}; + +Mirage.prototype.GetType = function() +{ + return this.type; +}; + +Mirage.prototype.IsInfinite = function() +{ + return this.isInfinite; +}; + +// ============================ + +Mirage.prototype.OnVisibilityChanged = function(msg) +{ + if (msg.player == this.player && msg.newVisibility == VIS_VISIBLE && this.parent == INVALID_ENTITY) + Engine.DestroyEntity(this.entity); +}; + +Mirage.prototype.OnDestroy = function(msg) +{ + if (this.parent == INVALID_ENTITY) + return; + + Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: this.parent }); +}; + +Engine.RegisterComponentType(IID_Mirage, "Mirage", Mirage); diff --git a/binaries/data/mods/public/simulation/components/ResourceGatherer.js b/binaries/data/mods/public/simulation/components/ResourceGatherer.js index 201eb679cc..2d5fc4cd0b 100644 --- a/binaries/data/mods/public/simulation/components/ResourceGatherer.js +++ b/binaries/data/mods/public/simulation/components/ResourceGatherer.js @@ -243,12 +243,16 @@ ResourceGatherer.prototype.GetTargetGatherRate = function(target) { var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); + var type; var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); - if (!cmpResourceSupply) + var cmpMirage = Engine.QueryInterface(target, IID_Mirage); + if (cmpResourceSupply) + type = cmpResourceSupply.GetType(); + else if (cmpMirage && cmpMirage.ResourceSupply()) + type = cmpMirage.GetType(); + else return 0; - var type = cmpResourceSupply.GetType(); - var rates = this.GetGatherRates(); var rate; @@ -261,7 +265,12 @@ ResourceGatherer.prototype.GetTargetGatherRate = function(target) rate = rates[type.generic] / cmpPlayer.GetCheatTimeMultiplier(); } + if (cmpMirage) + return rate || 0; + // 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) // Note to people looking to change in a template: This is a bit complicated. Basically, the lower that number is // the steeper diminishing returns will be. I suggest playing around with Wolfram Alpha or a graphing calculator a bit. // In each of the following links, replace 0.65 with the gather rate of your worker for the resource with diminishing returns and diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index 8aa0c0aaa8..22e1190467 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -1987,7 +1987,9 @@ UnitAI.prototype.UnitFsmSpec = { // 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); - if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity)) + var cmpMirage = Engine.QueryInterface(this.gatheringTarget, IID_Mirage); + if ((!cmpMirage || !cmpMirage.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; @@ -3896,6 +3898,10 @@ UnitAI.prototype.TargetIsAlive = function(ent) if (cmpFormation) return true; + var cmpMirage = Engine.QueryInterface(ent, IID_Mirage); + if (cmpMirage) + return true; + var cmpHealth = Engine.QueryInterface(ent, IID_Health); if (!cmpHealth) return false; @@ -4386,6 +4392,11 @@ UnitAI.prototype.CheckTargetVisible = function(target) 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(), false) == "hidden") return false; @@ -5007,8 +5018,15 @@ UnitAI.prototype.PerformGather = function(target, queued, force) // 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 = Engine.QueryInterface(target, IID_ResourceSupply); - var type = cmpResourceSupply.GetType(); + var cmpMirage = Engine.QueryInterface(target, IID_Mirage); + if (cmpResourceSupply) + type = cmpResourceSupply.GetType(); + else if (cmpMirage && cmpMirage.ResourceSupply()) + type = cmpMirage.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 @@ -5552,9 +5570,10 @@ UnitAI.prototype.CanGather = function(target) { if (this.IsTurret()) return false; - // The target must be a valid resource supply. + // The target must be a valid resource supply, or the mirage of one. var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); - if (!cmpResourceSupply) + var cmpMirage = Engine.QueryInterface(target, IID_Mirage); + if (!cmpResourceSupply && !(cmpMirage && cmpMirage.ResourceSupply())) return false; // Formation controllers should always respond to commands diff --git a/binaries/data/mods/public/simulation/components/interfaces/Messages.js b/binaries/data/mods/public/simulation/components/interfaces/Messages.js index c4f6c6f43b..ae3d8fc701 100644 --- a/binaries/data/mods/public/simulation/components/interfaces/Messages.js +++ b/binaries/data/mods/public/simulation/components/interfaces/Messages.js @@ -1,8 +1,9 @@ /** * Broadcast message * sent when one entity is changed to other: - * from Foundation component when building constuction is done - * and from Promotion component when unit is promoted + * - from Foundation component when building construction is done + * - from Promotion component when unit is promoted + * - from Mirage component when a fogged entity is re-discovered * Data: { entity: , newentity: } */ Engine.RegisterMessageType("EntityRenamed"); diff --git a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js index 0ae49bcb0c..197785d13f 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -398,6 +398,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), { visibleClasses: ["class3", "class4"], selectionGroupName: "Selection Group Name", }, + fogging: null, foundation: null, garrisonHolder: null, gate: null, @@ -425,6 +426,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedEntityState(-1, 10), { }, buildingAI: null, healer: null, + mirage: null, obstruction: null, turretParent: null, promotion: null, diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js index 6cc67a3565..90e28c5d59 100644 --- a/binaries/data/mods/public/simulation/helpers/Commands.js +++ b/binaries/data/mods/public/simulation/helpers/Commands.js @@ -974,6 +974,11 @@ function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd) "queued": cmd.queued }); } + + // Load a mirage for the owner of this new entity + var cmpFogging = Engine.QueryInterface(ent, IID_Fogging) + if (cmpFogging) + cmpFogging.LoadMirage(player); return ent; } diff --git a/binaries/data/mods/public/simulation/templates/template_gaia.xml b/binaries/data/mods/public/simulation/templates/template_gaia.xml index 27744daf94..7a5f54b1c0 100644 --- a/binaries/data/mods/public/simulation/templates/template_gaia.xml +++ b/binaries/data/mods/public/simulation/templates/template_gaia.xml @@ -1,5 +1,6 @@ + gaia Gaia diff --git a/binaries/data/mods/public/simulation/templates/template_structure.xml b/binaries/data/mods/public/simulation/templates/template_structure.xml index 6272e33fef..47cc0efea7 100644 --- a/binaries/data/mods/public/simulation/templates/template_structure.xml +++ b/binaries/data/mods/public/simulation/templates/template_structure.xml @@ -37,6 +37,7 @@ 3.0 9.8 + corpse 0 diff --git a/source/ps/TemplateLoader.cpp b/source/ps/TemplateLoader.cpp index 4ef7374912..3c985fc829 100644 --- a/source/ps/TemplateLoader.cpp +++ b/source/ps/TemplateLoader.cpp @@ -80,6 +80,21 @@ bool CTemplateLoader::LoadTemplateFile(const std::string& templateName, int dept return true; } + // Handle special case "mirage|foo" + if (templateName.find("mirage|") == 0) + { + // Load the base entity template, if it wasn't already loaded + std::string baseName = templateName.substr(7); + if (!LoadTemplateFile(baseName, depth+1)) + { + LOGERROR(L"Failed to load entity template '%hs'", baseName.c_str()); + return false; + } + // Copy a subset to the requested template + CopyMirageSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]); + return true; + } + // Handle special case "foundation|foo" if (templateName.find("foundation|") == 0) { @@ -383,6 +398,40 @@ void CTemplateLoader::CopyPreviewSubset(CParamNode& out, const CParamNode& in, b } } +void CTemplateLoader::CopyMirageSubset(CParamNode& out, const CParamNode& in) +{ + // Currently used for mirage entities replacing real ones in fog-of-war + + std::set permittedComponentTypes; + permittedComponentTypes.insert("Footprint"); + permittedComponentTypes.insert("Minimap"); + permittedComponentTypes.insert("Ownership"); + permittedComponentTypes.insert("Position"); + permittedComponentTypes.insert("Selectable"); + permittedComponentTypes.insert("VisualActor"); + + CParamNode::LoadXMLString(out, ""); + out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes); + + // Select a subset of identity data. We don't want to have, for example, a CC mirage + // that has also the CC class and then prevents construction of other CCs + std::set identitySubset; + identitySubset.insert("Civ"); + identitySubset.insert("GenericName"); + identitySubset.insert("SpecificName"); + identitySubset.insert("Tooltip"); + identitySubset.insert("History"); + identitySubset.insert("Icon"); + CParamNode identity; + CParamNode::LoadXMLString(identity, ""); + identity.CopyFilteredChildrenOfChild(in.GetChild("Entity"), "Identity", identitySubset); + CParamNode::LoadXMLString(out, (""+utf8_from_wstring(identity.ToXML())+"").c_str()); + + // Set the entity as mirage entity + CParamNode::LoadXMLString(out, ""); + CParamNode::LoadXMLString(out, "0truefalse"); +} + void CTemplateLoader::CopyFoundationSubset(CParamNode& out, const CParamNode& in) { // TODO: this is all kind of yucky and hard-coded; it'd be nice to have a more generic @@ -397,6 +446,7 @@ void CTemplateLoader::CopyFoundationSubset(CParamNode& out, const CParamNode& in permittedComponentTypes.insert("Obstruction"); permittedComponentTypes.insert("Selectable"); permittedComponentTypes.insert("Footprint"); + permittedComponentTypes.insert("Fogging"); permittedComponentTypes.insert("Armour"); permittedComponentTypes.insert("Health"); permittedComponentTypes.insert("StatusBars"); diff --git a/source/ps/TemplateLoader.h b/source/ps/TemplateLoader.h index 7217b69261..30888b60ba 100644 --- a/source/ps/TemplateLoader.h +++ b/source/ps/TemplateLoader.h @@ -78,6 +78,12 @@ private: */ void CopyPreviewSubset(CParamNode& out, const CParamNode& in, bool corpse); + /** + * Copy the components of an entity template necessary for a fogged "mirage" + * entity (position, actor) into a new entity template + */ + void CopyMirageSubset(CParamNode& out, const CParamNode& in); + /** * Copy the components of an entity template necessary for a construction foundation * (position, actor, armour, health, etc) into a new entity template diff --git a/source/simulation2/TypeList.h b/source/simulation2/TypeList.h index 8525a1ca62..eb5ac6a0ad 100644 --- a/source/simulation2/TypeList.h +++ b/source/simulation2/TypeList.h @@ -81,6 +81,9 @@ COMPONENT(CommandQueue) INTERFACE(Decay) COMPONENT(Decay) +INTERFACE(Fogging) +COMPONENT(FoggingScripted) + // Note: The VisualActor component relies on this component being initialized before itself, in order to support using // an entity's footprint shape for the selection boxes. This dependency is not strictly necessary, but it does avoid // some extra plumbing code to set up on-demand initialization. If you find yourself forced to break this dependency, @@ -97,6 +100,9 @@ COMPONENT(IdentityScripted) INTERFACE(Minimap) COMPONENT(Minimap) +INTERFACE(Mirage) +COMPONENT(MirageScripted) + INTERFACE(Motion) COMPONENT(MotionBall) COMPONENT(MotionScripted) diff --git a/source/simulation2/components/CCmpRangeManager.cpp b/source/simulation2/components/CCmpRangeManager.cpp index 4fca628d99..f61deab5f3 100644 --- a/source/simulation2/components/CCmpRangeManager.cpp +++ b/source/simulation2/components/CCmpRangeManager.cpp @@ -23,6 +23,9 @@ #include "ICmpTerrain.h" #include "simulation2/system/EntityMap.h" #include "simulation2/MessageTypes.h" +#include "simulation2/components/ICmpFogging.h" +#include "simulation2/components/ICmpMirage.h" +#include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/components/ICmpVision.h" @@ -564,7 +567,6 @@ public: case MT_Update: { m_DebugOverlayDirty = true; - UpdateTerritoriesLos(); ExecuteActiveQueries(); UpdateVisibilityData(); break; @@ -1401,21 +1403,50 @@ public: return VIS_VISIBLE; } + // Mirage entities, whatever their position, are visible for one specific player + CmpPtr cmpMirage(ent); + if (cmpMirage && cmpMirage->GetPlayer() != player) + return VIS_HIDDEN; + // Visible if within a visible region CLosQuerier los(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); if (los.IsVisible(i, j)) return VIS_VISIBLE; + if (!los.IsExplored(i, j)) + return VIS_HIDDEN; + // Fogged if the 'retain in fog' flag is set, and in a non-visible explored region - if (los.IsExplored(i, j)) - { - CmpPtr cmpVision(ent); - if (forceRetainInFog || (cmpVision && cmpVision->GetRetainInFog())) - return VIS_FOGGED; - } + CmpPtr cmpVision(ent); + if (!forceRetainInFog && !(cmpVision && cmpVision->GetRetainInFog())) + return VIS_HIDDEN; - // Otherwise not visible + if (cmpMirage && cmpMirage->GetPlayer() == player) + return VIS_FOGGED; + + CmpPtr cmpOwnership(ent); + if (!cmpOwnership) + return VIS_VISIBLE; + + if (cmpOwnership->GetOwner() == player) + { + CmpPtr cmpFogging(ent); + if (!cmpFogging) + return VIS_VISIBLE; + + // Fogged entities must not disappear while the mirage is not ready + if (!cmpFogging->IsMiraged(player)) + return VIS_FOGGED; + + return VIS_HIDDEN; + } + + // Fogged entities must not disappear while the mirage is not ready + CmpPtr cmpFogging(ent); + if (cmpFogging && cmpFogging->WasSeen(player) && !cmpFogging->IsMiraged(player)) + return VIS_FOGGED; + return VIS_HIDDEN; } @@ -1487,18 +1518,29 @@ public: EntityMap::iterator itEnts = m_EntityData.find(ent); if (itEnts == m_EntityData.end()) return; + + std::vector oldVisibilities; + std::vector newVisibilities; - for (player_id_t player = 1; player <= MAX_LOS_PLAYER_ID; ++player) + for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID+1; ++player) { u8 oldVis = (itEnts->second.visibilities >> (2*(player-1))) & 0x3; u8 newVis = GetLosVisibility(itEnts->first, player, false); + + oldVisibilities.push_back(oldVis); + newVisibilities.push_back(newVis); if (oldVis != newVis) - { - CMessageVisibilityChanged msg(player, ent, oldVis, newVis); - GetSimContext().GetComponentManager().PostMessage(ent, msg); itEnts->second.visibilities = (itEnts->second.visibilities & ~(0x3 << 2*(player-1))) | (newVis << 2*(player-1)); - } + } + + for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID+1; ++player) + { + if (oldVisibilities[player-1] == newVisibilities[player-1]) + continue; + + CMessageVisibilityChanged msg(player, ent, oldVisibilities[player-1], newVisibilities[player-1]); + GetSimContext().GetComponentManager().PostMessage(ent, msg); } } diff --git a/source/simulation2/components/ICmpFogging.cpp b/source/simulation2/components/ICmpFogging.cpp new file mode 100644 index 0000000000..a981adb187 --- /dev/null +++ b/source/simulation2/components/ICmpFogging.cpp @@ -0,0 +1,44 @@ +/* Copyright (C) 2014 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 "ICmpFogging.h" + +#include "simulation2/scripting/ScriptComponent.h" +#include "simulation2/system/InterfaceScripted.h" + +BEGIN_INTERFACE_WRAPPER(Fogging) +END_INTERFACE_WRAPPER(Fogging) + +class CCmpFoggingScripted : public ICmpFogging +{ +public: + DEFAULT_SCRIPT_WRAPPER(FoggingScripted) + + virtual bool WasSeen(player_id_t player) + { + return m_Script.Call("WasSeen", player); + } + + virtual bool IsMiraged(player_id_t player) + { + return m_Script.Call("IsMiraged", player); + } +}; + +REGISTER_COMPONENT_SCRIPT_WRAPPER(FoggingScripted) diff --git a/source/simulation2/components/ICmpFogging.h b/source/simulation2/components/ICmpFogging.h new file mode 100644 index 0000000000..cd759f601d --- /dev/null +++ b/source/simulation2/components/ICmpFogging.h @@ -0,0 +1,39 @@ +/* Copyright (C) 2014 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 . + */ + +#ifndef INCLUDED_ICMPFOGGING +#define INCLUDED_ICMPFOGGING + +#include "simulation2/system/Interface.h" + +#include "simulation2/helpers/Player.h" + +/** + * Handles the fogging of out-of-sight enemy entities, by creating mirage + * entities. + * This allows hiding changes, especially destruction status or health. + */ +class ICmpFogging : public IComponent +{ +public: + virtual bool WasSeen(player_id_t player) = 0; + virtual bool IsMiraged(player_id_t player) = 0; + + DECLARE_INTERFACE_TYPE(Fogging) +}; + +#endif // INCLUDED_ICMPFOGGING diff --git a/source/simulation2/components/ICmpMirage.cpp b/source/simulation2/components/ICmpMirage.cpp new file mode 100644 index 0000000000..80ceaca2c6 --- /dev/null +++ b/source/simulation2/components/ICmpMirage.cpp @@ -0,0 +1,39 @@ +/* Copyright (C) 2014 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 "ICmpMirage.h" + +#include "simulation2/scripting/ScriptComponent.h" +#include "simulation2/system/InterfaceScripted.h" + +BEGIN_INTERFACE_WRAPPER(Mirage) +END_INTERFACE_WRAPPER(Mirage) + +class CCmpMirageScripted : public ICmpMirage +{ +public: + DEFAULT_SCRIPT_WRAPPER(MirageScripted) + + virtual player_id_t GetPlayer() + { + return m_Script.Call("GetPlayer"); + } +}; + +REGISTER_COMPONENT_SCRIPT_WRAPPER(MirageScripted) diff --git a/source/simulation2/components/ICmpMirage.h b/source/simulation2/components/ICmpMirage.h new file mode 100644 index 0000000000..5edb22734f --- /dev/null +++ b/source/simulation2/components/ICmpMirage.h @@ -0,0 +1,37 @@ +/* Copyright (C) 2014 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 . + */ + +#ifndef INCLUDED_ICMPMIRAGE +#define INCLUDED_ICMPMIRAGE + +#include "simulation2/system/Interface.h" + +#include "simulation2/helpers/Player.h" + +/** + * Component allowing mirage entities to communicate with their parent entity. + * See ICmpFogging. + */ +class ICmpMirage : public IComponent +{ +public: + virtual player_id_t GetPlayer() = 0; + + DECLARE_INTERFACE_TYPE(Mirage) +}; + +#endif // INCLUDED_ICMPMIRAGE