diff --git a/binaries/data/mods/public/gui/common/functions_global_object.js b/binaries/data/mods/public/gui/common/functions_global_object.js
index e7a4194e44..eb7e1860a7 100644
--- a/binaries/data/mods/public/gui/common/functions_global_object.js
+++ b/binaries/data/mods/public/gui/common/functions_global_object.js
@@ -6,7 +6,7 @@
// *******************************************
// messageBox
// *******************************************
-// @params: int mbWidth, int mbHeight, string mbMessage, string mbTitle, int mbMode, arr mbButtonCaptions
+// @params: int mbWidth, int mbHeight, string mbMessage, string mbTitle, int mbMode, arr mbButtonCaptions, arr mbButtonsCode
// @return: void
// @desc: Displays a new modal message box.
// *******************************************
diff --git a/binaries/data/mods/public/gui/page_summary.xml b/binaries/data/mods/public/gui/page_summary.xml
new file mode 100644
index 0000000000..0239814247
--- /dev/null
+++ b/binaries/data/mods/public/gui/page_summary.xml
@@ -0,0 +1,9 @@
+
+
+ common/setup.xml
+ common/styles.xml
+ common/sprite1.xml
+ common/init.xml
+ summary/summary.xml
+ common/global.xml
+
diff --git a/binaries/data/mods/public/gui/session_new/session.js b/binaries/data/mods/public/gui/session_new/session.js
index fb7feff786..274b0b6edc 100644
--- a/binaries/data/mods/public/gui/session_new/session.js
+++ b/binaries/data/mods/public/gui/session_new/session.js
@@ -17,6 +17,9 @@ var g_IsTrainingQueueBlocked = false;
// Cache EntityStates
var g_EntityStates = {}; // {id:entState}
+// Whether the player has lost/won and reached the end of their game
+var g_GameEnded = false;
+
function GetEntityState(entId)
{
if (!(entId in g_EntityStates))
@@ -75,9 +78,36 @@ function init(initData, hotloadData)
function leaveGame()
{
+ var simState = Engine.GuiInterfaceCall("GetSimulationState");
+ var playerState = simState.players[Engine.GetPlayerID()];
+
+ var gameResult;
+ if (playerState.state == "won")
+ {
+ gameResult = "You have won the battle!";
+ }
+ else if (playerState.state == "defeated")
+ {
+ gameResult = "You have been defeated...";
+ }
+ else // "active"
+ {
+ gameResult = "You have abandoned the game.";
+
+ // Tell other players that we have given up and
+ // been defeated
+ Engine.PostNetworkCommand({
+ "type": "defeat-player",
+ "playerId": Engine.GetPlayerID()
+ });
+
+ }
+
stopMusic();
endGame();
- Engine.SwitchGuiPage("page_pregame.xml");
+
+ Engine.SwitchGuiPage("page_summary.xml", { "gameResult" : gameResult });
+
}
// Return some data that we'll use when hotloading this file after changes
@@ -88,6 +118,8 @@ function getHotloadData()
function onTick()
{
+ checkPlayerState();
+
while (true)
{
var message = Engine.PollNetworkClient();
@@ -121,6 +153,28 @@ function onTick()
getGUIObjectByName("resourcePop").textcolor = "white";
}
+function checkPlayerState()
+{
+ var simState = Engine.GuiInterfaceCall("GetSimulationState");
+ var playerState = simState.players[Engine.GetPlayerID()];
+
+ if (!g_GameEnded)
+ {
+ if (playerState.state == "defeated")
+ {
+ g_GameEnded = true;
+ messageBox(400, 200, "You have been defeated... Do you want to leave the game now?",
+ "Defeat", 0, ["Yes", "No!"], [leaveGame, null]);
+ }
+ else if (playerState.state == "won")
+ {
+ g_GameEnded = true;
+ messageBox(400, 200, "You have won the battle! Do you want to leave the game now?",
+ "Victory", 0, ["Yes", "No!"], [leaveGame, null]);
+ }
+ }
+}
+
function onSimulationUpdate()
{
g_Selection.dirty = false;
diff --git a/binaries/data/mods/public/gui/summary/summary.js b/binaries/data/mods/public/gui/summary/summary.js
new file mode 100644
index 0000000000..61c1f405d2
--- /dev/null
+++ b/binaries/data/mods/public/gui/summary/summary.js
@@ -0,0 +1,4 @@
+function init(data)
+{
+ getGUIObjectByName("summaryText").caption = data.gameResult;
+}
diff --git a/binaries/data/mods/public/gui/summary/summary.xml b/binaries/data/mods/public/gui/summary/summary.xml
new file mode 100644
index 0000000000..fff69b7104
--- /dev/null
+++ b/binaries/data/mods/public/gui/summary/summary.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
diff --git a/binaries/data/mods/public/maps/scenarios/Pathfinding_demo.xml b/binaries/data/mods/public/maps/scenarios/Pathfinding_demo.xml
index ec2334387c..0ecd3eabcd 100644
--- a/binaries/data/mods/public/maps/scenarios/Pathfinding_demo.xml
+++ b/binaries/data/mods/public/maps/scenarios/Pathfinding_demo.xml
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5657b71b257c6445e7c4c89002a8a418d4b8e7148608fe42751880550f274cce
-size 18224
+oid sha256:2553e1a19c4b3fee16f93e145af1e60ca0daf5b5adee94b895647cb5151d59eb
+size 18248
diff --git a/binaries/data/mods/public/maps/scenarios/Pathfinding_terrain_demo.xml b/binaries/data/mods/public/maps/scenarios/Pathfinding_terrain_demo.xml
index caf7196848..75dc4c04a0 100644
--- a/binaries/data/mods/public/maps/scenarios/Pathfinding_terrain_demo.xml
+++ b/binaries/data/mods/public/maps/scenarios/Pathfinding_terrain_demo.xml
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:11768719c63f14b4450657c6c37b817e1962e734515e9329adbf191d4c39a54a
-size 7694
+oid sha256:533c91ef3bb9bb4e5979d8ea0f8233d20ffb4d12a1d9f7ac854c0fafac4e5150
+size 7718
diff --git a/binaries/data/mods/public/maps/scenarios/Units_demo.xml b/binaries/data/mods/public/maps/scenarios/Units_demo.xml
index ff9b3ee434..e09871648b 100644
--- a/binaries/data/mods/public/maps/scenarios/Units_demo.xml
+++ b/binaries/data/mods/public/maps/scenarios/Units_demo.xml
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:08e09ebf02494ac877823a08bb372879af40b0ac7ccfada6266de21585444c90
-size 2277
+oid sha256:5c38dd28118693b4883b564e8e2c488d4fc04014586e0c66a913507ce18cf9a7
+size 2301
diff --git a/binaries/data/mods/public/simulation/components/EndGameManager.js b/binaries/data/mods/public/simulation/components/EndGameManager.js
new file mode 100644
index 0000000000..3201e27816
--- /dev/null
+++ b/binaries/data/mods/public/simulation/components/EndGameManager.js
@@ -0,0 +1,110 @@
+// Repetition interval (msecs) for checking end game conditions
+var g_ProgressInterval = 1000;
+
+/**
+ * System component which regularly checks victory/defeat conditions
+ * and if they are satisfied then it marks the player as victorious/defeated.
+ */
+function EndGameManager() {}
+
+EndGameManager.prototype.Schema =
+ "";
+
+EndGameManager.prototype.Init = function()
+{
+ // Game type, initialised from the map settings.
+ // One of: "conquest" (default) and "endless"
+ this.gameType = "conquest";
+};
+
+EndGameManager.prototype.SetGameType = function(newGameType)
+{
+ this.gameType = newGameType;
+};
+
+/**
+ * Begin checking the end-game conditions.
+ * Must be called once, after calling SetGameType.
+ */
+EndGameManager.prototype.Start = function()
+{
+ if (this.gameType != "endless")
+ {
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.timer = cmpTimer.SetTimeout(this.entity, IID_EndGameManager, "ProgressTimeout", g_ProgressInterval, {});
+ }
+};
+
+EndGameManager.prototype.OnDestroy = function()
+{
+ if (this.timer)
+ {
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.timer);
+ }
+};
+
+EndGameManager.prototype.ProgressTimeout = function(data)
+{
+ this.UpdatePlayerStates();
+
+ // Repeat the timer
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.timer = cmpTimer.SetTimeout(this.entity, IID_EndGameManager, "ProgressTimeout", g_ProgressInterval, data);
+};
+
+EndGameManager.prototype.UpdatePlayerStates = function()
+{
+ var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
+ switch (this.gameType)
+ {
+ case "conquest":
+
+ // If a player is currently active but has no suitable units left,
+ // mark that player as defeated
+ // (Start from player 1 since we ignore Gaia)
+ for (var i = 1; i < cmpPlayerManager.GetNumPlayers(); i++)
+ {
+ var playerEntityId = cmpPlayerManager.GetPlayerByID(i);
+ var cmpPlayer = Engine.QueryInterface(playerEntityId, IID_Player);
+ if (cmpPlayer.GetState() == "active")
+ {
+ if (cmpPlayer.GetConquestCriticalEntitiesCount() == 0)
+ {
+ Engine.PostMessage(playerEntityId, MT_PlayerDefeated, null);
+ }
+ }
+ }
+
+ // If there's only player remaining active, mark them as the winner
+ // TODO: update this code for allies
+
+ var alivePlayersCount = 0;
+ var lastAlivePlayerId;
+ // (Start from 1 to ignore Gaia)
+ for (var i = 1; i < cmpPlayerManager.GetNumPlayers(); i++)
+ {
+ var playerEntityId = cmpPlayerManager.GetPlayerByID(i);
+ var cmpPlayer = Engine.QueryInterface(playerEntityId, IID_Player);
+ if (cmpPlayer.GetState() == "active")
+ {
+ alivePlayersCount++;
+ lastAlivePlayerId = i;
+ }
+ }
+ if (alivePlayersCount == 1)
+ {
+ var playerEntityId = cmpPlayerManager.GetPlayerByID(lastAlivePlayerId);
+ var cmpPlayer = Engine.QueryInterface(playerEntityId, IID_Player);
+ cmpPlayer.SetState("won");
+ }
+
+ break;
+
+ default:
+ error("Invalid game type "+this.gameType);
+ break;
+ }
+};
+
+Engine.RegisterComponentType(IID_EndGameManager, "EndGameManager", EndGameManager);
diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js
index e7f3e5833b..a5c9b153d9 100644
--- a/binaries/data/mods/public/simulation/components/GuiInterface.js
+++ b/binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -36,7 +36,8 @@ GuiInterface.prototype.GetSimulationState = function(player)
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
- "trainingQueueBlocked": cmpPlayer.IsTrainingQueueBlocked()
+ "trainingQueueBlocked": cmpPlayer.IsTrainingQueueBlocked(),
+ "state": cmpPlayer.GetState()
};
ret.players.push(playerData);
}
diff --git a/binaries/data/mods/public/simulation/components/Identity.js b/binaries/data/mods/public/simulation/components/Identity.js
index 0a0f404aaa..013ad5eb04 100644
--- a/binaries/data/mods/public/simulation/components/Identity.js
+++ b/binaries/data/mods/public/simulation/components/Identity.js
@@ -62,12 +62,14 @@ Identity.prototype.Schema =
"Mechanical" +
"Super" +
"Hero" +
+ "Structure" +
"Civic" +
"Economic" +
"Defensive" +
"Village" +
"Town" +
"City" +
+ "ConquestCritical" +
"Bow" + // TODO: what are these used for?
"Javelin" +
"Spear" +
diff --git a/binaries/data/mods/public/simulation/components/Player.js b/binaries/data/mods/public/simulation/components/Player.js
index 8e91420ba6..a35b46934c 100644
--- a/binaries/data/mods/public/simulation/components/Player.js
+++ b/binaries/data/mods/public/simulation/components/Player.js
@@ -19,6 +19,8 @@ Player.prototype.Init = function()
"metal": 500,
"stone": 1000
};
+ this.state = "active"; // game state - one of "active", "defeated", "won"
+ this.conquestCriticalEntitiesCount = 0; // number of owned units with ConquestCritical class
};
Player.prototype.SetPlayerID = function(id)
@@ -146,12 +148,40 @@ Player.prototype.TrySubtractResources = function(amounts)
return true;
};
+Player.prototype.GetState = function()
+{
+ return this.state;
+};
+
+Player.prototype.SetState = function(newState)
+{
+ this.state = newState;
+};
+
+Player.prototype.GetConquestCriticalEntitiesCount = function()
+{
+ return this.conquestCriticalEntitiesCount;
+};
+
// Keep track of population effects of all entities that
// become owned or unowned by this player
Player.prototype.OnGlobalOwnershipChanged = function(msg)
{
+ var classes = [];
+
+ // Load class list only if we're going to need it
+ if (msg.from == this.playerID || msg.to == this.playerID)
+ {
+ var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
+ if (cmpIdentity)
+ classes = cmpIdentity.GetClassesList();
+ }
+
if (msg.from == this.playerID)
{
+ if (classes.indexOf("ConquestCritical") != -1)
+ this.conquestCriticalEntitiesCount--;
+
var cost = Engine.QueryInterface(msg.entity, IID_Cost);
if (cost)
{
@@ -162,6 +192,9 @@ Player.prototype.OnGlobalOwnershipChanged = function(msg)
if (msg.to == this.playerID)
{
+ if (classes.indexOf("ConquestCritical") != -1)
+ this.conquestCriticalEntitiesCount++;
+
var cost = Engine.QueryInterface(msg.entity, IID_Cost);
if (cost)
{
@@ -171,4 +204,19 @@ Player.prototype.OnGlobalOwnershipChanged = function(msg)
}
};
+Player.prototype.OnPlayerDefeated = function()
+{
+ this.state = "defeated";
+
+ // Reassign all player's entities to Gaia
+ var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ var entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID);
+ for each (var entity in entities)
+ {
+ // Note: maybe we need to reassign units and buildings only?
+ var cmpOwnership = Engine.QueryInterface(entity, IID_Ownership);
+ cmpOwnership.SetOwner(0);
+ }
+}
+
Engine.RegisterComponentType(IID_Player, "Player", Player);
diff --git a/binaries/data/mods/public/simulation/components/interfaces/EndGameManager.js b/binaries/data/mods/public/simulation/components/interfaces/EndGameManager.js
new file mode 100644
index 0000000000..77286b1598
--- /dev/null
+++ b/binaries/data/mods/public/simulation/components/interfaces/EndGameManager.js
@@ -0,0 +1,2 @@
+Engine.RegisterInterface("EndGameManager");
+Engine.RegisterMessageType("PlayerDefeated");
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 98ed8d383d..850fcc4ae2 100644
--- a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
+++ b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
@@ -31,6 +31,7 @@ AddMock(100, IID_Player, {
GetPopulationLimit: function() { return 20; },
GetResourceCounts: function() { return { food: 100 }; },
IsTrainingQueueBlocked: function() { return false; },
+ GetState: function() { return "active"; },
});
AddMock(101, IID_Player, {
@@ -41,6 +42,7 @@ AddMock(101, IID_Player, {
GetPopulationLimit: function() { return 30; },
GetResourceCounts: function() { return { food: 200 }; },
IsTrainingQueueBlocked: function() { return false; },
+ GetState: function() { return "active"; },
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
@@ -53,6 +55,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
popLimit: 20,
resourceCounts: { food: 100 },
trainingQueueBlocked: false,
+ state: "active",
},
{
name: "Player 2",
@@ -62,6 +65,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
popLimit: 30,
resourceCounts: { food: 200 },
trainingQueueBlocked: false,
+ state: "active",
}
]
});
diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js
index e5a9a64016..8470610b97 100644
--- a/binaries/data/mods/public/simulation/helpers/Commands.js
+++ b/binaries/data/mods/public/simulation/helpers/Commands.js
@@ -143,6 +143,14 @@ function ProcessCommand(player, cmd)
cmpRallyPoint.Unset();
}
break;
+
+ case "defeat-player":
+ // Get player entity by playerId
+ var cmpPlayerMananager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
+ var playerEnt = cmpPlayerManager.GetPlayerByID(cmd.playerId);
+ // Send "OnPlayerDefeated" message to player
+ Engine.PostMessage(playerEnt, MT_PlayerDefeated, null);
+ break;
default:
error("Ignoring unrecognised command type '" + cmd.type + "'");
diff --git a/binaries/data/mods/public/simulation/helpers/Setup.js b/binaries/data/mods/public/simulation/helpers/Setup.js
index b4dff7446c..91ed181d48 100644
--- a/binaries/data/mods/public/simulation/helpers/Setup.js
+++ b/binaries/data/mods/public/simulation/helpers/Setup.js
@@ -19,6 +19,13 @@ function LoadMapSettings(settings)
if (cmpRangeManager)
cmpRangeManager.SetLosRevealAll(true);
}
+
+ var cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
+ if (settings.GameType)
+ {
+ cmpEndGameManager.SetGameType(settings.GameType);
+ }
+ cmpEndGameManager.Start();
}
Engine.RegisterGlobal("LoadMapSettings", LoadMapSettings);
diff --git a/binaries/data/mods/public/simulation/templates/template_structure.xml b/binaries/data/mods/public/simulation/templates/template_structure.xml
index 0d2e1937b1..01658d0be7 100644
--- a/binaries/data/mods/public/simulation/templates/template_structure.xml
+++ b/binaries/data/mods/public/simulation/templates/template_structure.xml
@@ -2,7 +2,11 @@
Structure
- snPortraitSheetBuildings
+
+ Structure
+ ConquestCritical
+
+ snPortraitSheetBuildings
standard
diff --git a/binaries/data/mods/public/simulation/templates/template_unit.xml b/binaries/data/mods/public/simulation/templates/template_unit.xml
index c8d0900fc7..e6770b41f6 100644
--- a/binaries/data/mods/public/simulation/templates/template_unit.xml
+++ b/binaries/data/mods/public/simulation/templates/template_unit.xml
@@ -2,6 +2,9 @@
Unit
+
+ ConquestCritical
+
unit
diff --git a/source/simulation2/Simulation2.cpp b/source/simulation2/Simulation2.cpp
index e1a040082b..7a054e3aad 100644
--- a/source/simulation2/Simulation2.cpp
+++ b/source/simulation2/Simulation2.cpp
@@ -113,6 +113,7 @@ public:
LOAD_SCRIPTED_COMPONENT("GuiInterface");
LOAD_SCRIPTED_COMPONENT("PlayerManager");
LOAD_SCRIPTED_COMPONENT("Timer");
+ LOAD_SCRIPTED_COMPONENT("EndGameManager");
#undef LOAD_SCRIPTED_COMPONENT
}
diff --git a/source/simulation2/components/CCmpRangeManager.cpp b/source/simulation2/components/CCmpRangeManager.cpp
index 9c9fb39d50..8a1102bf5f 100644
--- a/source/simulation2/components/CCmpRangeManager.cpp
+++ b/source/simulation2/components/CCmpRangeManager.cpp
@@ -462,6 +462,22 @@ public:
return r;
}
+ virtual std::vector GetEntitiesByPlayer(int playerId)
+ {
+ std::vector entities;
+
+ u32 ownerMask = CalcOwnerMask(playerId);
+
+ for (std::map::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it)
+ {
+ // Check owner and add to list if it matches
+ if (CalcOwnerMask(it->second.owner) & ownerMask)
+ entities.push_back(it->first);
+ }
+
+ return entities;
+ }
+
virtual void SetDebugOverlay(bool enabled)
{
m_DebugOverlayEnabled = enabled;
diff --git a/source/simulation2/components/ICmpRangeManager.cpp b/source/simulation2/components/ICmpRangeManager.cpp
index 1c461b31a9..746d70bf73 100644
--- a/source/simulation2/components/ICmpRangeManager.cpp
+++ b/source/simulation2/components/ICmpRangeManager.cpp
@@ -28,6 +28,7 @@ DEFINE_INTERFACE_METHOD_1("DestroyActiveQuery", void, ICmpRangeManager, DestroyA
DEFINE_INTERFACE_METHOD_1("EnableActiveQuery", void, ICmpRangeManager, EnableActiveQuery, ICmpRangeManager::tag_t)
DEFINE_INTERFACE_METHOD_1("DisableActiveQuery", void, ICmpRangeManager, DisableActiveQuery, ICmpRangeManager::tag_t)
DEFINE_INTERFACE_METHOD_1("ResetActiveQuery", std::vector, ICmpRangeManager, ResetActiveQuery, ICmpRangeManager::tag_t)
+DEFINE_INTERFACE_METHOD_1("GetEntitiesByPlayer", std::vector, ICmpRangeManager, GetEntitiesByPlayer, int)
DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpRangeManager, SetDebugOverlay, bool)
DEFINE_INTERFACE_METHOD_1("SetLosRevealAll", void, ICmpRangeManager, SetLosRevealAll, bool)
END_INTERFACE_WRAPPER(RangeManager)
diff --git a/source/simulation2/components/ICmpRangeManager.h b/source/simulation2/components/ICmpRangeManager.h
index 561be80667..ed24f0a97a 100644
--- a/source/simulation2/components/ICmpRangeManager.h
+++ b/source/simulation2/components/ICmpRangeManager.h
@@ -125,6 +125,14 @@ public:
*/
virtual std::vector ResetActiveQuery(tag_t tag) = 0;
+ /**
+ * Returns list of all entities for specific player.
+ * (This is on this interface because it shares a lot of the implementation.
+ * Maybe it should be extended to be more like ExecuteQuery without
+ * the range parameter.)
+ */
+ virtual std::vector GetEntitiesByPlayer(int playerId) = 0;
+
/**
* Toggle the rendering of debug info.
*/