• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 19990896186

06 Dec 2025 04:03PM UTC coverage: 88.636% (-0.02%) from 88.653%
19990896186

Pull #3598

github

web-flow
Merge 8e811c0e8 into 0f899e40c
Pull Request #3598: Docs migrate playground

5316 of 7259 branches covered (73.23%)

14718 of 16605 relevant lines covered (88.64%)

24703.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.5
/src/engine/Timer.ts
1
import type { Scene } from './Scene';
2
import { Logger } from './Util/Log';
3
import type * as ex from './index';
4
import { Random } from './Math/Random';
5
import { EventEmitter } from './EventEmitter';
6

7
/**
8
 * Built in events supported by all entities
9
 */
10
export interface TimerEvents {
11
  start: void;
12
  stop: void;
13
  pause: void;
14
  resume: void;
15
  cancel: void;
16

17
  action: void;
18
  complete: void;
19
}
20

21
export const TimerEvents = {
248✔
22
  Start: 'start',
23
  Stop: 'stop',
24
  Pause: 'pause',
25
  Resume: 'resume',
26
  Cancel: 'cancel',
27

28
  Action: 'action',
29
  Complete: 'complete'
30
} as const;
31

32
export interface TimerOptions {
33
  /**
34
   * If true the timer repeats every interval infinitely
35
   */
36
  repeats?: boolean;
37
  /**
38
   * If a number is specified then it will only repeat a number of times
39
   */
40
  numberOfRepeats?: number;
41
  /**
42
   * @deprecated use action: () => void, will be removed in v1.0
43
   */
44
  fcn?: () => void;
45
  /**
46
   * Action to perform every time the timer fires
47
   */
48
  action?: () => void;
49
  /**
50
   * Interval in milliseconds for the timer to fire
51
   */
52
  interval: number;
53
  /**
54
   * Optionally specify a random range of milliseconds for the timer to fire
55
   */
56
  randomRange?: [number, number];
57
  /**
58
   * Optionally provide a random instance to use for random behavior, otherwise a new random will be created seeded from the current time.
59
   */
60
  random?: ex.Random;
61
  /**
62
   * Optionally provide a callback to fire once when the timer completes its last action callback.
63
   */
64
  onComplete?: () => void;
65
}
66

67
/**
68
 * The Excalibur timer hooks into the internal timer and fires callbacks,
69
 * after a certain interval, optionally repeating.
70
 */
71
export class Timer {
248✔
72
  private _logger = Logger.getInstance();
76✔
73
  private static _MAX_ID: number = 0;
74
  public id: number = 0;
76✔
75
  public events = new EventEmitter<TimerEvents>();
76✔
76

77
  private _elapsedTime: number = 0;
76✔
78
  private _totalTimeAlive: number = 0;
76✔
79

80
  private _running = false;
76✔
81

82
  private _numberOfTicks: number = 0;
76✔
83
  private _callbacks: Array<() => void>;
84

85
  public interval: number = 10;
76✔
86
  public repeats: boolean = false;
76✔
87
  public maxNumberOfRepeats: number = -1;
76✔
88
  public randomRange: [number, number] = [0, 0];
76✔
89
  public random: ex.Random;
90
  private _baseInterval = 10;
76✔
91
  private _generateRandomInterval = () => {
76✔
92
    return this._baseInterval + this.random.integer(this.randomRange[0], this.randomRange[1]);
16✔
93
  };
94

95
  // eslint-disable-next-line @typescript-eslint/no-empty-function
96
  private _onComplete: () => void = () => {};
76✔
97
  private _complete = false;
76✔
98
  public get complete() {
99
    return this._complete;
334✔
100
  }
101

102
  public scene: Scene = null;
76✔
103

104
  constructor(options: TimerOptions) {
105
    const fcn = options.action ?? options.fcn;
76✔
106
    const interval = options.interval;
76✔
107
    const repeats = options.repeats;
76✔
108
    const numberOfRepeats = options.numberOfRepeats;
76✔
109
    const randomRange = options.randomRange;
76✔
110
    const random = options.random;
76✔
111
    this._onComplete = options.onComplete ?? this._onComplete;
76✔
112

113
    if (!!numberOfRepeats && numberOfRepeats >= 0) {
76✔
114
      this.maxNumberOfRepeats = numberOfRepeats;
4✔
115
      if (!repeats) {
4!
116
        throw new Error('repeats must be set to true if numberOfRepeats is set');
×
117
      }
118
    }
119

120
    this.id = Timer._MAX_ID++;
76✔
121
    this._callbacks = [];
76✔
122
    this._baseInterval = this.interval = interval;
76✔
123
    if (!!randomRange) {
76✔
124
      if (randomRange[0] > randomRange[1]) {
6!
125
        throw new Error('min value must be lower than max value for range');
×
126
      }
127
      //We use the instance of ex.Random to generate the range
128
      this.random = random ?? new Random();
6✔
129
      this.randomRange = randomRange;
6✔
130

131
      this.interval = this._generateRandomInterval();
6✔
132
      this.on(() => {
6✔
133
        this.interval = this._generateRandomInterval();
10✔
134
      });
135
    }
136
    this.repeats = repeats || this.repeats;
76✔
137
    if (fcn) {
76✔
138
      this.on(fcn);
58✔
139
    }
140
  }
141

142
  /**
143
   * Adds a new callback to be fired after the interval is complete
144
   * @param action The callback to be added to the callback list, to be fired after the interval is complete.
145
   */
146
  public on(action: () => void) {
147
    this._callbacks.push(action);
69✔
148
  }
149

150
  /**
151
   * Removes a callback from the callback list to be fired after the interval is complete.
152
   * @param action The callback to be removed from the callback list, to be fired after the interval is complete.
153
   */
154
  public off(action: () => void) {
155
    const index = this._callbacks.indexOf(action);
1✔
156
    this._callbacks.splice(index, 1);
1✔
157
  }
158
  /**
159
   * Updates the timer after a certain number of milliseconds have elapsed. This is used internally by the engine.
160
   * @param elapsed  Number of elapsed milliseconds since the last update.
161
   */
162
  public update(elapsed: number) {
163
    if (this._running) {
184✔
164
      this._totalTimeAlive += elapsed;
168✔
165
      this._elapsedTime += elapsed;
168✔
166

167
      if (this.maxNumberOfRepeats > -1 && this._numberOfTicks >= this.maxNumberOfRepeats) {
168✔
168
        this._complete = true;
4✔
169
        this._running = false;
4✔
170
        this._elapsedTime = 0;
4✔
171
        this._onComplete();
4✔
172
        this.events.emit('complete');
4✔
173
      }
174

175
      if (!this.complete && this._elapsedTime >= this.interval) {
168✔
176
        this._callbacks.forEach((c) => {
155✔
177
          c.call(this);
151✔
178
        });
179
        this._numberOfTicks++;
155✔
180
        this.events.emit('action');
155✔
181
        if (this.repeats) {
155✔
182
          this._elapsedTime = 0;
138✔
183
        } else {
184
          this._complete = true;
17✔
185
          this._running = false;
17✔
186
          this._elapsedTime = 0;
17✔
187
          this._onComplete();
17✔
188
          this.events.emit('complete');
17✔
189
        }
190
      }
191
    }
192
  }
193

194
  /**
195
   * Resets the timer so that it can be reused, and optionally reconfigure the timers interval.
196
   *
197
   * Warning** you may need to call `timer.start()` again if the timer had completed
198
   * @param newInterval If specified, sets a new non-negative interval in milliseconds to refire the callback
199
   * @param newNumberOfRepeats If specified, sets a new non-negative upper limit to the number of time this timer executes
200
   */
201
  public reset(newInterval?: number, newNumberOfRepeats?: number) {
202
    if (!!newInterval && newInterval >= 0) {
6✔
203
      this._baseInterval = this.interval = newInterval;
4✔
204
    }
205

206
    if (!!this.maxNumberOfRepeats && this.maxNumberOfRepeats >= 0) {
6✔
207
      this.maxNumberOfRepeats = newNumberOfRepeats;
1✔
208
      if (!this.repeats) {
1!
209
        throw new Error('repeats must be set to true if numberOfRepeats is set');
×
210
      }
211
    }
212

213
    this._complete = false;
6✔
214
    this._elapsedTime = 0;
6✔
215
    this._numberOfTicks = 0;
6✔
216
  }
217

218
  public get timesRepeated(): number {
219
    return this._numberOfTicks;
3✔
220
  }
221

222
  public getTimeRunning(): number {
223
    return this._totalTimeAlive;
1✔
224
  }
225

226
  /**
227
   * @returns milliseconds until the next action callback, if complete will return 0
228
   */
229
  public get timeToNextAction() {
230
    if (this.complete) {
6✔
231
      return 0;
1✔
232
    }
233
    return this.interval - this._elapsedTime;
5✔
234
  }
235

236
  /**
237
   * @returns milliseconds elapsed toward the next action
238
   */
239
  public get timeElapsedTowardNextAction() {
240
    return this._elapsedTime;
6✔
241
  }
242

243
  public get isRunning() {
244
    return this._running;
3✔
245
  }
246

247
  /**
248
   * Pauses the timer, time will no longer increment towards the next call
249
   */
250
  public pause(): Timer {
251
    this._running = false;
9✔
252
    this.events.emit('pause');
9✔
253
    return this;
9✔
254
  }
255

256
  /**
257
   * Resumes the timer, time will now increment towards the next call.
258
   */
259
  public resume(): Timer {
260
    this._running = true;
5✔
261
    this.events.emit('resume');
5✔
262
    return this;
5✔
263
  }
264

265
  /**
266
   * Starts the timer, if the timer was complete it will restart the timer and reset the elapsed time counter
267
   */
268
  public start(): Timer {
269
    if (!this.scene) {
42✔
270
      this._logger.warn('Cannot start a timer not part of a scene, timer wont start until added');
5✔
271
    }
272

273
    this._running = true;
42✔
274
    if (this.complete) {
42!
275
      this._complete = false;
×
276
      this._elapsedTime = 0;
×
277
      this._numberOfTicks = 0;
×
278
    } else {
279
      this.events.emit('start');
42✔
280
    }
281

282
    return this;
42✔
283
  }
284

285
  /**
286
   * Stops the timer and resets the elapsed time counter towards the next action invocation
287
   */
288
  public stop(): Timer {
289
    this._running = false;
3✔
290
    this._elapsedTime = 0;
3✔
291
    this._numberOfTicks = 0;
3✔
292
    this.events.emit('stop');
3✔
293
    return this;
3✔
294
  }
295

296
  /**
297
   * Cancels the timer, preventing any further executions.
298
   */
299
  public cancel() {
300
    this.pause();
2✔
301
    if (this.scene) {
2!
302
      this.scene.cancelTimer(this);
2✔
303
      this.events.emit('cancel');
2✔
304
    }
305
  }
306
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc