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 @@ Click to select grouped units, double-click to focus the grouped units and right-click to disband the group. +