diff --git a/README.md b/README.md
index 868a9e25..ba124fe7 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,26 @@
-Javascript Pseudo 3D Racer
+Javascript-Racer
==========================
-An Outrun-style pseudo-3d racing game in HTML5 and Javascript
+An Outrun-style pseudo-3d racing game in HTML5 and pure Javascript (no JQuery), playable on desktop and mobile devices, and based on the awesome engine by [Jakes Gordon (Code InComplete)](https://github.com/jakesgordon/javascript-racer).
- * [play the game](http://codeincomplete.com/projects/racer/v4.final.html)
- * view the [source](https://github.com/jakesgordon/javascript-racer)
+![javascript-racer screenshot night time](https://github.com/lrq3000/javascript-racer/raw/master/screenshots/screenshot1.png)
+![javascript-racer screenshot daylight](https://github.com/lrq3000/javascript-racer/raw/master/screenshots/screenshot2.png)
+
+ * [play the game](https://lrq3000.github.io/javascript-racer/v5.game.html) ([or the original game](http://codeincomplete.com/projects/racer/v4.final.html))
+ * view the [source](https://github.com/lrq3000/javascript-racer) ([or the original engine source](https://github.com/jakesgordon/javascript-racer))
* read about [how it works](http://codeincomplete.com/posts/2012/6/22/javascript_racer/)
-Incrementally built up in 4 parts:
+Incrementally built up in 5 parts:
- * play the [straight road demo](http://codeincomplete.com/projects/racer/v1.straight.html)
- * play the [curves demo](http://codeincomplete.com/projects/racer/v2.curves.html)
- * play the [hills demo](http://codeincomplete.com/projects/racer/v3.hills.html)
- * play the [final version](http://codeincomplete.com/projects/racer/v4.final.html)
+ * play the [straight road demo](https://lrq3000.github.io/javascript-racer/v1.straight.html)
+ * play the [curves demo](https://lrq3000.github.io/javascript-racer/v2.curves.html)
+ * play the [hills demo](https://lrq3000.github.io/javascript-racer/v3.hills.html)
+ * play the [final version - fastest lap game mode](https://lrq3000.github.io/javascript-racer/v4.final.html)
+ * play the [final game - out of time game mode](https://lrq3000.github.io/javascript-racer/v5.game.html)
With detailed descriptions of how each part works:
+ * read [the intro post](http://codeincomplete.com/posts/2012/6/22/javascript_racer/)
* read more about [v1 - straight roads](http://codeincomplete.com/posts/2012/6/23/javascript_racer_v1_straight)
* read more about [v2 - curves](http://codeincomplete.com/posts/2012/6/24/javascript_racer_v2_curves/)
* read more about [v3 - hills](http://codeincomplete.com/posts/2012/6/26/javascript_racer_v3_hills/)
@@ -35,8 +40,8 @@ Currently supported browsers include:
* Chrome (v19+) works great, 60fps at high res... provided you dont have a bad GPU driver
* IE9 - ok, 30fps at medium res... not great, but at least it works
-The current state of mobile browser performance is pretty dismal. Dont expect this to be playable on
-any mobile device.
+The current state of mobile browser performance is better than before but still just barely enough to run the game.
+Optimizing would surely help for mobile support.
>> _NOTE: I havent actually spent anytime optimizing for performance yet. So it might be possible to
make it play well on older browsers, but that's not really what this project is about._
@@ -62,8 +67,8 @@ really be considered just how to get started with a pseudo-3d racing game.
If we were to try to turn it into a real game we would have to consider:
* car sound fx
- * better synchronized music
- * full screen mode
+ * synchronized music change
+ * enhance full screen mode to include the HUD
* HUD fx (flash on fastest lap, confetti, color coded speedometer, etc)
* more accurate sprite collision
* better car AI (steering, braking etc)
@@ -79,13 +84,14 @@ If we were to try to turn it into a real game we would have to consider:
* multiple stages, different maps
* a lap map, with current position indicator
* road splits and joins
- * day/night cycle
+ * cars coming in opposite direction
* weather effects
* tunnels, bridges, clouds, walls, buildings
* city, desert, ocean
* add city of seattle and space needle to background
* 'bad guys' - add some competetor drivers to race against as well as the 'traffic'
- * different game modes - fastest lap, 1-on-1 racing, collect coins ? shoot bad guys ?
+ * different game modes - 1-on-1 racing, collect coins ? shoot bad guys ?
+ * a nice retro intro (using [codef](https://github.com/N0NameN0/CODEF) or [phaser](http://phaser.io/examples/v2/demoscene/atari-intro)?)
* a whole lot of gameplay tuning
* ...
* ...
@@ -99,13 +105,4 @@ Related Links
License
=======
-[MIT](http://en.wikipedia.org/wiki/MIT_License) license.
-
->> NOTE: the music tracks included in this project are royalty free resources paid for and licensed
-from [Lucky Lion Studios](http://luckylionstudios.com/). They are licensed ONLY for use in this
-project and should not be reproduced.
-
->> NOTE: the sprite graphics are placeholder graphics [borrowed](http://pixel.garoux.net/game/44) from the old
-genesis version of outrun and used here as teaching examples. If there are any pixel artists out there who want to
-provide original art to turn this into a real game please get in touch!
-
+[MIT](http://en.wikipedia.org/wiki/MIT_License) license including resources, except for some resources that are under creative commons or public domain (see thanks.txt). In any case, all assets are under permissive licenses allowing reuse and modification including for commercial purpose.
diff --git a/common.css b/common.css
index 5bb2d2b7..d0190d98 100644
--- a/common.css
+++ b/common.css
@@ -3,14 +3,15 @@
/* common styles used for v1 through v4 */
/****************************************/
-body { font-family: Arial, Helvetica, sans-serif; }
-#stats { border: 2px solid black; }
+body { font-family: Arial, Helvetica, sans-serif; background-color: black; color: white; }
+a { color: magenta; }
+#stats { border: 2px solid lime; }
#controls { width: 28em; float: left; padding: 1em; font-size: 0.7em; }
#controls th { text-align: right; vertical-align: middle; }
-#instructions { clear: left; float: left; width: 17em; padding: 1em; border: 1px solid black; box-shadow: 0 0 5px black; }
-#racer { position: relative; z-index: 0; width: 640px; height: 480px; margin-left: 20em; border: 2px solid black; }
-#canvas { position: absolute; z-index: 0; width: 640px; height: 480px; z-index: 0; background-color: #72D7EE; }
-#mute { background-position: 0px 0px; width: 32px; height: 32px; background: url(images/mute.png); display: inline-block; cursor: pointer; position: absolute; margin-left: 20em; }
+#instructions { clear: left; float: left; width: 17em; padding: 1em; border: 1px solid black; box-shadow: 0 0 5px lime; }
+#racer { position: relative; z-index: 0; width: 640px; height: 480px; margin-left: 20em; border: 2px solid magenta; }
+#canvas { position: absolute; z-index: 0; width: 640px; height: 480px; z-index: 0; background-color: black; /* default color when drawing out of bounds (eg, we are up a high hill and the background is too low)*/ }
+#mute { background-position: 0px 0px; width: 32px; height: 32px; background: url(images/mute.png); display: inline-block; cursor: pointer; margin-left: 20em; }
#mute.on { background-position: -32px 0px; }
/**************************************************/
@@ -20,9 +21,87 @@ body { font-family: Arial, Helvetica, sans-serif; }
#hud { position: absolute; z-index: 1; width: 640px; padding: 5px 0; font-family: Verdana, Geneva, sans-serif; font-size: 0.8em; background-color: rgba(255,0,0,0.4); color: black; border-bottom: 2px solid black; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; }
#hud .hud { background-color: rgba(255,255,255,0.6); padding: 5px; border: 1px solid black; margin: 0 5px; transition-property: background-color; transition-duration: 2s; -webkit-transition-property: background-color; -webkit-transition-duration: 2s; }
#hud #speed { float: right; }
-#hud #current_lap_time { float: left; }
+#hud #current_lap_time, #hud #current_level, #hud #turbo_left { float: left; }
#hud #last_lap_time { float: left; display: none; }
-#hud #fast_lap_time { display: block; width: 12em; margin: 0 auto; text-align: center; transition-property: background-color; transition-duration: 2s; -webkit-transition-property: background-color; -webkit-transition-duration: 2s; }
+#hud #fast_lap_time, #hud #remaining_time { display: block; width: 12em; margin: 0 auto; text-align: center; transition-property: background-color; transition-duration: 2s; -webkit-transition-property: background-color; -webkit-transition-duration: 2s; }
#hud .value { color: black; font-weight: bold; }
#hud .fastest { background-color: rgba(255,215,0,0.5); }
+#hud .magenta { background-color: magenta; }
+
+@keyframes anim-heartbeat {
+ 0% {font-size: 175%;}
+ 25% {font-size: 150%;}
+ 50% {font-size: 125%;}
+ 100% {font-size: 100%;}
+}
+
+.warninglow {
+ color: red;
+ font-weight: bold;
+ animation-name: anim-heartbeat;
+ animation-duration: 5s;
+ animation-iteration-count: infinite;
+}
+
+/* Fullscreen mode (on double click, managed in JS */
+#canvas:fullscreen {
+ width: auto;
+ height: 100%; /* height is smaller than width, so resize by height and adapt width automatically */
+}
+#canvas:-webkit-full-screen {
+ width: auto;
+ height: 100%;
+}
+
+#canvas:-moz-full-screen {
+ width: auto;
+ height: 100%;
+}
+
+#canvas:-ms-fullscreen {
+ width: auto;
+ height: auto;
+ margin: auto;
+}
+
+/* Mobile gamepad */
+#gamepad {
+ clear: left; width: 17em; border: 1px solid black; box-shadow: 0 0 5px lime;
+ margin-left: 20em;
+ width: 640px;
+ height: 300px;
+ background: rgba(255,0,0,0.4);
+ position: relative;
+ text-align: center;
+ display: inline-block;
+}
+.gamepad-button {
+ background: grey;
+ width: 33%;
+ height: 50%;
+ display: block;
+ float: left;
+
+ /* Disable selection of text for gamepad buttons (so that user can press without selecting the text) */
+ -moz-user-select: none;
+ -khtml-user-select: none;
+ -webkit-user-select: none;
+ /*
+ Introduced in IE 10.
+ See http://ie.microsoft.com/testdrive/HTML5/msUserSelect/
+ */
+ -ms-user-select: none;
+ user-select: none;
+}
+.gamepad-button span {
+ display: inline-block;
+ vertical-align: middle;
+}
+.gamepad-button:active { background: blue; }
+#gamepad-turbo { }
+#gamepad-up { }
+#gamepad-left { clear: both; }
+#gamepad-right { float: right; }
+#gamepad-down { }
+*/
diff --git a/common.js b/common.js
index 7b2068d0..8afa9ba2 100644
--- a/common.js
+++ b/common.js
@@ -107,6 +107,8 @@ var Game = { // a modified version of the game loop from my previous boulderdas
Game.setKeyListener(options.keys);
+ Game.setDivListener(options.keys);
+
var canvas = options.canvas, // canvas render target is provided by caller
update = options.update, // method to update game logic is provided by caller
render = options.render, // method to render the game is provided by caller
@@ -157,6 +159,7 @@ var Game = { // a modified version of the game loop from my previous boulderdas
//---------------------------------------------------------------------------
setKeyListener: function(keys) {
+ // Setup listeners on keys to activate functions
var onkey = function(keyCode, mode) {
var n, k;
for(n = 0 ; n < keys.length ; n++) {
@@ -175,6 +178,30 @@ var Game = { // a modified version of the game loop from my previous boulderdas
//---------------------------------------------------------------------------
+ setDivListener: function(keys) {
+ // Setup listeners on div to activate functions (for mobile devices)
+ var n, k;
+ for(n = 0 ; n < keys.length ; n++) {
+ k = keys[n]
+ if (k.div) {
+ elt = document.getElementById(k.div);
+ if (elt) { // if the specified div element does not exist, just skip (probably the gamepad is not coded in the html)
+ if (k.mode == 'up') {
+ elt.onmouseup = k.action;
+ elt.addEventListener('mouseup', k.action);
+ elt.addEventListener('touchend', k.action);
+ } else {
+ elt.onmousedown = k.action; // fallback for old devices
+ elt.addEventListener('mousedown', k.action);
+ elt.addEventListener('touchstart', k.action);
+ }
+ }
+ }
+ }
+ },
+
+ //---------------------------------------------------------------------------
+
stats: function(parentId, id) { // construct mr.doobs FPS counter - along with friendly good/bad/ok message box
var result = new Stats();
@@ -266,7 +293,7 @@ var Render = {
//---------------------------------------------------------------------------
- background: function(ctx, background, width, height, layer, rotation, offset) {
+ background: function(ctx, background, width, height, layer, rotation, offset, alpha) {
rotation = rotation || 0;
offset = offset || 0;
@@ -284,9 +311,15 @@ var Render = {
var destW = Math.floor(width * (sourceW/imageW));
var destH = height;
+ ctx.save(); // save the current drawing parameters
+ ctx.globalAlpha = alpha; // change alpha for next drawing
+
ctx.drawImage(background, sourceX, sourceY, sourceW, sourceH, destX, destY, destW, destH);
if (sourceW < imageW)
ctx.drawImage(background, layer.x, sourceY, imageW-sourceW, sourceH, destW-1, destY, width-destW, destH);
+
+ // restore previous alpha and drawing parameters
+ ctx.restore()
},
//---------------------------------------------------------------------------
@@ -350,23 +383,28 @@ var KEY = {
A: 65,
D: 68,
S: 83,
- W: 87
+ W: 87,
+ SPACE: 32,
+ CTRL: 17,
};
var COLORS = {
- SKY: '#72D7EE',
- TREE: '#005108',
- FOG: '#005108',
- LIGHT: { road: '#6B6B6B', grass: '#10AA10', rumble: '#555555', lane: '#CCCCCC' },
- DARK: { road: '#696969', grass: '#009A00', rumble: '#BBBBBB' },
- START: { road: 'white', grass: 'white', rumble: 'white' },
- FINISH: { road: 'black', grass: 'black', rumble: 'black' }
+ SKY: 'black',
+ TREE: 'cyan',
+ FOG: 'magenta',
+ LIGHT: { road: 'black', grass: 'purple', rumble: 'cyan', lane: 'cyan' },
+ DARK: { road: 'black', grass: 'black', rumble: 'cyan' },
+ START: { road: 'magenta', grass: 'magenta', rumble: 'magenta' },
+ FINISH: { road: 'cyan', grass: 'cyan', rumble: 'cyan' }
};
var BACKGROUND = {
HILLS: { x: 5, y: 5, w: 1280, h: 480 },
SKY: { x: 5, y: 495, w: 1280, h: 480 },
- TREES: { x: 5, y: 985, w: 1280, h: 480 }
+ TREES: { x: 5, y: 985, w: 1280, h: 480 },
+ HILLS2: { x: 5, y: 1475, w: 1280, h: 480 },
+ SKY2: { x: 5, y: 1965, w: 1280, h: 480 },
+ TREES2: { x: 5, y: 2445, w: 1280, h: 480 },
};
var SPRITES = {
diff --git a/images/background.png b/images/background.png
index 6590b026..ce592811 100644
Binary files a/images/background.png and b/images/background.png differ
diff --git a/images/background.xcf b/images/background.xcf
new file mode 100644
index 00000000..1dd2d1c5
Binary files /dev/null and b/images/background.xcf differ
diff --git a/images/background/background.svg b/images/background/background.svg
index 0bbcd8d6..1d7a33a9 100644
--- a/images/background/background.svg
+++ b/images/background/background.svg
@@ -2,6 +2,7 @@
diff --git a/images/background/background2.svg b/images/background/background2.svg
new file mode 100644
index 00000000..1c56712e
--- /dev/null
+++ b/images/background/background2.svg
@@ -0,0 +1,2856 @@
+
+
+
+
diff --git a/images/background/grid.png b/images/background/grid.png
new file mode 100644
index 00000000..a19ca45a
Binary files /dev/null and b/images/background/grid.png differ
diff --git a/images/background/hills.png b/images/background/hills.png
deleted file mode 100755
index f1d2be53..00000000
Binary files a/images/background/hills.png and /dev/null differ
diff --git a/images/background/mountains.png b/images/background/mountains.png
new file mode 100644
index 00000000..6ca7473c
Binary files /dev/null and b/images/background/mountains.png differ
diff --git a/images/background/mountains2.png b/images/background/mountains2.png
new file mode 100644
index 00000000..91b3c0b8
Binary files /dev/null and b/images/background/mountains2.png differ
diff --git a/images/background/sky.png b/images/background/sky.png
index 26c332df..45b26748 100755
Binary files a/images/background/sky.png and b/images/background/sky.png differ
diff --git a/images/background/sky2.png b/images/background/sky2.png
new file mode 100644
index 00000000..14d36a18
Binary files /dev/null and b/images/background/sky2.png differ
diff --git a/images/background/trees.png b/images/background/trees.png
deleted file mode 100755
index 57fb422e..00000000
Binary files a/images/background/trees.png and /dev/null differ
diff --git a/images/background/trees2.png b/images/background/trees2.png
new file mode 100644
index 00000000..592e655d
Binary files /dev/null and b/images/background/trees2.png differ
diff --git a/images/sprites.png b/images/sprites.png
index 0b302ae6..88233dd5 100644
Binary files a/images/sprites.png and b/images/sprites.png differ
diff --git a/images/sprites.svg b/images/sprites.svg
new file mode 100644
index 00000000..7f970d01
--- /dev/null
+++ b/images/sprites.svg
@@ -0,0 +1,29302 @@
+
+
+
+
diff --git a/images/sprites/billboard01.png b/images/sprites/billboard01.png
deleted file mode 100755
index 9c100cd4..00000000
Binary files a/images/sprites/billboard01.png and /dev/null differ
diff --git a/images/sprites/billboard02.png b/images/sprites/billboard02.png
deleted file mode 100755
index 1db594e2..00000000
Binary files a/images/sprites/billboard02.png and /dev/null differ
diff --git a/images/sprites/billboard03.png b/images/sprites/billboard03.png
deleted file mode 100755
index 9a43480a..00000000
Binary files a/images/sprites/billboard03.png and /dev/null differ
diff --git a/images/sprites/billboard04.png b/images/sprites/billboard04.png
deleted file mode 100755
index abfd4b14..00000000
Binary files a/images/sprites/billboard04.png and /dev/null differ
diff --git a/images/sprites/billboard05.png b/images/sprites/billboard05.png
deleted file mode 100755
index c470e0d0..00000000
Binary files a/images/sprites/billboard05.png and /dev/null differ
diff --git a/images/sprites/billboard06.png b/images/sprites/billboard06.png
deleted file mode 100755
index 0ba9efb4..00000000
Binary files a/images/sprites/billboard06.png and /dev/null differ
diff --git a/images/sprites/billboard07.png b/images/sprites/billboard07.png
deleted file mode 100755
index 646c95b0..00000000
Binary files a/images/sprites/billboard07.png and /dev/null differ
diff --git a/images/sprites/billboard08.png b/images/sprites/billboard08.png
deleted file mode 100755
index 50f59eb0..00000000
Binary files a/images/sprites/billboard08.png and /dev/null differ
diff --git a/images/sprites/billboard09.png b/images/sprites/billboard09.png
deleted file mode 100755
index 976f6e5c..00000000
Binary files a/images/sprites/billboard09.png and /dev/null differ
diff --git a/images/sprites/boulder1.png b/images/sprites/boulder1.png
deleted file mode 100755
index fe98901c..00000000
Binary files a/images/sprites/boulder1.png and /dev/null differ
diff --git a/images/sprites/boulder2.png b/images/sprites/boulder2.png
deleted file mode 100755
index 67b62f6b..00000000
Binary files a/images/sprites/boulder2.png and /dev/null differ
diff --git a/images/sprites/boulder3.png b/images/sprites/boulder3.png
deleted file mode 100755
index 637d69a6..00000000
Binary files a/images/sprites/boulder3.png and /dev/null differ
diff --git a/images/sprites/bush1.png b/images/sprites/bush1.png
deleted file mode 100755
index b22d74da..00000000
Binary files a/images/sprites/bush1.png and /dev/null differ
diff --git a/images/sprites/bush2.png b/images/sprites/bush2.png
deleted file mode 100755
index 61d4b3c1..00000000
Binary files a/images/sprites/bush2.png and /dev/null differ
diff --git a/images/sprites/cactus.png b/images/sprites/cactus.png
deleted file mode 100755
index a52df7e9..00000000
Binary files a/images/sprites/cactus.png and /dev/null differ
diff --git a/images/sprites/car01.png b/images/sprites/car01.png
deleted file mode 100755
index 8efdb849..00000000
Binary files a/images/sprites/car01.png and /dev/null differ
diff --git a/images/sprites/car02.png b/images/sprites/car02.png
deleted file mode 100755
index 54ab5a16..00000000
Binary files a/images/sprites/car02.png and /dev/null differ
diff --git a/images/sprites/car03.png b/images/sprites/car03.png
deleted file mode 100755
index dbc629ed..00000000
Binary files a/images/sprites/car03.png and /dev/null differ
diff --git a/images/sprites/car04.png b/images/sprites/car04.png
deleted file mode 100755
index 611b7ab3..00000000
Binary files a/images/sprites/car04.png and /dev/null differ
diff --git a/images/sprites/column.png b/images/sprites/column.png
deleted file mode 100755
index 6b1e0586..00000000
Binary files a/images/sprites/column.png and /dev/null differ
diff --git a/images/sprites/dead_tree1.png b/images/sprites/dead_tree1.png
deleted file mode 100755
index 399c9f6a..00000000
Binary files a/images/sprites/dead_tree1.png and /dev/null differ
diff --git a/images/sprites/dead_tree2.png b/images/sprites/dead_tree2.png
deleted file mode 100755
index ad7e52e8..00000000
Binary files a/images/sprites/dead_tree2.png and /dev/null differ
diff --git a/images/sprites/palm_tree.png b/images/sprites/palm_tree.png
deleted file mode 100755
index 8044b5ce..00000000
Binary files a/images/sprites/palm_tree.png and /dev/null differ
diff --git a/images/sprites/player_left.png b/images/sprites/player_left.png
deleted file mode 100755
index 457d943a..00000000
Binary files a/images/sprites/player_left.png and /dev/null differ
diff --git a/images/sprites/player_right.png b/images/sprites/player_right.png
deleted file mode 100755
index ca26b0aa..00000000
Binary files a/images/sprites/player_right.png and /dev/null differ
diff --git a/images/sprites/player_straight.png b/images/sprites/player_straight.png
deleted file mode 100755
index ef1c8cd2..00000000
Binary files a/images/sprites/player_straight.png and /dev/null differ
diff --git a/images/sprites/player_uphill_left.png b/images/sprites/player_uphill_left.png
deleted file mode 100755
index 8aaa6cf7..00000000
Binary files a/images/sprites/player_uphill_left.png and /dev/null differ
diff --git a/images/sprites/player_uphill_right.png b/images/sprites/player_uphill_right.png
deleted file mode 100755
index 3e3dfdb9..00000000
Binary files a/images/sprites/player_uphill_right.png and /dev/null differ
diff --git a/images/sprites/player_uphill_straight.png b/images/sprites/player_uphill_straight.png
deleted file mode 100755
index 634d7a07..00000000
Binary files a/images/sprites/player_uphill_straight.png and /dev/null differ
diff --git a/images/sprites/semi.png b/images/sprites/semi.png
deleted file mode 100755
index f679c1bb..00000000
Binary files a/images/sprites/semi.png and /dev/null differ
diff --git a/images/sprites/stump.png b/images/sprites/stump.png
deleted file mode 100755
index 6319df26..00000000
Binary files a/images/sprites/stump.png and /dev/null differ
diff --git a/images/sprites/tree1.png b/images/sprites/tree1.png
deleted file mode 100755
index 95440940..00000000
Binary files a/images/sprites/tree1.png and /dev/null differ
diff --git a/images/sprites/tree2.png b/images/sprites/tree2.png
deleted file mode 100755
index d3c8a120..00000000
Binary files a/images/sprites/tree2.png and /dev/null differ
diff --git a/images/sprites/truck.png b/images/sprites/truck.png
deleted file mode 100755
index d72b5203..00000000
Binary files a/images/sprites/truck.png and /dev/null differ
diff --git a/index.html b/index.html
index e61ae58f..47e21a0f 100644
--- a/index.html
+++ b/index.html
@@ -2,6 +2,7 @@
Javascript Racer
+
@@ -11,7 +12,8 @@
Version 1 - Straight
Version 2 - Curves
Version 3 - Hills
- Version 4 - Final
+ Version 4 - Final (fastest lap gamemode)
+ Version 5 - Game (out of time gamemode)
diff --git a/music/jsracer_soundtrack.mp3 b/music/jsracer_soundtrack.mp3
new file mode 100644
index 00000000..705ce9c4
Binary files /dev/null and b/music/jsracer_soundtrack.mp3 differ
diff --git a/music/jsracer_soundtrack.ogg b/music/jsracer_soundtrack.ogg
new file mode 100644
index 00000000..94a156c6
Binary files /dev/null and b/music/jsracer_soundtrack.ogg differ
diff --git a/music/racer.mp3 b/music/racer.mp3
deleted file mode 100755
index 15a5c83e..00000000
Binary files a/music/racer.mp3 and /dev/null differ
diff --git a/music/racer.ogg b/music/racer.ogg
deleted file mode 100755
index d85ddfeb..00000000
Binary files a/music/racer.ogg and /dev/null differ
diff --git a/racer.js b/racer.js
new file mode 100644
index 00000000..0d713282
--- /dev/null
+++ b/racer.js
@@ -0,0 +1,925 @@
+function racer(gamemode) {
+ var fps = 60; // how many 'update' frames per second
+ var step = 1/fps; // how long is each frame (in seconds)
+ var width = 1024; // logical canvas width
+ var height = 768; // logical canvas height
+ var centrifugal = 0.3; // centrifugal force multiplier when going around curves
+ var offRoadDecel = 0.99; // speed multiplier when off road (e.g. you lose 2% speed each update frame)
+ var skySpeed = 0.001; // background sky layer scroll speed when going around curve (or up hill)
+ var hillSpeed = 0.002; // background hill layer scroll speed when going around curve (or up hill)
+ var treeSpeed = 0.003; // background tree layer scroll speed when going around curve (or up hill)
+ var skyOffset = 0; // current sky scroll offset
+ var hillOffset = 0; // current hill scroll offset
+ var treeOffset = 0; // current tree scroll offset
+ var segments = []; // array of road segments
+ var cars = []; // array of cars on the road
+ var stats = Game.stats('fps'); // mr.doobs FPS counter
+ var canvas = Dom.get('canvas'); // our canvas...
+ var ctx = canvas.getContext('2d'); // ...and its drawing context
+ var background = null; // our background image (loaded below)
+ var sprites = null; // our spritesheet (loaded below)
+ var resolution = null; // scaling factor to provide resolution independence (computed)
+ var roadWidth = 2000; // actually half the roads width, easier math if the road spans from -roadWidth to +roadWidth
+ var segmentLength = 200; // length of a single segment
+ var rumbleLength = 3; // number of segments per red/white rumble strip
+ var trackLength = null; // z length of entire track (computed)
+ var lanes = 3; // number of lanes
+ var fieldOfView = 100; // angle (degrees) for field of view
+ var cameraHeight = 1000; // z height of camera
+ var cameraDepth = null; // z distance camera is from screen (computed)
+ var drawDistance = 300; // number of segments to draw
+ var playerX = 0; // player x offset from center of road (-1 to 1 to stay independent of roadWidth)
+ var playerZ = null; // player relative z distance from camera (computed)
+ var fogDensity = 5; // exponential fog density
+ var position = 0; // current camera Z position (add playerZ to get player's absolute Z position)
+ var speed = 0; // current speed
+ var maxSpeed = segmentLength/step; // top speed (ensure we can't move more than 1 segment in a single frame to make collision detection easier)
+ var accel = maxSpeed/5; // acceleration rate - tuned until it 'felt' right
+ var breaking = -maxSpeed; // deceleration rate when braking
+ var decel = -maxSpeed/5; // 'natural' deceleration rate when neither accelerating, nor braking
+ var offRoadDecel = -maxSpeed/2; // off road deceleration is somewhere in between
+ var offRoadLimit = maxSpeed/4; // limit when off road deceleration no longer applies (e.g. you can always go at least this speed even when off road)
+ var totalCars = 20; // total number of cars on the road
+ var currentLapTime = 0; // current lap time
+ var lastLapTime = null; // last lap time
+ var enableTilt = false; // enable horizon tilt
+ var currentRotation = 0; // horizon tilt initialization
+ var randomTrack = true; // enable random procedural generation of the track
+ var randomTrackLength = 5; // if random track is enable, how many track segments/constructs to build?
+ //var gamemode = 1; // Gamemode: 0: fastest lap, 1: out of time DEPRECATED: defined as argument now
+
+ // Gamemode 1: out of time
+ var remainingTime = 0; // internal variable - remaining time left to pass the next finish line or it's game over, will be calculated automatically
+ var difficultyStart = 4; // Starting difficulty (track length)
+ var difficultyIncrement = 0.5; // How much to increment the difficulty (and track length) each time player finish a track?
+ var difficultyGap = 2.0; // After how many track finishes do we start to increase the difficulty in terms of number of cars on road, number of turns, etc
+ var difficultyMax = 14; // Maximum difficulty, after this there will be no increase in difficulty
+ var difficultyCurrent = difficultyStart; // Current difficulty (will be modified ingame)
+ var remainingTimeIncrease = 0.00009; // Multiplier of the trackLength to get seconds that will be added to the remainingTime, in other words this defines the time left to the player to finish the track proportionally to the track length (a higher value makes the game easier)
+ var remainingTimeStartBonus = 2.0; // Multiplier of the remaining time given for the first level (to make game easier for newscomers), also because the player has no momentum at the beginning
+ var remainingTimeThreshold = 20; // When only this amount of time is left, the remaining time HUD will be highlighted (set to 0 to disable)
+ var currentLevel = 0; // Internal variable, just a value to display the current level
+ var gameOverFlag = false; // this flag will be set if game over was triggered
+ var changeBackgroundEvery = 3; // change the background image every few levels, set to 0 to disable
+ var changeBackgroundCurrentAlpha = 0.0; // internal variable from 0.0 to 1.0 to specify the current state of background switching (via progressing blending animation)
+ var currentBackground = 0; // internal variable to track which background we currently draw
+ var changeBackgroundFlag = false; // internal variable to start the background change
+ var turboLeft = 3; // number of turbos left
+ var turboDuration = 10.0; // duration of turbo in seconds
+ var turboAnimation = 2.0; // duration of animation to do progressive increase/decrease of fov
+ var turboFovIncrement = 1.4; // multiplier of fov during turbo
+ var turboMaxSpeed = maxSpeed * 1.5; // maximum speed under turbo
+ var turboGiveEvery = changeBackgroundEvery; // give a new turbo every x levels (set to 0 to disable)
+ var turboCentrifugal = centrifugal/2; // torque when under turbo (else the player cannot turn in curves)
+ var turboTriggered = false; // internal variable - turbo triggered by player?
+ var turboTimeDone = 0.0; // internal variable - turbo being consumed, since how much time (allow to do animation and such)
+ var turboCurrentFov = fieldOfView; // internal variable - current fov while doing turbo
+
+ var keyLeft = false;
+ var keyRight = false;
+ var keyFaster = false;
+ var keySlower = false;
+
+ // Add variables to update HUD
+ var hud = {
+ speed: { value: null, dom: Dom.get('speed_value') },
+ current_lap_time: { value: null, dom: Dom.get('current_lap_time_value') },
+ current_level: { value: null, dom: Dom.get('current_level_value') },
+ remaining_time: { value: null, dom: Dom.get('remaining_time_value') },
+ last_lap_time: { value: null, dom: Dom.get('last_lap_time_value') },
+ fast_lap_time: { value: null, dom: Dom.get('fast_lap_time_value') },
+ }
+
+ if (gamemode == 1) {
+ // Out of time gamemode-only HUD elements
+ hud["turbo_left"] = { value: null, dom: Dom.get('turbo_left_value') }
+ }
+
+ // Hide either the current lap time or the remaining time HUD meter according to the selected gamemode
+ if (gamemode == 1) {
+ document.getElementById('current_lap_time').style.display = 'none';
+ document.getElementById('fast_lap_time').style.display = 'none';
+ document.getElementById('last_lap_time').style.display = 'none';
+ } else {
+ try {
+ document.getElementById('remaining_time').style.display = 'none';
+ document.getElementById('current_level').style.display = 'none';
+ document.getElementById('turbo_left').style.display = 'none';
+ } catch(exc) {};
+ }
+
+ //=========================================================================
+ // UPDATE THE GAME WORLD
+ //=========================================================================
+
+ function update(dt) {
+
+ var n, car, carW, sprite, spriteW;
+ var playerSegment = findSegment(position+playerZ);
+ var playerW = SPRITES.PLAYER_STRAIGHT.w * SPRITES.SCALE;
+ var speedPercent = speed/maxSpeed;
+ var dx = dt * 2 * speedPercent; // at top speed, should be able to cross from left to right (-1 to 1) in 1 second
+ var startPosition = position;
+
+ updateCars(dt, playerSegment, playerW);
+
+ position = Util.increase(position, dt * speed, trackLength);
+
+ if (!gameOverFlag) {
+ if (keyLeft)
+ playerX = playerX - dx;
+ else if (keyRight)
+ playerX = playerX + dx;
+ }
+
+ if (turboTriggered && gamemode == 1) {
+ // give more torque under turbo
+ playerX = playerX - (dx * speedPercent * playerSegment.curve * turboCentrifugal);
+ } else {
+ // else manage the torque as usual
+ playerX = playerX - (dx * speedPercent * playerSegment.curve * centrifugal);
+ }
+
+ if (!gameOverFlag) {
+ if (keyFaster)
+ speed = Util.accelerate(speed, accel, dt);
+ else if (keySlower)
+ speed = Util.accelerate(speed, breaking, dt);
+ else
+ speed = Util.accelerate(speed, decel, dt);
+ } else { // game over flag, just decelerate the car until full stop
+ speed = Util.accelerate(speed, decel, dt);
+ }
+
+
+ if ((playerX < -1) || (playerX > 1)) {
+
+ if (speed > offRoadLimit)
+ speed = Util.accelerate(speed, offRoadDecel, dt);
+
+ for(n = 0 ; n < playerSegment.sprites.length ; n++) {
+ sprite = playerSegment.sprites[n];
+ spriteW = sprite.source.w * SPRITES.SCALE;
+ if (Util.overlap(playerX, playerW, sprite.offset + spriteW/2 * (sprite.offset > 0 ? 1 : -1), spriteW)) {
+ speed = maxSpeed/5;
+ position = Util.increase(playerSegment.p1.world.z, -playerZ, trackLength); // stop in front of sprite (at front of segment)
+ break;
+ }
+ }
+ }
+
+ for(n = 0 ; n < playerSegment.cars.length ; n++) {
+ car = playerSegment.cars[n];
+ carW = car.sprite.w * SPRITES.SCALE;
+ if (speed > car.speed) {
+ if (Util.overlap(playerX, playerW, car.offset, carW, 0.8)) {
+ speed = car.speed * (car.speed/speed);
+ position = Util.increase(car.z, -playerZ, trackLength);
+ break;
+ }
+ }
+ }
+
+ playerX = Util.limit(playerX, -3, 3); // dont ever let it go too far out of bounds
+ if (!turboTriggered) {
+ // Normal speed limit, no turbo
+ speed = Util.limit(speed, 0, maxSpeed); // or exceed maxSpeed
+ } else if (gamemode == 1) {
+ // Turbo management
+ speed = Util.limit(speed, 0, turboMaxSpeed); // do not exceed turbo max speed
+ accel = turboMaxSpeed / 3; // increase acceleration
+ turboTimeDone += dt; // increase the current consumed time of turbo
+ if (turboTimeDone < turboDuration) {
+ // if turbo time is left, we can continue
+ if (turboTimeDone < turboAnimation) {
+ // turbo initialization animation, increase fov
+ turboFov = fieldOfView * turboFovIncrement;
+ if (turboCurrentFov < turboFov) {
+ turboCurrentFov += (turboFov - fieldOfView) * (dt/turboAnimation);
+ updateFOV(turboCurrentFov);
+ }
+ } else if (turboDuration <= (turboTimeDone + turboAnimation)) {
+ // turbo end animation, decrease fov
+ if (turboCurrentFov > fieldOfView) {
+ turboCurrentFov -= (turboFov - fieldOfView) * (dt/turboAnimation);
+ updateFOV(turboCurrentFov);
+ }
+ if (speed > maxSpeed) {
+ // also decrease speed gradually
+ speed -= (turboMaxSpeed - maxSpeed) * (dt/turboAnimation)*3; // *3 is magic value to overcome the fact that the car will still get acceleration next frame and is still capped at turboMaxSpeed (see Util.limit above). By multiplying by 2 we cancel the next increase.
+ }
+ }
+ } else {
+ // no turbo time left, disable the turbo mode
+ turboTriggered = false;
+ updateFOV(fieldOfView); // reinit fieldOfView
+ }
+ }
+
+ skyOffset = Util.increase(skyOffset, skySpeed * playerSegment.curve * (position-startPosition)/segmentLength, 1);
+ hillOffset = Util.increase(hillOffset, hillSpeed * playerSegment.curve * (position-startPosition)/segmentLength, 1);
+ treeOffset = Util.increase(treeOffset, treeSpeed * playerSegment.curve * (position-startPosition)/segmentLength, 1);
+
+ if (position > playerZ) {
+ if (currentLapTime && (startPosition < playerZ)) { // arrived at finish line, update last lap time + generate new track if enabled
+ if (gamemode == 1) { // Out of time gamemode
+ // Increase level (only useful for display, internally we use difficultyCurrent)
+ currentLevel += 1;
+ // Give the player some more time
+ var remainingTimePast = remainingTime;
+ remainingTime += trackLength * remainingTimeIncrease;
+ if ((remainingTimePast < remainingTimeThreshold) & (remainingTime > remainingTimeThreshold)) {
+ Dom.removeClassName('remaining_time_value', 'warninglow'); // remove any warning if there was one
+ Dom.addClassName('remaining_time_value', 'value');
+ }
+ // Increase current difficulty unless we are already at the max
+ if (difficultyCurrent < difficultyMax) {
+ difficultyCurrent += difficultyIncrement;
+ }
+ if (randomTrack) { // generate procedurally a new track when arriving at the finish line according to difficulty
+ // Generate a new track length according to difficulty
+ randomTrackLength = Math.floor(difficultyCurrent);
+ // Regenerate the new track
+ resetRoad(randomTrack, randomTrackLength);
+ // If we crossed the difficulty gap (ie, every few levels), then we increase the number of cars
+ if (((difficultyCurrent % difficultyGap) == 0) & (difficultyCurrent < difficultyMax)) {
+ // Double the number of cars (keep in mind the track extended and we kept the same number of cars, so it's not too much to double)
+ totalCars += Math.floor(totalCars);
+ // And we redraw all cars TODO: make it look better (cars on screen at finish line will disappear)
+ resetCars();
+ }
+ }
+ // Change background if enabled and we have passed enough levels
+ if ((changeBackgroundEvery > 0) & (currentLevel % changeBackgroundEvery == 0)) {
+ changeBackgroundFlag = true;
+ }
+ // Add a turbo if passed enough levels
+ if ((turboGiveEvery > 0) & (currentLevel % turboGiveEvery == 0)) {
+ turboLeft += 1;
+ }
+ } else { // fastest lap time gamemode
+ lastLapTime = currentLapTime;
+ currentLapTime = 0;
+ if ((lastLapTime <= Util.toFloat(Dom.storage.fast_lap_time)) | (Util.toFloat(Dom.storage.fast_lap_time) == 0)) {
+ Dom.storage.fast_lap_time = lastLapTime;
+ updateHud('fast_lap_time', formatTime(lastLapTime));
+ Dom.addClassName('fast_lap_time', 'fastest');
+ Dom.addClassName('last_lap_time', 'fastest');
+ } else {
+ Dom.removeClassName('fast_lap_time', 'fastest');
+ Dom.removeClassName('last_lap_time', 'fastest');
+ }
+ updateHud('last_lap_time', formatTime(lastLapTime));
+ Dom.show('last_lap_time');
+ }
+ } else {
+ // Else we are not yet at the finish line, we increase the time/decrease remaining time
+ currentLapTime += dt;
+ if (remainingTime > 0) {
+ remainingTime -= dt;
+ } else {
+ remainingTime = 0;
+ if (currentLevel == 0) { // first level, we give some time to the player
+ remainingTime += trackLength * remainingTimeIncrease * remainingTimeStartBonus;
+ }
+ }
+
+ // Highlight remaining time if quite low
+ if ((gamemode == 1) & (remainingTime < remainingTimeThreshold)) {
+ Dom.removeClassName('remaining_time_value', 'value');
+ Dom.addClassName('remaining_time_value', 'warninglow');
+ }
+
+ // Highlight turbo when in use
+ if (gamemode == 1) {
+ if (turboTriggered) {
+ Dom.addClassName('turbo_left', 'magenta');
+ } else {
+ Dom.removeClassName('turbo_left', 'magenta');
+ }
+ }
+
+ // Call game over if conditions are met
+ if ((gamemode == 1) & (remainingTime <= 0)) { // gamemode out of time and no remaining time left
+ gameOverFlag = true;
+ }
+ }
+ }
+
+ // Update HUD
+ updateHud('speed', 5 * Math.round(speed/500));
+ if (gamemode == 1) {
+ updateHud('remaining_time', formatTime(remainingTime));
+ updateHud('current_level', currentLevel);
+ updateHud('turbo_left', turboLeft);
+ } else {
+ updateHud('current_lap_time', formatTime(currentLapTime));
+ }
+ }
+
+ //-------------------------------------------------------------------------
+
+ function updateCars(dt, playerSegment, playerW) {
+ var n, car, oldSegment, newSegment;
+ for(n = 0 ; n < cars.length ; n++) {
+ car = cars[n];
+ oldSegment = findSegment(car.z);
+ car.offset = car.offset + updateCarOffset(car, oldSegment, playerSegment, playerW);
+ car.z = Util.increase(car.z, dt * car.speed, trackLength);
+ car.percent = Util.percentRemaining(car.z, segmentLength); // useful for interpolation during rendering phase
+ newSegment = findSegment(car.z);
+ if (oldSegment != newSegment) {
+ index = oldSegment.cars.indexOf(car);
+ oldSegment.cars.splice(index, 1);
+ newSegment.cars.push(car);
+ }
+ }
+ }
+
+ function updateCarOffset(car, carSegment, playerSegment, playerW) {
+
+ var i, j, dir, segment, otherCar, otherCarW, lookahead = 20, carW = car.sprite.w * SPRITES.SCALE;
+
+ // optimization, dont bother steering around other cars when 'out of sight' of the player
+ if ((carSegment.index - playerSegment.index) > drawDistance)
+ return 0;
+
+ for(i = 1 ; i < lookahead ; i++) {
+ segment = segments[(carSegment.index+i)%segments.length];
+
+ if ((segment === playerSegment) && (car.speed > speed) && (Util.overlap(playerX, playerW, car.offset, carW, 1.2))) {
+ if (playerX > 0.5)
+ dir = -1;
+ else if (playerX < -0.5)
+ dir = 1;
+ else
+ dir = (car.offset > playerX) ? 1 : -1;
+ return dir * 1/i * (car.speed-speed)/maxSpeed; // the closer the cars (smaller i) and the greated the speed ratio, the larger the offset
+ }
+
+ for(j = 0 ; j < segment.cars.length ; j++) {
+ otherCar = segment.cars[j];
+ otherCarW = otherCar.sprite.w * SPRITES.SCALE;
+ if ((car.speed > otherCar.speed) && Util.overlap(car.offset, carW, otherCar.offset, otherCarW, 1.2)) {
+ if (otherCar.offset > 0.5)
+ dir = -1;
+ else if (otherCar.offset < -0.5)
+ dir = 1;
+ else
+ dir = (car.offset > otherCar.offset) ? 1 : -1;
+ return dir * 1/i * (car.speed-otherCar.speed)/maxSpeed;
+ }
+ }
+ }
+
+ // if no cars ahead, but I have somehow ended up off road, then steer back on
+ if (car.offset < -0.9)
+ return 0.1;
+ else if (car.offset > 0.9)
+ return -0.1;
+ else
+ return 0;
+ }
+
+ //-------------------------------------------------------------------------
+
+ function updateHud(key, value) { // accessing DOM can be slow, so only do it if value has changed
+ if (hud[key].value !== value) {
+ hud[key].value = value;
+ Dom.set(hud[key].dom, value);
+ }
+ }
+
+ function formatTime(dt) {
+ var minutes = Math.floor(dt/60);
+ var seconds = Math.floor(dt - (minutes * 60));
+ var tenths = Math.floor(10 * (dt - Math.floor(dt)));
+ if (minutes > 0)
+ return minutes + "." + (seconds < 10 ? "0" : "") + seconds + "." + tenths;
+ else
+ return seconds + "." + tenths;
+ }
+
+ //=========================================================================
+ // RENDER THE GAME WORLD
+ //=========================================================================
+
+ function render() {
+
+ var baseSegment = findSegment(position);
+ var basePercent = Util.percentRemaining(position, segmentLength);
+ var playerSegment = findSegment(position+playerZ);
+ var playerPercent = Util.percentRemaining(position+playerZ, segmentLength);
+ var playerY = Util.interpolate(playerSegment.p1.world.y, playerSegment.p2.world.y, playerPercent);
+ var maxy = height;
+
+ var x = 0;
+ var dx = - (baseSegment.curve * basePercent);
+
+ // Clear the canvas
+ ctx.clearRect(0, 0, width, height);
+
+ // Order the background layers
+ if (currentBackground == 0) {
+ // Build the list of positions in the image to extract the appropriate background
+ // Depending on the current background, load as current the night or day version
+ background_pos_cur = [BACKGROUND.SKY, BACKGROUND.HILLS, BACKGROUND.TREES];
+ background_pos_next = [BACKGROUND.SKY2, BACKGROUND.HILLS2, BACKGROUND.TREES2];
+ } else {
+ background_pos_cur = [BACKGROUND.SKY2, BACKGROUND.HILLS2, BACKGROUND.TREES2];
+ background_pos_next = [BACKGROUND.SKY, BACKGROUND.HILLS, BACKGROUND.TREES];
+ }
+ // Draw the background layers
+ if (!changeBackgroundFlag) {
+ // No switching, we draw one set of backgrounds
+ Render.background(ctx, background, width, height, background_pos_cur[0], skyOffset, resolution * skySpeed * playerY, 1.0);
+ Render.background(ctx, background, width, height, background_pos_cur[1], hillOffset, resolution * hillSpeed * playerY, 1.0);
+ Render.background(ctx, background, width, height, background_pos_cur[2], treeOffset, resolution * treeSpeed * playerY, 1.0);
+ } else {
+ // else we are in the process of switching, do a progressive blending
+ // continue the blending
+ changeBackgroundCurrentAlpha += 0.01; // increase the alpha for one, and decrease for the next background set
+ Render.background(ctx, background, width, height, background_pos_cur[0], skyOffset, resolution * skySpeed * playerY, 1.0-changeBackgroundCurrentAlpha);
+ Render.background(ctx, background, width, height, background_pos_cur[1], hillOffset, resolution * hillSpeed * playerY, 1.0-changeBackgroundCurrentAlpha);
+ Render.background(ctx, background, width, height, background_pos_cur[2], treeOffset, resolution * treeSpeed * playerY, 1.0-changeBackgroundCurrentAlpha);
+ Render.background(ctx, background, width, height, background_pos_next[0], skyOffset, resolution * skySpeed * playerY, changeBackgroundCurrentAlpha);
+ Render.background(ctx, background, width, height, background_pos_next[1], hillOffset, resolution * hillSpeed * playerY, changeBackgroundCurrentAlpha);
+ Render.background(ctx, background, width, height, background_pos_next[2], treeOffset, resolution * treeSpeed * playerY, changeBackgroundCurrentAlpha);
+ if (changeBackgroundCurrentAlpha >= 1.0) {
+ // blending is done, disable the flags and reinit all related vars
+ // Note: it is important to still do the drawing (and not put it in an if statement) because else the last drawing won't be done, there will be no background for a split-second and this will produce a flickering effect
+ currentBackground = (currentBackground + 1) % 2
+ changeBackgroundCurrentAlpha = 0.0;
+ changeBackgroundFlag = false;
+ }
+ }
+
+ var n, i, segment, car, sprite, spriteScale, spriteX, spriteY;
+
+ for(n = 0 ; n < drawDistance ; n++) {
+
+ segment = segments[(baseSegment.index + n) % segments.length];
+ segment.looped = segment.index < baseSegment.index;
+ segment.fog = Util.exponentialFog(n/drawDistance, fogDensity);
+ segment.clip = maxy;
+
+ Util.project(segment.p1, (playerX * roadWidth) - x, playerY + cameraHeight, position - (segment.looped ? trackLength : 0), cameraDepth, width, height, roadWidth);
+ Util.project(segment.p2, (playerX * roadWidth) - x - dx, playerY + cameraHeight, position - (segment.looped ? trackLength : 0), cameraDepth, width, height, roadWidth);
+
+ x = x + dx;
+ dx = dx + segment.curve;
+
+ if ((segment.p1.camera.z <= cameraDepth) || // behind us
+ (segment.p2.screen.y >= segment.p1.screen.y) || // back face cull
+ (segment.p2.screen.y >= maxy)) // clip by (already rendered) hill
+ continue;
+
+ Render.segment(ctx, width, lanes,
+ segment.p1.screen.x,
+ segment.p1.screen.y,
+ segment.p1.screen.w,
+ segment.p2.screen.x,
+ segment.p2.screen.y,
+ segment.p2.screen.w,
+ segment.fog,
+ segment.color);
+
+ maxy = segment.p1.screen.y;
+ }
+
+ for(n = (drawDistance-1) ; n > 0 ; n--) {
+ segment = segments[(baseSegment.index + n) % segments.length];
+
+ for(i = 0 ; i < segment.cars.length ; i++) {
+ car = segment.cars[i];
+ sprite = car.sprite;
+ spriteScale = Util.interpolate(segment.p1.screen.scale, segment.p2.screen.scale, car.percent);
+ spriteX = Util.interpolate(segment.p1.screen.x, segment.p2.screen.x, car.percent) + (spriteScale * car.offset * roadWidth * width/2);
+ spriteY = Util.interpolate(segment.p1.screen.y, segment.p2.screen.y, car.percent);
+ Render.sprite(ctx, width, height, resolution, roadWidth, sprites, car.sprite, spriteScale, spriteX, spriteY, -0.5, -1, segment.clip);
+ }
+
+ for(i = 0 ; i < segment.sprites.length ; i++) {
+ sprite = segment.sprites[i];
+ spriteScale = segment.p1.screen.scale;
+ spriteX = segment.p1.screen.x + (spriteScale * sprite.offset * roadWidth * width/2);
+ spriteY = segment.p1.screen.y;
+ Render.sprite(ctx, width, height, resolution, roadWidth, sprites, sprite.source, spriteScale, spriteX, spriteY, (sprite.offset < 0 ? -1 : 0), -1, segment.clip);
+ }
+
+ if (segment == playerSegment) {
+ Render.player(ctx, width, height, resolution, roadWidth, sprites, speed/maxSpeed,
+ cameraDepth/playerZ,
+ width/2,
+ (height/2) - (cameraDepth/playerZ * Util.interpolate(playerSegment.p1.camera.y, playerSegment.p2.camera.y, playerPercent) * height/2),
+ speed * (keyLeft ? -1 : keyRight ? 1 : 0),
+ playerSegment.p2.world.y - playerSegment.p1.world.y);
+ }
+ }
+
+ // start horizon tilt
+ if (enableTilt) {
+ rotation=0;
+ if (baseSegment.curve==0) {
+ rotation=-currentRotation;
+ currentRotation=0;
+ } else {
+ newrot = Math.round(baseSegment.curve*speed/maxSpeed*100)/100;
+ rotation=newrot - currentRotation ;
+ currentRotation = newrot ;
+ }
+ if (rotation!=0) {
+ //ctx.save(); // doesn't help with moire problem
+ ctx.translate(canvas.width/2,canvas.height/2);
+ ctx.rotate(-rotation*(Math.PI/90));
+ ctx.translate(-canvas.width/2,-canvas.height/2);
+ //ctx.restore();
+ }
+ }
+
+ // Draw "Game Over" screen
+ if (gameOverFlag) {
+ ctx.font = "3em Arial";
+ ctx.fillStyle = "magenta";
+ ctx.textAlign = "center";
+ ctx.fillText("GAME OVER", canvas.width/2, canvas.height/2);
+ ctx.fillText("(refresh to restart)", canvas.width/2, canvas.height/1.5);
+ }
+ }
+
+ function findSegment(z) {
+ return segments[Math.floor(z/segmentLength) % segments.length];
+ }
+
+ //=========================================================================
+ // BUILD ROAD GEOMETRY
+ //=========================================================================
+
+ function lastY() { return (segments.length == 0) ? 0 : segments[segments.length-1].p2.world.y; }
+
+ function addSegment(curve, y) {
+ var n = segments.length;
+ segments.push({
+ index: n,
+ p1: { world: { y: lastY(), z: n *segmentLength }, camera: {}, screen: {} },
+ p2: { world: { y: y, z: (n+1)*segmentLength }, camera: {}, screen: {} },
+ curve: curve,
+ sprites: [],
+ cars: [],
+ color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
+ });
+ }
+
+ function addSprite(n, sprite, offset) {
+ segments[n].sprites.push({ source: sprite, offset: offset });
+ }
+
+ function addRoad(enter, hold, leave, curve, y) {
+ var startY = lastY();
+ var endY = startY + (Util.toInt(y, 0) * segmentLength);
+ var n, total = enter + hold + leave;
+ for(n = 0 ; n < enter ; n++)
+ addSegment(Util.easeIn(0, curve, n/enter), Util.easeInOut(startY, endY, n/total));
+ for(n = 0 ; n < hold ; n++)
+ addSegment(curve, Util.easeInOut(startY, endY, (enter+n)/total));
+ for(n = 0 ; n < leave ; n++)
+ addSegment(Util.easeInOut(curve, 0, n/leave), Util.easeInOut(startY, endY, (enter+hold+n)/total));
+ }
+
+ var ROAD = {
+ LENGTH: { NONE: 0, SHORT: 25, MEDIUM: 50, LONG: 100 },
+ HILL: { NONE: 0, LOW: 20, MEDIUM: 40, HIGH: 60 },
+ CURVE: { NONE: 0, EASY: 2, MEDIUM: 4, HARD: 6 }
+ };
+
+ function addStraight(num) {
+ num = num || ROAD.LENGTH.MEDIUM;
+ addRoad(num, num, num, 0, 0);
+ }
+
+ function addHill(num, height) {
+ num = num || ROAD.LENGTH.MEDIUM;
+ height = height || ROAD.HILL.MEDIUM;
+ addRoad(num, num, num, 0, height);
+ }
+
+ function addCurve(num, curve, height) {
+ num = num || ROAD.LENGTH.MEDIUM;
+ curve = curve || ROAD.CURVE.MEDIUM;
+ height = height || ROAD.HILL.NONE;
+ addRoad(num, num, num, curve, height);
+ }
+
+ function addLowRollingHills(num, height) {
+ num = num || ROAD.LENGTH.SHORT;
+ height = height || ROAD.HILL.LOW;
+ addRoad(num, num, num, 0, height/2);
+ addRoad(num, num, num, 0, -height);
+ addRoad(num, num, num, ROAD.CURVE.EASY, height);
+ addRoad(num, num, num, 0, 0);
+ addRoad(num, num, num, -ROAD.CURVE.EASY, height/2);
+ addRoad(num, num, num, 0, 0);
+ }
+
+ function addSCurves() {
+ addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -ROAD.CURVE.EASY, ROAD.HILL.NONE);
+ addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.CURVE.MEDIUM, ROAD.HILL.MEDIUM);
+ addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.CURVE.EASY, -ROAD.HILL.LOW);
+ addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -ROAD.CURVE.EASY, ROAD.HILL.MEDIUM);
+ addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -ROAD.CURVE.MEDIUM, -ROAD.HILL.MEDIUM);
+ }
+
+ function addBumps() {
+ addRoad(10, 10, 10, 0, 5);
+ addRoad(10, 10, 10, 0, -2);
+ addRoad(10, 10, 10, 0, -5);
+ addRoad(10, 10, 10, 0, 8);
+ addRoad(10, 10, 10, 0, 5);
+ addRoad(10, 10, 10, 0, -7);
+ addRoad(10, 10, 10, 0, 5);
+ addRoad(10, 10, 10, 0, -2);
+ }
+
+ function addDownhillToEnd(num) {
+ num = num || 200;
+ addRoad(num, num, num, -ROAD.CURVE.EASY, -lastY()/segmentLength);
+ }
+
+ function resetRoad(random, mintracklength) {
+ segments = [];
+
+ random = true;
+ if (random==true) {
+ // Build the list of possible constructs
+ var constructs = ['straight', 'scurves', 'curve', 'bumps', 'hill', 'lowrollinghills'];
+ // Minimum track length needs to be 2 (between start and end), else it will fail
+ if (!mintracklength) {
+ mintracklength = 2;
+ }
+
+ // Build start part of the track
+ addStraight(ROAD.LENGTH.LONG); // use a long road length to hide the regeneration of the next track when arriving at finish line. TODO: fix this by preloading in parallel the new track, and when riding the last construct before the finish line, modify the rendering functions to render the new track
+
+ // Procedurally and randomly build the rest of the track
+ i = -1;
+ while ((i+=1) < mintracklength) { // TODO: sometimes, 2 tracks are not enough and the loading fails, try to find out why? (there must be an incompatibility between constructs somewhere)
+ // Pick randomly a construct
+ var randc = constructs[Math.floor(Math.random() * constructs.length)];
+ if (randc == 'straight') {
+ var posvals = [ROAD.LENGTH.SHORT, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.LONG, null];
+ var randval = posvals[Math.floor(Math.random() * posvals.length)];
+ addStraight(randval);
+ } else if (randc == 'scurves') {
+ addSCurves();
+ } else if (randc == 'curve') {
+ var posroad = [ROAD.LENGTH.SHORT, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.LONG];
+ var randroad = posroad[Math.floor(Math.random() * posroad.length)];
+ var poscurve = [ROAD.CURVE.SHORT, ROAD.CURVE.MEDIUM, ROAD.CURVE.LONG];
+ var randcurve = poscurve[Math.floor(Math.random() * poscurve.length)];
+ var poshill = [ROAD.HILL.LOW, ROAD.HILL.MEDIUM, ROAD.HILL.HIGH];
+ var randhill = poshill[Math.floor(Math.random() * poshill.length)];
+ addCurve(randroad, randcurve, randhill);
+ } else if (randc == 'bumps') {
+ addBumps();
+ } else if (randc == 'hill') {
+ var posroad = [ROAD.LENGTH.SHORT, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.LONG];
+ var randroad = posroad[Math.floor(Math.random() * posroad.length)];
+ var poshill = [ROAD.HILL.LOW, ROAD.HILL.MEDIUM, ROAD.HILL.HIGH];
+ var randhill = poshill[Math.floor(Math.random() * poshill.length)];
+ addHill(randroad, randhill);
+ } else if (randc == 'curve') {
+ addLowRollingHills();
+ }
+ }
+ // Build end part of the track
+ addDownhillToEnd();
+ } else {
+ addStraight(ROAD.LENGTH.SHORT);
+ addLowRollingHills();
+ addSCurves();
+ addCurve(ROAD.LENGTH.MEDIUM, ROAD.CURVE.MEDIUM, ROAD.HILL.LOW);
+ addBumps();
+ addLowRollingHills();
+ addCurve(ROAD.LENGTH.LONG*2, ROAD.CURVE.MEDIUM, ROAD.HILL.MEDIUM);
+ addStraight();
+ addHill(ROAD.LENGTH.MEDIUM, ROAD.HILL.HIGH);
+ addSCurves();
+ addCurve(ROAD.LENGTH.LONG, -ROAD.CURVE.MEDIUM, ROAD.HILL.NONE);
+ addHill(ROAD.LENGTH.LONG, ROAD.HILL.HIGH);
+ addCurve(ROAD.LENGTH.LONG, ROAD.CURVE.MEDIUM, -ROAD.HILL.LOW);
+ addBumps();
+ addHill(ROAD.LENGTH.LONG, -ROAD.HILL.MEDIUM);
+ addStraight();
+ addSCurves();
+ addDownhillToEnd();
+ }
+
+ try { // workaround for the exception raised sometimes when the tracklength is too small (<5). TODO: fix the root cause of this.
+ resetSprites(); // reset (or create) the environmental sprites
+ } catch (exc) {console.log(exc);}
+ //resetCars(); // don't necessarily reset cars, if we generate procedurally we just want the cars to continue
+
+ segments[findSegment(playerZ).index + 2].color = COLORS.START;
+ segments[findSegment(playerZ).index + 3].color = COLORS.START;
+ for(var n = 0 ; n < rumbleLength ; n++)
+ segments[segments.length-1-n].color = COLORS.FINISH;
+
+ trackLength = segments.length * segmentLength;
+ }
+
+ function resetSprites() {
+ var n, i;
+
+ addSprite(20, SPRITES.BILLBOARD07, -1);
+ addSprite(40, SPRITES.BILLBOARD06, -1);
+ addSprite(60, SPRITES.BILLBOARD08, -1);
+ addSprite(80, SPRITES.BILLBOARD09, -1);
+ addSprite(100, SPRITES.BILLBOARD01, -1);
+ addSprite(120, SPRITES.BILLBOARD02, -1);
+ addSprite(140, SPRITES.BILLBOARD03, -1);
+ addSprite(160, SPRITES.BILLBOARD04, -1);
+ addSprite(180, SPRITES.BILLBOARD05, -1);
+
+ addSprite(240, SPRITES.BILLBOARD07, -1.2);
+ addSprite(240, SPRITES.BILLBOARD06, 1.2);
+ addSprite(segments.length - 25, SPRITES.BILLBOARD07, -1.2);
+ addSprite(segments.length - 25, SPRITES.BILLBOARD06, 1.2);
+
+ for(n = 10 ; n < 200 ; n += 4 + Math.floor(n/100)) {
+ addSprite(n, SPRITES.PALM_TREE, 0.5 + Math.random()*0.5);
+ addSprite(n, SPRITES.PALM_TREE, 1 + Math.random()*2);
+ }
+
+ for(n = 250 ; n < 1000 ; n += 5) {
+ addSprite(n, SPRITES.COLUMN, 1.1);
+ addSprite(n + Util.randomInt(0,5), SPRITES.TREE1, -1 - (Math.random() * 2));
+ addSprite(n + Util.randomInt(0,5), SPRITES.TREE2, -1 - (Math.random() * 2));
+ }
+
+ for(n = 200 ; n < segments.length ; n += 3) {
+ addSprite(n, Util.randomChoice(SPRITES.PLANTS), Util.randomChoice([1,-1]) * (2 + Math.random() * 5));
+ }
+
+ var side, sprite, offset;
+ for(n = 1000 ; n < (segments.length-50) ; n += 100) {
+ side = Util.randomChoice([1, -1]);
+ addSprite(n + Util.randomInt(0, 50), Util.randomChoice(SPRITES.BILLBOARDS), -side);
+ for(i = 0 ; i < 20 ; i++) {
+ sprite = Util.randomChoice(SPRITES.PLANTS);
+ offset = side * (1.5 + Math.random());
+ addSprite(n + Util.randomInt(0, 50), sprite, offset);
+ }
+
+ }
+
+ }
+
+ function resetCars() {
+ cars = [];
+ var n, car, segment, offset, z, sprite, speed;
+ for (var n = 0 ; n < totalCars ; n++) {
+ offset = Math.random() * Util.randomChoice([-0.8, 0.8]);
+ z = (Math.floor(Math.random() * (segments.length-400)) * segmentLength) + (100*segmentLength); // ensure that cars do not respawn just in front of the player, so we generate cars in all segments except the first and last (after and before finish line) - TODO: fix me in a more elegant way, without using constants
+ sprite = Util.randomChoice(SPRITES.CARS);
+ speed = maxSpeed/4 + Math.random() * maxSpeed/(sprite == SPRITES.SEMI ? 4 : 2);
+ car = { offset: offset, z: z, sprite: sprite, speed: speed };
+ segment = findSegment(car.z);
+ segment.cars.push(car);
+ cars.push(car);
+ }
+ }
+
+ //=========================================================================
+ // THE GAME LOOP
+ //=========================================================================
+
+ Game.run({
+ canvas: canvas, render: render, update: update, stats: stats, step: step,
+ images: ["background", "sprites"],
+ keys: [
+ { keys: [KEY.LEFT, KEY.A], div: 'gamepad-left', mode: 'down', action: function() { keyLeft = true; } },
+ { keys: [KEY.RIGHT, KEY.D], div: 'gamepad-right', mode: 'down', action: function() { keyRight = true; } },
+ { keys: [KEY.UP, KEY.W], div: 'gamepad-up', mode: 'down', action: function() { keyFaster = true; } },
+ { keys: [KEY.DOWN, KEY.S], div: 'gamepad-down', mode: 'down', action: function() { keySlower = true; } },
+ { keys: [KEY.LEFT, KEY.A], div: 'gamepad-left', mode: 'up', action: function() { keyLeft = false; } },
+ { keys: [KEY.RIGHT, KEY.D], div: 'gamepad-right', mode: 'up', action: function() { keyRight = false; } },
+ { keys: [KEY.UP, KEY.W], div: 'gamepad-up', mode: 'up', action: function() { keyFaster = false; } },
+ { keys: [KEY.DOWN, KEY.S], div: 'gamepad-down', mode: 'up', action: function() { keySlower = false; } },
+ { keys: [KEY.SPACE, KEY.CTRL], div: 'gamepad-turbo', mode: 'down', action: triggerTurbo }
+ ],
+ ready: function(images) {
+ background = images[0];
+ sprites = images[1];
+ reset();
+ Dom.storage.fast_lap_time = Dom.storage.fast_lap_time || 180;
+ updateHud('fast_lap_time', formatTime(Util.toFloat(Dom.storage.fast_lap_time)));
+ // Allow to put in fullscreen
+ var e = document.getElementById('canvas');
+ e.ondblclick = fullscreenOnClick;
+ }
+ });
+
+ function reset(options) {
+ options = options || {};
+ canvas.width = width = Util.toInt(options.width, width);
+ canvas.height = height = Util.toInt(options.height, height);
+ lanes = Util.toInt(options.lanes, lanes);
+ roadWidth = Util.toInt(options.roadWidth, roadWidth);
+ cameraHeight = Util.toInt(options.cameraHeight, cameraHeight);
+ drawDistance = Util.toInt(options.drawDistance, drawDistance);
+ fogDensity = Util.toInt(options.fogDensity, fogDensity);
+ fieldOfView = Util.toInt(options.fieldOfView, fieldOfView);
+ segmentLength = Util.toInt(options.segmentLength, segmentLength);
+ rumbleLength = Util.toInt(options.rumbleLength, rumbleLength);
+ cameraDepth = 1 / Math.tan((fieldOfView/2) * Math.PI/180);
+ playerZ = (cameraHeight * cameraDepth);
+ resolution = height/480;
+ refreshTweakUI();
+
+ if ((segments.length==0) || (options.segmentLength) || (options.rumbleLength)) {
+ resetRoad(randomTrack, randomTrackLength); // only rebuild road when necessary
+ resetCars();
+ }
+ }
+
+ function updateFOV(fov) {
+ cameraDepth = 1 / Math.tan((fov/2) * Math.PI/180);
+ playerZ = (cameraHeight * cameraDepth);
+ }
+
+ //=========================================================================
+ // TWEAK UI HANDLERS
+ //=========================================================================
+
+ Dom.on('resolution', 'change', function(ev) {
+ var w, h, ratio;
+ switch(ev.target.options[ev.target.selectedIndex].value) {
+ case 'fine': w = 1280; h = 960; ratio=w/width; break;
+ case 'high': w = 1024; h = 768; ratio=w/width; break;
+ case 'medium': w = 640; h = 480; ratio=w/width; break;
+ case 'low': w = 480; h = 360; ratio=w/width; break;
+ }
+ reset({ width: w, height: h })
+ Dom.blur(ev);
+ });
+
+ Dom.on('lanes', 'change', function(ev) { Dom.blur(ev); reset({ lanes: ev.target.options[ev.target.selectedIndex].value }); });
+ Dom.on('roadWidth', 'change', function(ev) { Dom.blur(ev); reset({ roadWidth: Util.limit(Util.toInt(ev.target.value), Util.toInt(ev.target.getAttribute('min')), Util.toInt(ev.target.getAttribute('max'))) }); });
+ Dom.on('cameraHeight', 'change', function(ev) { Dom.blur(ev); reset({ cameraHeight: Util.limit(Util.toInt(ev.target.value), Util.toInt(ev.target.getAttribute('min')), Util.toInt(ev.target.getAttribute('max'))) }); });
+ Dom.on('drawDistance', 'change', function(ev) { Dom.blur(ev); reset({ drawDistance: Util.limit(Util.toInt(ev.target.value), Util.toInt(ev.target.getAttribute('min')), Util.toInt(ev.target.getAttribute('max'))) }); });
+ Dom.on('fieldOfView', 'change', function(ev) { Dom.blur(ev); reset({ fieldOfView: Util.limit(Util.toInt(ev.target.value), Util.toInt(ev.target.getAttribute('min')), Util.toInt(ev.target.getAttribute('max'))) }); });
+ Dom.on('fogDensity', 'change', function(ev) { Dom.blur(ev); reset({ fogDensity: Util.limit(Util.toInt(ev.target.value), Util.toInt(ev.target.getAttribute('min')), Util.toInt(ev.target.getAttribute('max'))) }); });
+
+ function refreshTweakUI() {
+ Dom.get('lanes').selectedIndex = lanes-1;
+ Dom.get('currentRoadWidth').innerHTML = Dom.get('roadWidth').value = roadWidth;
+ Dom.get('currentCameraHeight').innerHTML = Dom.get('cameraHeight').value = cameraHeight;
+ Dom.get('currentDrawDistance').innerHTML = Dom.get('drawDistance').value = drawDistance;
+ Dom.get('currentFieldOfView').innerHTML = Dom.get('fieldOfView').value = fieldOfView;
+ Dom.get('currentFogDensity').innerHTML = Dom.get('fogDensity').value = fogDensity;
+ }
+
+ function fullscreenOnClick() {
+ // Manage full screen mode on double click
+ // from: https://www.sitepoint.com/use-html5-full-screen-api/
+ if (document.fullscreenElement ||
+ document.webkitFullscreenElement ||
+ document.mozFullScreenElement ||
+ document.msFullscreenElement
+ ) {
+ // exit full-screen
+ if (document.exitFullscreen) {
+ document.exitFullscreen();
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen();
+ } else if (document.mozCancelFullScreen) {
+ document.mozCancelFullScreen();
+ } else if (document.msExitFullscreen) {
+ document.msExitFullscreen();
+ }
+ } else {
+ // go full-screen
+ var e = document.getElementById('canvas');
+ if (e.requestFullscreen) {
+ e.requestFullscreen();
+ } else if (e.webkitRequestFullscreen) {
+ e.webkitRequestFullscreen();
+ } else if (e.mozRequestFullScreen) {
+ e.mozRequestFullScreen();
+ } else if (e.msRequestFullscreen) {
+ e.msRequestFullscreen();
+ }
+ }
+ }
+
+ function triggerTurbo() {
+ // turbo trigger function
+ if (gamemode == 1 && turboLeft > 0 && !turboTriggered) {
+ turboCurrentFov = fieldOfView;
+ turboTimeDone = 0.0;
+ turboTriggered = true;
+ turboLeft -= 1;
+ }
+ }
+ //=========================================================================
+}
diff --git a/screenshots/screenshot1.png b/screenshots/screenshot1.png
new file mode 100644
index 00000000..6c52cfe0
Binary files /dev/null and b/screenshots/screenshot1.png differ
diff --git a/screenshots/screenshot2.png b/screenshots/screenshot2.png
new file mode 100644
index 00000000..ec7427df
Binary files /dev/null and b/screenshots/screenshot2.png differ
diff --git a/thanks.txt b/thanks.txt
new file mode 100644
index 00000000..74a4f920
--- /dev/null
+++ b/thanks.txt
@@ -0,0 +1,25 @@
+Big thank you for the resources under creative commons to the following people:
+Graphic Art:
+ * GDJ for the futuristic city in the background (licensed under CC0): https://pixabay.com/fr/avenir-futuriste-science-fiction-1751262/
+Music:
+ * Aries Beats for the amazing music tracks ("Retro Wave" and "Night Ride") licensed under Creative Commons CC-BY 3.0, checkout his channel for more: https://www.youtube.com/channel/UCVz37WOEw1mPyIpx8xPHBmg
+ * TeknoAXE for the amazing "Edge of Tomorrow" music track! (licensed under Creative Commons Attribution 4.0 International License). Checkout his channel for more: https://www.youtube.com/channel/UCtgf00GvfFQVsYBA7V7RwUw
+Sprites:
+* Textured ball by Firkin: https://openclipart.org/detail/298970/textured-ball
+* Hot Dog by oksmith: https://openclipart.org/detail/298968/hot-dog
+* Palm Tree by Kasahorow: https://openclipart.org/detail/297853/palm-tree
+* Antenna Square by jcartier: https://openclipart.org/detail/17312/antenna-square
+* Pineapple by johnny_automatic: https://openclipart.org/detail/1974/pineapple
+* Miniature coconut palm by johnny_automatic: https://openclipart.org/detail/6194/miniature-coconut-palm
+* Ice cream 1 by Firkin: https://openclipart.org/detail/237386/ice-cream-1
+* Bananas by nicubunu: https://openclipart.org/detail/12987/bananas
+* Bananeira by dinhochiz: https://openclipart.org/detail/204886/bananeira
+* Mushrooms 1 by opk: https://openclipart.org/detail/173450/mushrooms-1
+* Comic TV by Chrisdesign: https://openclipart.org/detail/8366/comic-tv
+* Vintage Sign by laurianne: https://openclipart.org/detail/126265/vintage-sign
+* Silver robot by Pippi2011: https://openclipart.org/detail/183281/silver-robot
+* The rest by Lrq3000 using Inkscape tutorials by Nick Saporito: https://www.youtube.com/channel/UCEQXp_fcqwPcqrzNtWJ1w9w
+Background:
+* Seaside Sunrise by Viscious-Speed: https://openclipart.org/detail/234135/seaside-sunrise
+* Jakes Gordon for the trees during daytime
+* Retro night-time background by Lrq3000
diff --git a/v1.straight.html b/v1.straight.html
index 112a7b09..c33aaeff 100644
--- a/v1.straight.html
+++ b/v1.straight.html
@@ -15,7 +15,8 @@
straight |
curves |
hills |
- final
+ fastest lap |
+ out of time
|
@@ -75,8 +76,8 @@
diff --git a/v2.curves.html b/v2.curves.html
index 0ef09153..9d765506 100644
--- a/v2.curves.html
+++ b/v2.curves.html
@@ -15,7 +15,8 @@
straight |
curves |
hills |
- final
+ fastest lap |
+ out of time
|
@@ -75,8 +76,8 @@
diff --git a/v3.hills.html b/v3.hills.html
index 18810357..783b3ad9 100644
--- a/v3.hills.html
+++ b/v3.hills.html
@@ -15,7 +15,8 @@
straight |
curves |
hills |
- final
+ fastest lap |
+ out of time
|
@@ -75,8 +76,8 @@
diff --git a/v4.final.html b/v4.final.html
index 81d69357..725311ec 100644
--- a/v4.final.html
+++ b/v4.final.html
@@ -15,7 +15,8 @@
straight |
curves |
hills |
- final
+ fastest lap |
+ out of time
|
@@ -65,8 +66,16 @@
Use the arrow keys to drive the car.
+
Try to make the fastest time record!
+
Double click for fullscreen.
+
+
+
0 mph
@@ -80,608 +89,19 @@
Loading...
-
-
+
+
Turbo
+
^
+
<
+
>
+
v
+
+
+
+
+
+
+
Use the arrow keys to drive the car.
+
Cross the finish line before time runs out.
+
Double click for fullscreen. Press CONTROL for turbo.
+
+
+
+
+
+
+
+ 0 mph
+ Time: 0.0
+ Level: 0
+ Turbo: 0
+ Time: 0.0
+ Last Lap: 0.0
+ Fastest Lap: 0.0
+
+
+ Loading...
+
+
+
+
Turbo
+
^
+
<
+
>
+
v
+
+
+
+
+
+
+
+
diff --git a/v5.game.html b/v5.game.html
new file mode 100644
index 00000000..d2e1038a
--- /dev/null
+++ b/v5.game.html
@@ -0,0 +1,111 @@
+
+
+
+