mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-07-10 00:45:47 -07:00
Units sometimes ignored targets that entered their LoS. The cause isd0fc8ff67d: 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). Reverts7f1ee23d88,050c5401b1(with the exception of the iber monument footprint), and the template changes ind0fc8ff67ditself. 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.
285 lines
7.8 KiB
JavaScript
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);
|