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