Index: binaries/data/mods/public/gui/session/selection.js
===================================================================
--- binaries/data/mods/public/gui/session/selection.js
+++ binaries/data/mods/public/gui/session/selection.js
@@ -110,6 +110,15 @@
return this.groups[key];
};
+/**
+ * @return true if the group has at least one member
+ */
+EntityGroups.prototype.hasMembers = function()
+{
+ return Object.keys(this.groups).length != 0;
+};
+
+
EntityGroups.prototype.getTotalCount = function()
{
let totalCount = 0;
@@ -513,36 +522,204 @@
function EntityGroupsContainer()
{
this.groups = [];
+ this.info = [];
+ // Inits groups from 0 to 9
for (var i = 0; i < 10; ++i)
+ {
this.groups[i] = new EntityGroups();
+ var data = {}; // hitpoints, maxGroupHitPoints, numberOfUnits, initialNumberOfUnits, deathCycle, dirty, attack
+ this.info[i] = data;
+ }
}
/**
- * Add entities to a group.
- * @param {string} groupName - The number of the group to add the entities to.
- * @param {number[]} ents - The entities to add to the group.
- */
-EntityGroupsContainer.prototype.addEntities = function(groupName, ents)
-{
- if (Engine.ConfigDB_GetValue("user", "gui.session.disjointcontrolgroups") == "true")
- for (let ent of ents)
- for (let group of this.groups)
- if (ent in group.ents)
- group.removeEnt(ent);
-
- this.groups[groupName].add(ents);
+ * Adds entities to the group
+ * @param groupID: id of the group
+ * @param ents: array of entity IDs
+ */
+EntityGroupsContainer.prototype.addEntities = function(groupID, ents)
+{
+ for (var ent in ents)
+ for (let gID in this.groups)
+ {
+ let group = this.groups[gID];
+ if (ent in group.ents)
+ {
+ group.removeEnt(ent);
+ // Fade and icon should not be triggered if a unit comes to another group.
+ this.removeEnt(gID, ent);
+ break;
+ }
+ }
+ // Add entities to group
+ this.groups[groupID].add(ents);
+ this.updateAdditionalData(groupID, ents, true);
};
+/**
+ * Removes a entity from the info data
+ * @param groupID: id of the group
+ * @param ents: array of entity IDs
+ */
+EntityGroupsContainer.prototype.removeEnt = function(groupID, ent)
+{
+ if (!this.hasMembers(groupID))
+ return;
+ var entState = GetEntityState(+ent);
+ if (!entState)
+ return;
+
+ // update the data like this unit would never be in this group
+ var data = this.info[groupID];
+ data.hitpoints -= entState.hitpoints;
+ data.maxGroupHitPoints -= entState.maxHitpoints;
+ data.numberOfUnits--;
+ data.initialNumberOfUnits--;
+ data.dirty = true;
+}
+
+/**
+ * Updates the additional info of a group; can be called for init or update but should only be called, if needed.
+ * @param groupID: id of the group
+ * @param ents: array of entity IDs
+ * @param init: true, if init
+ */
+EntityGroupsContainer.prototype.updateAdditionalData = function(groupID, ents, init)
+{
+ if (!this.hasMembers(groupID))
+ return;
+
+ if (init)
+ {
+ let data = {};
+ data.dirty = true; // True, if GUI should be updated
+ data.attack = false; // True, if GUI should start color attack animation
+ data.hitpoints = 0;
+ data.maxGroupHitPoints = 0;
+ data.numberOfUnits = ents.length;
+ data.initialNumberOfUnits = data.numberOfUnits;
+ data.deathCycle = 0; // Used for death icon
+
+ // Update the additional info
+ if (data.numberOfUnits > 0)
+ this.info[groupID] = data;
+ else
+ this.info[groupID] = {};
+ }
+
+ // update the values needed for the GUI
+ this.updateHealth(groupID);
+ this.updateMaxHealth(groupID);
+ this.updateMembers(groupID);
+}
+
+/**
+ * updates the current health points of a group
+ * @param groupID: id of the group
+ */
+EntityGroupsContainer.prototype.updateHealth = function(groupID)
+{
+ if (!this.hasMembers(groupID))
+ return;
+
+ var data = this.info[groupID];
+ var hitpoints = 0;
+ // Calculate health
+ for (let ent in this.groups[groupID].ents)
+ {
+ let entState = GetEntityState(+ent);
+ hitpoints += entState.hitpoints;
+ }
+
+ // check, if attack is running.
+ if (hitpoints < data.hitpoints)
+ data.attack = true;
+ // update prev points
+ if (hitpoints != data.hitpoints)
+ {
+ data.hitpoints = hitpoints;
+ data.dirty = true;
+ }
+}
+
+/**
+ * Updates the member count of the group
+ * @param groupID: id of the group
+ */
+EntityGroupsContainer.prototype.updateMembers = function(groupID)
+{
+ if (!this.hasMembers(groupID))
+ return;
+
+ var data = this.info[groupID];
+ var numberOfUnits = this.groups[groupID].getTotalCount();
+ if (numberOfUnits < data.numberOfUnits)
+ {
+ // Update unit count
+ data.numberOfUnits = numberOfUnits;
+ data.deathCycle = 1; // Show death symbol
+ // Update max health of the group
+ this.updateMaxHealth(groupID);
+ data.dirty = true;
+ }
+}
+
+/**
+ * Updates the max health a group can have
+ * @param groupID: id of the group
+ */
+EntityGroupsContainer.prototype.updateMaxHealth = function(groupID)
+{
+ if (!this.hasMembers(groupID))
+ return;
+
+ var data = this.info[groupID];
+ data.maxGroupHitPoints = 0;
+ for (let ent in this.groups[groupID].ents)
+ {
+ let entState = GetEntityState(+ent);
+ data.maxGroupHitPoints += entState.maxHitpoints;
+ }
+}
+
+/**
+ * Updates the death cycle icon
+ * @param groupID: id of the group
+ */
+EntityGroupsContainer.prototype.updateDeathCycle = function(groupID)
+{
+ if (!this.hasMembers(groupID))
+ return;
+
+ var data = this.info[groupID];
+ if (data.deathCycle > 0)
+ data.deathCycle++;
+ if (data.deathCycle >= g_groupDeathShowIconTicks)
+ data.dirty = true;
+}
+
+
EntityGroupsContainer.prototype.update = function()
{
this.checkRenamedEntities();
- for (let group of this.groups)
- for (var ent in group.ents)
+ for (let groupID in this.groups)
{
+ // Nothing to do, if no members are there
+ if (!this.hasMembers(groupID))
+ continue;
+
+ let group = this.groups[groupID];
+ for (let ent in group.ents)
+ {
var entState = GetEntityState(+ent);
// Remove deleted units
if (!entState)
group.removeEnt(ent);
+ }
+ // update the values needed for the GUI
+ this.updateHealth(groupID);
+ this.updateMembers(groupID);
+ this.updateDeathCycle(groupID);
}
};
@@ -556,18 +733,102 @@
if (renamedEntities.length > 0)
{
var renamedLookup = {};
- for (let renamedEntity of renamedEntities)
+ for (let renamedEntity in renamedEntities)
renamedLookup[renamedEntity.entity] = renamedEntity.newentity;
- for (let group of this.groups)
- for (let renamedEntity of renamedEntities)
+ for (let groupID in this.groups)
+ {
+ let group = this.groups[groupID];
+ for (let renamedEntity in renamedEntities)
+ {
// Reconstruct the group if at least one entity has been renamed.
if (renamedEntity.entity in group.ents)
{
group.rebuildGroup(renamedLookup);
+ this.updateAdditionalData(groupID, Object.keys(group.ents), false);
break;
}
+ }
+ }
}
};
+/**
+ * Resets the initialNumberOfUnits
+ * @param groupID: id of the group
+ */
+EntityGroupsContainer.prototype.resetMemberStatus = function(groupID)
+{
+ if (!this.hasMembers(groupID))
+ return;
+
+ var data = this.info[groupID];
+ data.initialNumberOfUnits = data.numberOfUnits;
+ data.dirty = true;
+}
+
+/**
+ * Checks if a group with that ID has members
+ * @param groupID: id of the group
+ */
+EntityGroupsContainer.prototype.hasMembers = function(groupID)
+{
+ return this.groups[groupID].hasMembers();
+}
+
+/**
+ * Gets the dirty state for the GUI
+ * @param groupID: id of the group
+ */
+EntityGroupsContainer.prototype.isDirty = function(groupID)
+{
+ return this.hasMembers(groupID) && this.info[groupID].dirty;
+}
+
+/**
+ * Sets the dirty state to false after the GUI redrawed the stuff
+ * @param groupID: id of the group
+ */
+EntityGroupsContainer.prototype.resetDirty = function(groupID)
+{
+ if (!this.hasMembers(groupID))
+ return;
+
+ this.info[groupID].dirty = false;
+}
+
+/**
+ * Gets the attack state for the GUI
+ * @param groupID: id of the group
+ */
+EntityGroupsContainer.prototype.isAttacked = function(groupID)
+{
+ return this.hasMembers(groupID) && this.info[groupID].attack;
+}
+
+/**
+ * Sets the attack state to false after the color fade was started
+ * @param groupID: id of the group
+ */
+EntityGroupsContainer.prototype.resetAttack = function(groupID)
+{
+ if (!this.hasMembers(groupID))
+ return;
+
+ this.info[groupID].attack = false;
+}
+
+/**
+ * Sets the death cycle back to zero (off)
+ * @param groupID: id of the group
+ */
+EntityGroupsContainer.prototype.resetDeathCycle = function(groupID)
+{
+ if (!this.hasMembers(groupID))
+ return;
+
+ this.info[groupID].deathCycle = 0;
+}
+
+
var g_Groups = new EntityGroupsContainer();
Index: binaries/data/mods/public/gui/session/session.js
===================================================================
--- binaries/data/mods/public/gui/session/session.js
+++ binaries/data/mods/public/gui/session/session.js
@@ -180,6 +180,10 @@
*/
var g_MilitaryTypes = ["Melee", "Ranged"];
+// Simulation ticks until the deathIcon of a group is removed.
+var g_groupDeathShowIconTicks = 60;
+
+
function GetSimState()
{
if (!g_SimState)
@@ -311,6 +315,7 @@
updatePlayerData();
initializeMusic(); // before changing the perspective
Engine.SetBoundingBoxDebugOverlay(false);
+ initLayoutGroupButtons(); // Init group buttons
for (let handler of g_PlayersInitHandlers)
handler();
@@ -435,6 +440,30 @@
global.music.setState(global.music.states.PEACE);
}
+/** Inits the layout of the group buttons (adds spacers, labels and events to buttons)
+ */
+function initLayoutGroupButtons()
+{
+ var guiName = "Group";
+ for (let i = 0; i < 10; ++i)
+ {
+ // add label
+ let label = Engine.GetGUIObjectByName("unit"+guiName+"Label["+i+"]").caption = i;
+
+ // add button events and hide it
+ let button = Engine.GetGUIObjectByName("unit"+guiName+"Button["+i+"]");
+ button.onpress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); } })(i);
+ button.onDoublepress = (function(i) { return function() { performGroup("snap", i); } })(i);
+ button.onpressright = (function(i) { return function() { performGroup("breakUp", i); } })(i);
+ button.hidden = true;
+ setPanelObjectPosition(button, i, 1);
+ // get the members status bar and add a event
+ let memberButton = Engine.GetGUIObjectByName("unit"+guiName+"MembersButton["+i+"]");
+ memberButton.onpress = (function(i) { return function() { g_Groups.resetMemberStatus(i); } })(i);
+ }
+}
+
+
function resetTemplates()
{
// Update GUI and clear player-dependent cache
@@ -570,7 +599,8 @@
function getSavedGameData()
{
return {
- "groups": g_Groups.groups
+ "groups": g_Groups.groups,
+ "additionalGroupInfo": g_Groups.info,
};
}
@@ -589,6 +619,8 @@
{
g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups;
g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents;
+ g_Groups.info[groupNumber] = data.additionalGroupInfo[groupNumber];
+ g_Groups.info[groupNumber].dirty = true;
}
updateGroups();
}
@@ -727,6 +759,59 @@
}
}
+/**
+ * Changes the visibility of a status bar
+ * @param nameOfBar: name of the bar
+ * @param hide: true, if bar should be hidden
+ */
+function changeVisibilityStatusBar(nameOfBar, hide)
+{
+ // Get the bar and update it
+ var statusBar = Engine.GetGUIObjectByName(nameOfBar);
+ if (!statusBar)
+ return;
+
+ statusBar.hidden = hide;
+}
+
+/**
+* updates a status bar on the GUI
+* nameOfBar: name of the bar
+* points: points to show
+* maxPoints: max points
+* direction: gets less from (right to left) 0; (top to bottom) 1; (left to right) 2; (bottom to top) 3;
+*/
+function updateGUIStatusBar(nameOfBar, points, maxPoints, direction)
+{
+ // check, if optional direction parameter is valid.
+ if (!direction || !(direction >= 0 && direction < 4))
+ direction = 0;
+
+ // get the bar and update it
+ let statusBar = Engine.GetGUIObjectByName(nameOfBar);
+ if (!statusBar)
+ return;
+
+ let healthSize = statusBar.size;
+ let value = 100*Math.max(0, Math.min(1, points / maxPoints));
+
+ // inverse bar
+ if(direction == 2 || direction == 3)
+ value = 100 - value;
+
+ if (direction == 0)
+ healthSize.rright = value;
+ else if (direction == 1)
+ healthSize.rbottom = value;
+ else if (direction == 2)
+ healthSize.rleft = value;
+ else if (direction == 3)
+ healthSize.rtop = value;
+
+ statusBar.size = healthSize;
+}
+
+
function updateGroups()
{
g_Groups.update();
@@ -747,6 +832,10 @@
button.onDoublePress = (function(i) { return function() { performGroup("snap", i); }; })(i);
button.onPressRight = (function(i) { return function() { performGroup("breakUp", i); }; })(i);
+ // check, if status bars must be redrawn
+ if (!g_Groups.isDirty(i))
+ continue;
+
// Choose the icon of the most common template (or the most costly if it's not unique)
if (g_Groups.groups[i].getTotalCount() > 0)
{
@@ -760,9 +849,45 @@
icon ? ("stretched:session/portraits/" + icon) : "groupsIcon";
}
+ // update the status bars
+ let data = g_Groups.info[i];
+ updateGUIStatusBar("unitGroupHealthBar["+ i +"]", data.hitpoints, data.maxGroupHitPoints);
+
+ if(data.initialNumberOfUnits == 1)
+ changeVisibilityStatusBar("unitGroupMembersButton["+ i +"]", true);
+ else
+ {
+ changeVisibilityStatusBar("unitGroupMembersButton["+ i +"]", false);
+ updateGUIStatusBar("unitGroupMembersBar["+ i +"]", data.numberOfUnits, data.initialNumberOfUnits, 3);
+ }
+
+ // check, if the attack should be started
+ if (g_Groups.isAttacked(i))
+ {
+ startColorFade("unitGroupHitOverlay["+i+"]", 100, 10000, colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit);
+ g_Groups.resetAttack(i);
+ }
+
+ // check, if the death symbol should be triggered
+ if (data.deathCycle > 0)
+ {
+ let deathIcon = Engine.GetGUIObjectByName("unitGroupIconDeath["+ i +"]");
+ // show the symbol
+ if (data.deathCycle >= g_groupDeathShowIconTicks)
+ {
+ deathIcon.hidden = true;
+ g_Groups.resetDeathCycle(i);
+ }
+ else if (data.deathCycle >= 1)
+ deathIcon.hidden = false;
+ }
+
+ // reset the dirty state after the updates are complete
+ g_Groups.resetDirty(i);
setPanelObjectPosition(button, i, 1);
- }
-}
+ }
+ }
+
/**
* Toggles the display of status bars for all of the player's entities.
Index: binaries/data/mods/public/gui/session/session_objects/selection_group_icons.xml
===================================================================
--- binaries/data/mods/public/gui/session/session_objects/selection_group_icons.xml
+++ binaries/data/mods/public/gui/session/session_objects/selection_group_icons.xml
@@ -6,7 +6,28 @@
Index: binaries/data/mods/public/gui/session/sprites.xml
===================================================================
--- binaries/data/mods/public/gui/session/sprites.xml
+++ binaries/data/mods/public/gui/session/sprites.xml
@@ -9,6 +9,21 @@
/>
+
+
+
+
+
+
+
+
+
+
+
+