0ad/binaries/data/mods/public/simulation/components/Gate.js
wraitii 973de1ccd7 Fix foundation committing following 592453c62f (pushing)
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.
2021-04-06 12:28:53 +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(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);