Line data Source code
1 : function Gate() {}
2 :
3 1 : Gate.prototype.Schema =
4 : "<a:help>Controls behavior of wall gates</a:help>" +
5 : "<a:example>" +
6 : "<PassRange>20</PassRange>" +
7 : "</a:example>" +
8 : "<element name='PassRange' a:help='Units must be within this distance (in meters) of the gate for it to open'>" +
9 : "<ref name='nonNegativeDecimal'/>" +
10 : "</element>";
11 :
12 : /**
13 : * Initialize Gate component
14 : */
15 1 : Gate.prototype.Init = function()
16 : {
17 2 : this.allies = [];
18 2 : this.ignoreList = [];
19 2 : this.opened = false;
20 2 : this.locked = false;
21 : };
22 :
23 1 : Gate.prototype.OnOwnershipChanged = function(msg)
24 : {
25 2 : if (msg.to != INVALID_PLAYER)
26 : {
27 2 : this.SetupRangeQuery(msg.to);
28 : // Set the initial state, but don't play unlocking sound
29 2 : if (!this.locked)
30 1 : this.UnlockGate(true);
31 : }
32 : };
33 :
34 1 : Gate.prototype.OnDiplomacyChanged = function(msg)
35 : {
36 1 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
37 1 : if (cmpOwnership && cmpOwnership.GetOwner() == msg.player)
38 : {
39 1 : this.allies = [];
40 1 : this.ignoreList = [];
41 1 : this.SetupRangeQuery(msg.player);
42 : }
43 : };
44 :
45 : /**
46 : * Cleanup on destroy
47 : */
48 1 : Gate.prototype.OnDestroy = function()
49 : {
50 : // Clean up range query
51 0 : var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
52 0 : if (this.unitsQuery)
53 0 : cmpRangeManager.DestroyActiveQuery(this.unitsQuery);
54 :
55 : // Cancel the closing-blocked timer if it's running.
56 0 : if (this.timer)
57 : {
58 0 : var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
59 0 : cmpTimer.CancelTimer(this.timer);
60 0 : this.timer = undefined;
61 : }
62 : };
63 :
64 : /**
65 : * Setup the range query to detect units coming in & out of range
66 : */
67 1 : Gate.prototype.SetupRangeQuery = function(owner)
68 : {
69 3 : var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
70 :
71 3 : if (this.unitsQuery)
72 0 : cmpRangeManager.DestroyActiveQuery(this.unitsQuery);
73 :
74 : // Only allied units can make the gate open.
75 3 : var players = QueryPlayerIDInterface(owner).GetAllies();
76 :
77 3 : var range = this.GetPassRange();
78 3 : if (range > 0)
79 : {
80 : // Only find entities with IID_UnitAI interface
81 3 : this.unitsQuery = cmpRangeManager.CreateActiveQuery(this.entity, 0, range, players, IID_UnitAI, cmpRangeManager.GetEntityFlagMask("normal"), true);
82 3 : cmpRangeManager.EnableActiveQuery(this.unitsQuery);
83 : }
84 : };
85 :
86 : /**
87 : * Called when units enter or leave range
88 : */
89 1 : Gate.prototype.OnRangeUpdate = function(msg)
90 : {
91 3 : if (msg.tag != this.unitsQuery)
92 0 : return;
93 :
94 3 : if (msg.added.length > 0)
95 2 : for (let entity of msg.added)
96 : {
97 : // Ignore entities that cannot move as those won't be able to go through the gate.
98 2 : let unitAI = Engine.QueryInterface(entity, IID_UnitAI);
99 2 : if (!unitAI || !unitAI.AbleToMove())
100 0 : this.ignoreList.push(entity);
101 2 : this.allies.push(entity);
102 : }
103 :
104 3 : if (msg.removed.length > 0)
105 1 : for (let entity of msg.removed)
106 : {
107 1 : let index = this.ignoreList.indexOf(entity);
108 1 : if (index !== -1)
109 1 : this.ignoreList.splice(index, 1);
110 1 : this.allies.splice(this.allies.indexOf(entity), 1);
111 : }
112 :
113 3 : this.OperateGate();
114 : };
115 :
116 1 : Gate.prototype.OnGlobalUnitAbleToMoveChanged = function(msg)
117 : {
118 1 : if (this.allies.indexOf(msg.entity) === -1)
119 0 : return;
120 :
121 1 : let index = this.ignoreList.indexOf(msg.entity);
122 1 : if (msg.ableToMove && index !== -1)
123 0 : this.ignoreList.splice(index, 1);
124 1 : else if (!msg.ableToMove && index === -1)
125 1 : this.ignoreList.push(msg.entity);
126 :
127 1 : this.OperateGate();
128 : };
129 :
130 : /**
131 : * Get the range in which units are detected
132 : */
133 1 : Gate.prototype.GetPassRange = function()
134 : {
135 3 : return +this.template.PassRange;
136 : };
137 :
138 1 : Gate.prototype.ShouldOpen = function()
139 : {
140 10 : return this.allies.some(ent => this.ignoreList.indexOf(ent) === -1);
141 : };
142 :
143 : /**
144 : * Attempt to open or close the gate.
145 : * An ally must be in range to open the gate, but an unlocked gate will only close
146 : * if there are no allies in range and no units are inside the gate's obstruction.
147 : */
148 1 : Gate.prototype.OperateGate = function()
149 : {
150 : // Cancel the closing-blocked timer if it's running.
151 7 : if (this.timer)
152 : {
153 0 : var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
154 0 : cmpTimer.CancelTimer(this.timer);
155 0 : this.timer = undefined;
156 : }
157 7 : if (this.opened && (this.locked || !this.ShouldOpen()))
158 2 : this.CloseGate();
159 5 : else if (!this.opened && this.ShouldOpen())
160 3 : this.OpenGate();
161 : };
162 :
163 1 : Gate.prototype.IsLocked = function()
164 : {
165 1 : return this.locked;
166 : };
167 :
168 : /**
169 : * Lock the gate, with sound. It will close at the next opportunity.
170 : */
171 1 : Gate.prototype.LockGate = function()
172 : {
173 2 : this.locked = true;
174 :
175 : // Delete animal corpses to prevent units trying to gather the unreachable entity
176 2 : let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
177 2 : if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true))
178 0 : for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction())
179 0 : Engine.DestroyEntity(ent);
180 :
181 : // If the door is closed, enable 'block pathfinding'
182 : // Else 'block pathfinding' will be enabled the next time the gate close
183 2 : if (!this.opened)
184 : {
185 1 : if (cmpObstruction)
186 1 : cmpObstruction.SetDisableBlockMovementPathfinding(false, false, 0);
187 : }
188 : else
189 1 : this.OperateGate();
190 :
191 : // TODO: Possibly move the lock/unlock sounds to UI? Needs testing
192 2 : PlaySound("gate_locked", this.entity);
193 : };
194 :
195 : /**
196 : * Unlock the gate, with sound. May open the gate if allied units are within range.
197 : * If quiet is true, no sound will be played (used for initial setup).
198 : */
199 1 : Gate.prototype.UnlockGate = function(quiet)
200 : {
201 2 : var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
202 2 : if (!cmpObstruction)
203 0 : return;
204 :
205 : // Disable 'block pathfinding'
206 2 : cmpObstruction.SetDisableBlockMovementPathfinding(this.opened, true, 0);
207 2 : this.locked = false;
208 :
209 : // TODO: Possibly move the lock/unlock sounds to UI? Needs testing
210 2 : if (!quiet)
211 1 : PlaySound("gate_unlocked", this.entity);
212 :
213 : // If the gate is closed, open it if necessary
214 2 : if (!this.opened)
215 2 : this.OperateGate();
216 : };
217 :
218 : /**
219 : * Open the gate if unlocked, with sound and animation.
220 : */
221 1 : Gate.prototype.OpenGate = function()
222 : {
223 : // Do not open the gate if it has been locked
224 3 : if (this.locked)
225 1 : return;
226 :
227 2 : var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
228 2 : if (!cmpObstruction)
229 0 : return;
230 :
231 : // Disable 'block movement'
232 2 : cmpObstruction.SetDisableBlockMovementPathfinding(true, true, 0);
233 2 : this.opened = true;
234 :
235 2 : PlaySound("gate_opening", this.entity);
236 2 : var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
237 2 : if (cmpVisual)
238 0 : cmpVisual.SelectAnimation("gate_opening", true, 1.0);
239 : };
240 :
241 : /**
242 : * Close the gate, with sound and animation.
243 : *
244 : * The gate may fail to close due to unit obstruction. If this occurs, the
245 : * gate will start a timer and attempt to close on each simulation update.
246 : */
247 1 : Gate.prototype.CloseGate = function()
248 : {
249 2 : let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
250 2 : if (!cmpObstruction)
251 0 : return;
252 :
253 : // The gate can't be closed if there are entities colliding with it.
254 : // NB: because walls are overlapping, they requires special care to not break
255 : // in particular, walls do not block construction, so walls from skirmish maps
256 : // do not appear in this check even if they have different control groups from the gate.
257 : // This no longer works if gates are made to check for entities blocking movement.
258 : // Fixing that would let us change this code, but it sounds decidedly non-trivial.
259 2 : let collisions = cmpObstruction.GetEntitiesBlockingConstruction();
260 2 : if (collisions.length)
261 : {
262 0 : if (!this.timer)
263 : {
264 : // Set an "instant" timer which will run on the next simulation turn.
265 0 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
266 0 : this.timer = cmpTimer.SetTimeout(this.entity, IID_Gate, "OperateGate", 0);
267 : }
268 0 : return;
269 : }
270 :
271 : // If we ordered the gate to be locked, enable 'block movement' and 'block pathfinding'
272 2 : if (this.locked)
273 1 : cmpObstruction.SetDisableBlockMovementPathfinding(false, false, 0);
274 : // Else just enable 'block movement'
275 : else
276 1 : cmpObstruction.SetDisableBlockMovementPathfinding(false, true, 0);
277 2 : this.opened = false;
278 :
279 2 : PlaySound("gate_closing", this.entity);
280 2 : let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
281 2 : if (cmpVisual)
282 0 : cmpVisual.SelectAnimation("gate_closing", true, 1.0);
283 : };
284 :
285 1 : Engine.RegisterComponentType(IID_Gate, "Gate", Gate);
|