Line data Source code
1 : function Trigger() {}
2 :
3 4 : Trigger.prototype.Schema =
4 : "<a:component type='system'/><empty/>";
5 :
6 : /**
7 : * Events we're able to receive and call handlers for.
8 : */
9 4 : Trigger.prototype.eventNames =
10 : [
11 : "OnCinemaPathEnded",
12 : "OnCinemaQueueEnded",
13 : "OnConstructionStarted",
14 : "OnDiplomacyChanged",
15 : "OnDeserialized",
16 : "OnInitGame",
17 : "OnInterval",
18 : "OnEntityRenamed",
19 : "OnOwnershipChanged",
20 : "OnPlayerCommand",
21 : "OnPlayerDefeated",
22 : "OnPlayerWon",
23 : "OnRange",
24 : "OnResearchFinished",
25 : "OnResearchQueued",
26 : "OnStructureBuilt",
27 : "OnTrainingFinished",
28 : "OnTrainingQueued",
29 : "OnTreasureCollected"
30 : ];
31 :
32 4 : Trigger.prototype.Init = function()
33 : {
34 : // Difficulty used by trigger scripts (as defined in data/settings/trigger_difficulties.json).
35 4 : this.difficulty = undefined;
36 :
37 4 : this.triggerPoints = {};
38 :
39 : // Each event has its own set of actions determined by the map maker.
40 4 : this.triggers = {};
41 4 : for (const eventName of this.eventNames)
42 76 : this.triggers[eventName] = {};
43 : };
44 :
45 4 : Trigger.prototype.RegisterTriggerPoint = function(ref, ent)
46 : {
47 0 : if (!this.triggerPoints[ref])
48 0 : this.triggerPoints[ref] = [];
49 0 : this.triggerPoints[ref].push(ent);
50 : };
51 :
52 4 : Trigger.prototype.RemoveRegisteredTriggerPoint = function(ref, ent)
53 : {
54 0 : if (!this.triggerPoints[ref])
55 : {
56 0 : warn("no trigger points found with ref "+ref);
57 0 : return;
58 : }
59 0 : const i = this.triggerPoints[ref].indexOf(ent);
60 0 : if (i == -1)
61 : {
62 0 : warn("entity " + ent + " wasn't found under the trigger points with ref "+ref);
63 0 : return;
64 : }
65 0 : this.triggerPoints[ref].splice(i, 1);
66 : };
67 :
68 4 : Trigger.prototype.GetTriggerPoints = function(ref)
69 : {
70 0 : return this.triggerPoints[ref] || [];
71 : };
72 :
73 : /**
74 : * Create a trigger listening on a specific event.
75 : *
76 : * @param {string} event - One of eventNames
77 : * @param {string} name - Name of the trigger.
78 : * If no action is specified in triggerData, the action will be the trigger name.
79 : * @param {Object} triggerData - f.e. enabled or not, delay for timers, range for range triggers.
80 : * @param {Object} customData - User-defined data that will be forwarded to the action.
81 : *
82 : * @example
83 : * triggerData = { enabled: true, interval: 1000, delay: 500 }
84 : *
85 : * General settings:
86 : * enabled = false * If the trigger is enabled by default.
87 : * action = name * The function (on Trigger) to call. Defaults to the trigger name.
88 : *
89 : * Range trigger:
90 : * entities = [id1, id2] * Ids of the source
91 : * players = [1,2,3,...] * list of player ids
92 : * minRange = 0 * Minimum range for the query
93 : * maxRange = -1 * Maximum range for the query (-1 = no maximum)
94 : * requiredComponent = 0 * Required component id the entities will have
95 : */
96 4 : Trigger.prototype.RegisterTrigger = function(event, name, triggerData, customData = undefined)
97 : {
98 0 : if (!this.triggers[event])
99 : {
100 0 : warn("Trigger.js: Invalid trigger event \"" + event + "\".");
101 0 : return;
102 : }
103 0 : if (this.triggers[event][name])
104 : {
105 0 : warn("Trigger.js: Trigger \"" + name + "\" has been registered before. Aborting...");
106 0 : return;
107 : }
108 : // clone the data to be sure it's only modified locally
109 : // We could run into triggers overwriting each other's data otherwise.
110 : // F.e. getting the wrong timer tag
111 0 : triggerData = clone(triggerData) || { "enabled": false };
112 0 : if (!triggerData.action)
113 0 : triggerData.action = name;
114 :
115 0 : this.triggers[event][name] = { "triggerData": triggerData, "customData": customData };
116 :
117 : // setup range query
118 0 : if (event == "OnRange")
119 : {
120 0 : if (!triggerData.entities)
121 : {
122 0 : warn("Trigger.js: Range triggers should carry extra data");
123 0 : return;
124 : }
125 0 : triggerData.queries = [];
126 0 : for (const ent of triggerData.entities)
127 : {
128 0 : const cmpTriggerPoint = Engine.QueryInterface(ent, IID_TriggerPoint);
129 0 : if (!cmpTriggerPoint)
130 : {
131 0 : warn("Trigger.js: Range triggers must be defined on trigger points");
132 0 : continue;
133 : }
134 0 : triggerData.queries.push(cmpTriggerPoint.RegisterRangeTrigger(name, triggerData));
135 : }
136 : }
137 :
138 0 : if (triggerData.enabled)
139 0 : this.EnableTrigger(event, name);
140 : };
141 :
142 4 : Trigger.prototype.DisableTrigger = function(event, name)
143 : {
144 0 : if (!this.triggers[event][name])
145 : {
146 0 : warn("Trigger.js: Disabling unknown trigger " + name);
147 0 : return;
148 : }
149 :
150 0 : const triggerData = this.triggers[event][name].triggerData;
151 : // special casing interval and range triggers for performance
152 0 : if (event == "OnInterval")
153 : {
154 0 : if (!triggerData.timer) // don't disable it a second time
155 0 : return;
156 0 : const cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
157 0 : cmpTimer.CancelTimer(triggerData.timer);
158 0 : triggerData.timer = null;
159 : }
160 0 : else if (event == "OnRange")
161 : {
162 0 : if (!triggerData.queries)
163 : {
164 0 : warn("Trigger.js: Range query wasn't set up before trying to disable it.");
165 0 : return;
166 : }
167 0 : const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
168 0 : for (const query of triggerData.queries)
169 0 : cmpRangeManager.DisableActiveQuery(query);
170 : }
171 :
172 0 : triggerData.enabled = false;
173 : };
174 :
175 4 : Trigger.prototype.EnableTrigger = function(event, name)
176 : {
177 0 : if (!this.triggers[event][name])
178 : {
179 0 : warn("Trigger.js: Enabling unknown trigger " + name);
180 0 : return;
181 : }
182 0 : const triggerData = this.triggers[event][name].triggerData;
183 : // special casing interval and range triggers for performance
184 0 : if (event == "OnInterval")
185 : {
186 0 : if (triggerData.timer) // don't enable it a second time
187 0 : return;
188 0 : if (!triggerData.interval)
189 : {
190 0 : warn("Trigger.js: An interval trigger should have an intervel in its data");
191 0 : return;
192 : }
193 0 : const cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
194 0 : triggerData.timer = cmpTimer.SetInterval(this.entity, IID_Trigger, "DoAction",
195 : triggerData.delay || 0, triggerData.interval, { "action": name });
196 : }
197 0 : else if (event == "OnRange")
198 : {
199 0 : if (!triggerData.queries)
200 : {
201 0 : warn("Trigger.js: Range query wasn't set up before");
202 0 : return;
203 : }
204 0 : const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
205 0 : for (const query of triggerData.queries)
206 0 : cmpRangeManager.EnableActiveQuery(query);
207 : }
208 :
209 0 : triggerData.enabled = true;
210 : };
211 :
212 4 : Trigger.prototype.OnGlobalInitGame = function(msg)
213 : {
214 0 : this.CallEvent("OnInitGame", {});
215 : };
216 :
217 4 : Trigger.prototype.OnGlobalConstructionFinished = function(msg)
218 : {
219 0 : this.CallEvent("OnStructureBuilt", { "building": msg.newentity, "foundation": msg.entity });
220 : };
221 :
222 4 : Trigger.prototype.OnGlobalTrainingFinished = function(msg)
223 : {
224 0 : this.CallEvent("OnTrainingFinished", msg);
225 : // The data for this one is {"entities": createdEnts,
226 : // "owner": cmpOwnership.GetOwner(),
227 : // "metadata": metadata}
228 : // See function "SpawnUnits" in ProductionQueue for more details
229 : };
230 :
231 4 : Trigger.prototype.OnGlobalResearchFinished = function(msg)
232 : {
233 0 : this.CallEvent("OnResearchFinished", msg);
234 : // The data for this one is { "player": playerID, "tech": tech }
235 : };
236 :
237 4 : Trigger.prototype.OnGlobalCinemaPathEnded = function(msg)
238 : {
239 0 : this.CallEvent("OnCinemaPathEnded", msg);
240 : };
241 :
242 4 : Trigger.prototype.OnGlobalCinemaQueueEnded = function(msg)
243 : {
244 0 : this.CallEvent("OnCinemaQueueEnded", msg);
245 : };
246 :
247 4 : Trigger.prototype.OnGlobalDeserialized = function(msg)
248 : {
249 0 : this.CallEvent("OnDeserialized", msg);
250 : };
251 :
252 4 : Trigger.prototype.OnGlobalEntityRenamed = function(msg)
253 : {
254 0 : this.CallEvent("OnEntityRenamed", msg);
255 : };
256 :
257 4 : Trigger.prototype.OnGlobalOwnershipChanged = function(msg)
258 : {
259 0 : this.CallEvent("OnOwnershipChanged", msg);
260 : // data is {"entity": ent, "from": playerId, "to": playerId}
261 : };
262 :
263 4 : Trigger.prototype.OnGlobalPlayerDefeated = function(msg)
264 : {
265 0 : this.CallEvent("OnPlayerDefeated", msg);
266 : };
267 :
268 4 : Trigger.prototype.OnGlobalPlayerWon = function(msg)
269 : {
270 0 : this.CallEvent("OnPlayerWon", msg);
271 : };
272 :
273 4 : Trigger.prototype.OnGlobalDiplomacyChanged = function(msg)
274 : {
275 0 : this.CallEvent("OnDiplomacyChanged", msg);
276 : };
277 :
278 : /**
279 : * Execute a function after a certain delay.
280 : *
281 : * @param {number} time - Delay in milliseconds.
282 : * @param {string} action - Name of the action function.
283 : * @param {Object} eventData - Arbitrary object that will be passed to the action function.
284 : * @return {number} The ID of the timer, so it can be stopped later.
285 : */
286 4 : Trigger.prototype.DoAfterDelay = function(time, action, eventData)
287 : {
288 0 : const cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
289 0 : return cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, "DoAction", time, {
290 : "action": action,
291 : "eventData": eventData
292 : });
293 : };
294 :
295 : /**
296 : * Execute a function each time a certain delay has passed.
297 : *
298 : * @param {number} interval - Interval in milleseconds between consecutive calls.
299 : * @param {string} action - Name of the action function.
300 : * @param {Object} eventData - Arbitrary object that will be passed to the action function.
301 : * @param {number} [start] - Optional initial delay in milleseconds before starting the calls.
302 : * If not given, interval will be used.
303 : * @return {number} the ID of the timer, so it can be stopped later.
304 : */
305 4 : Trigger.prototype.DoRepeatedly = function(time, action, eventData, start)
306 : {
307 0 : const cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
308 0 : return cmpTimer.SetInterval(SYSTEM_ENTITY, IID_Trigger, "DoAction", start !== undefined ? start : time, time, {
309 : "action": action,
310 : "eventData": eventData
311 : });
312 : };
313 :
314 : /**
315 : * This function executes the actions bound to the events.
316 : * It's either called directlty from other simulation scripts,
317 : * or from message listeners in this file
318 : *
319 : * @param {string} event - One of eventNames
320 : * @param {Object} data - will be passed to the actions
321 : */
322 4 : Trigger.prototype.CallEvent = function(event, eventData)
323 : {
324 7 : if (!this.triggers[event])
325 : {
326 0 : warn("Trigger.js: Unknown trigger event called:\"" + event + "\".");
327 0 : return;
328 : }
329 :
330 7 : for (const name in this.triggers[event])
331 0 : if (this.triggers[event][name].triggerData.enabled)
332 0 : this.DoAction({
333 : "action": this.triggers[event][name].triggerData.action,
334 : "eventData": eventData,
335 : "customData": this.triggers[event][name].customData,
336 : "triggerData": this.triggers[event][name].triggerData
337 : });
338 : };
339 :
340 : /**
341 : * Call the action method of a trigger with the given event Data.
342 : * By default, call the trigger even if it is currently disabled.
343 : */
344 4 : Trigger.prototype.CallTrigger = function(event, name, eventData, evenIfDisabled = true)
345 : {
346 0 : if (!this.triggers[event]?.[name])
347 : {
348 0 : warn(`Trigger.js: called a trigger '${name}' for event '${event}' that wasn't found`);
349 0 : return;
350 : }
351 :
352 0 : if (!evenIfDisabled && !this.triggers[event][name].triggerData.enabled)
353 0 : return;
354 :
355 0 : this.DoAction({
356 : "action": this.triggers[event][name].triggerData.action,
357 : "eventData": eventData,
358 : "customData": this.triggers[event][name].customData,
359 : "triggerData": this.triggers[event][name].triggerData
360 : });
361 : };
362 :
363 :
364 : /**
365 : * Called by the trigger listeners to execute the actual action. Including sanity checks.
366 : * Intended for internal use, prefer CallEvent or CallTrigger.
367 : */
368 4 : Trigger.prototype.DoAction = function(msg)
369 : {
370 0 : if (this[msg.action])
371 0 : this[msg.action](msg?.eventData, msg?.customData, msg?.triggerData);
372 : else
373 0 : warn("Trigger.js: called a trigger action '" + msg.action + "' that wasn't found");
374 : };
375 :
376 : /**
377 : * Level of difficulty used by trigger scripts.
378 : */
379 4 : Trigger.prototype.GetDifficulty = function()
380 : {
381 0 : return this.difficulty;
382 : };
383 :
384 4 : Trigger.prototype.SetDifficulty = function(diff)
385 : {
386 0 : this.difficulty = diff;
387 : };
388 :
389 4 : Engine.RegisterSystemComponentType(IID_Trigger, "Trigger", Trigger);
|