Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
titaniumnetwork-dev
GitHub Repository: titaniumnetwork-dev/Incognito-old
Path: blob/main/static/src/gs/public/dinosaur/index.js
1333 views
1
// Copyright (c) 2014 The Chromium Authors. All rights reserved.
2
// Use of this source code is governed by a BSD-style license that can be
3
// found in the LICENSE file.
4
// extract from chromium source code by @liuwayong
5
(function () {
6
'use strict';
7
/**
8
* T-Rex runner.
9
* @param {string} outerContainerId Outer containing element id.
10
* @param {Object} opt_config
11
* @constructor
12
* @export
13
*/
14
function Runner(outerContainerId, opt_config) {
15
// Singleton
16
if (Runner.instance_) {
17
return Runner.instance_;
18
}
19
Runner.instance_ = this;
20
21
this.outerContainerEl = document.querySelector(outerContainerId);
22
this.containerEl = null;
23
this.snackbarEl = null;
24
this.detailsButton = this.outerContainerEl.querySelector('#details-button');
25
26
this.config = opt_config || Runner.config;
27
28
this.dimensions = Runner.defaultDimensions;
29
30
this.canvas = null;
31
this.canvasCtx = null;
32
33
this.tRex = null;
34
35
this.distanceMeter = null;
36
this.distanceRan = 0;
37
38
this.highestScore = 0;
39
40
this.time = 0;
41
this.runningTime = 0;
42
this.msPerFrame = 1000 / FPS;
43
this.currentSpeed = this.config.SPEED;
44
45
this.obstacles = [];
46
47
this.activated = false; // Whether the easter egg has been activated.
48
this.playing = false; // Whether the game is currently in play state.
49
this.crashed = false;
50
this.paused = false;
51
this.inverted = false;
52
this.invertTimer = 0;
53
this.resizeTimerId_ = null;
54
55
this.playCount = 0;
56
57
// Sound FX.
58
this.audioBuffer = null;
59
this.soundFx = {};
60
61
// Global web audio context for playing sounds.
62
this.audioContext = null;
63
64
// Images.
65
this.images = {};
66
this.imagesLoaded = 0;
67
68
if (this.isDisabled()) {
69
this.setupDisabledRunner();
70
} else {
71
this.loadImages();
72
}
73
}
74
window['Runner'] = Runner;
75
76
77
/**
78
* Default game width.
79
* @const
80
*/
81
var DEFAULT_WIDTH = 600;
82
83
/**
84
* Frames per second.
85
* @const
86
*/
87
var FPS = 60;
88
89
/** @const */
90
var IS_HIDPI = window.devicePixelRatio > 1;
91
92
/** @const */
93
var IS_IOS = /iPad|iPhone|iPod/.test(window.navigator.platform);
94
95
/** @const */
96
var IS_MOBILE = /Android/.test(window.navigator.userAgent) || IS_IOS;
97
98
/** @const */
99
var IS_TOUCH_ENABLED = 'ontouchstart' in window;
100
101
/**
102
* Default game configuration.
103
* @enum {number}
104
*/
105
Runner.config = {
106
ACCELERATION: 0.001,
107
BG_CLOUD_SPEED: 0.2,
108
BOTTOM_PAD: 10,
109
CLEAR_TIME: 3000,
110
CLOUD_FREQUENCY: 0.5,
111
GAMEOVER_CLEAR_TIME: 750,
112
GAP_COEFFICIENT: 0.6,
113
GRAVITY: 0.6,
114
INITIAL_JUMP_VELOCITY: 12,
115
INVERT_FADE_DURATION: 12000,
116
INVERT_DISTANCE: 700,
117
MAX_BLINK_COUNT: 3,
118
MAX_CLOUDS: 6,
119
MAX_OBSTACLE_LENGTH: 3,
120
MAX_OBSTACLE_DUPLICATION: 2,
121
MAX_SPEED: 13,
122
MIN_JUMP_HEIGHT: 35,
123
MOBILE_SPEED_COEFFICIENT: 1.2,
124
RESOURCE_TEMPLATE_ID: 'audio-resources',
125
SPEED: 6,
126
SPEED_DROP_COEFFICIENT: 3
127
};
128
129
130
/**
131
* Default dimensions.
132
* @enum {string}
133
*/
134
Runner.defaultDimensions = {
135
WIDTH: DEFAULT_WIDTH,
136
HEIGHT: 150
137
};
138
139
140
/**
141
* CSS class names.
142
* @enum {string}
143
*/
144
Runner.classes = {
145
CANVAS: 'runner-canvas',
146
CONTAINER: 'runner-container',
147
CRASHED: 'crashed',
148
ICON: 'icon-offline',
149
INVERTED: 'inverted',
150
SNACKBAR: 'snackbar',
151
SNACKBAR_SHOW: 'snackbar-show',
152
TOUCH_CONTROLLER: 'controller'
153
};
154
155
156
/**
157
* Sprite definition layout of the spritesheet.
158
* @enum {Object}
159
*/
160
Runner.spriteDefinition = {
161
LDPI: {
162
CACTUS_LARGE: { x: 332, y: 2 },
163
CACTUS_SMALL: { x: 228, y: 2 },
164
CLOUD: { x: 86, y: 2 },
165
HORIZON: { x: 2, y: 54 },
166
MOON: { x: 484, y: 2 },
167
PTERODACTYL: { x: 134, y: 2 },
168
RESTART: { x: 2, y: 2 },
169
TEXT_SPRITE: { x: 655, y: 2 },
170
TREX: { x: 848, y: 2 },
171
STAR: { x: 645, y: 2 }
172
},
173
HDPI: {
174
CACTUS_LARGE: { x: 652, y: 2 },
175
CACTUS_SMALL: { x: 446, y: 2 },
176
CLOUD: { x: 166, y: 2 },
177
HORIZON: { x: 2, y: 104 },
178
MOON: { x: 954, y: 2 },
179
PTERODACTYL: { x: 260, y: 2 },
180
RESTART: { x: 2, y: 2 },
181
TEXT_SPRITE: { x: 1294, y: 2 },
182
TREX: { x: 1678, y: 2 },
183
STAR: { x: 1276, y: 2 }
184
}
185
};
186
187
188
/**
189
* Sound FX. Reference to the ID of the audio tag on interstitial page.
190
* @enum {string}
191
*/
192
Runner.sounds = {
193
BUTTON_PRESS: 'offline-sound-press',
194
HIT: 'offline-sound-hit',
195
SCORE: 'offline-sound-reached'
196
};
197
198
199
/**
200
* Key code mapping.
201
* @enum {Object}
202
*/
203
Runner.keycodes = {
204
JUMP: { '38': 1, '32': 1 }, // Up, spacebar
205
DUCK: { '40': 1 }, // Down
206
RESTART: { '13': 1 } // Enter
207
};
208
209
210
/**
211
* Runner event names.
212
* @enum {string}
213
*/
214
Runner.events = {
215
ANIM_END: 'webkitAnimationEnd',
216
CLICK: 'click',
217
KEYDOWN: 'keydown',
218
KEYUP: 'keyup',
219
MOUSEDOWN: 'mousedown',
220
MOUSEUP: 'mouseup',
221
RESIZE: 'resize',
222
TOUCHEND: 'touchend',
223
TOUCHSTART: 'touchstart',
224
VISIBILITY: 'visibilitychange',
225
BLUR: 'blur',
226
FOCUS: 'focus',
227
LOAD: 'load'
228
};
229
230
231
Runner.prototype = {
232
/**
233
* Whether the easter egg has been disabled. CrOS enterprise enrolled devices.
234
* @return {boolean}
235
*/
236
isDisabled: function () {
237
// return loadTimeData && loadTimeData.valueExists('disabledEasterEgg');
238
return false;
239
},
240
241
/**
242
* For disabled instances, set up a snackbar with the disabled message.
243
*/
244
setupDisabledRunner: function () {
245
this.containerEl = document.createElement('div');
246
this.containerEl.className = Runner.classes.SNACKBAR;
247
this.containerEl.textContent = loadTimeData.getValue('disabledEasterEgg');
248
this.outerContainerEl.appendChild(this.containerEl);
249
250
// Show notification when the activation key is pressed.
251
document.addEventListener(Runner.events.KEYDOWN, function (e) {
252
if (Runner.keycodes.JUMP[e.keyCode]) {
253
this.containerEl.classList.add(Runner.classes.SNACKBAR_SHOW);
254
document.querySelector('.icon').classList.add('icon-disabled');
255
}
256
}.bind(this));
257
},
258
259
/**
260
* Setting individual settings for debugging.
261
* @param {string} setting
262
* @param {*} value
263
*/
264
updateConfigSetting: function (setting, value) {
265
if (setting in this.config && value != undefined) {
266
this.config[setting] = value;
267
268
switch (setting) {
269
case 'GRAVITY':
270
case 'MIN_JUMP_HEIGHT':
271
case 'SPEED_DROP_COEFFICIENT':
272
this.tRex.config[setting] = value;
273
break;
274
case 'INITIAL_JUMP_VELOCITY':
275
this.tRex.setJumpVelocity(value);
276
break;
277
case 'SPEED':
278
this.setSpeed(value);
279
break;
280
}
281
}
282
},
283
284
/**
285
* Cache the appropriate image sprite from the page and get the sprite sheet
286
* definition.
287
*/
288
loadImages: function () {
289
if (IS_HIDPI) {
290
Runner.imageSprite = document.getElementById('offline-resources-2x');
291
this.spriteDef = Runner.spriteDefinition.HDPI;
292
} else {
293
Runner.imageSprite = document.getElementById('offline-resources-1x');
294
this.spriteDef = Runner.spriteDefinition.LDPI;
295
}
296
297
if (Runner.imageSprite.complete) {
298
this.init();
299
} else {
300
// If the images are not yet loaded, add a listener.
301
Runner.imageSprite.addEventListener(Runner.events.LOAD,
302
this.init.bind(this));
303
}
304
},
305
306
/**
307
* Load and decode base 64 encoded sounds.
308
*/
309
loadSounds: function () {
310
if (!IS_IOS) {
311
this.audioContext = new AudioContext();
312
313
var resourceTemplate =
314
document.getElementById(this.config.RESOURCE_TEMPLATE_ID).content;
315
316
for (var sound in Runner.sounds) {
317
var soundSrc =
318
resourceTemplate.getElementById(Runner.sounds[sound]).src;
319
soundSrc = soundSrc.substr(soundSrc.indexOf(',') + 1);
320
var buffer = decodeBase64ToArrayBuffer(soundSrc);
321
322
// Async, so no guarantee of order in array.
323
this.audioContext.decodeAudioData(buffer, function (index, audioData) {
324
this.soundFx[index] = audioData;
325
}.bind(this, sound));
326
}
327
}
328
},
329
330
/**
331
* Sets the game speed. Adjust the speed accordingly if on a smaller screen.
332
* @param {number} opt_speed
333
*/
334
setSpeed: function (opt_speed) {
335
var speed = opt_speed || this.currentSpeed;
336
337
// Reduce the speed on smaller mobile screens.
338
if (this.dimensions.WIDTH < DEFAULT_WIDTH) {
339
var mobileSpeed = speed * this.dimensions.WIDTH / DEFAULT_WIDTH *
340
this.config.MOBILE_SPEED_COEFFICIENT;
341
this.currentSpeed = mobileSpeed > speed ? speed : mobileSpeed;
342
} else if (opt_speed) {
343
this.currentSpeed = opt_speed;
344
}
345
},
346
347
/**
348
* Game initialiser.
349
*/
350
init: function () {
351
// Hide the static icon.
352
document.querySelector('.' + Runner.classes.ICON).style.visibility =
353
'hidden';
354
355
this.adjustDimensions();
356
this.setSpeed();
357
358
this.containerEl = document.createElement('div');
359
this.containerEl.className = Runner.classes.CONTAINER;
360
361
// Player canvas container.
362
this.canvas = createCanvas(this.containerEl, this.dimensions.WIDTH,
363
this.dimensions.HEIGHT, Runner.classes.PLAYER);
364
365
this.canvasCtx = this.canvas.getContext('2d');
366
this.canvasCtx.fillStyle = '#f7f7f7';
367
this.canvasCtx.fill();
368
Runner.updateCanvasScaling(this.canvas);
369
370
// Horizon contains clouds, obstacles and the ground.
371
this.horizon = new Horizon(this.canvas, this.spriteDef, this.dimensions,
372
this.config.GAP_COEFFICIENT);
373
374
// Distance meter
375
this.distanceMeter = new DistanceMeter(this.canvas,
376
this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH);
377
378
// Draw t-rex
379
this.tRex = new Trex(this.canvas, this.spriteDef.TREX);
380
381
this.outerContainerEl.appendChild(this.containerEl);
382
383
if (IS_MOBILE) {
384
this.createTouchController();
385
}
386
387
this.startListening();
388
this.update();
389
390
window.addEventListener(Runner.events.RESIZE,
391
this.debounceResize.bind(this));
392
},
393
394
/**
395
* Create the touch controller. A div that covers whole screen.
396
*/
397
createTouchController: function () {
398
this.touchController = document.createElement('div');
399
this.touchController.className = Runner.classes.TOUCH_CONTROLLER;
400
this.outerContainerEl.appendChild(this.touchController);
401
},
402
403
/**
404
* Debounce the resize event.
405
*/
406
debounceResize: function () {
407
if (!this.resizeTimerId_) {
408
this.resizeTimerId_ =
409
setInterval(this.adjustDimensions.bind(this), 250);
410
}
411
},
412
413
/**
414
* Adjust game space dimensions on resize.
415
*/
416
adjustDimensions: function () {
417
clearInterval(this.resizeTimerId_);
418
this.resizeTimerId_ = null;
419
420
var boxStyles = window.getComputedStyle(this.outerContainerEl);
421
var padding = Number(boxStyles.paddingLeft.substr(0,
422
boxStyles.paddingLeft.length - 2));
423
424
this.dimensions.WIDTH = this.outerContainerEl.offsetWidth - padding * 2;
425
426
// Redraw the elements back onto the canvas.
427
if (this.canvas) {
428
this.canvas.width = this.dimensions.WIDTH;
429
this.canvas.height = this.dimensions.HEIGHT;
430
431
Runner.updateCanvasScaling(this.canvas);
432
433
this.distanceMeter.calcXPos(this.dimensions.WIDTH);
434
this.clearCanvas();
435
this.horizon.update(0, 0, true);
436
this.tRex.update(0);
437
438
// Outer container and distance meter.
439
if (this.playing || this.crashed || this.paused) {
440
this.containerEl.style.width = this.dimensions.WIDTH + 'px';
441
this.containerEl.style.height = this.dimensions.HEIGHT + 'px';
442
this.distanceMeter.update(0, Math.ceil(this.distanceRan));
443
this.stop();
444
} else {
445
this.tRex.draw(0, 0);
446
}
447
448
// Game over panel.
449
if (this.crashed && this.gameOverPanel) {
450
this.gameOverPanel.updateDimensions(this.dimensions.WIDTH);
451
this.gameOverPanel.draw();
452
}
453
}
454
},
455
456
/**
457
* Play the game intro.
458
* Canvas container width expands out to the full width.
459
*/
460
playIntro: function () {
461
if (!this.activated && !this.crashed) {
462
this.playingIntro = true;
463
this.tRex.playingIntro = true;
464
465
// CSS animation definition.
466
var keyframes = '@-webkit-keyframes intro { ' +
467
'from { width:' + Trex.config.WIDTH + 'px }' +
468
'to { width: ' + this.dimensions.WIDTH + 'px }' +
469
'}';
470
471
// create a style sheet to put the keyframe rule in
472
// and then place the style sheet in the html head
473
var sheet = document.createElement('style');
474
sheet.innerHTML = keyframes;
475
document.head.appendChild(sheet);
476
477
this.containerEl.addEventListener(Runner.events.ANIM_END,
478
this.startGame.bind(this));
479
480
this.containerEl.style.webkitAnimation = 'intro .4s ease-out 1 both';
481
this.containerEl.style.width = this.dimensions.WIDTH + 'px';
482
483
// if (this.touchController) {
484
// this.outerContainerEl.appendChild(this.touchController);
485
// }
486
this.playing = true;
487
this.activated = true;
488
} else if (this.crashed) {
489
this.restart();
490
}
491
},
492
493
494
/**
495
* Update the game status to started.
496
*/
497
startGame: function () {
498
this.runningTime = 0;
499
this.playingIntro = false;
500
this.tRex.playingIntro = false;
501
this.containerEl.style.webkitAnimation = '';
502
this.playCount++;
503
504
// Handle tabbing off the page. Pause the current game.
505
document.addEventListener(Runner.events.VISIBILITY,
506
this.onVisibilityChange.bind(this));
507
508
window.addEventListener(Runner.events.BLUR,
509
this.onVisibilityChange.bind(this));
510
511
window.addEventListener(Runner.events.FOCUS,
512
this.onVisibilityChange.bind(this));
513
},
514
515
clearCanvas: function () {
516
this.canvasCtx.clearRect(0, 0, this.dimensions.WIDTH,
517
this.dimensions.HEIGHT);
518
},
519
520
/**
521
* Update the game frame and schedules the next one.
522
*/
523
update: function () {
524
this.updatePending = false;
525
526
var now = getTimeStamp();
527
var deltaTime = now - (this.time || now);
528
this.time = now;
529
530
if (this.playing) {
531
this.clearCanvas();
532
533
if (this.tRex.jumping) {
534
this.tRex.updateJump(deltaTime);
535
}
536
537
this.runningTime += deltaTime;
538
var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
539
540
// First jump triggers the intro.
541
if (this.tRex.jumpCount == 1 && !this.playingIntro) {
542
this.playIntro();
543
}
544
545
// The horizon doesn't move until the intro is over.
546
if (this.playingIntro) {
547
this.horizon.update(0, this.currentSpeed, hasObstacles);
548
} else {
549
deltaTime = !this.activated ? 0 : deltaTime;
550
this.horizon.update(deltaTime, this.currentSpeed, hasObstacles,
551
this.inverted);
552
}
553
554
// Check for collisions.
555
var collision = hasObstacles &&
556
checkForCollision(this.horizon.obstacles[0], this.tRex);
557
558
if (!collision) {
559
this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;
560
561
if (this.currentSpeed < this.config.MAX_SPEED) {
562
this.currentSpeed += this.config.ACCELERATION;
563
}
564
} else {
565
this.gameOver();
566
}
567
568
var playAchievementSound = this.distanceMeter.update(deltaTime,
569
Math.ceil(this.distanceRan));
570
571
if (playAchievementSound) {
572
this.playSound(this.soundFx.SCORE);
573
}
574
575
// Night mode.
576
if (this.invertTimer > this.config.INVERT_FADE_DURATION) {
577
this.invertTimer = 0;
578
this.invertTrigger = false;
579
this.invert();
580
} else if (this.invertTimer) {
581
this.invertTimer += deltaTime;
582
} else {
583
var actualDistance =
584
this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan));
585
586
if (actualDistance > 0) {
587
this.invertTrigger = !(actualDistance %
588
this.config.INVERT_DISTANCE);
589
590
if (this.invertTrigger && this.invertTimer === 0) {
591
this.invertTimer += deltaTime;
592
this.invert();
593
}
594
}
595
}
596
}
597
598
if (this.playing || (!this.activated &&
599
this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) {
600
this.tRex.update(deltaTime);
601
this.scheduleNextUpdate();
602
}
603
},
604
605
/**
606
* Event handler.
607
*/
608
handleEvent: function (e) {
609
return (function (evtType, events) {
610
switch (evtType) {
611
case events.KEYDOWN:
612
case events.TOUCHSTART:
613
case events.MOUSEDOWN:
614
this.onKeyDown(e);
615
break;
616
case events.KEYUP:
617
case events.TOUCHEND:
618
case events.MOUSEUP:
619
this.onKeyUp(e);
620
break;
621
}
622
}.bind(this))(e.type, Runner.events);
623
},
624
625
/**
626
* Bind relevant key / mouse / touch listeners.
627
*/
628
startListening: function () {
629
// Keys.
630
document.addEventListener(Runner.events.KEYDOWN, this);
631
document.addEventListener(Runner.events.KEYUP, this);
632
633
if (IS_MOBILE) {
634
// Mobile only touch devices.
635
this.touchController.addEventListener(Runner.events.TOUCHSTART, this);
636
this.touchController.addEventListener(Runner.events.TOUCHEND, this);
637
this.containerEl.addEventListener(Runner.events.TOUCHSTART, this);
638
} else {
639
// Mouse.
640
document.addEventListener(Runner.events.MOUSEDOWN, this);
641
document.addEventListener(Runner.events.MOUSEUP, this);
642
}
643
},
644
645
/**
646
* Remove all listeners.
647
*/
648
stopListening: function () {
649
document.removeEventListener(Runner.events.KEYDOWN, this);
650
document.removeEventListener(Runner.events.KEYUP, this);
651
652
if (IS_MOBILE) {
653
this.touchController.removeEventListener(Runner.events.TOUCHSTART, this);
654
this.touchController.removeEventListener(Runner.events.TOUCHEND, this);
655
this.containerEl.removeEventListener(Runner.events.TOUCHSTART, this);
656
} else {
657
document.removeEventListener(Runner.events.MOUSEDOWN, this);
658
document.removeEventListener(Runner.events.MOUSEUP, this);
659
}
660
},
661
662
/**
663
* Process keydown.
664
* @param {Event} e
665
*/
666
onKeyDown: function (e) {
667
// Prevent native page scrolling whilst tapping on mobile.
668
if (IS_MOBILE && this.playing) {
669
e.preventDefault();
670
}
671
672
if (e.target != this.detailsButton) {
673
if (!this.crashed && (Runner.keycodes.JUMP[e.keyCode] ||
674
e.type == Runner.events.TOUCHSTART)) {
675
if (!this.playing) {
676
this.loadSounds();
677
this.playing = true;
678
this.update();
679
if (window.errorPageController) {
680
errorPageController.trackEasterEgg();
681
}
682
}
683
// Play sound effect and jump on starting the game for the first time.
684
if (!this.tRex.jumping && !this.tRex.ducking) {
685
this.playSound(this.soundFx.BUTTON_PRESS);
686
this.tRex.startJump(this.currentSpeed);
687
}
688
}
689
690
if (this.crashed && e.type == Runner.events.TOUCHSTART &&
691
e.currentTarget == this.containerEl) {
692
this.restart();
693
}
694
}
695
696
if (this.playing && !this.crashed && Runner.keycodes.DUCK[e.keyCode]) {
697
e.preventDefault();
698
if (this.tRex.jumping) {
699
// Speed drop, activated only when jump key is not pressed.
700
this.tRex.setSpeedDrop();
701
} else if (!this.tRex.jumping && !this.tRex.ducking) {
702
// Duck.
703
this.tRex.setDuck(true);
704
}
705
}
706
},
707
708
709
/**
710
* Process key up.
711
* @param {Event} e
712
*/
713
onKeyUp: function (e) {
714
var keyCode = String(e.keyCode);
715
var isjumpKey = Runner.keycodes.JUMP[keyCode] ||
716
e.type == Runner.events.TOUCHEND ||
717
e.type == Runner.events.MOUSEDOWN;
718
719
if (this.isRunning() && isjumpKey) {
720
this.tRex.endJump();
721
} else if (Runner.keycodes.DUCK[keyCode]) {
722
this.tRex.speedDrop = false;
723
this.tRex.setDuck(false);
724
} else if (this.crashed) {
725
// Check that enough time has elapsed before allowing jump key to restart.
726
var deltaTime = getTimeStamp() - this.time;
727
728
if (Runner.keycodes.RESTART[keyCode] || this.isLeftClickOnCanvas(e) ||
729
(deltaTime >= this.config.GAMEOVER_CLEAR_TIME &&
730
Runner.keycodes.JUMP[keyCode])) {
731
this.restart();
732
}
733
} else if (this.paused && isjumpKey) {
734
// Reset the jump state
735
this.tRex.reset();
736
this.play();
737
}
738
},
739
740
/**
741
* Returns whether the event was a left click on canvas.
742
* On Windows right click is registered as a click.
743
* @param {Event} e
744
* @return {boolean}
745
*/
746
isLeftClickOnCanvas: function (e) {
747
return e.button != null && e.button < 2 &&
748
e.type == Runner.events.MOUSEUP && e.target == this.canvas;
749
},
750
751
/**
752
* RequestAnimationFrame wrapper.
753
*/
754
scheduleNextUpdate: function () {
755
if (!this.updatePending) {
756
this.updatePending = true;
757
this.raqId = requestAnimationFrame(this.update.bind(this));
758
}
759
},
760
761
/**
762
* Whether the game is running.
763
* @return {boolean}
764
*/
765
isRunning: function () {
766
return !!this.raqId;
767
},
768
769
/**
770
* Game over state.
771
*/
772
gameOver: function () {
773
this.playSound(this.soundFx.HIT);
774
vibrate(200);
775
776
this.stop();
777
this.crashed = true;
778
this.distanceMeter.acheivement = false;
779
780
this.tRex.update(100, Trex.status.CRASHED);
781
782
// Game over panel.
783
if (!this.gameOverPanel) {
784
this.gameOverPanel = new GameOverPanel(this.canvas,
785
this.spriteDef.TEXT_SPRITE, this.spriteDef.RESTART,
786
this.dimensions);
787
} else {
788
this.gameOverPanel.draw();
789
}
790
791
// Update the high score.
792
if (this.distanceRan > this.highestScore) {
793
this.highestScore = Math.ceil(this.distanceRan);
794
this.distanceMeter.setHighScore(this.highestScore);
795
}
796
797
// Reset the time clock.
798
this.time = getTimeStamp();
799
},
800
801
stop: function () {
802
this.playing = false;
803
this.paused = true;
804
cancelAnimationFrame(this.raqId);
805
this.raqId = 0;
806
},
807
808
play: function () {
809
if (!this.crashed) {
810
this.playing = true;
811
this.paused = false;
812
this.tRex.update(0, Trex.status.RUNNING);
813
this.time = getTimeStamp();
814
this.update();
815
}
816
},
817
818
restart: function () {
819
if (!this.raqId) {
820
this.playCount++;
821
this.runningTime = 0;
822
this.playing = true;
823
this.crashed = false;
824
this.distanceRan = 0;
825
this.setSpeed(this.config.SPEED);
826
this.time = getTimeStamp();
827
this.containerEl.classList.remove(Runner.classes.CRASHED);
828
this.clearCanvas();
829
this.distanceMeter.reset(this.highestScore);
830
this.horizon.reset();
831
this.tRex.reset();
832
this.playSound(this.soundFx.BUTTON_PRESS);
833
this.invert(true);
834
this.update();
835
}
836
},
837
838
/**
839
* Pause the game if the tab is not in focus.
840
*/
841
onVisibilityChange: function (e) {
842
if (document.hidden || document.webkitHidden || e.type == 'blur' ||
843
document.visibilityState != 'visible') {
844
this.stop();
845
} else if (!this.crashed) {
846
this.tRex.reset();
847
this.play();
848
}
849
},
850
851
/**
852
* Play a sound.
853
* @param {SoundBuffer} soundBuffer
854
*/
855
playSound: function (soundBuffer) {
856
if (soundBuffer) {
857
var sourceNode = this.audioContext.createBufferSource();
858
sourceNode.buffer = soundBuffer;
859
sourceNode.connect(this.audioContext.destination);
860
sourceNode.start(0);
861
}
862
},
863
864
/**
865
* Inverts the current page / canvas colors.
866
* @param {boolean} Whether to reset colors.
867
*/
868
invert: function (reset) {
869
if (reset) {
870
document.body.classList.toggle(Runner.classes.INVERTED, false);
871
this.invertTimer = 0;
872
this.inverted = false;
873
} else {
874
this.inverted = document.body.classList.toggle(Runner.classes.INVERTED,
875
this.invertTrigger);
876
}
877
}
878
};
879
880
881
/**
882
* Updates the canvas size taking into
883
* account the backing store pixel ratio and
884
* the device pixel ratio.
885
*
886
* See article by Paul Lewis:
887
* http://www.html5rocks.com/en/tutorials/canvas/hidpi/
888
*
889
* @param {HTMLCanvasElement} canvas
890
* @param {number} opt_width
891
* @param {number} opt_height
892
* @return {boolean} Whether the canvas was scaled.
893
*/
894
Runner.updateCanvasScaling = function (canvas, opt_width, opt_height) {
895
var context = canvas.getContext('2d');
896
897
// Query the various pixel ratios
898
var devicePixelRatio = Math.floor(window.devicePixelRatio) || 1;
899
var backingStoreRatio = Math.floor(context.webkitBackingStorePixelRatio) || 1;
900
var ratio = devicePixelRatio / backingStoreRatio;
901
902
// Upscale the canvas if the two ratios don't match
903
if (devicePixelRatio !== backingStoreRatio) {
904
var oldWidth = opt_width || canvas.width;
905
var oldHeight = opt_height || canvas.height;
906
907
canvas.width = oldWidth * ratio;
908
canvas.height = oldHeight * ratio;
909
910
canvas.style.width = oldWidth + 'px';
911
canvas.style.height = oldHeight + 'px';
912
913
// Scale the context to counter the fact that we've manually scaled
914
// our canvas element.
915
context.scale(ratio, ratio);
916
return true;
917
} else if (devicePixelRatio == 1) {
918
// Reset the canvas width / height. Fixes scaling bug when the page is
919
// zoomed and the devicePixelRatio changes accordingly.
920
canvas.style.width = canvas.width + 'px';
921
canvas.style.height = canvas.height + 'px';
922
}
923
return false;
924
};
925
926
927
/**
928
* Get random number.
929
* @param {number} min
930
* @param {number} max
931
* @param {number}
932
*/
933
function getRandomNum(min, max) {
934
return Math.floor(Math.random() * (max - min + 1)) + min;
935
}
936
937
938
/**
939
* Vibrate on mobile devices.
940
* @param {number} duration Duration of the vibration in milliseconds.
941
*/
942
function vibrate(duration) {
943
if (IS_MOBILE && window.navigator.vibrate) {
944
window.navigator.vibrate(duration);
945
}
946
}
947
948
949
/**
950
* Create canvas element.
951
* @param {HTMLElement} container Element to append canvas to.
952
* @param {number} width
953
* @param {number} height
954
* @param {string} opt_classname
955
* @return {HTMLCanvasElement}
956
*/
957
function createCanvas(container, width, height, opt_classname) {
958
var canvas = document.createElement('canvas');
959
canvas.className = opt_classname ? Runner.classes.CANVAS + ' ' +
960
opt_classname : Runner.classes.CANVAS;
961
canvas.width = width;
962
canvas.height = height;
963
container.appendChild(canvas);
964
965
return canvas;
966
}
967
968
969
/**
970
* Decodes the base 64 audio to ArrayBuffer used by Web Audio.
971
* @param {string} base64String
972
*/
973
function decodeBase64ToArrayBuffer(base64String) {
974
var len = (base64String.length / 4) * 3;
975
var str = atob(base64String);
976
var arrayBuffer = new ArrayBuffer(len);
977
var bytes = new Uint8Array(arrayBuffer);
978
979
for (var i = 0; i < len; i++) {
980
bytes[i] = str.charCodeAt(i);
981
}
982
return bytes.buffer;
983
}
984
985
986
/**
987
* Return the current timestamp.
988
* @return {number}
989
*/
990
function getTimeStamp() {
991
return IS_IOS ? new Date().getTime() : performance.now();
992
}
993
994
995
//******************************************************************************
996
997
998
/**
999
* Game over panel.
1000
* @param {!HTMLCanvasElement} canvas
1001
* @param {Object} textImgPos
1002
* @param {Object} restartImgPos
1003
* @param {!Object} dimensions Canvas dimensions.
1004
* @constructor
1005
*/
1006
function GameOverPanel(canvas, textImgPos, restartImgPos, dimensions) {
1007
this.canvas = canvas;
1008
this.canvasCtx = canvas.getContext('2d');
1009
this.canvasDimensions = dimensions;
1010
this.textImgPos = textImgPos;
1011
this.restartImgPos = restartImgPos;
1012
this.draw();
1013
};
1014
1015
1016
/**
1017
* Dimensions used in the panel.
1018
* @enum {number}
1019
*/
1020
GameOverPanel.dimensions = {
1021
TEXT_X: 0,
1022
TEXT_Y: 13,
1023
TEXT_WIDTH: 191,
1024
TEXT_HEIGHT: 11,
1025
RESTART_WIDTH: 36,
1026
RESTART_HEIGHT: 32
1027
};
1028
1029
1030
GameOverPanel.prototype = {
1031
/**
1032
* Update the panel dimensions.
1033
* @param {number} width New canvas width.
1034
* @param {number} opt_height Optional new canvas height.
1035
*/
1036
updateDimensions: function (width, opt_height) {
1037
this.canvasDimensions.WIDTH = width;
1038
if (opt_height) {
1039
this.canvasDimensions.HEIGHT = opt_height;
1040
}
1041
},
1042
1043
/**
1044
* Draw the panel.
1045
*/
1046
draw: function () {
1047
var dimensions = GameOverPanel.dimensions;
1048
1049
var centerX = this.canvasDimensions.WIDTH / 2;
1050
1051
// Game over text.
1052
var textSourceX = dimensions.TEXT_X;
1053
var textSourceY = dimensions.TEXT_Y;
1054
var textSourceWidth = dimensions.TEXT_WIDTH;
1055
var textSourceHeight = dimensions.TEXT_HEIGHT;
1056
1057
var textTargetX = Math.round(centerX - (dimensions.TEXT_WIDTH / 2));
1058
var textTargetY = Math.round((this.canvasDimensions.HEIGHT - 25) / 3);
1059
var textTargetWidth = dimensions.TEXT_WIDTH;
1060
var textTargetHeight = dimensions.TEXT_HEIGHT;
1061
1062
var restartSourceWidth = dimensions.RESTART_WIDTH;
1063
var restartSourceHeight = dimensions.RESTART_HEIGHT;
1064
var restartTargetX = centerX - (dimensions.RESTART_WIDTH / 2);
1065
var restartTargetY = this.canvasDimensions.HEIGHT / 2;
1066
1067
if (IS_HIDPI) {
1068
textSourceY *= 2;
1069
textSourceX *= 2;
1070
textSourceWidth *= 2;
1071
textSourceHeight *= 2;
1072
restartSourceWidth *= 2;
1073
restartSourceHeight *= 2;
1074
}
1075
1076
textSourceX += this.textImgPos.x;
1077
textSourceY += this.textImgPos.y;
1078
1079
// Game over text from sprite.
1080
this.canvasCtx.drawImage(Runner.imageSprite,
1081
textSourceX, textSourceY, textSourceWidth, textSourceHeight,
1082
textTargetX, textTargetY, textTargetWidth, textTargetHeight);
1083
1084
// Restart button.
1085
this.canvasCtx.drawImage(Runner.imageSprite,
1086
this.restartImgPos.x, this.restartImgPos.y,
1087
restartSourceWidth, restartSourceHeight,
1088
restartTargetX, restartTargetY, dimensions.RESTART_WIDTH,
1089
dimensions.RESTART_HEIGHT);
1090
}
1091
};
1092
1093
1094
//******************************************************************************
1095
1096
/**
1097
* Check for a collision.
1098
* @param {!Obstacle} obstacle
1099
* @param {!Trex} tRex T-rex object.
1100
* @param {HTMLCanvasContext} opt_canvasCtx Optional canvas context for drawing
1101
* collision boxes.
1102
* @return {Array<CollisionBox>}
1103
*/
1104
function checkForCollision(obstacle, tRex, opt_canvasCtx) {
1105
var obstacleBoxXPos = Runner.defaultDimensions.WIDTH + obstacle.xPos;
1106
1107
// Adjustments are made to the bounding box as there is a 1 pixel white
1108
// border around the t-rex and obstacles.
1109
var tRexBox = new CollisionBox(
1110
tRex.xPos + 1,
1111
tRex.yPos + 1,
1112
tRex.config.WIDTH - 2,
1113
tRex.config.HEIGHT - 2);
1114
1115
var obstacleBox = new CollisionBox(
1116
obstacle.xPos + 1,
1117
obstacle.yPos + 1,
1118
obstacle.typeConfig.width * obstacle.size - 2,
1119
obstacle.typeConfig.height - 2);
1120
1121
// Debug outer box
1122
if (opt_canvasCtx) {
1123
drawCollisionBoxes(opt_canvasCtx, tRexBox, obstacleBox);
1124
}
1125
1126
// Simple outer bounds check.
1127
if (boxCompare(tRexBox, obstacleBox)) {
1128
var collisionBoxes = obstacle.collisionBoxes;
1129
var tRexCollisionBoxes = tRex.ducking ?
1130
Trex.collisionBoxes.DUCKING : Trex.collisionBoxes.RUNNING;
1131
1132
// Detailed axis aligned box check.
1133
for (var t = 0; t < tRexCollisionBoxes.length; t++) {
1134
for (var i = 0; i < collisionBoxes.length; i++) {
1135
// Adjust the box to actual positions.
1136
var adjTrexBox =
1137
createAdjustedCollisionBox(tRexCollisionBoxes[t], tRexBox);
1138
var adjObstacleBox =
1139
createAdjustedCollisionBox(collisionBoxes[i], obstacleBox);
1140
var crashed = boxCompare(adjTrexBox, adjObstacleBox);
1141
1142
// Draw boxes for debug.
1143
if (opt_canvasCtx) {
1144
drawCollisionBoxes(opt_canvasCtx, adjTrexBox, adjObstacleBox);
1145
}
1146
1147
if (crashed) {
1148
return [adjTrexBox, adjObstacleBox];
1149
}
1150
}
1151
}
1152
}
1153
return false;
1154
};
1155
1156
1157
/**
1158
* Adjust the collision box.
1159
* @param {!CollisionBox} box The original box.
1160
* @param {!CollisionBox} adjustment Adjustment box.
1161
* @return {CollisionBox} The adjusted collision box object.
1162
*/
1163
function createAdjustedCollisionBox(box, adjustment) {
1164
return new CollisionBox(
1165
box.x + adjustment.x,
1166
box.y + adjustment.y,
1167
box.width,
1168
box.height);
1169
};
1170
1171
1172
/**
1173
* Draw the collision boxes for debug.
1174
*/
1175
function drawCollisionBoxes(canvasCtx, tRexBox, obstacleBox) {
1176
canvasCtx.save();
1177
canvasCtx.strokeStyle = '#f00';
1178
canvasCtx.strokeRect(tRexBox.x, tRexBox.y, tRexBox.width, tRexBox.height);
1179
1180
canvasCtx.strokeStyle = '#0f0';
1181
canvasCtx.strokeRect(obstacleBox.x, obstacleBox.y,
1182
obstacleBox.width, obstacleBox.height);
1183
canvasCtx.restore();
1184
};
1185
1186
1187
/**
1188
* Compare two collision boxes for a collision.
1189
* @param {CollisionBox} tRexBox
1190
* @param {CollisionBox} obstacleBox
1191
* @return {boolean} Whether the boxes intersected.
1192
*/
1193
function boxCompare(tRexBox, obstacleBox) {
1194
var crashed = false;
1195
var tRexBoxX = tRexBox.x;
1196
var tRexBoxY = tRexBox.y;
1197
1198
var obstacleBoxX = obstacleBox.x;
1199
var obstacleBoxY = obstacleBox.y;
1200
1201
// Axis-Aligned Bounding Box method.
1202
if (tRexBox.x < obstacleBoxX + obstacleBox.width &&
1203
tRexBox.x + tRexBox.width > obstacleBoxX &&
1204
tRexBox.y < obstacleBox.y + obstacleBox.height &&
1205
tRexBox.height + tRexBox.y > obstacleBox.y) {
1206
crashed = true;
1207
}
1208
1209
return crashed;
1210
};
1211
1212
1213
//******************************************************************************
1214
1215
/**
1216
* Collision box object.
1217
* @param {number} x X position.
1218
* @param {number} y Y Position.
1219
* @param {number} w Width.
1220
* @param {number} h Height.
1221
*/
1222
function CollisionBox(x, y, w, h) {
1223
this.x = x;
1224
this.y = y;
1225
this.width = w;
1226
this.height = h;
1227
};
1228
1229
1230
//******************************************************************************
1231
1232
/**
1233
* Obstacle.
1234
* @param {HTMLCanvasCtx} canvasCtx
1235
* @param {Obstacle.type} type
1236
* @param {Object} spritePos Obstacle position in sprite.
1237
* @param {Object} dimensions
1238
* @param {number} gapCoefficient Mutipler in determining the gap.
1239
* @param {number} speed
1240
* @param {number} opt_xOffset
1241
*/
1242
function Obstacle(canvasCtx, type, spriteImgPos, dimensions,
1243
gapCoefficient, speed, opt_xOffset) {
1244
1245
this.canvasCtx = canvasCtx;
1246
this.spritePos = spriteImgPos;
1247
this.typeConfig = type;
1248
this.gapCoefficient = gapCoefficient;
1249
this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);
1250
this.dimensions = dimensions;
1251
this.remove = false;
1252
this.xPos = dimensions.WIDTH + (opt_xOffset || 0);
1253
this.yPos = 0;
1254
this.width = 0;
1255
this.collisionBoxes = [];
1256
this.gap = 0;
1257
this.speedOffset = 0;
1258
1259
// For animated obstacles.
1260
this.currentFrame = 0;
1261
this.timer = 0;
1262
1263
this.init(speed);
1264
};
1265
1266
/**
1267
* Coefficient for calculating the maximum gap.
1268
* @const
1269
*/
1270
Obstacle.MAX_GAP_COEFFICIENT = 1.5;
1271
1272
/**
1273
* Maximum obstacle grouping count.
1274
* @const
1275
*/
1276
Obstacle.MAX_OBSTACLE_LENGTH = 3,
1277
1278
1279
Obstacle.prototype = {
1280
/**
1281
* Initialise the DOM for the obstacle.
1282
* @param {number} speed
1283
*/
1284
init: function (speed) {
1285
this.cloneCollisionBoxes();
1286
1287
// Only allow sizing if we're at the right speed.
1288
if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
1289
this.size = 1;
1290
}
1291
1292
this.width = this.typeConfig.width * this.size;
1293
1294
// Check if obstacle can be positioned at various heights.
1295
if (Array.isArray(this.typeConfig.yPos)) {
1296
var yPosConfig = IS_MOBILE ? this.typeConfig.yPosMobile :
1297
this.typeConfig.yPos;
1298
this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];
1299
} else {
1300
this.yPos = this.typeConfig.yPos;
1301
}
1302
1303
this.draw();
1304
1305
// Make collision box adjustments,
1306
// Central box is adjusted to the size as one box.
1307
// ____ ______ ________
1308
// _| |-| _| |-| _| |-|
1309
// | |<->| | | |<--->| | | |<----->| |
1310
// | | 1 | | | | 2 | | | | 3 | |
1311
// |_|___|_| |_|_____|_| |_|_______|_|
1312
//
1313
if (this.size > 1) {
1314
this.collisionBoxes[1].width = this.width - this.collisionBoxes[0].width -
1315
this.collisionBoxes[2].width;
1316
this.collisionBoxes[2].x = this.width - this.collisionBoxes[2].width;
1317
}
1318
1319
// For obstacles that go at a different speed from the horizon.
1320
if (this.typeConfig.speedOffset) {
1321
this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :
1322
-this.typeConfig.speedOffset;
1323
}
1324
1325
this.gap = this.getGap(this.gapCoefficient, speed);
1326
},
1327
1328
/**
1329
* Draw and crop based on size.
1330
*/
1331
draw: function () {
1332
var sourceWidth = this.typeConfig.width;
1333
var sourceHeight = this.typeConfig.height;
1334
1335
if (IS_HIDPI) {
1336
sourceWidth = sourceWidth * 2;
1337
sourceHeight = sourceHeight * 2;
1338
}
1339
1340
// X position in sprite.
1341
var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +
1342
this.spritePos.x;
1343
1344
// Animation frames.
1345
if (this.currentFrame > 0) {
1346
sourceX += sourceWidth * this.currentFrame;
1347
}
1348
1349
this.canvasCtx.drawImage(Runner.imageSprite,
1350
sourceX, this.spritePos.y,
1351
sourceWidth * this.size, sourceHeight,
1352
this.xPos, this.yPos,
1353
this.typeConfig.width * this.size, this.typeConfig.height);
1354
},
1355
1356
/**
1357
* Obstacle frame update.
1358
* @param {number} deltaTime
1359
* @param {number} speed
1360
*/
1361
update: function (deltaTime, speed) {
1362
if (!this.remove) {
1363
if (this.typeConfig.speedOffset) {
1364
speed += this.speedOffset;
1365
}
1366
this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime);
1367
1368
// Update frame
1369
if (this.typeConfig.numFrames) {
1370
this.timer += deltaTime;
1371
if (this.timer >= this.typeConfig.frameRate) {
1372
this.currentFrame =
1373
this.currentFrame == this.typeConfig.numFrames - 1 ?
1374
0 : this.currentFrame + 1;
1375
this.timer = 0;
1376
}
1377
}
1378
this.draw();
1379
1380
if (!this.isVisible()) {
1381
this.remove = true;
1382
}
1383
}
1384
},
1385
1386
/**
1387
* Calculate a random gap size.
1388
* - Minimum gap gets wider as speed increses
1389
* @param {number} gapCoefficient
1390
* @param {number} speed
1391
* @return {number} The gap size.
1392
*/
1393
getGap: function (gapCoefficient, speed) {
1394
var minGap = Math.round(this.width * speed +
1395
this.typeConfig.minGap * gapCoefficient);
1396
var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
1397
return getRandomNum(minGap, maxGap);
1398
},
1399
1400
/**
1401
* Check if obstacle is visible.
1402
* @return {boolean} Whether the obstacle is in the game area.
1403
*/
1404
isVisible: function () {
1405
return this.xPos + this.width > 0;
1406
},
1407
1408
/**
1409
* Make a copy of the collision boxes, since these will change based on
1410
* obstacle type and size.
1411
*/
1412
cloneCollisionBoxes: function () {
1413
var collisionBoxes = this.typeConfig.collisionBoxes;
1414
1415
for (var i = collisionBoxes.length - 1; i >= 0; i--) {
1416
this.collisionBoxes[i] = new CollisionBox(collisionBoxes[i].x,
1417
collisionBoxes[i].y, collisionBoxes[i].width,
1418
collisionBoxes[i].height);
1419
}
1420
}
1421
};
1422
1423
1424
/**
1425
* Obstacle definitions.
1426
* minGap: minimum pixel space betweeen obstacles.
1427
* multipleSpeed: Speed at which multiples are allowed.
1428
* speedOffset: speed faster / slower than the horizon.
1429
* minSpeed: Minimum speed which the obstacle can make an appearance.
1430
*/
1431
Obstacle.types = [
1432
{
1433
type: 'CACTUS_SMALL',
1434
width: 17,
1435
height: 35,
1436
yPos: 105,
1437
multipleSpeed: 4,
1438
minGap: 120,
1439
minSpeed: 0,
1440
collisionBoxes: [
1441
new CollisionBox(0, 7, 5, 27),
1442
new CollisionBox(4, 0, 6, 34),
1443
new CollisionBox(10, 4, 7, 14)
1444
]
1445
},
1446
{
1447
type: 'CACTUS_LARGE',
1448
width: 25,
1449
height: 50,
1450
yPos: 90,
1451
multipleSpeed: 7,
1452
minGap: 120,
1453
minSpeed: 0,
1454
collisionBoxes: [
1455
new CollisionBox(0, 12, 7, 38),
1456
new CollisionBox(8, 0, 7, 49),
1457
new CollisionBox(13, 10, 10, 38)
1458
]
1459
},
1460
{
1461
type: 'PTERODACTYL',
1462
width: 46,
1463
height: 40,
1464
yPos: [100, 75, 50], // Variable height.
1465
yPosMobile: [100, 50], // Variable height mobile.
1466
multipleSpeed: 999,
1467
minSpeed: 8.5,
1468
minGap: 150,
1469
collisionBoxes: [
1470
new CollisionBox(15, 15, 16, 5),
1471
new CollisionBox(18, 21, 24, 6),
1472
new CollisionBox(2, 14, 4, 3),
1473
new CollisionBox(6, 10, 4, 7),
1474
new CollisionBox(10, 8, 6, 9)
1475
],
1476
numFrames: 2,
1477
frameRate: 1000 / 6,
1478
speedOffset: .8
1479
}
1480
];
1481
1482
1483
//******************************************************************************
1484
/**
1485
* T-rex game character.
1486
* @param {HTMLCanvas} canvas
1487
* @param {Object} spritePos Positioning within image sprite.
1488
* @constructor
1489
*/
1490
function Trex(canvas, spritePos) {
1491
this.canvas = canvas;
1492
this.canvasCtx = canvas.getContext('2d');
1493
this.spritePos = spritePos;
1494
this.xPos = 0;
1495
this.yPos = 0;
1496
// Position when on the ground.
1497
this.groundYPos = 0;
1498
this.currentFrame = 0;
1499
this.currentAnimFrames = [];
1500
this.blinkDelay = 0;
1501
this.blinkCount = 0;
1502
this.animStartTime = 0;
1503
this.timer = 0;
1504
this.msPerFrame = 1000 / FPS;
1505
this.config = Trex.config;
1506
// Current status.
1507
this.status = Trex.status.WAITING;
1508
1509
this.jumping = false;
1510
this.ducking = false;
1511
this.jumpVelocity = 0;
1512
this.reachedMinHeight = false;
1513
this.speedDrop = false;
1514
this.jumpCount = 0;
1515
this.jumpspotX = 0;
1516
1517
this.init();
1518
};
1519
1520
1521
/**
1522
* T-rex player config.
1523
* @enum {number}
1524
*/
1525
Trex.config = {
1526
DROP_VELOCITY: -5,
1527
GRAVITY: 0.6,
1528
HEIGHT: 47,
1529
HEIGHT_DUCK: 25,
1530
INIITAL_JUMP_VELOCITY: -10,
1531
INTRO_DURATION: 1500,
1532
MAX_JUMP_HEIGHT: 30,
1533
MIN_JUMP_HEIGHT: 30,
1534
SPEED_DROP_COEFFICIENT: 3,
1535
SPRITE_WIDTH: 262,
1536
START_X_POS: 50,
1537
WIDTH: 44,
1538
WIDTH_DUCK: 59
1539
};
1540
1541
1542
/**
1543
* Used in collision detection.
1544
* @type {Array<CollisionBox>}
1545
*/
1546
Trex.collisionBoxes = {
1547
DUCKING: [
1548
new CollisionBox(1, 18, 55, 25)
1549
],
1550
RUNNING: [
1551
new CollisionBox(22, 0, 17, 16),
1552
new CollisionBox(1, 18, 30, 9),
1553
new CollisionBox(10, 35, 14, 8),
1554
new CollisionBox(1, 24, 29, 5),
1555
new CollisionBox(5, 30, 21, 4),
1556
new CollisionBox(9, 34, 15, 4)
1557
]
1558
};
1559
1560
1561
/**
1562
* Animation states.
1563
* @enum {string}
1564
*/
1565
Trex.status = {
1566
CRASHED: 'CRASHED',
1567
DUCKING: 'DUCKING',
1568
JUMPING: 'JUMPING',
1569
RUNNING: 'RUNNING',
1570
WAITING: 'WAITING'
1571
};
1572
1573
/**
1574
* Blinking coefficient.
1575
* @const
1576
*/
1577
Trex.BLINK_TIMING = 7000;
1578
1579
1580
/**
1581
* Animation config for different states.
1582
* @enum {Object}
1583
*/
1584
Trex.animFrames = {
1585
WAITING: {
1586
frames: [44, 0],
1587
msPerFrame: 1000 / 3
1588
},
1589
RUNNING: {
1590
frames: [88, 132],
1591
msPerFrame: 1000 / 12
1592
},
1593
CRASHED: {
1594
frames: [220],
1595
msPerFrame: 1000 / 60
1596
},
1597
JUMPING: {
1598
frames: [0],
1599
msPerFrame: 1000 / 60
1600
},
1601
DUCKING: {
1602
frames: [264, 323],
1603
msPerFrame: 1000 / 8
1604
}
1605
};
1606
1607
1608
Trex.prototype = {
1609
/**
1610
* T-rex player initaliser.
1611
* Sets the t-rex to blink at random intervals.
1612
*/
1613
init: function () {
1614
this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
1615
Runner.config.BOTTOM_PAD;
1616
this.yPos = this.groundYPos;
1617
this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
1618
1619
this.draw(0, 0);
1620
this.update(0, Trex.status.WAITING);
1621
},
1622
1623
/**
1624
* Setter for the jump velocity.
1625
* The approriate drop velocity is also set.
1626
*/
1627
setJumpVelocity: function (setting) {
1628
this.config.INIITAL_JUMP_VELOCITY = -setting;
1629
this.config.DROP_VELOCITY = -setting / 2;
1630
},
1631
1632
/**
1633
* Set the animation status.
1634
* @param {!number} deltaTime
1635
* @param {Trex.status} status Optional status to switch to.
1636
*/
1637
update: function (deltaTime, opt_status) {
1638
this.timer += deltaTime;
1639
1640
// Update the status.
1641
if (opt_status) {
1642
this.status = opt_status;
1643
this.currentFrame = 0;
1644
this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
1645
this.currentAnimFrames = Trex.animFrames[opt_status].frames;
1646
1647
if (opt_status == Trex.status.WAITING) {
1648
this.animStartTime = getTimeStamp();
1649
this.setBlinkDelay();
1650
}
1651
}
1652
1653
// Game intro animation, T-rex moves in from the left.
1654
if (this.playingIntro && this.xPos < this.config.START_X_POS) {
1655
this.xPos += Math.round((this.config.START_X_POS /
1656
this.config.INTRO_DURATION) * deltaTime);
1657
}
1658
1659
if (this.status == Trex.status.WAITING) {
1660
this.blink(getTimeStamp());
1661
} else {
1662
this.draw(this.currentAnimFrames[this.currentFrame], 0);
1663
}
1664
1665
// Update the frame position.
1666
if (this.timer >= this.msPerFrame) {
1667
this.currentFrame = this.currentFrame ==
1668
this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
1669
this.timer = 0;
1670
}
1671
1672
// Speed drop becomes duck if the down key is still being pressed.
1673
if (this.speedDrop && this.yPos == this.groundYPos) {
1674
this.speedDrop = false;
1675
this.setDuck(true);
1676
}
1677
},
1678
1679
/**
1680
* Draw the t-rex to a particular position.
1681
* @param {number} x
1682
* @param {number} y
1683
*/
1684
draw: function (x, y) {
1685
var sourceX = x;
1686
var sourceY = y;
1687
var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?
1688
this.config.WIDTH_DUCK : this.config.WIDTH;
1689
var sourceHeight = this.config.HEIGHT;
1690
1691
if (IS_HIDPI) {
1692
sourceX *= 2;
1693
sourceY *= 2;
1694
sourceWidth *= 2;
1695
sourceHeight *= 2;
1696
}
1697
1698
// Adjustments for sprite sheet position.
1699
sourceX += this.spritePos.x;
1700
sourceY += this.spritePos.y;
1701
1702
// Ducking.
1703
if (this.ducking && this.status != Trex.status.CRASHED) {
1704
this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
1705
sourceWidth, sourceHeight,
1706
this.xPos, this.yPos,
1707
this.config.WIDTH_DUCK, this.config.HEIGHT);
1708
} else {
1709
// Crashed whilst ducking. Trex is standing up so needs adjustment.
1710
if (this.ducking && this.status == Trex.status.CRASHED) {
1711
this.xPos++;
1712
}
1713
// Standing / running
1714
this.canvasCtx.drawImage(Runner.imageSprite, sourceX, sourceY,
1715
sourceWidth, sourceHeight,
1716
this.xPos, this.yPos,
1717
this.config.WIDTH, this.config.HEIGHT);
1718
}
1719
},
1720
1721
/**
1722
* Sets a random time for the blink to happen.
1723
*/
1724
setBlinkDelay: function () {
1725
this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
1726
},
1727
1728
/**
1729
* Make t-rex blink at random intervals.
1730
* @param {number} time Current time in milliseconds.
1731
*/
1732
blink: function (time) {
1733
var deltaTime = time - this.animStartTime;
1734
1735
if (deltaTime >= this.blinkDelay) {
1736
this.draw(this.currentAnimFrames[this.currentFrame], 0);
1737
1738
if (this.currentFrame == 1) {
1739
// Set new random delay to blink.
1740
this.setBlinkDelay();
1741
this.animStartTime = time;
1742
this.blinkCount++;
1743
}
1744
}
1745
},
1746
1747
/**
1748
* Initialise a jump.
1749
* @param {number} speed
1750
*/
1751
startJump: function (speed) {
1752
if (!this.jumping) {
1753
this.update(0, Trex.status.JUMPING);
1754
// Tweak the jump velocity based on the speed.
1755
this.jumpVelocity = this.config.INIITAL_JUMP_VELOCITY - (speed / 10);
1756
this.jumping = true;
1757
this.reachedMinHeight = false;
1758
this.speedDrop = false;
1759
}
1760
},
1761
1762
/**
1763
* Jump is complete, falling down.
1764
*/
1765
endJump: function () {
1766
if (this.reachedMinHeight &&
1767
this.jumpVelocity < this.config.DROP_VELOCITY) {
1768
this.jumpVelocity = this.config.DROP_VELOCITY;
1769
}
1770
},
1771
1772
/**
1773
* Update frame for a jump.
1774
* @param {number} deltaTime
1775
* @param {number} speed
1776
*/
1777
updateJump: function (deltaTime, speed) {
1778
var msPerFrame = Trex.animFrames[this.status].msPerFrame;
1779
var framesElapsed = deltaTime / msPerFrame;
1780
1781
// Speed drop makes Trex fall faster.
1782
if (this.speedDrop) {
1783
this.yPos += Math.round(this.jumpVelocity *
1784
this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
1785
} else {
1786
this.yPos += Math.round(this.jumpVelocity * framesElapsed);
1787
}
1788
1789
this.jumpVelocity += this.config.GRAVITY * framesElapsed;
1790
1791
// Minimum height has been reached.
1792
if (this.yPos < this.minJumpHeight || this.speedDrop) {
1793
this.reachedMinHeight = true;
1794
}
1795
1796
// Reached max height
1797
if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
1798
this.endJump();
1799
}
1800
1801
// Back down at ground level. Jump completed.
1802
if (this.yPos > this.groundYPos) {
1803
this.reset();
1804
this.jumpCount++;
1805
}
1806
1807
this.update(deltaTime);
1808
},
1809
1810
/**
1811
* Set the speed drop. Immediately cancels the current jump.
1812
*/
1813
setSpeedDrop: function () {
1814
this.speedDrop = true;
1815
this.jumpVelocity = 1;
1816
},
1817
1818
/**
1819
* @param {boolean} isDucking.
1820
*/
1821
setDuck: function (isDucking) {
1822
if (isDucking && this.status != Trex.status.DUCKING) {
1823
this.update(0, Trex.status.DUCKING);
1824
this.ducking = true;
1825
} else if (this.status == Trex.status.DUCKING) {
1826
this.update(0, Trex.status.RUNNING);
1827
this.ducking = false;
1828
}
1829
},
1830
1831
/**
1832
* Reset the t-rex to running at start of game.
1833
*/
1834
reset: function () {
1835
this.yPos = this.groundYPos;
1836
this.jumpVelocity = 0;
1837
this.jumping = false;
1838
this.ducking = false;
1839
this.update(0, Trex.status.RUNNING);
1840
this.midair = false;
1841
this.speedDrop = false;
1842
this.jumpCount = 0;
1843
}
1844
};
1845
1846
1847
//******************************************************************************
1848
1849
/**
1850
* Handles displaying the distance meter.
1851
* @param {!HTMLCanvasElement} canvas
1852
* @param {Object} spritePos Image position in sprite.
1853
* @param {number} canvasWidth
1854
* @constructor
1855
*/
1856
function DistanceMeter(canvas, spritePos, canvasWidth) {
1857
this.canvas = canvas;
1858
this.canvasCtx = canvas.getContext('2d');
1859
this.image = Runner.imageSprite;
1860
this.spritePos = spritePos;
1861
this.x = 0;
1862
this.y = 5;
1863
1864
this.currentDistance = 0;
1865
this.maxScore = 0;
1866
this.highScore = 0;
1867
this.container = null;
1868
1869
this.digits = [];
1870
this.acheivement = false;
1871
this.defaultString = '';
1872
this.flashTimer = 0;
1873
this.flashIterations = 0;
1874
this.invertTrigger = false;
1875
1876
this.config = DistanceMeter.config;
1877
this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS;
1878
this.init(canvasWidth);
1879
};
1880
1881
1882
/**
1883
* @enum {number}
1884
*/
1885
DistanceMeter.dimensions = {
1886
WIDTH: 10,
1887
HEIGHT: 13,
1888
DEST_WIDTH: 11
1889
};
1890
1891
1892
/**
1893
* Y positioning of the digits in the sprite sheet.
1894
* X position is always 0.
1895
* @type {Array<number>}
1896
*/
1897
DistanceMeter.yPos = [0, 13, 27, 40, 53, 67, 80, 93, 107, 120];
1898
1899
1900
/**
1901
* Distance meter config.
1902
* @enum {number}
1903
*/
1904
DistanceMeter.config = {
1905
// Number of digits.
1906
MAX_DISTANCE_UNITS: 5,
1907
1908
// Distance that causes achievement animation.
1909
ACHIEVEMENT_DISTANCE: 100,
1910
1911
// Used for conversion from pixel distance to a scaled unit.
1912
COEFFICIENT: 0.025,
1913
1914
// Flash duration in milliseconds.
1915
FLASH_DURATION: 1000 / 4,
1916
1917
// Flash iterations for achievement animation.
1918
FLASH_ITERATIONS: 3
1919
};
1920
1921
1922
DistanceMeter.prototype = {
1923
/**
1924
* Initialise the distance meter to '00000'.
1925
* @param {number} width Canvas width in px.
1926
*/
1927
init: function (width) {
1928
var maxDistanceStr = '';
1929
1930
this.calcXPos(width);
1931
this.maxScore = this.maxScoreUnits;
1932
for (var i = 0; i < this.maxScoreUnits; i++) {
1933
this.draw(i, 0);
1934
this.defaultString += '0';
1935
maxDistanceStr += '9';
1936
}
1937
1938
this.maxScore = parseInt(maxDistanceStr);
1939
},
1940
1941
/**
1942
* Calculate the xPos in the canvas.
1943
* @param {number} canvasWidth
1944
*/
1945
calcXPos: function (canvasWidth) {
1946
this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
1947
(this.maxScoreUnits + 1));
1948
},
1949
1950
/**
1951
* Draw a digit to canvas.
1952
* @param {number} digitPos Position of the digit.
1953
* @param {number} value Digit value 0-9.
1954
* @param {boolean} opt_highScore Whether drawing the high score.
1955
*/
1956
draw: function (digitPos, value, opt_highScore) {
1957
var sourceWidth = DistanceMeter.dimensions.WIDTH;
1958
var sourceHeight = DistanceMeter.dimensions.HEIGHT;
1959
var sourceX = DistanceMeter.dimensions.WIDTH * value;
1960
var sourceY = 0;
1961
1962
var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
1963
var targetY = this.y;
1964
var targetWidth = DistanceMeter.dimensions.WIDTH;
1965
var targetHeight = DistanceMeter.dimensions.HEIGHT;
1966
1967
// For high DPI we 2x source values.
1968
if (IS_HIDPI) {
1969
sourceWidth *= 2;
1970
sourceHeight *= 2;
1971
sourceX *= 2;
1972
}
1973
1974
sourceX += this.spritePos.x;
1975
sourceY += this.spritePos.y;
1976
1977
this.canvasCtx.save();
1978
1979
if (opt_highScore) {
1980
// Left of the current score.
1981
var highScoreX = this.x - (this.maxScoreUnits * 2) *
1982
DistanceMeter.dimensions.WIDTH;
1983
this.canvasCtx.translate(highScoreX, this.y);
1984
} else {
1985
this.canvasCtx.translate(this.x, this.y);
1986
}
1987
1988
this.canvasCtx.drawImage(this.image, sourceX, sourceY,
1989
sourceWidth, sourceHeight,
1990
targetX, targetY,
1991
targetWidth, targetHeight
1992
);
1993
1994
this.canvasCtx.restore();
1995
},
1996
1997
/**
1998
* Covert pixel distance to a 'real' distance.
1999
* @param {number} distance Pixel distance ran.
2000
* @return {number} The 'real' distance ran.
2001
*/
2002
getActualDistance: function (distance) {
2003
return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
2004
},
2005
2006
/**
2007
* Update the distance meter.
2008
* @param {number} distance
2009
* @param {number} deltaTime
2010
* @return {boolean} Whether the acheivement sound fx should be played.
2011
*/
2012
update: function (deltaTime, distance) {
2013
var paint = true;
2014
var playSound = false;
2015
2016
if (!this.acheivement) {
2017
distance = this.getActualDistance(distance);
2018
// Score has gone beyond the initial digit count.
2019
if (distance > this.maxScore && this.maxScoreUnits ==
2020
this.config.MAX_DISTANCE_UNITS) {
2021
this.maxScoreUnits++;
2022
this.maxScore = parseInt(this.maxScore + '9');
2023
} else {
2024
this.distance = 0;
2025
}
2026
2027
if (distance > 0) {
2028
// Acheivement unlocked
2029
if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
2030
// Flash score and play sound.
2031
this.acheivement = true;
2032
this.flashTimer = 0;
2033
playSound = true;
2034
}
2035
2036
// Create a string representation of the distance with leading 0.
2037
var distanceStr = (this.defaultString +
2038
distance).substr(-this.maxScoreUnits);
2039
this.digits = distanceStr.split('');
2040
} else {
2041
this.digits = this.defaultString.split('');
2042
}
2043
} else {
2044
// Control flashing of the score on reaching acheivement.
2045
if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
2046
this.flashTimer += deltaTime;
2047
2048
if (this.flashTimer < this.config.FLASH_DURATION) {
2049
paint = false;
2050
} else if (this.flashTimer >
2051
this.config.FLASH_DURATION * 2) {
2052
this.flashTimer = 0;
2053
this.flashIterations++;
2054
}
2055
} else {
2056
this.acheivement = false;
2057
this.flashIterations = 0;
2058
this.flashTimer = 0;
2059
}
2060
}
2061
2062
// Draw the digits if not flashing.
2063
if (paint) {
2064
for (var i = this.digits.length - 1; i >= 0; i--) {
2065
this.draw(i, parseInt(this.digits[i]));
2066
}
2067
}
2068
2069
this.drawHighScore();
2070
return playSound;
2071
},
2072
2073
/**
2074
* Draw the high score.
2075
*/
2076
drawHighScore: function () {
2077
this.canvasCtx.save();
2078
this.canvasCtx.globalAlpha = .8;
2079
for (var i = this.highScore.length - 1; i >= 0; i--) {
2080
this.draw(i, parseInt(this.highScore[i], 10), true);
2081
}
2082
this.canvasCtx.restore();
2083
},
2084
2085
/**
2086
* Set the highscore as a array string.
2087
* Position of char in the sprite: H - 10, I - 11.
2088
* @param {number} distance Distance ran in pixels.
2089
*/
2090
setHighScore: function (distance) {
2091
distance = this.getActualDistance(distance);
2092
var highScoreStr = (this.defaultString +
2093
distance).substr(-this.maxScoreUnits);
2094
2095
this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
2096
},
2097
2098
/**
2099
* Reset the distance meter back to '00000'.
2100
*/
2101
reset: function () {
2102
this.update(0);
2103
this.acheivement = false;
2104
}
2105
};
2106
2107
2108
//******************************************************************************
2109
2110
/**
2111
* Cloud background item.
2112
* Similar to an obstacle object but without collision boxes.
2113
* @param {HTMLCanvasElement} canvas Canvas element.
2114
* @param {Object} spritePos Position of image in sprite.
2115
* @param {number} containerWidth
2116
*/
2117
function Cloud(canvas, spritePos, containerWidth) {
2118
this.canvas = canvas;
2119
this.canvasCtx = this.canvas.getContext('2d');
2120
this.spritePos = spritePos;
2121
this.containerWidth = containerWidth;
2122
this.xPos = containerWidth;
2123
this.yPos = 0;
2124
this.remove = false;
2125
this.cloudGap = getRandomNum(Cloud.config.MIN_CLOUD_GAP,
2126
Cloud.config.MAX_CLOUD_GAP);
2127
2128
this.init();
2129
};
2130
2131
2132
/**
2133
* Cloud object config.
2134
* @enum {number}
2135
*/
2136
Cloud.config = {
2137
HEIGHT: 14,
2138
MAX_CLOUD_GAP: 400,
2139
MAX_SKY_LEVEL: 30,
2140
MIN_CLOUD_GAP: 100,
2141
MIN_SKY_LEVEL: 71,
2142
WIDTH: 46
2143
};
2144
2145
2146
Cloud.prototype = {
2147
/**
2148
* Initialise the cloud. Sets the Cloud height.
2149
*/
2150
init: function () {
2151
this.yPos = getRandomNum(Cloud.config.MAX_SKY_LEVEL,
2152
Cloud.config.MIN_SKY_LEVEL);
2153
this.draw();
2154
},
2155
2156
/**
2157
* Draw the cloud.
2158
*/
2159
draw: function () {
2160
this.canvasCtx.save();
2161
var sourceWidth = Cloud.config.WIDTH;
2162
var sourceHeight = Cloud.config.HEIGHT;
2163
2164
if (IS_HIDPI) {
2165
sourceWidth = sourceWidth * 2;
2166
sourceHeight = sourceHeight * 2;
2167
}
2168
2169
this.canvasCtx.drawImage(Runner.imageSprite, this.spritePos.x,
2170
this.spritePos.y,
2171
sourceWidth, sourceHeight,
2172
this.xPos, this.yPos,
2173
Cloud.config.WIDTH, Cloud.config.HEIGHT);
2174
2175
this.canvasCtx.restore();
2176
},
2177
2178
/**
2179
* Update the cloud position.
2180
* @param {number} speed
2181
*/
2182
update: function (speed) {
2183
if (!this.remove) {
2184
this.xPos -= Math.ceil(speed);
2185
this.draw();
2186
2187
// Mark as removeable if no longer in the canvas.
2188
if (!this.isVisible()) {
2189
this.remove = true;
2190
}
2191
}
2192
},
2193
2194
/**
2195
* Check if the cloud is visible on the stage.
2196
* @return {boolean}
2197
*/
2198
isVisible: function () {
2199
return this.xPos + Cloud.config.WIDTH > 0;
2200
}
2201
};
2202
2203
2204
//******************************************************************************
2205
2206
/**
2207
* Nightmode shows a moon and stars on the horizon.
2208
*/
2209
function NightMode(canvas, spritePos, containerWidth) {
2210
this.spritePos = spritePos;
2211
this.canvas = canvas;
2212
this.canvasCtx = canvas.getContext('2d');
2213
this.xPos = containerWidth - 50;
2214
this.yPos = 30;
2215
this.currentPhase = 0;
2216
this.opacity = 0;
2217
this.containerWidth = containerWidth;
2218
this.stars = [];
2219
this.drawStars = false;
2220
this.placeStars();
2221
};
2222
2223
/**
2224
* @enum {number}
2225
*/
2226
NightMode.config = {
2227
FADE_SPEED: 0.035,
2228
HEIGHT: 40,
2229
MOON_SPEED: 0.25,
2230
NUM_STARS: 2,
2231
STAR_SIZE: 9,
2232
STAR_SPEED: 0.3,
2233
STAR_MAX_Y: 70,
2234
WIDTH: 20
2235
};
2236
2237
NightMode.phases = [140, 120, 100, 60, 40, 20, 0];
2238
2239
NightMode.prototype = {
2240
/**
2241
* Update moving moon, changing phases.
2242
* @param {boolean} activated Whether night mode is activated.
2243
* @param {number} delta
2244
*/
2245
update: function (activated, delta) {
2246
// Moon phase.
2247
if (activated && this.opacity == 0) {
2248
this.currentPhase++;
2249
2250
if (this.currentPhase >= NightMode.phases.length) {
2251
this.currentPhase = 0;
2252
}
2253
}
2254
2255
// Fade in / out.
2256
if (activated && (this.opacity < 1 || this.opacity == 0)) {
2257
this.opacity += NightMode.config.FADE_SPEED;
2258
} else if (this.opacity > 0) {
2259
this.opacity -= NightMode.config.FADE_SPEED;
2260
}
2261
2262
// Set moon positioning.
2263
if (this.opacity > 0) {
2264
this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED);
2265
2266
// Update stars.
2267
if (this.drawStars) {
2268
for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
2269
this.stars[i].x = this.updateXPos(this.stars[i].x,
2270
NightMode.config.STAR_SPEED);
2271
}
2272
}
2273
this.draw();
2274
} else {
2275
this.opacity = 0;
2276
this.placeStars();
2277
}
2278
this.drawStars = true;
2279
},
2280
2281
updateXPos: function (currentPos, speed) {
2282
if (currentPos < -NightMode.config.WIDTH) {
2283
currentPos = this.containerWidth;
2284
} else {
2285
currentPos -= speed;
2286
}
2287
return currentPos;
2288
},
2289
2290
draw: function () {
2291
var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 :
2292
NightMode.config.WIDTH;
2293
var moonSourceHeight = NightMode.config.HEIGHT;
2294
var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase];
2295
var moonOutputWidth = moonSourceWidth;
2296
var starSize = NightMode.config.STAR_SIZE;
2297
var starSourceX = Runner.spriteDefinition.LDPI.STAR.x;
2298
2299
if (IS_HIDPI) {
2300
moonSourceWidth *= 2;
2301
moonSourceHeight *= 2;
2302
moonSourceX = this.spritePos.x +
2303
(NightMode.phases[this.currentPhase] * 2);
2304
starSize *= 2;
2305
starSourceX = Runner.spriteDefinition.HDPI.STAR.x;
2306
}
2307
2308
this.canvasCtx.save();
2309
this.canvasCtx.globalAlpha = this.opacity;
2310
2311
// Stars.
2312
if (this.drawStars) {
2313
for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
2314
this.canvasCtx.drawImage(Runner.imageSprite,
2315
starSourceX, this.stars[i].sourceY, starSize, starSize,
2316
Math.round(this.stars[i].x), this.stars[i].y,
2317
NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE);
2318
}
2319
}
2320
2321
// Moon.
2322
this.canvasCtx.drawImage(Runner.imageSprite, moonSourceX,
2323
this.spritePos.y, moonSourceWidth, moonSourceHeight,
2324
Math.round(this.xPos), this.yPos,
2325
moonOutputWidth, NightMode.config.HEIGHT);
2326
2327
this.canvasCtx.globalAlpha = 1;
2328
this.canvasCtx.restore();
2329
},
2330
2331
// Do star placement.
2332
placeStars: function () {
2333
var segmentSize = Math.round(this.containerWidth /
2334
NightMode.config.NUM_STARS);
2335
2336
for (var i = 0; i < NightMode.config.NUM_STARS; i++) {
2337
this.stars[i] = {};
2338
this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1));
2339
this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y);
2340
2341
if (IS_HIDPI) {
2342
this.stars[i].sourceY = Runner.spriteDefinition.HDPI.STAR.y +
2343
NightMode.config.STAR_SIZE * 2 * i;
2344
} else {
2345
this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y +
2346
NightMode.config.STAR_SIZE * i;
2347
}
2348
}
2349
},
2350
2351
reset: function () {
2352
this.currentPhase = 0;
2353
this.opacity = 0;
2354
this.update(false);
2355
}
2356
2357
};
2358
2359
2360
//******************************************************************************
2361
2362
/**
2363
* Horizon Line.
2364
* Consists of two connecting lines. Randomly assigns a flat / bumpy horizon.
2365
* @param {HTMLCanvasElement} canvas
2366
* @param {Object} spritePos Horizon position in sprite.
2367
* @constructor
2368
*/
2369
function HorizonLine(canvas, spritePos) {
2370
this.spritePos = spritePos;
2371
this.canvas = canvas;
2372
this.canvasCtx = canvas.getContext('2d');
2373
this.sourceDimensions = {};
2374
this.dimensions = HorizonLine.dimensions;
2375
this.sourceXPos = [this.spritePos.x, this.spritePos.x +
2376
this.dimensions.WIDTH];
2377
this.xPos = [];
2378
this.yPos = 0;
2379
this.bumpThreshold = 0.5;
2380
2381
this.setSourceDimensions();
2382
this.draw();
2383
};
2384
2385
2386
/**
2387
* Horizon line dimensions.
2388
* @enum {number}
2389
*/
2390
HorizonLine.dimensions = {
2391
WIDTH: 600,
2392
HEIGHT: 12,
2393
YPOS: 127
2394
};
2395
2396
2397
HorizonLine.prototype = {
2398
/**
2399
* Set the source dimensions of the horizon line.
2400
*/
2401
setSourceDimensions: function () {
2402
2403
for (var dimension in HorizonLine.dimensions) {
2404
if (IS_HIDPI) {
2405
if (dimension != 'YPOS') {
2406
this.sourceDimensions[dimension] =
2407
HorizonLine.dimensions[dimension] * 2;
2408
}
2409
} else {
2410
this.sourceDimensions[dimension] =
2411
HorizonLine.dimensions[dimension];
2412
}
2413
this.dimensions[dimension] = HorizonLine.dimensions[dimension];
2414
}
2415
2416
this.xPos = [0, HorizonLine.dimensions.WIDTH];
2417
this.yPos = HorizonLine.dimensions.YPOS;
2418
},
2419
2420
/**
2421
* Return the crop x position of a type.
2422
*/
2423
getRandomType: function () {
2424
return Math.random() > this.bumpThreshold ? this.dimensions.WIDTH : 0;
2425
},
2426
2427
/**
2428
* Draw the horizon line.
2429
*/
2430
draw: function () {
2431
this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[0],
2432
this.spritePos.y,
2433
this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2434
this.xPos[0], this.yPos,
2435
this.dimensions.WIDTH, this.dimensions.HEIGHT);
2436
2437
this.canvasCtx.drawImage(Runner.imageSprite, this.sourceXPos[1],
2438
this.spritePos.y,
2439
this.sourceDimensions.WIDTH, this.sourceDimensions.HEIGHT,
2440
this.xPos[1], this.yPos,
2441
this.dimensions.WIDTH, this.dimensions.HEIGHT);
2442
},
2443
2444
/**
2445
* Update the x position of an indivdual piece of the line.
2446
* @param {number} pos Line position.
2447
* @param {number} increment
2448
*/
2449
updateXPos: function (pos, increment) {
2450
var line1 = pos;
2451
var line2 = pos == 0 ? 1 : 0;
2452
2453
this.xPos[line1] -= increment;
2454
this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH;
2455
2456
if (this.xPos[line1] <= -this.dimensions.WIDTH) {
2457
this.xPos[line1] += this.dimensions.WIDTH * 2;
2458
this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH;
2459
this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x;
2460
}
2461
},
2462
2463
/**
2464
* Update the horizon line.
2465
* @param {number} deltaTime
2466
* @param {number} speed
2467
*/
2468
update: function (deltaTime, speed) {
2469
var increment = Math.floor(speed * (FPS / 1000) * deltaTime);
2470
2471
if (this.xPos[0] <= 0) {
2472
this.updateXPos(0, increment);
2473
} else {
2474
this.updateXPos(1, increment);
2475
}
2476
this.draw();
2477
},
2478
2479
/**
2480
* Reset horizon to the starting position.
2481
*/
2482
reset: function () {
2483
this.xPos[0] = 0;
2484
this.xPos[1] = HorizonLine.dimensions.WIDTH;
2485
}
2486
};
2487
2488
2489
//******************************************************************************
2490
2491
/**
2492
* Horizon background class.
2493
* @param {HTMLCanvasElement} canvas
2494
* @param {Object} spritePos Sprite positioning.
2495
* @param {Object} dimensions Canvas dimensions.
2496
* @param {number} gapCoefficient
2497
* @constructor
2498
*/
2499
function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
2500
this.canvas = canvas;
2501
this.canvasCtx = this.canvas.getContext('2d');
2502
this.config = Horizon.config;
2503
this.dimensions = dimensions;
2504
this.gapCoefficient = gapCoefficient;
2505
this.obstacles = [];
2506
this.obstacleHistory = [];
2507
this.horizonOffsets = [0, 0];
2508
this.cloudFrequency = this.config.CLOUD_FREQUENCY;
2509
this.spritePos = spritePos;
2510
this.nightMode = null;
2511
2512
// Cloud
2513
this.clouds = [];
2514
this.cloudSpeed = this.config.BG_CLOUD_SPEED;
2515
2516
// Horizon
2517
this.horizonLine = null;
2518
this.init();
2519
};
2520
2521
2522
/**
2523
* Horizon config.
2524
* @enum {number}
2525
*/
2526
Horizon.config = {
2527
BG_CLOUD_SPEED: 0.2,
2528
BUMPY_THRESHOLD: .3,
2529
CLOUD_FREQUENCY: .5,
2530
HORIZON_HEIGHT: 16,
2531
MAX_CLOUDS: 6
2532
};
2533
2534
2535
Horizon.prototype = {
2536
/**
2537
* Initialise the horizon. Just add the line and a cloud. No obstacles.
2538
*/
2539
init: function () {
2540
this.addCloud();
2541
this.horizonLine = new HorizonLine(this.canvas, this.spritePos.HORIZON);
2542
this.nightMode = new NightMode(this.canvas, this.spritePos.MOON,
2543
this.dimensions.WIDTH);
2544
},
2545
2546
/**
2547
* @param {number} deltaTime
2548
* @param {number} currentSpeed
2549
* @param {boolean} updateObstacles Used as an override to prevent
2550
* the obstacles from being updated / added. This happens in the
2551
* ease in section.
2552
* @param {boolean} showNightMode Night mode activated.
2553
*/
2554
update: function (deltaTime, currentSpeed, updateObstacles, showNightMode) {
2555
this.runningTime += deltaTime;
2556
this.horizonLine.update(deltaTime, currentSpeed);
2557
this.nightMode.update(showNightMode);
2558
this.updateClouds(deltaTime, currentSpeed);
2559
2560
if (updateObstacles) {
2561
this.updateObstacles(deltaTime, currentSpeed);
2562
}
2563
},
2564
2565
/**
2566
* Update the cloud positions.
2567
* @param {number} deltaTime
2568
* @param {number} currentSpeed
2569
*/
2570
updateClouds: function (deltaTime, speed) {
2571
var cloudSpeed = this.cloudSpeed / 1000 * deltaTime * speed;
2572
var numClouds = this.clouds.length;
2573
2574
if (numClouds) {
2575
for (var i = numClouds - 1; i >= 0; i--) {
2576
this.clouds[i].update(cloudSpeed);
2577
}
2578
2579
var lastCloud = this.clouds[numClouds - 1];
2580
2581
// Check for adding a new cloud.
2582
if (numClouds < this.config.MAX_CLOUDS &&
2583
(this.dimensions.WIDTH - lastCloud.xPos) > lastCloud.cloudGap &&
2584
this.cloudFrequency > Math.random()) {
2585
this.addCloud();
2586
}
2587
2588
// Remove expired clouds.
2589
this.clouds = this.clouds.filter(function (obj) {
2590
return !obj.remove;
2591
});
2592
} else {
2593
this.addCloud();
2594
}
2595
},
2596
2597
/**
2598
* Update the obstacle positions.
2599
* @param {number} deltaTime
2600
* @param {number} currentSpeed
2601
*/
2602
updateObstacles: function (deltaTime, currentSpeed) {
2603
// Obstacles, move to Horizon layer.
2604
var updatedObstacles = this.obstacles.slice(0);
2605
2606
for (var i = 0; i < this.obstacles.length; i++) {
2607
var obstacle = this.obstacles[i];
2608
obstacle.update(deltaTime, currentSpeed);
2609
2610
// Clean up existing obstacles.
2611
if (obstacle.remove) {
2612
updatedObstacles.shift();
2613
}
2614
}
2615
this.obstacles = updatedObstacles;
2616
2617
if (this.obstacles.length > 0) {
2618
var lastObstacle = this.obstacles[this.obstacles.length - 1];
2619
2620
if (lastObstacle && !lastObstacle.followingObstacleCreated &&
2621
lastObstacle.isVisible() &&
2622
(lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
2623
this.dimensions.WIDTH) {
2624
this.addNewObstacle(currentSpeed);
2625
lastObstacle.followingObstacleCreated = true;
2626
}
2627
} else {
2628
// Create new obstacles.
2629
this.addNewObstacle(currentSpeed);
2630
}
2631
},
2632
2633
removeFirstObstacle: function () {
2634
this.obstacles.shift();
2635
},
2636
2637
/**
2638
* Add a new obstacle.
2639
* @param {number} currentSpeed
2640
*/
2641
addNewObstacle: function (currentSpeed) {
2642
var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
2643
var obstacleType = Obstacle.types[obstacleTypeIndex];
2644
2645
// Check for multiples of the same type of obstacle.
2646
// Also check obstacle is available at current speed.
2647
if (this.duplicateObstacleCheck(obstacleType.type) ||
2648
currentSpeed < obstacleType.minSpeed) {
2649
this.addNewObstacle(currentSpeed);
2650
} else {
2651
var obstacleSpritePos = this.spritePos[obstacleType.type];
2652
2653
this.obstacles.push(new Obstacle(this.canvasCtx, obstacleType,
2654
obstacleSpritePos, this.dimensions,
2655
this.gapCoefficient, currentSpeed, obstacleType.width));
2656
2657
this.obstacleHistory.unshift(obstacleType.type);
2658
2659
if (this.obstacleHistory.length > 1) {
2660
this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);
2661
}
2662
}
2663
},
2664
2665
/**
2666
* Returns whether the previous two obstacles are the same as the next one.
2667
* Maximum duplication is set in config value MAX_OBSTACLE_DUPLICATION.
2668
* @return {boolean}
2669
*/
2670
duplicateObstacleCheck: function (nextObstacleType) {
2671
var duplicateCount = 0;
2672
2673
for (var i = 0; i < this.obstacleHistory.length; i++) {
2674
duplicateCount = this.obstacleHistory[i] == nextObstacleType ?
2675
duplicateCount + 1 : 0;
2676
}
2677
return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
2678
},
2679
2680
/**
2681
* Reset the horizon layer.
2682
* Remove existing obstacles and reposition the horizon line.
2683
*/
2684
reset: function () {
2685
this.obstacles = [];
2686
this.horizonLine.reset();
2687
this.nightMode.reset();
2688
},
2689
2690
/**
2691
* Update the canvas width and scaling.
2692
* @param {number} width Canvas width.
2693
* @param {number} height Canvas height.
2694
*/
2695
resize: function (width, height) {
2696
this.canvas.width = width;
2697
this.canvas.height = height;
2698
},
2699
2700
/**
2701
* Add a new cloud to the horizon.
2702
*/
2703
addCloud: function () {
2704
this.clouds.push(new Cloud(this.canvas, this.spritePos.CLOUD,
2705
this.dimensions.WIDTH));
2706
}
2707
};
2708
})();
2709
2710
2711
function onDocumentLoad() {
2712
new Runner('.interstitial-wrapper');
2713
}
2714
2715
document.addEventListener('DOMContentLoaded', onDocumentLoad);
2716
2717