mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
Previously it was made so that "template data" was a sort of super type of "entity state" and that the former could always be replaced by the latter. Using this we can simplify some code passages by removing GetTemplateData calls and using the entity's state directly instead.
611 lines
15 KiB
JavaScript
611 lines
15 KiB
JavaScript
// Limits selection size
|
|
var g_MaxSelectionSize = 300;
|
|
|
|
// Alpha value of hovered/mouseover/highlighted selection overlays
|
|
// (should probably be greater than always visible alpha value,
|
|
// see CCmpSelectable)
|
|
var g_HighlightedAlpha = 0.75;
|
|
|
|
function _setHighlight(ents, alpha, selected)
|
|
{
|
|
if (ents.length)
|
|
Engine.GuiInterfaceCall("SetSelectionHighlight", { "entities": ents, "alpha": alpha, "selected": selected });
|
|
}
|
|
|
|
function _setStatusBars(ents, enabled)
|
|
{
|
|
if (!ents.length)
|
|
return;
|
|
Engine.GuiInterfaceCall("SetStatusBars", {
|
|
"entities": ents,
|
|
"enabled": enabled,
|
|
"showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true",
|
|
"showExperience": Engine.ConfigDB_GetValue("user", "gui.session.experiencestatusbar") == "true"
|
|
});
|
|
}
|
|
|
|
function _setMotionOverlay(ents, enabled, motionDebugOverlay, force = false)
|
|
{
|
|
if (!force && !motionDebugOverlay)
|
|
return;
|
|
|
|
// Get entities plus their formation controllers (if any)
|
|
const resultSet = new Set();
|
|
for (const ent of ents)
|
|
{
|
|
resultSet.add(ent);
|
|
const entState = GetEntityState(ent);
|
|
if (entState?.unitAI?.formationController)
|
|
{
|
|
resultSet.add(entState.unitAI.formationController);
|
|
}
|
|
}
|
|
|
|
if (resultSet.size)
|
|
Engine.GuiInterfaceCall("SetMotionDebugOverlay", {
|
|
"entities": resultSet,
|
|
"enabled": enabled
|
|
});
|
|
}
|
|
|
|
function _playSound(ent)
|
|
{
|
|
Engine.GuiInterfaceCall("PlaySound", { "name": "select", "entity": ent });
|
|
}
|
|
|
|
/**
|
|
* EntityGroups class for managing grouped entities
|
|
*/
|
|
function EntityGroups()
|
|
{
|
|
this.groups = {};
|
|
this.ents = {};
|
|
}
|
|
|
|
EntityGroups.prototype.reset = function()
|
|
{
|
|
this.groups = {};
|
|
this.ents = {};
|
|
};
|
|
|
|
EntityGroups.prototype.add = function(ents)
|
|
{
|
|
for (const ent of ents)
|
|
{
|
|
if (this.ents[ent])
|
|
continue;
|
|
const entState = GetEntityState(ent);
|
|
|
|
// When this function is called during group rebuild, deleted
|
|
// entities will not yet have been removed, so entities might
|
|
// still be present in the group despite not existing.
|
|
if (!entState)
|
|
continue;
|
|
|
|
let key = entState.selectionGroupName || entState.templateName;
|
|
|
|
// Group the ents by player and template
|
|
if (entState.player !== undefined)
|
|
key = "p" + entState.player + "&" + key;
|
|
|
|
if (this.groups[key])
|
|
this.groups[key] += 1;
|
|
else
|
|
this.groups[key] = 1;
|
|
|
|
this.ents[ent] = key;
|
|
}
|
|
};
|
|
|
|
EntityGroups.prototype.removeEnt = function(ent)
|
|
{
|
|
var key = this.ents[ent];
|
|
|
|
// Remove the entity
|
|
delete this.ents[ent];
|
|
--this.groups[key];
|
|
|
|
// Remove the entire group
|
|
if (this.groups[key] == 0)
|
|
delete this.groups[key];
|
|
};
|
|
|
|
EntityGroups.prototype.rebuildGroup = function(renamed)
|
|
{
|
|
var oldGroup = this.ents;
|
|
this.reset();
|
|
|
|
var toAdd = [];
|
|
for (var ent in oldGroup)
|
|
toAdd.push(renamed[ent] ? renamed[ent] : +ent);
|
|
|
|
this.add(toAdd);
|
|
};
|
|
|
|
EntityGroups.prototype.getCount = function(key)
|
|
{
|
|
return this.groups[key];
|
|
};
|
|
|
|
EntityGroups.prototype.getTotalCount = function()
|
|
{
|
|
let totalCount = 0;
|
|
for (const key in this.groups)
|
|
totalCount += this.groups[key];
|
|
return totalCount;
|
|
};
|
|
|
|
EntityGroups.prototype.getKeys = function()
|
|
{
|
|
// Preserve order even when shuffling units around
|
|
// Can be optimized by moving the sorting elsewhere
|
|
return Object.keys(this.groups).sort();
|
|
};
|
|
|
|
EntityGroups.prototype.getEntsByKey = function(key)
|
|
{
|
|
var ents = [];
|
|
for (var ent in this.ents)
|
|
if (this.ents[ent] == key)
|
|
ents.push(+ent);
|
|
|
|
return ents;
|
|
};
|
|
|
|
/**
|
|
* get a list of entities grouped by a key
|
|
*/
|
|
EntityGroups.prototype.getEntsGrouped = function()
|
|
{
|
|
return this.getKeys().map(key => ({
|
|
"ents": this.getEntsByKey(key),
|
|
"key": key
|
|
}));
|
|
};
|
|
|
|
/**
|
|
* Gets all ents in every group except ones of the specified group
|
|
*/
|
|
EntityGroups.prototype.getEntsByKeyInverse = function(key)
|
|
{
|
|
var ents = [];
|
|
for (var ent in this.ents)
|
|
if (this.ents[ent] != key)
|
|
ents.push(+ent);
|
|
|
|
return ents;
|
|
};
|
|
|
|
/**
|
|
* EntitySelection class for managing the entity selection list and the primary selection
|
|
*/
|
|
function EntitySelection()
|
|
{
|
|
// Private properties:
|
|
this.selected = new Set();
|
|
|
|
// For mouseover-highlighted entity IDs in these.
|
|
this.highlighted = new Set();
|
|
|
|
this.motionDebugOverlay = false;
|
|
|
|
// Public properties:
|
|
this.dirty = false; // set whenever the selection has changed
|
|
this.groups = new EntityGroups();
|
|
|
|
this.UpdateFormationSelectionBehaviour();
|
|
registerConfigChangeHandler(changes =>
|
|
{
|
|
if (changes.has("gui.session.selectformationasone"))
|
|
this.UpdateFormationSelectionBehaviour();
|
|
});
|
|
}
|
|
|
|
EntitySelection.prototype.UpdateFormationSelectionBehaviour = function()
|
|
{
|
|
this.SelectFormationAsOne = Engine.ConfigDB_GetValue("user", "gui.session.selectformationasone") == "true";
|
|
};
|
|
|
|
/**
|
|
* Deselect everything but entities of the chosen type.
|
|
*/
|
|
EntitySelection.prototype.makePrimarySelection = function(key)
|
|
{
|
|
const ents = this.groups.getEntsByKey(key);
|
|
this.reset();
|
|
this.addList(ents, false, false, false);
|
|
};
|
|
|
|
/**
|
|
* Deselect entities of the chosen type.
|
|
*/
|
|
EntitySelection.prototype.removeGroupFromSelection = function(key)
|
|
{
|
|
this.removeList(this.groups.getEntsByKey(key), false);
|
|
};
|
|
|
|
/**
|
|
* Get a list of the template names
|
|
*/
|
|
EntitySelection.prototype.getTemplateNames = function()
|
|
{
|
|
const templateNames = [];
|
|
for (const ent of this.selected)
|
|
{
|
|
const entState = GetEntityState(ent);
|
|
if (entState)
|
|
templateNames.push(entState.templateName);
|
|
}
|
|
return templateNames;
|
|
};
|
|
|
|
/**
|
|
* Update the selection to take care of changes (like units that have been killed).
|
|
*/
|
|
EntitySelection.prototype.update = function()
|
|
{
|
|
this.checkRenamedEntities();
|
|
|
|
const controlsAll = g_SimState.players[g_ViewedPlayer] && g_SimState.players[g_ViewedPlayer].controlsAll;
|
|
const removeOwnerChanges = !g_IsObserver && !controlsAll && this.selected.size > 1;
|
|
|
|
let changed = false;
|
|
|
|
for (const ent of this.selected)
|
|
{
|
|
const entState = GetEntityState(ent);
|
|
|
|
if (!entState)
|
|
{
|
|
this.selected.delete(ent);
|
|
this.groups.removeEnt(ent);
|
|
changed = true;
|
|
continue;
|
|
}
|
|
|
|
// Remove non-visible units (e.g. moved back into fog-of-war)
|
|
// At the next update, mirages will be renamed to the real
|
|
// entity they replace, so just ignore them now
|
|
// Futhermore, when multiple selection, remove units which have changed ownership
|
|
if (entState.visibility == "hidden" && !entState.mirage ||
|
|
removeOwnerChanges && entState.player != g_ViewedPlayer)
|
|
{
|
|
// Disable any highlighting of the disappeared unit
|
|
_setHighlight([ent], 0, false);
|
|
_setStatusBars([ent], false);
|
|
_setMotionOverlay([ent], false, this.motionDebugOverlay);
|
|
|
|
this.selected.delete(ent);
|
|
this.groups.removeEnt(ent);
|
|
changed = true;
|
|
continue;
|
|
}
|
|
}
|
|
// Refresh the motion overlay
|
|
if (this.motionDebugOverlay)
|
|
_setMotionOverlay([...this.selected], true, this.motionDebugOverlay);
|
|
|
|
if (changed)
|
|
this.onChange();
|
|
};
|
|
|
|
/**
|
|
* Update selection if some selected entities were renamed
|
|
* (in case of unit promotion or finishing building structure)
|
|
*/
|
|
EntitySelection.prototype.checkRenamedEntities = function()
|
|
{
|
|
var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities");
|
|
if (renamedEntities.length > 0)
|
|
{
|
|
var renamedLookup = {};
|
|
for (const renamedEntity of renamedEntities)
|
|
renamedLookup[renamedEntity.entity] = renamedEntity.newentity;
|
|
|
|
// Reconstruct the selection if at least one entity has been renamed.
|
|
for (const renamedEntity of renamedEntities)
|
|
if (this.selected.has(renamedEntity.entity))
|
|
{
|
|
this.rebuildSelection(renamedLookup);
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Add entities to selection. Play selection sound unless quiet is true
|
|
*/
|
|
EntitySelection.prototype.addList = function(ents, quiet, force = false, addFormationMembers = true)
|
|
{
|
|
force = force || g_SimState.players[g_ViewedPlayer]?.controlsAll;
|
|
// If someone else's player is the sole selected unit, don't allow adding to the selection.
|
|
const firstEntState = this.selected.size == 1 && GetEntityState(this.getFirstSelected());
|
|
if (firstEntState && firstEntState.player != g_ViewedPlayer && !force)
|
|
return;
|
|
|
|
const added = [];
|
|
|
|
for (const ent of addFormationMembers ? this.addFormationMembers(ents) : ents)
|
|
{
|
|
if (this.selected.size >= g_MaxSelectionSize)
|
|
break;
|
|
|
|
if (this.selected.has(ent))
|
|
continue;
|
|
|
|
const entState = GetEntityState(ent);
|
|
if (!entState)
|
|
continue;
|
|
|
|
const isUnowned = g_ViewedPlayer != -1 && entState.player != g_ViewedPlayer ||
|
|
g_ViewedPlayer == -1 && entState.player == 0;
|
|
|
|
if (isUnowned && (ents.length > 1 || this.selected.size) && !force)
|
|
continue;
|
|
|
|
added.push(ent);
|
|
this.selected.add(ent);
|
|
}
|
|
|
|
_setHighlight(added, 1, true);
|
|
_setStatusBars(added, true);
|
|
_setMotionOverlay(added, this.motionDebugOverlay, this.motionDebugOverlay);
|
|
if (added.length)
|
|
{
|
|
// Play the sound if the entity is controllable by us or Gaia-owned.
|
|
var owner = GetEntityState(added[0]).player;
|
|
if (!quiet && (controlsPlayer(owner) || g_IsObserver || owner == 0))
|
|
_playSound(added[0]);
|
|
}
|
|
|
|
this.groups.add(this.toList()); // Create Selection Groups
|
|
this.onChange();
|
|
};
|
|
|
|
/**
|
|
* @param {number[]} ents - The entities to remove.
|
|
* @param {boolean} addFormationMembers - If true we need to add formation members.
|
|
*/
|
|
EntitySelection.prototype.removeList = function(ents, addFormationMembers = true)
|
|
{
|
|
const removed = [];
|
|
|
|
for (const ent of addFormationMembers ? this.addFormationMembers(ents) : ents)
|
|
if (this.selected.has(ent))
|
|
{
|
|
this.groups.removeEnt(ent);
|
|
removed.push(ent);
|
|
this.selected.delete(ent);
|
|
}
|
|
|
|
_setHighlight(removed, 0, false);
|
|
_setStatusBars(removed, false);
|
|
_setMotionOverlay(removed, false, this.motionDebugOverlay);
|
|
|
|
this.onChange();
|
|
};
|
|
|
|
EntitySelection.prototype.reset = function()
|
|
{
|
|
_setHighlight(this.toList(), 0, false);
|
|
_setStatusBars(this.toList(), false);
|
|
_setMotionOverlay(this.toList(), false, this.motionDebugOverlay);
|
|
this.selected.clear();
|
|
this.groups.reset();
|
|
this.onChange();
|
|
};
|
|
|
|
EntitySelection.prototype.rebuildSelection = function(renamed)
|
|
{
|
|
const toAdd = [];
|
|
for (const ent of this.selected)
|
|
toAdd.push(renamed[ent] || ent);
|
|
this.reset();
|
|
|
|
this.addList(toAdd, true); // don't play selection sounds
|
|
};
|
|
|
|
EntitySelection.prototype.getFirstSelected = function()
|
|
{
|
|
for (const ent of this.selected)
|
|
return ent;
|
|
return undefined;
|
|
};
|
|
|
|
/**
|
|
* TODO: This array should not be recreated every call
|
|
*/
|
|
EntitySelection.prototype.toList = function()
|
|
{
|
|
return Array.from(this.selected);
|
|
};
|
|
|
|
/**
|
|
* @return {number} - The number of entities selected.
|
|
*/
|
|
EntitySelection.prototype.size = function()
|
|
{
|
|
return this.selected.size;
|
|
};
|
|
|
|
EntitySelection.prototype.find = function(condition)
|
|
{
|
|
for (const ent of this.selected)
|
|
if (condition(ent))
|
|
return ent;
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* @param {function} condition - A function.
|
|
* @return {number[]} - The entities passing the condition.
|
|
*/
|
|
EntitySelection.prototype.filter = function(condition)
|
|
{
|
|
const result = [];
|
|
for (const ent of this.selected)
|
|
if (condition(ent))
|
|
result.push(ent);
|
|
return result;
|
|
};
|
|
|
|
EntitySelection.prototype.setHighlightList = function(entities)
|
|
{
|
|
const highlighted = new Set();
|
|
const ents = this.addFormationMembers(entities);
|
|
for (const ent of ents)
|
|
highlighted.add(ent);
|
|
|
|
const removed = [];
|
|
const added = [];
|
|
|
|
// Remove highlighting for the old units that are no longer highlighted
|
|
// (excluding ones that are actively selected too).
|
|
for (const ent of this.highlighted)
|
|
if (!highlighted.has(ent) && !this.selected.has(ent))
|
|
removed.push(ent);
|
|
|
|
// Add new highlighting for units that aren't already highlighted.
|
|
for (const ent of ents)
|
|
if (!this.highlighted.has(ent) && !this.selected.has(ent))
|
|
added.push(ent);
|
|
|
|
_setHighlight(removed, 0, false);
|
|
_setStatusBars(removed, false);
|
|
|
|
_setHighlight(added, g_HighlightedAlpha, true);
|
|
_setStatusBars(added, true);
|
|
|
|
this.highlighted = highlighted;
|
|
};
|
|
|
|
EntitySelection.prototype.SetMotionDebugOverlay = function(enabled)
|
|
{
|
|
this.motionDebugOverlay = enabled;
|
|
_setMotionOverlay(this.toList(), enabled, this.motionDebugOverlay, true);
|
|
};
|
|
|
|
EntitySelection.prototype.onChange = function()
|
|
{
|
|
this.dirty = true;
|
|
if (this.isSelection)
|
|
onSelectionChange();
|
|
};
|
|
|
|
EntitySelection.prototype.selectAndMoveTo = function(entityID)
|
|
{
|
|
const entState = GetEntityState(entityID);
|
|
if (!entState || !entState.position)
|
|
return;
|
|
|
|
this.reset();
|
|
this.addList([entityID]);
|
|
|
|
setCameraFollow(entityID);
|
|
};
|
|
|
|
/**
|
|
* Adds the formation members of a selected entities to the selection.
|
|
* @param {number[]} entities - The entity IDs of selected entities.
|
|
* @return {number[]} - Some more entity IDs if part of a formation was selected.
|
|
*/
|
|
EntitySelection.prototype.addFormationMembers = function(entities)
|
|
{
|
|
if (!entities.length || !this.SelectFormationAsOne || Engine.HotkeyIsPressed("selection.singleselection"))
|
|
return entities;
|
|
|
|
const result = new Set(entities);
|
|
for (const entity of entities)
|
|
{
|
|
const entState = GetEntityState(+entity);
|
|
if (entState?.unitAI?.formationController)
|
|
for (const member of GetEntityState(+entState.unitAI.formationController).formation.members)
|
|
result.add(member);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Cache some quantities which depends only on selection
|
|
*/
|
|
|
|
var g_Selection = new EntitySelection();
|
|
g_Selection.isSelection = true;
|
|
|
|
var g_canMoveIntoFormation = {};
|
|
var g_allBuildableEntities;
|
|
var g_allTrainableEntities;
|
|
|
|
// Reset cached quantities
|
|
function onSelectionChange()
|
|
{
|
|
g_canMoveIntoFormation = {};
|
|
g_allBuildableEntities = undefined;
|
|
g_allTrainableEntities = undefined;
|
|
}
|
|
|
|
/**
|
|
* EntityGroupsContainer class for managing grouped entities
|
|
*/
|
|
function EntityGroupsContainer()
|
|
{
|
|
this.groups = [];
|
|
for (var i = 0; i < 10; ++i)
|
|
this.groups[i] = new EntityGroups();
|
|
}
|
|
|
|
/**
|
|
* 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 (const ent of ents)
|
|
for (const group of this.groups)
|
|
if (ent in group.ents)
|
|
group.removeEnt(ent);
|
|
|
|
this.groups[groupName].add(ents);
|
|
};
|
|
|
|
EntityGroupsContainer.prototype.update = function()
|
|
{
|
|
this.checkRenamedEntities();
|
|
for (const group of this.groups)
|
|
for (var ent in group.ents)
|
|
{
|
|
var entState = GetEntityState(+ent);
|
|
// Remove deleted units
|
|
if (!entState)
|
|
group.removeEnt(ent);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update control group if some entities in the group were renamed
|
|
* (in case of unit promotion or finishing building structure)
|
|
*/
|
|
EntityGroupsContainer.prototype.checkRenamedEntities = function()
|
|
{
|
|
var renamedEntities = Engine.GuiInterfaceCall("GetRenamedEntities");
|
|
if (renamedEntities.length > 0)
|
|
{
|
|
var renamedLookup = {};
|
|
for (const renamedEntity of renamedEntities)
|
|
renamedLookup[renamedEntity.entity] = renamedEntity.newentity;
|
|
|
|
for (const group of this.groups)
|
|
for (const renamedEntity of renamedEntities)
|
|
// Reconstruct the group if at least one entity has been renamed.
|
|
if (renamedEntity.entity in group.ents)
|
|
{
|
|
group.rebuildGroup(renamedLookup);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
var g_Groups = new EntityGroupsContainer();
|