mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-07-10 00:45:47 -07:00
592453c62f added a flag to "GetBlockMovementFlag" to return either the
current or the template value, and by default it returns the template
value.
Foundation logic needed it to return the current value.
Gates are also updated to avoid changing behaviour in unpredictable
ways.
Differential Revision: https://code.wildfiregames.com/D3796
This was SVN commit r25196.
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(true))
|
|
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);
|