0ad/binaries/data/mods/public/simulation/components/Gate.js
wraitii 93a352ad16 Fix UnitAI range queries - allow queries to ignore sizes - partial revert of d0fc8ff67d
Units sometimes ignored targets that entered their LoS. The cause is
d0fc8ff67d: range queries returned units farther away, and those units
might actually be out of range if distance is computed center-to-center,
which both UnitAI and LOS do. This meant that code relying on range
query updates was possibly broken, and indeed units missed things (see
ticket).

This introduces a boolean to switch between pre-d0fc8ff67d behaviour
(entity-as-point, center-to-center range queries) and post-d0fc8ff67d
(entities-as-circumscribing-circle, edge-to-edge range queries).
The former is used for UnitAI (where the new behaviour bugged), auras
(where varying structure sizes made it awkward) and build
restrictions(which simply did not really need it).

Reverts 7f1ee23d88, 050c5401b1 (with the exception of the iber monument
footprint), and the template changes in d0fc8ff67d itself.

It also reduces alertRaiser ranges slightly, this was missed in the
original diff.

#3381 is not reopened as BuildingAI still uses the new range queries.

Reported by: Freagarach
Comments By: Angen
Fixes #5968

Differential Revision: https://code.wildfiregames.com/D3456
This was SVN commit r24776.
2021-01-23 18:57:46 +00:00

285 lines
7.8 KiB
JavaScript

function Gate() {}
Gate.prototype.Schema =
"<a:help>Controls behavior of wall gates</a:help>" +
"<a:example>" +
"<PassRange>20</PassRange>" +
"</a:example>" +
"<element name='PassRange' a:help='Units must be within this distance (in meters) of the gate for it to open'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>";
/**
* Initialize Gate component
*/
Gate.prototype.Init = function()
{
this.allies = [];
this.ignoreList = [];
this.opened = false;
this.locked = false;
};
Gate.prototype.OnOwnershipChanged = function(msg)
{
if (msg.to != INVALID_PLAYER)
{
this.SetupRangeQuery(msg.to);
// Set the initial state, but don't play unlocking sound
if (!this.locked)
this.UnlockGate(true);
}
};
Gate.prototype.OnDiplomacyChanged = function(msg)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() == msg.player)
{
this.allies = [];
this.ignoreList = [];
this.SetupRangeQuery(msg.player);
}
};
/**
* Cleanup on destroy
*/
Gate.prototype.OnDestroy = function()
{
// Clean up range query
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.unitsQuery)
cmpRangeManager.DestroyActiveQuery(this.unitsQuery);
// Cancel the closing-blocked timer if it's running.
if (this.timer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
}
};
/**
* Setup the range query to detect units coming in & out of range
*/
Gate.prototype.SetupRangeQuery = function(owner)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.unitsQuery)
cmpRangeManager.DestroyActiveQuery(this.unitsQuery);
// Only allied units can make the gate open.
var players = QueryPlayerIDInterface(owner).GetAllies();
var range = this.GetPassRange();
if (range > 0)
{
// Only find entities with IID_UnitAI interface
this.unitsQuery = cmpRangeManager.CreateActiveQuery(this.entity, 0, range, players, IID_UnitAI, cmpRangeManager.GetEntityFlagMask("normal"), true);
cmpRangeManager.EnableActiveQuery(this.unitsQuery);
}
};
/**
* Called when units enter or leave range
*/
Gate.prototype.OnRangeUpdate = function(msg)
{
if (msg.tag != this.unitsQuery)
return;
if (msg.added.length > 0)
for (let entity of msg.added)
{
// Ignore entities that cannot move as those won't be able to go through the gate.
let unitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (!unitAI || !unitAI.AbleToMove())
this.ignoreList.push(entity);
this.allies.push(entity);
}
if (msg.removed.length > 0)
for (let entity of msg.removed)
{
let index = this.ignoreList.indexOf(entity);
if (index !== -1)
this.ignoreList.splice(index, 1);
this.allies.splice(this.allies.indexOf(entity), 1);
}
this.OperateGate();
};
Gate.prototype.OnGlobalUnitAbleToMoveChanged = function(msg)
{
if (this.allies.indexOf(msg.entity) === -1)
return;
let index = this.ignoreList.indexOf(msg.entity);
if (msg.ableToMove && index !== -1)
this.ignoreList.splice(index, 1);
else if (!msg.ableToMove && index === -1)
this.ignoreList.push(msg.entity);
this.OperateGate();
};
/**
* Get the range in which units are detected
*/
Gate.prototype.GetPassRange = function()
{
return +this.template.PassRange;
};
Gate.prototype.ShouldOpen = function()
{
return this.allies.some(ent => this.ignoreList.indexOf(ent) === -1);
};
/**
* Attempt to open or close the gate.
* An ally must be in range to open the gate, but an unlocked gate will only close
* if there are no allies in range and no units are inside the gate's obstruction.
*/
Gate.prototype.OperateGate = function()
{
// Cancel the closing-blocked timer if it's running.
if (this.timer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
}
if (this.opened && (this.locked || !this.ShouldOpen()))
this.CloseGate();
else if (!this.opened && this.ShouldOpen())
this.OpenGate();
};
Gate.prototype.IsLocked = function()
{
return this.locked;
};
/**
* Lock the gate, with sound. It will close at the next opportunity.
*/
Gate.prototype.LockGate = function()
{
this.locked = true;
// Delete animal corpses to prevent units trying to gather the unreachable entity
let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction && cmpObstruction.GetBlockMovementFlag())
for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction())
Engine.DestroyEntity(ent);
// If the door is closed, enable 'block pathfinding'
// Else 'block pathfinding' will be enabled the next time the gate close
if (!this.opened)
{
if (cmpObstruction)
cmpObstruction.SetDisableBlockMovementPathfinding(false, false, 0);
}
else
this.OperateGate();
// TODO: Possibly move the lock/unlock sounds to UI? Needs testing
PlaySound("gate_locked", this.entity);
};
/**
* Unlock the gate, with sound. May open the gate if allied units are within range.
* If quiet is true, no sound will be played (used for initial setup).
*/
Gate.prototype.UnlockGate = function(quiet)
{
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (!cmpObstruction)
return;
// Disable 'block pathfinding'
cmpObstruction.SetDisableBlockMovementPathfinding(this.opened, true, 0);
this.locked = false;
// TODO: Possibly move the lock/unlock sounds to UI? Needs testing
if (!quiet)
PlaySound("gate_unlocked", this.entity);
// If the gate is closed, open it if necessary
if (!this.opened)
this.OperateGate();
};
/**
* Open the gate if unlocked, with sound and animation.
*/
Gate.prototype.OpenGate = function()
{
// Do not open the gate if it has been locked
if (this.locked)
return;
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (!cmpObstruction)
return;
// Disable 'block movement'
cmpObstruction.SetDisableBlockMovementPathfinding(true, true, 0);
this.opened = true;
PlaySound("gate_opening", this.entity);
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation("gate_opening", true, 1.0);
};
/**
* Close the gate, with sound and animation.
*
* The gate may fail to close due to unit obstruction. If this occurs, the
* gate will start a timer and attempt to close on each simulation update.
*/
Gate.prototype.CloseGate = function()
{
let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (!cmpObstruction)
return;
// The gate can't be closed if there are entities colliding with it.
// NB: because walls are overlapping, they requires special care to not break
// in particular, walls do not block construction, so walls from skirmish maps
// do not appear in this check even if they have different control groups from the gate.
// This no longer works if gates are made to check for entities blocking movement.
// Fixing that would let us change this code, but it sounds decidedly non-trivial.
let collisions = cmpObstruction.GetEntitiesBlockingConstruction();
if (collisions.length)
{
if (!this.timer)
{
// Set an "instant" timer which will run on the next simulation turn.
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_Gate, "OperateGate", 0);
}
return;
}
// If we ordered the gate to be locked, enable 'block movement' and 'block pathfinding'
if (this.locked)
cmpObstruction.SetDisableBlockMovementPathfinding(false, false, 0);
// Else just enable 'block movement'
else
cmpObstruction.SetDisableBlockMovementPathfinding(false, true, 0);
this.opened = false;
PlaySound("gate_closing", this.entity);
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation("gate_closing", true, 1.0);
};
Engine.RegisterComponentType(IID_Gate, "Gate", Gate);