libraries_setup #3

Merged
fnicon merged 13 commits from libraries_setup into main 2026-03-18 14:29:14 +00:00
135 changed files with 13863 additions and 20 deletions

6
.gitmodules vendored Normal file
View File

@ -0,0 +1,6 @@
[submodule "game/lib/Concord"]
path = game/lib/Concord
url = https://github.com/Keyslam-Group/Concord
[submodule "game/lib/classic"]
path = game/lib/classic
url = https://github.com/rxi/classic/

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

View File

@ -0,0 +1,22 @@
Copyright (c) 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
===============================================================================

View File

@ -0,0 +1,111 @@
Javascript Pseudo 3D Racer
==========================
An Outrun-style pseudo-3d racing game in HTML5 and Javascript
* [play the game](https://jakesgordon.com/games/racer/)
* view the [source](https://github.com/jakesgordon/javascript-racer)
* read about [how it works](https://jakesgordon.com/writing/javascript-racer/)
Incrementally built up in 4 parts:
* play the [straight road demo](https://jakesgordon.com/games/racer/v1-straight/)
* play the [curves demo](https://jakesgordon.com/games/racer/v2-curves/)
* play the [hills demo](https://jakesgordon.com/games/racer/v3-hills/)
* play the [final version](https://jakesgordon.com/games/racer/)
With detailed descriptions of how each part works:
* read more about [v1 - straight roads](https://jakesgordon.com/writing/javascript-racer-v1-straight/)
* read more about [v2 - curves](https://jakesgordon.com/writing/javascript-racer-v2-curves/)
* read more about [v3 - hills](https://jakesgordon.com/writing/javascript-racer-v3-hills/)
* read more about v4 - final (coming soon)
A note on performance
=====================
The performance of this game is **very** machine/browser dependent. It works quite well in modern
browsers, especially those with GPU canvas acceleration, but a bad graphics driver can kill it stone
dead. So your mileage may vary. There are controls provided to change the rendering resolution
and the draw distance to scale to fit your machine.
Currently supported browsers include:
* Firefox (v12+) works great, 60fps at high res - Nice!
* 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.
>> _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._
A note on code structure
========================
This project happens to be implemented in javascript (because its easy for prototyping) but
is not intended to demonstrate javascript techniques or best practices. In fact, in order to
keep it simple to understand it embeds the javascript for each example directly in the HTML
page (horror!) and, even worse, uses global variables and functions (OMG!).
If I was building a real game I would have much more structure and organization to the
code, but since its just a racing game tech demo, I have elected to [KISS](http://en.wikipedia.org/wiki/KISS_principle).
FUTURE
======
It's quite astounding what it takes to actually [finish](https://jakesgordon.com/writing/defining-finished/)
a game, even a simple one. And this is not a project that I plan on polishing into a finished state. It should
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
* HUD fx (flash on fastest lap, confetti, color coded speedometer, etc)
* more accurate sprite collision
* better car AI (steering, braking etc)
* an actual crash when colliding at high speed
* more bounce when car is off road
* screen shake when off-road or collision
* throw up dirt particles when off road
* more dynamic camera (lower at faster speed, swoop over hills etc)
* automatic resolution & drawDistance detection
* projection based curves ? x,y rotation
* sub-pixel aliasing artifacts on curves
* smarter fog to cover sprites (blue against sky, cover sprites)
* multiple stages, different maps
* a lap map, with current position indicator
* road splits and joins
* day/night cycle
* 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 ?
* a whole lot of gameplay tuning
* ...
* ...
Related Links
=============
* [Lou's Pseudo-3d Page](http://www.extentofthejam.com/pseudo/) - high level how-to guide
* [Racer 10k](https://github.com/onaluf/RacerJS) - another javascript racing game
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!

View File

@ -0,0 +1,36 @@
desc 'recreate sprite sheets'
task 'resprite' do
require 'sprite_factory'
SpriteFactory.run!('images/sprites', :layout => :packed, :output_style => 'images/sprites.js', :margin => 5, :nocomments => true) do |images|
SpriteHelper.javascript_style("SPRITES", images)
end
SpriteFactory.run!('images/background', :layout => :vertical, :output_style => 'images/background.js', :margin => 5, :nocomments => true) do |images|
SpriteHelper.javascript_style("BACKGROUND", images)
end
end
#------------------------------------------------------------------------------
module SpriteHelper
# slightly unusual use of sprite-factory to generate a javascript object structure instead of CSS attributes...
def self.javascript_style(variable, images)
maxname = images.keys.inject(0) {|n,key| [n,key.length].max }
rules = []
images.each do |name, i|
name = name.upcase
whitespace = ' '*(maxname-name.length)
x = '%4d' % i[:cssx]
y = '%4d' % i[:cssy]
w = '%4d' % i[:cssw]
h = '%4d' % i[:cssh]
rules << " #{name}: #{whitespace}{ x: #{x}, y: #{y}, w: #{w}, h: #{h} }"
end
"var #{variable} = {\n#{rules.join(",\n")}\n};"
end
end

View File

@ -0,0 +1,28 @@
/****************************************/
/* common styles used for v1 through v4 */
/****************************************/
body { font-family: Arial, Helvetica, sans-serif; }
#stats { border: 2px solid black; }
#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; }
#mute.on { background-position: -32px 0px; }
/**************************************************/
/* rudimentary heads up display (only used in v4) */
/**************************************************/
#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 #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 .value { color: black; font-weight: bold; }
#hud .fastest { background-color: rgba(255,215,0,0.5); }

View File

@ -0,0 +1,414 @@
//=========================================================================
// minimalist DOM helpers
//=========================================================================
var Dom = {
get: function(id) { return ((id instanceof HTMLElement) || (id === document)) ? id : document.getElementById(id); },
set: function(id, html) { Dom.get(id).innerHTML = html; },
on: function(ele, type, fn, capture) { Dom.get(ele).addEventListener(type, fn, capture); },
un: function(ele, type, fn, capture) { Dom.get(ele).removeEventListener(type, fn, capture); },
show: function(ele, type) { Dom.get(ele).style.display = (type || 'block'); },
blur: function(ev) { ev.target.blur(); },
addClassName: function(ele, name) { Dom.toggleClassName(ele, name, true); },
removeClassName: function(ele, name) { Dom.toggleClassName(ele, name, false); },
toggleClassName: function(ele, name, on) {
ele = Dom.get(ele);
var classes = ele.className.split(' ');
var n = classes.indexOf(name);
on = (typeof on == 'undefined') ? (n < 0) : on;
if (on && (n < 0))
classes.push(name);
else if (!on && (n >= 0))
classes.splice(n, 1);
ele.className = classes.join(' ');
},
storage: window.localStorage || {}
}
//=========================================================================
// general purpose helpers (mostly math)
//=========================================================================
var Util = {
timestamp: function() { return new Date().getTime(); },
toInt: function(obj, def) { if (obj !== null) { var x = parseInt(obj, 10); if (!isNaN(x)) return x; } return Util.toInt(def, 0); },
toFloat: function(obj, def) { if (obj !== null) { var x = parseFloat(obj); if (!isNaN(x)) return x; } return Util.toFloat(def, 0.0); },
limit: function(value, min, max) { return Math.max(min, Math.min(value, max)); },
randomInt: function(min, max) { return Math.round(Util.interpolate(min, max, Math.random())); },
randomChoice: function(options) { return options[Util.randomInt(0, options.length-1)]; },
percentRemaining: function(n, total) { return (n%total)/total; },
accelerate: function(v, accel, dt) { return v + (accel * dt); },
interpolate: function(a,b,percent) { return a + (b-a)*percent },
easeIn: function(a,b,percent) { return a + (b-a)*Math.pow(percent,2); },
easeOut: function(a,b,percent) { return a + (b-a)*(1-Math.pow(1-percent,2)); },
easeInOut: function(a,b,percent) { return a + (b-a)*((-Math.cos(percent*Math.PI)/2) + 0.5); },
exponentialFog: function(distance, density) { return 1 / (Math.pow(Math.E, (distance * distance * density))); },
increase: function(start, increment, max) { // with looping
var result = start + increment;
while (result >= max)
result -= max;
while (result < 0)
result += max;
return result;
},
project: function(p, cameraX, cameraY, cameraZ, cameraDepth, width, height, roadWidth) {
p.camera.x = (p.world.x || 0) - cameraX;
p.camera.y = (p.world.y || 0) - cameraY;
p.camera.z = (p.world.z || 0) - cameraZ;
p.screen.scale = cameraDepth/p.camera.z;
p.screen.x = Math.round((width/2) + (p.screen.scale * p.camera.x * width/2));
p.screen.y = Math.round((height/2) - (p.screen.scale * p.camera.y * height/2));
p.screen.w = Math.round( (p.screen.scale * roadWidth * width/2));
},
overlap: function(x1, w1, x2, w2, percent) {
var half = (percent || 1)/2;
var min1 = x1 - (w1*half);
var max1 = x1 + (w1*half);
var min2 = x2 - (w2*half);
var max2 = x2 + (w2*half);
return ! ((max1 < min2) || (min1 > max2));
}
}
//=========================================================================
// POLYFILL for requestAnimationFrame
//=========================================================================
if (!window.requestAnimationFrame) { // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
window.requestAnimationFrame = window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback, element) {
window.setTimeout(callback, 1000 / 60);
}
}
//=========================================================================
// GAME LOOP helpers
//=========================================================================
var Game = { // a modified version of the game loop from my previous boulderdash game - see https://jakesgordon.com/writing/javascript-boulderdash/#gameloop
run: function(options) {
Game.loadImages(options.images, function(images) {
options.ready(images); // tell caller to initialize itself because images are loaded and we're ready to rumble
Game.setKeyListener(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
step = options.step, // fixed frame step (1/fps) is specified by caller
stats = options.stats, // stats instance is provided by caller
now = null,
last = Util.timestamp(),
dt = 0,
gdt = 0;
function frame() {
now = Util.timestamp();
dt = Math.min(1, (now - last) / 1000); // using requestAnimationFrame have to be able to handle large delta's caused when it 'hibernates' in a background or non-visible tab
gdt = gdt + dt;
while (gdt > step) {
gdt = gdt - step;
update(step);
}
render();
stats.update();
last = now;
requestAnimationFrame(frame, canvas);
}
frame(); // lets get this party started
Game.playMusic();
});
},
//---------------------------------------------------------------------------
loadImages: function(names, callback) { // load multiple images and callback when ALL images have loaded
var result = [];
var count = names.length;
var onload = function() {
if (--count == 0)
callback(result);
};
for(var n = 0 ; n < names.length ; n++) {
var name = names[n];
result[n] = document.createElement('img');
Dom.on(result[n], 'load', onload);
result[n].src = "images/" + name + ".png";
}
},
//---------------------------------------------------------------------------
setKeyListener: function(keys) {
var onkey = function(keyCode, mode) {
var n, k;
for(n = 0 ; n < keys.length ; n++) {
k = keys[n];
k.mode = k.mode || 'up';
if ((k.key == keyCode) || (k.keys && (k.keys.indexOf(keyCode) >= 0))) {
if (k.mode == mode) {
k.action.call();
}
}
}
};
Dom.on(document, 'keydown', function(ev) { onkey(ev.keyCode, 'down'); } );
Dom.on(document, 'keyup', function(ev) { onkey(ev.keyCode, 'up'); } );
},
//---------------------------------------------------------------------------
stats: function(parentId, id) { // construct mr.doobs FPS counter - along with friendly good/bad/ok message box
var result = new Stats();
result.domElement.id = id || 'stats';
Dom.get(parentId).appendChild(result.domElement);
var msg = document.createElement('div');
msg.style.cssText = "border: 2px solid gray; padding: 5px; margin-top: 5px; text-align: left; font-size: 1.15em; text-align: right;";
msg.innerHTML = "Your canvas performance is ";
Dom.get(parentId).appendChild(msg);
var value = document.createElement('span');
value.innerHTML = "...";
msg.appendChild(value);
setInterval(function() {
var fps = result.current();
var ok = (fps > 50) ? 'good' : (fps < 30) ? 'bad' : 'ok';
var color = (fps > 50) ? 'green' : (fps < 30) ? 'red' : 'gray';
value.innerHTML = ok;
value.style.color = color;
msg.style.borderColor = color;
}, 5000);
return result;
},
//---------------------------------------------------------------------------
playMusic: function() {
var music = Dom.get('music');
music.loop = true;
music.volume = 0.05; // shhhh! annoying music!
music.muted = (Dom.storage.muted === "true");
music.play();
Dom.toggleClassName('mute', 'on', music.muted);
Dom.on('mute', 'click', function() {
Dom.storage.muted = music.muted = !music.muted;
Dom.toggleClassName('mute', 'on', music.muted);
});
}
}
//=========================================================================
// canvas rendering helpers
//=========================================================================
var Render = {
polygon: function(ctx, x1, y1, x2, y2, x3, y3, x4, y4, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x3, y3);
ctx.lineTo(x4, y4);
ctx.closePath();
ctx.fill();
},
//---------------------------------------------------------------------------
segment: function(ctx, width, lanes, x1, y1, w1, x2, y2, w2, fog, color) {
var r1 = Render.rumbleWidth(w1, lanes),
r2 = Render.rumbleWidth(w2, lanes),
l1 = Render.laneMarkerWidth(w1, lanes),
l2 = Render.laneMarkerWidth(w2, lanes),
lanew1, lanew2, lanex1, lanex2, lane;
ctx.fillStyle = color.grass;
ctx.fillRect(0, y2, width, y1 - y2);
Render.polygon(ctx, x1-w1-r1, y1, x1-w1, y1, x2-w2, y2, x2-w2-r2, y2, color.rumble);
Render.polygon(ctx, x1+w1+r1, y1, x1+w1, y1, x2+w2, y2, x2+w2+r2, y2, color.rumble);
Render.polygon(ctx, x1-w1, y1, x1+w1, y1, x2+w2, y2, x2-w2, y2, color.road);
if (color.lane) {
lanew1 = w1*2/lanes;
lanew2 = w2*2/lanes;
lanex1 = x1 - w1 + lanew1;
lanex2 = x2 - w2 + lanew2;
for(lane = 1 ; lane < lanes ; lanex1 += lanew1, lanex2 += lanew2, lane++)
Render.polygon(ctx, lanex1 - l1/2, y1, lanex1 + l1/2, y1, lanex2 + l2/2, y2, lanex2 - l2/2, y2, color.lane);
}
Render.fog(ctx, 0, y1, width, y2-y1, fog);
},
//---------------------------------------------------------------------------
background: function(ctx, background, width, height, layer, rotation, offset) {
rotation = rotation || 0;
offset = offset || 0;
var imageW = layer.w/2;
var imageH = layer.h;
var sourceX = layer.x + Math.floor(layer.w * rotation);
var sourceY = layer.y
var sourceW = Math.min(imageW, layer.x+layer.w-sourceX);
var sourceH = imageH;
var destX = 0;
var destY = offset;
var destW = Math.floor(width * (sourceW/imageW));
var destH = height;
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);
},
//---------------------------------------------------------------------------
sprite: function(ctx, width, height, resolution, roadWidth, sprites, sprite, scale, destX, destY, offsetX, offsetY, clipY) {
// scale for projection AND relative to roadWidth (for tweakUI)
var destW = (sprite.w * scale * width/2) * (SPRITES.SCALE * roadWidth);
var destH = (sprite.h * scale * width/2) * (SPRITES.SCALE * roadWidth);
destX = destX + (destW * (offsetX || 0));
destY = destY + (destH * (offsetY || 0));
var clipH = clipY ? Math.max(0, destY+destH-clipY) : 0;
if (clipH < destH)
ctx.drawImage(sprites, sprite.x, sprite.y, sprite.w, sprite.h - (sprite.h*clipH/destH), destX, destY, destW, destH - clipH);
},
//---------------------------------------------------------------------------
player: function(ctx, width, height, resolution, roadWidth, sprites, speedPercent, scale, destX, destY, steer, updown) {
var bounce = (1.5 * Math.random() * speedPercent * resolution) * Util.randomChoice([-1,1]);
var sprite;
if (steer < 0)
sprite = (updown > 0) ? SPRITES.PLAYER_UPHILL_LEFT : SPRITES.PLAYER_LEFT;
else if (steer > 0)
sprite = (updown > 0) ? SPRITES.PLAYER_UPHILL_RIGHT : SPRITES.PLAYER_RIGHT;
else
sprite = (updown > 0) ? SPRITES.PLAYER_UPHILL_STRAIGHT : SPRITES.PLAYER_STRAIGHT;
Render.sprite(ctx, width, height, resolution, roadWidth, sprites, sprite, scale, destX, destY + bounce, -0.5, -1);
},
//---------------------------------------------------------------------------
fog: function(ctx, x, y, width, height, fog) {
if (fog < 1) {
ctx.globalAlpha = (1-fog)
ctx.fillStyle = COLORS.FOG;
ctx.fillRect(x, y, width, height);
ctx.globalAlpha = 1;
}
},
rumbleWidth: function(projectedRoadWidth, lanes) { return projectedRoadWidth/Math.max(6, 2*lanes); },
laneMarkerWidth: function(projectedRoadWidth, lanes) { return projectedRoadWidth/Math.max(32, 8*lanes); }
}
//=============================================================================
// RACING GAME CONSTANTS
//=============================================================================
var KEY = {
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
A: 65,
D: 68,
S: 83,
W: 87
};
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' }
};
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 }
};
var SPRITES = {
PALM_TREE: { x: 5, y: 5, w: 215, h: 540 },
BILLBOARD08: { x: 230, y: 5, w: 385, h: 265 },
TREE1: { x: 625, y: 5, w: 360, h: 360 },
DEAD_TREE1: { x: 5, y: 555, w: 135, h: 332 },
BILLBOARD09: { x: 150, y: 555, w: 328, h: 282 },
BOULDER3: { x: 230, y: 280, w: 320, h: 220 },
COLUMN: { x: 995, y: 5, w: 200, h: 315 },
BILLBOARD01: { x: 625, y: 375, w: 300, h: 170 },
BILLBOARD06: { x: 488, y: 555, w: 298, h: 190 },
BILLBOARD05: { x: 5, y: 897, w: 298, h: 190 },
BILLBOARD07: { x: 313, y: 897, w: 298, h: 190 },
BOULDER2: { x: 621, y: 897, w: 298, h: 140 },
TREE2: { x: 1205, y: 5, w: 282, h: 295 },
BILLBOARD04: { x: 1205, y: 310, w: 268, h: 170 },
DEAD_TREE2: { x: 1205, y: 490, w: 150, h: 260 },
BOULDER1: { x: 1205, y: 760, w: 168, h: 248 },
BUSH1: { x: 5, y: 1097, w: 240, h: 155 },
CACTUS: { x: 929, y: 897, w: 235, h: 118 },
BUSH2: { x: 255, y: 1097, w: 232, h: 152 },
BILLBOARD03: { x: 5, y: 1262, w: 230, h: 220 },
BILLBOARD02: { x: 245, y: 1262, w: 215, h: 220 },
STUMP: { x: 995, y: 330, w: 195, h: 140 },
SEMI: { x: 1365, y: 490, w: 122, h: 144 },
TRUCK: { x: 1365, y: 644, w: 100, h: 78 },
CAR03: { x: 1383, y: 760, w: 88, h: 55 },
CAR02: { x: 1383, y: 825, w: 80, h: 59 },
CAR04: { x: 1383, y: 894, w: 80, h: 57 },
CAR01: { x: 1205, y: 1018, w: 80, h: 56 },
PLAYER_UPHILL_LEFT: { x: 1383, y: 961, w: 80, h: 45 },
PLAYER_UPHILL_STRAIGHT: { x: 1295, y: 1018, w: 80, h: 45 },
PLAYER_UPHILL_RIGHT: { x: 1385, y: 1018, w: 80, h: 45 },
PLAYER_LEFT: { x: 995, y: 480, w: 80, h: 41 },
PLAYER_STRAIGHT: { x: 1085, y: 480, w: 80, h: 41 },
PLAYER_RIGHT: { x: 995, y: 531, w: 80, h: 41 }
};
SPRITES.SCALE = 0.3 * (1/SPRITES.PLAYER_STRAIGHT.w) // the reference sprite width should be 1/3rd the (half-)roadWidth
SPRITES.BILLBOARDS = [SPRITES.BILLBOARD01, SPRITES.BILLBOARD02, SPRITES.BILLBOARD03, SPRITES.BILLBOARD04, SPRITES.BILLBOARD05, SPRITES.BILLBOARD06, SPRITES.BILLBOARD07, SPRITES.BILLBOARD08, SPRITES.BILLBOARD09];
SPRITES.PLANTS = [SPRITES.TREE1, SPRITES.TREE2, SPRITES.DEAD_TREE1, SPRITES.DEAD_TREE2, SPRITES.PALM_TREE, SPRITES.BUSH1, SPRITES.BUSH2, SPRITES.CACTUS, SPRITES.STUMP, SPRITES.BOULDER1, SPRITES.BOULDER2, SPRITES.BOULDER3];
SPRITES.CARS = [SPRITES.CAR01, SPRITES.CAR02, SPRITES.CAR03, SPRITES.CAR04, SPRITES.SEMI, SPRITES.TRUCK];

View File

@ -0,0 +1,5 @@
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 }
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,36 @@
var SPRITES = {
PALM_TREE: { x: 5, y: 5, w: 215, h: 540 },
BILLBOARD08: { x: 230, y: 5, w: 385, h: 265 },
TREE1: { x: 625, y: 5, w: 360, h: 360 },
DEAD_TREE1: { x: 5, y: 555, w: 135, h: 332 },
BILLBOARD09: { x: 150, y: 555, w: 328, h: 282 },
BOULDER3: { x: 230, y: 280, w: 320, h: 220 },
COLUMN: { x: 995, y: 5, w: 200, h: 315 },
BILLBOARD01: { x: 625, y: 375, w: 300, h: 170 },
BILLBOARD06: { x: 488, y: 555, w: 298, h: 190 },
BILLBOARD05: { x: 5, y: 897, w: 298, h: 190 },
BILLBOARD07: { x: 313, y: 897, w: 298, h: 190 },
BOULDER2: { x: 621, y: 897, w: 298, h: 140 },
TREE2: { x: 1205, y: 5, w: 282, h: 295 },
BILLBOARD04: { x: 1205, y: 310, w: 268, h: 170 },
DEAD_TREE2: { x: 1205, y: 490, w: 150, h: 260 },
BOULDER1: { x: 1205, y: 760, w: 168, h: 248 },
BUSH1: { x: 5, y: 1097, w: 240, h: 155 },
CACTUS: { x: 929, y: 897, w: 235, h: 118 },
BUSH2: { x: 255, y: 1097, w: 232, h: 152 },
BILLBOARD03: { x: 5, y: 1262, w: 230, h: 220 },
BILLBOARD02: { x: 245, y: 1262, w: 215, h: 220 },
STUMP: { x: 995, y: 330, w: 195, h: 140 },
SEMI: { x: 1365, y: 490, w: 122, h: 144 },
TRUCK: { x: 1365, y: 644, w: 100, h: 78 },
CAR03: { x: 1383, y: 760, w: 88, h: 55 },
CAR02: { x: 1383, y: 825, w: 80, h: 59 },
CAR04: { x: 1383, y: 894, w: 80, h: 57 },
CAR01: { x: 1205, y: 1018, w: 80, h: 56 },
PLAYER_UPHILL_LEFT: { x: 1383, y: 961, w: 80, h: 45 },
PLAYER_UPHILL_STRAIGHT: { x: 1295, y: 1018, w: 80, h: 45 },
PLAYER_UPHILL_RIGHT: { x: 1385, y: 1018, w: 80, h: 45 },
PLAYER_LEFT: { x: 995, y: 480, w: 80, h: 41 },
PLAYER_STRAIGHT: { x: 1085, y: 480, w: 80, h: 41 },
PLAYER_RIGHT: { x: 995, y: 531, w: 80, h: 41 }
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>Javascript Racer</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body>
<ul>
<li><a href='v1.straight.html'>Version 1 - Straight</a></li>
<li><a href='v2.curves.html'>Version 2 - Curves</a></li>
<li><a href='v3.hills.html'>Version 3 - Hills</a></li>
<li><a href='v4.final.html'>Version 4 - Final</a></li>
</ul>
</body>
</html>

View File

@ -0,0 +1,143 @@
/**
* @author mrdoob / http://mrdoob.com/
*/
var Stats = function () {
var startTime = Date.now(), prevTime = startTime;
var ms = 0, msMin = 1000, msMax = 0;
var fps = 0, fpsMin = 1000, fpsMax = 0;
var frames = 0, mode = 0;mode
var container = document.createElement( 'div' );
container.id = 'stats';
container.addEventListener( 'mousedown', function ( event ) { event.preventDefault(); setMode( ++ mode % 2 ) }, false );
container.style.cssText = 'width:80px;opacity:0.9;cursor:pointer';
var fpsDiv = document.createElement( 'div' );
fpsDiv.id = 'fps';
fpsDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#002';
container.appendChild( fpsDiv );
var fpsText = document.createElement( 'div' );
fpsText.id = 'fpsText';
fpsText.style.cssText = 'color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px';
fpsText.innerHTML = 'FPS';
fpsDiv.appendChild( fpsText );
var fpsGraph = document.createElement( 'div' );
fpsGraph.id = 'fpsGraph';
fpsGraph.style.cssText = 'position:relative;width:74px;height:30px;background-color:#0ff';
fpsDiv.appendChild( fpsGraph );
while ( fpsGraph.children.length < 74 ) {
var bar = document.createElement( 'span' );
bar.style.cssText = 'width:1px;height:30px;float:left;background-color:#113';
fpsGraph.appendChild( bar );
}
var msDiv = document.createElement( 'div' );
msDiv.id = 'ms';
msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;display:none';
container.appendChild( msDiv );
var msText = document.createElement( 'div' );
msText.id = 'msText';
msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px';
msText.innerHTML = 'MS';
msDiv.appendChild( msText );
var msGraph = document.createElement( 'div' );
msGraph.id = 'msGraph';
msGraph.style.cssText = 'position:relative;width:74px;height:30px;background-color:#0f0';
msDiv.appendChild( msGraph );
while ( msGraph.children.length < 74 ) {
var bar = document.createElement( 'span' );
bar.style.cssText = 'width:1px;height:30px;float:left;background-color:#131';
msGraph.appendChild( bar );
}
var setMode = function ( value ) {
mode = value;
switch ( mode ) {
case 0:
fpsDiv.style.display = 'block';
msDiv.style.display = 'none';
break;
case 1:
fpsDiv.style.display = 'none';
msDiv.style.display = 'block';
break;
}
}
var updateGraph = function ( dom, value ) {
var child = dom.appendChild( dom.firstChild );
child.style.height = value + 'px';
}
return {
domElement: container,
setMode: setMode,
current: function() { return fps; },
begin: function () {
startTime = Date.now();
},
end: function () {
var time = Date.now();
ms = time - startTime;
msMin = Math.min( msMin, ms );
msMax = Math.max( msMax, ms );
msText.textContent = ms + ' MS (' + msMin + '-' + msMax + ')';
updateGraph( msGraph, Math.min( 30, 30 - ( ms / 200 ) * 30 ) );
frames ++;
if ( time > prevTime + 1000 ) {
fps = Math.round( ( frames * 1000 ) / ( time - prevTime ) );
fpsMin = Math.min( fpsMin, fps );
fpsMax = Math.max( fpsMax, fps );
fpsText.textContent = fps + ' FPS (' + fpsMin + '-' + fpsMax + ')';
updateGraph( fpsGraph, Math.min( 30, 30 - ( fps / 100 ) * 30 ) );
prevTime = time;
frames = 0;
}
return time;
},
update: function () {
startTime = this.end();
}
}
};

View File

@ -0,0 +1,314 @@
<!DOCTYPE html>
<html>
<head>
<title>Javascript Racer - v1 (straight)</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link href="common.css" rel="stylesheet" type="text/css" />
</head>
<body>
<table id="controls">
<tr>
<td colspan="2">
<a href='v1.straight.html'>straight</a> |
<a href='v2.curves.html'>curves</a> |
<a href='v3.hills.html'>hills</a> |
<a href='v4.final.html'>final</a>
</td>
</tr>
<tr><td id="fps" colspan="2" align="right"></td></tr>
<tr>
<th><label for="resolution">Resolution :</label></th>
<td>
<select id="resolution" style="width:100%">
<option value='fine'>Fine (1280x960)</option>
<option selected value='high'>High (1024x768)</option>
<option value='medium'>Medium (640x480)</option>
<option value='low'>Low (480x360)</option>
</select>
</td>
</tr>
<tr>
<th><label for="lanes">Lanes :</label></th>
<td>
<select id="lanes">
<option>1</option>
<option>2</option>
<option selected>3</option>
<option>4</option>
</select>
</td>
</tr>
<tr>
<th><label for="roadWidth">Road Width (<span id="currentRoadWidth"></span>) :</label></th>
<td><input id="roadWidth" type='range' min='500' max='3000' title="integer (500-3000)"></td>
</tr>
<tr>
<th><label for="cameraHeight">CameraHeight (<span id="currentCameraHeight"></span>) :</label></th>
<td><input id="cameraHeight" type='range' min='500' max='5000' title="integer (500-5000)"></td>
</tr>
<tr>
<th><label for="drawDistance">Draw Distance (<span id="currentDrawDistance"></span>) :</label></th>
<td><input id="drawDistance" type='range' min='100' max='500' title="integer (100-500)"></td>
</tr>
<tr>
<th><label for="fieldOfView">Field of View (<span id="currentFieldOfView"></span>) :</label></th>
<td><input id="fieldOfView" type='range' min='80' max='140' title="integer (80-140)"></td>
</tr>
<tr>
<th><label for="fogDensity">Fog Density (<span id="currentFogDensity"></span>) :</label></th>
<td><input id="fogDensity" type='range' min='0' max='50' title="integer (0-50)"></td>
</tr>
</table>
<div id='instructions'>
<p>Use the <b>arrow keys</b> to drive the car.</p>
</div>
<div id="racer">
<canvas id="canvas">
Sorry, this example cannot be run because your browser does not support the &lt;canvas&gt; element
</canvas>
Loading...
</div>
<audio id='music'>
<source src="music/racer.ogg">
<source src="music/racer.mp3">
</audio>
<span id="mute"></span>
<script src="stats.js"></script>
<script src="common.js"></script>
<script>
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 segments = []; // array of road segments
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 keyLeft = false;
var keyRight = false;
var keyFaster = false;
var keySlower = false;
//=========================================================================
// UPDATE THE GAME WORLD
//=========================================================================
function update(dt) {
position = Util.increase(position, dt * speed, trackLength);
var dx = dt * 2 * (speed/maxSpeed); // at top speed, should be able to cross from left to right (-1 to 1) in 1 second
if (keyLeft)
playerX = playerX - dx;
else if (keyRight)
playerX = playerX + dx;
if (keyFaster)
speed = Util.accelerate(speed, accel, dt);
else if (keySlower)
speed = Util.accelerate(speed, breaking, dt);
else
speed = Util.accelerate(speed, decel, dt);
if (((playerX < -1) || (playerX > 1)) && (speed > offRoadLimit))
speed = Util.accelerate(speed, offRoadDecel, dt);
playerX = Util.limit(playerX, -2, 2); // dont ever let player go too far out of bounds
speed = Util.limit(speed, 0, maxSpeed); // or exceed maxSpeed
}
//=========================================================================
// RENDER THE GAME WORLD
//=========================================================================
function render() {
var baseSegment = findSegment(position);
var maxy = height;
ctx.clearRect(0, 0, width, height);
Render.background(ctx, background, width, height, BACKGROUND.SKY);
Render.background(ctx, background, width, height, BACKGROUND.HILLS);
Render.background(ctx, background, width, height, BACKGROUND.TREES);
var n, segment;
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);
Util.project(segment.p1, (playerX * roadWidth), cameraHeight, position - (segment.looped ? trackLength : 0), cameraDepth, width, height, roadWidth);
Util.project(segment.p2, (playerX * roadWidth), cameraHeight, position - (segment.looped ? trackLength : 0), cameraDepth, width, height, roadWidth);
if ((segment.p1.camera.z <= cameraDepth) || // behind us
(segment.p2.screen.y >= maxy)) // clip by (already rendered) segment
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.p2.screen.y;
}
Render.player(ctx, width, height, resolution, roadWidth, sprites, speed/maxSpeed,
cameraDepth/playerZ,
width/2,
height,
speed * (keyLeft ? -1 : keyRight ? 1 : 0),
0);
}
//=========================================================================
// BUILD ROAD GEOMETRY
//=========================================================================
function resetRoad() {
segments = [];
for(var n = 0 ; n < 500 ; n++) {
segments.push({
index: n,
p1: { world: { z: n *segmentLength }, camera: {}, screen: {} },
p2: { world: { z: (n+1)*segmentLength }, camera: {}, screen: {} },
color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
});
}
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 findSegment(z) {
return segments[Math.floor(z/segmentLength) % segments.length];
}
//=========================================================================
// THE GAME LOOP
//=========================================================================
Game.run({
canvas: canvas, render: render, update: update, stats: stats, step: step,
images: ["background", "sprites"],
keys: [
{ keys: [KEY.LEFT, KEY.A], mode: 'down', action: function() { keyLeft = true; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'down', action: function() { keyRight = true; } },
{ keys: [KEY.UP, KEY.W], mode: 'down', action: function() { keyFaster = true; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'down', action: function() { keySlower = true; } },
{ keys: [KEY.LEFT, KEY.A], mode: 'up', action: function() { keyLeft = false; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'up', action: function() { keyRight = false; } },
{ keys: [KEY.UP, KEY.W], mode: 'up', action: function() { keyFaster = false; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'up', action: function() { keySlower = false; } }
],
ready: function(images) {
background = images[0];
sprites = images[1];
reset();
}
});
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(); // only rebuild road when necessary
}
//=========================================================================
// 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;
}
//=========================================================================
</script>
</body>

View File

@ -0,0 +1,387 @@
<!DOCTYPE html>
<html>
<head>
<title>Javascript Racer - v2 (curves)</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link href="common.css" rel="stylesheet" type="text/css" />
</head>
<body>
<table id="controls">
<tr>
<td colspan="2">
<a href='v1.straight.html'>straight</a> |
<a href='v2.curves.html'>curves</a> |
<a href='v3.hills.html'>hills</a> |
<a href='v4.final.html'>final</a>
</td>
</tr>
<tr><td id="fps" colspan="2" align="right"></td></tr>
<tr>
<th><label for="resolution">Resolution :</label></th>
<td>
<select id="resolution" style="width:100%">
<option value='fine'>Fine (1280x960)</option>
<option selected value='high'>High (1024x768)</option>
<option value='medium'>Medium (640x480)</option>
<option value='low'>Low (480x360)</option>
</select>
</td>
</tr>
<tr>
<th><label for="lanes">Lanes :</label></th>
<td>
<select id="lanes">
<option>1</option>
<option>2</option>
<option selected>3</option>
<option>4</option>
</select>
</td>
</tr>
<tr>
<th><label for="roadWidth">Road Width (<span id="currentRoadWidth"></span>) :</label></th>
<td><input id="roadWidth" type='range' min='500' max='3000' title="integer (500-3000)"></td>
</tr>
<tr>
<th><label for="cameraHeight">CameraHeight (<span id="currentCameraHeight"></span>) :</label></th>
<td><input id="cameraHeight" type='range' min='500' max='5000' title="integer (500-5000)"></td>
</tr>
<tr>
<th><label for="drawDistance">Draw Distance (<span id="currentDrawDistance"></span>) :</label></th>
<td><input id="drawDistance" type='range' min='100' max='500' title="integer (100-500)"></td>
</tr>
<tr>
<th><label for="fieldOfView">Field of View (<span id="currentFieldOfView"></span>) :</label></th>
<td><input id="fieldOfView" type='range' min='80' max='140' title="integer (80-140)"></td>
</tr>
<tr>
<th><label for="fogDensity">Fog Density (<span id="currentFogDensity"></span>) :</label></th>
<td><input id="fogDensity" type='range' min='0' max='50' title="integer (0-50)"></td>
</tr>
</table>
<div id='instructions'>
<p>Use the <b>arrow keys</b> to drive the car.</p>
</div>
<div id="racer">
<canvas id="canvas">
Sorry, this example cannot be run because your browser does not support the &lt;canvas&gt; element
</canvas>
Loading...
</div>
<audio id='music'>
<source src="music/racer.ogg">
<source src="music/racer.mp3">
</audio>
<span id="mute"></span>
<script src="stats.js"></script>
<script src="common.js"></script>
<script>
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 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 keyLeft = false;
var keyRight = false;
var keyFaster = false;
var keySlower = false;
//=========================================================================
// UPDATE THE GAME WORLD
//=========================================================================
function update(dt) {
var playerSegment = findSegment(position+playerZ);
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
position = Util.increase(position, dt * speed, trackLength);
skyOffset = Util.increase(skyOffset, skySpeed * playerSegment.curve * speedPercent, 1);
hillOffset = Util.increase(hillOffset, hillSpeed * playerSegment.curve * speedPercent, 1);
treeOffset = Util.increase(treeOffset, treeSpeed * playerSegment.curve * speedPercent, 1);
if (keyLeft)
playerX = playerX - dx;
else if (keyRight)
playerX = playerX + dx;
playerX = playerX - (dx * speedPercent * playerSegment.curve * centrifugal);
if (keyFaster)
speed = Util.accelerate(speed, accel, dt);
else if (keySlower)
speed = Util.accelerate(speed, breaking, dt);
else
speed = Util.accelerate(speed, decel, dt);
if (((playerX < -1) || (playerX > 1)) && (speed > offRoadLimit))
speed = Util.accelerate(speed, offRoadDecel, dt);
playerX = Util.limit(playerX, -2, 2); // dont ever let player go too far out of bounds
speed = Util.limit(speed, 0, maxSpeed); // or exceed maxSpeed
}
//=========================================================================
// RENDER THE GAME WORLD
//=========================================================================
function render() {
var baseSegment = findSegment(position);
var basePercent = Util.percentRemaining(position, segmentLength);
var maxy = height;
var x = 0;
var dx = - (baseSegment.curve * basePercent);
ctx.clearRect(0, 0, width, height);
Render.background(ctx, background, width, height, BACKGROUND.SKY, skyOffset);
Render.background(ctx, background, width, height, BACKGROUND.HILLS, hillOffset);
Render.background(ctx, background, width, height, BACKGROUND.TREES, treeOffset);
var n, segment;
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);
Util.project(segment.p1, (playerX * roadWidth) - x, cameraHeight, position - (segment.looped ? trackLength : 0), cameraDepth, width, height, roadWidth);
Util.project(segment.p2, (playerX * roadWidth) - x - dx, 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 >= maxy)) // clip by (already rendered) segment
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.p2.screen.y;
}
Render.player(ctx, width, height, resolution, roadWidth, sprites, speed/maxSpeed,
cameraDepth/playerZ,
width/2,
height,
speed * (keyLeft ? -1 : keyRight ? 1 : 0),
0);
}
//=========================================================================
// BUILD ROAD GEOMETRY
//=========================================================================
function addSegment(curve) {
var n = segments.length;
segments.push({
index: n,
p1: { world: { z: n *segmentLength }, camera: {}, screen: {} },
p2: { world: { z: (n+1)*segmentLength }, camera: {}, screen: {} },
curve: curve,
color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
});
}
function addRoad(enter, hold, leave, curve) {
var n;
for(n = 0 ; n < enter ; n++)
addSegment(Util.easeIn(0, curve, n/enter));
for(n = 0 ; n < hold ; n++)
addSegment(curve);
for(n = 0 ; n < leave ; n++)
addSegment(Util.easeInOut(curve, 0, n/leave));
}
var ROAD = {
LENGTH: { NONE: 0, SHORT: 25, MEDIUM: 50, LONG: 100 },
CURVE: { NONE: 0, EASY: 2, MEDIUM: 4, HARD: 6 }
};
function addStraight(num) {
num = num || ROAD.LENGTH.MEDIUM;
addRoad(num, num, num, 0);
}
function addCurve(num, curve) {
num = num || ROAD.LENGTH.MEDIUM;
curve = curve || ROAD.CURVE.MEDIUM;
addRoad(num, num, num, curve);
}
function addSCurves() {
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -ROAD.CURVE.EASY);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.CURVE.MEDIUM);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.CURVE.EASY);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -ROAD.CURVE.EASY);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -ROAD.CURVE.MEDIUM);
}
function resetRoad() {
segments = [];
addStraight(ROAD.LENGTH.SHORT/4);
addSCurves();
addStraight(ROAD.LENGTH.LONG);
addCurve(ROAD.LENGTH.MEDIUM, ROAD.CURVE.MEDIUM);
addCurve(ROAD.LENGTH.LONG, ROAD.CURVE.MEDIUM);
addStraight();
addSCurves();
addCurve(ROAD.LENGTH.LONG, -ROAD.CURVE.MEDIUM);
addCurve(ROAD.LENGTH.LONG, ROAD.CURVE.MEDIUM);
addStraight();
addSCurves();
addCurve(ROAD.LENGTH.LONG, -ROAD.CURVE.EASY);
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 findSegment(z) {
return segments[Math.floor(z/segmentLength) % segments.length];
}
//=========================================================================
// THE GAME LOOP
//=========================================================================
Game.run({
canvas: canvas, render: render, update: update, stats: stats, step: step,
images: ["background", "sprites"],
keys: [
{ keys: [KEY.LEFT, KEY.A], mode: 'down', action: function() { keyLeft = true; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'down', action: function() { keyRight = true; } },
{ keys: [KEY.UP, KEY.W], mode: 'down', action: function() { keyFaster = true; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'down', action: function() { keySlower = true; } },
{ keys: [KEY.LEFT, KEY.A], mode: 'up', action: function() { keyLeft = false; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'up', action: function() { keyRight = false; } },
{ keys: [KEY.UP, KEY.W], mode: 'up', action: function() { keyFaster = false; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'up', action: function() { keySlower = false; } }
],
ready: function(images) {
background = images[0];
sprites = images[1];
reset();
}
});
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(); // only rebuild road when necessary
}
//=========================================================================
// 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;
}
//=========================================================================
</script>
</body>

View File

@ -0,0 +1,419 @@
<!DOCTYPE html>
<html>
<head>
<title>Javascript Racer - v3 (hills)</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link href="common.css" rel="stylesheet" type="text/css" />
</head>
<body>
<table id="controls">
<tr>
<td colspan="2">
<a href='v1.straight.html'>straight</a> |
<a href='v2.curves.html'>curves</a> |
<a href='v3.hills.html'>hills</a> |
<a href='v4.final.html'>final</a>
</td>
</tr>
<tr><td id="fps" colspan="2" align="right"></td></tr>
<tr>
<th><label for="resolution">Resolution :</label></th>
<td>
<select id="resolution" style="width:100%">
<option value='fine'>Fine (1280x960)</option>
<option selected value='high'>High (1024x768)</option>
<option value='medium'>Medium (640x480)</option>
<option value='low'>Low (480x360)</option>
</select>
</td>
</tr>
<tr>
<th><label for="lanes">Lanes :</label></th>
<td>
<select id="lanes">
<option>1</option>
<option>2</option>
<option selected>3</option>
<option>4</option>
</select>
</td>
</tr>
<tr>
<th><label for="roadWidth">Road Width (<span id="currentRoadWidth"></span>) :</label></th>
<td><input id="roadWidth" type='range' min='500' max='3000' title="integer (500-3000)"></td>
</tr>
<tr>
<th><label for="cameraHeight">CameraHeight (<span id="currentCameraHeight"></span>) :</label></th>
<td><input id="cameraHeight" type='range' min='500' max='5000' title="integer (500-5000)"></td>
</tr>
<tr>
<th><label for="drawDistance">Draw Distance (<span id="currentDrawDistance"></span>) :</label></th>
<td><input id="drawDistance" type='range' min='100' max='500' title="integer (100-500)"></td>
</tr>
<tr>
<th><label for="fieldOfView">Field of View (<span id="currentFieldOfView"></span>) :</label></th>
<td><input id="fieldOfView" type='range' min='80' max='140' title="integer (80-140)"></td>
</tr>
<tr>
<th><label for="fogDensity">Fog Density (<span id="currentFogDensity"></span>) :</label></th>
<td><input id="fogDensity" type='range' min='0' max='50' title="integer (0-50)"></td>
</tr>
</table>
<div id='instructions'>
<p>Use the <b>arrow keys</b> to drive the car.</p>
</div>
<div id="racer">
<canvas id="canvas">
Sorry, this example cannot be run because your browser does not support the &lt;canvas&gt; element
</canvas>
Loading...
</div>
<audio id='music'>
<source src="music/racer.ogg">
<source src="music/racer.mp3">
</audio>
<span id="mute"></span>
<script src="stats.js"></script>
<script src="common.js"></script>
<script>
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 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 keyLeft = false;
var keyRight = false;
var keyFaster = false;
var keySlower = false;
//=========================================================================
// UPDATE THE GAME WORLD
//=========================================================================
function update(dt) {
var playerSegment = findSegment(position+playerZ);
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
position = Util.increase(position, dt * speed, trackLength);
skyOffset = Util.increase(skyOffset, skySpeed * playerSegment.curve * speedPercent, 1);
hillOffset = Util.increase(hillOffset, hillSpeed * playerSegment.curve * speedPercent, 1);
treeOffset = Util.increase(treeOffset, treeSpeed * playerSegment.curve * speedPercent, 1);
if (keyLeft)
playerX = playerX - dx;
else if (keyRight)
playerX = playerX + dx;
playerX = playerX - (dx * speedPercent * playerSegment.curve * centrifugal);
if (keyFaster)
speed = Util.accelerate(speed, accel, dt);
else if (keySlower)
speed = Util.accelerate(speed, breaking, dt);
else
speed = Util.accelerate(speed, decel, dt);
if (((playerX < -1) || (playerX > 1)) && (speed > offRoadLimit))
speed = Util.accelerate(speed, offRoadDecel, dt);
playerX = Util.limit(playerX, -2, 2); // dont ever let it go too far out of bounds
speed = Util.limit(speed, 0, maxSpeed); // or exceed maxSpeed
}
//=========================================================================
// 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);
ctx.clearRect(0, 0, width, height);
Render.background(ctx, background, width, height, BACKGROUND.SKY, skyOffset, resolution * skySpeed * playerY);
Render.background(ctx, background, width, height, BACKGROUND.HILLS, hillOffset, resolution * hillSpeed * playerY);
Render.background(ctx, background, width, height, BACKGROUND.TREES, treeOffset, resolution * treeSpeed * playerY);
var n, segment;
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);
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) segment
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.p2.screen.y;
}
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);
}
//=========================================================================
// 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,
color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
});
}
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, 0, height);
addRoad(num, num, num, 0, 0);
addRoad(num, num, num, 0, 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 addDownhillToEnd(num) {
num = num || 200;
addRoad(num, num, num, -ROAD.CURVE.EASY, -lastY()/segmentLength);
}
function resetRoad() {
segments = [];
addStraight(ROAD.LENGTH.SHORT/2);
addHill(ROAD.LENGTH.SHORT, ROAD.HILL.LOW);
addLowRollingHills();
addCurve(ROAD.LENGTH.MEDIUM, ROAD.CURVE.MEDIUM, ROAD.HILL.LOW);
addLowRollingHills();
addCurve(ROAD.LENGTH.LONG, ROAD.CURVE.MEDIUM, ROAD.HILL.MEDIUM);
addStraight();
addCurve(ROAD.LENGTH.LONG, -ROAD.CURVE.MEDIUM, ROAD.HILL.MEDIUM);
addHill(ROAD.LENGTH.LONG, ROAD.HILL.HIGH);
addCurve(ROAD.LENGTH.LONG, ROAD.CURVE.MEDIUM, -ROAD.HILL.LOW);
addHill(ROAD.LENGTH.LONG, -ROAD.HILL.MEDIUM);
addStraight();
addDownhillToEnd();
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 findSegment(z) {
return segments[Math.floor(z/segmentLength) % segments.length];
}
//=========================================================================
// THE GAME LOOP
//=========================================================================
Game.run({
canvas: canvas, render: render, update: update, stats: stats, step: step,
images: ["background", "sprites"],
keys: [
{ keys: [KEY.LEFT, KEY.A], mode: 'down', action: function() { keyLeft = true; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'down', action: function() { keyRight = true; } },
{ keys: [KEY.UP, KEY.W], mode: 'down', action: function() { keyFaster = true; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'down', action: function() { keySlower = true; } },
{ keys: [KEY.LEFT, KEY.A], mode: 'up', action: function() { keyLeft = false; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'up', action: function() { keyRight = false; } },
{ keys: [KEY.UP, KEY.W], mode: 'up', action: function() { keyFaster = false; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'up', action: function() { keySlower = false; } }
],
ready: function(images) {
background = images[0];
sprites = images[1];
reset();
}
});
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(); // only rebuild road when necessary
}
//=========================================================================
// 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;
}
//=========================================================================
</script>
</body>

View File

@ -0,0 +1,688 @@
<!DOCTYPE html>
<html>
<head>
<title>Javascript Racer - v4 (final)</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link href="common.css" rel="stylesheet" type="text/css" />
</head>
<body>
<table id="controls">
<tr>
<td colspan="2">
<a href='v1.straight.html'>straight</a> |
<a href='v2.curves.html'>curves</a> |
<a href='v3.hills.html'>hills</a> |
<a href='v4.final.html'>final</a>
</td>
</tr>
<tr><td id="fps" colspan="2" align="right"></td></tr>
<tr>
<th><label for="resolution">Resolution :</label></th>
<td>
<select id="resolution" style="width:100%">
<option value='fine'>Fine (1280x960)</option>
<option selected value='high'>High (1024x768)</option>
<option value='medium'>Medium (640x480)</option>
<option value='low'>Low (480x360)</option>
</select>
</td>
</tr>
<tr>
<th><label for="lanes">Lanes :</label></th>
<td>
<select id="lanes">
<option>1</option>
<option>2</option>
<option selected>3</option>
<option>4</option>
</select>
</td>
</tr>
<tr>
<th><label for="roadWidth">Road Width (<span id="currentRoadWidth"></span>) :</label></th>
<td><input id="roadWidth" type='range' min='500' max='3000' title="integer (500-3000)"></td>
</tr>
<tr>
<th><label for="cameraHeight">CameraHeight (<span id="currentCameraHeight"></span>) :</label></th>
<td><input id="cameraHeight" type='range' min='500' max='5000' title="integer (500-5000)"></td>
</tr>
<tr>
<th><label for="drawDistance">Draw Distance (<span id="currentDrawDistance"></span>) :</label></th>
<td><input id="drawDistance" type='range' min='100' max='500' title="integer (100-500)"></td>
</tr>
<tr>
<th><label for="fieldOfView">Field of View (<span id="currentFieldOfView"></span>) :</label></th>
<td><input id="fieldOfView" type='range' min='80' max='140' title="integer (80-140)"></td>
</tr>
<tr>
<th><label for="fogDensity">Fog Density (<span id="currentFogDensity"></span>) :</label></th>
<td><input id="fogDensity" type='range' min='0' max='50' title="integer (0-50)"></td>
</tr>
</table>
<div id='instructions'>
<p>Use the <b>arrow keys</b> to drive the car.</p>
</div>
<div id="racer">
<div id="hud">
<span id="speed" class="hud"><span id="speed_value" class="value">0</span> mph</span>
<span id="current_lap_time" class="hud">Time: <span id="current_lap_time_value" class="value">0.0</span></span>
<span id="last_lap_time" class="hud">Last Lap: <span id="last_lap_time_value" class="value">0.0</span></span>
<span id="fast_lap_time" class="hud">Fastest Lap: <span id="fast_lap_time_value" class="value">0.0</span></span>
</div>
<canvas id="canvas">
Sorry, this example cannot be run because your browser does not support the &lt;canvas&gt; element
</canvas>
Loading...
</div>
<audio id='music'>
<source src="music/racer.ogg">
<source src="music/racer.mp3">
</audio>
<span id="mute"></span>
<script src="stats.js"></script>
<script src="common.js"></script>
<script>
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 = 200; // total number of cars on the road
var currentLapTime = 0; // current lap time
var lastLapTime = null; // last lap time
var keyLeft = false;
var keyRight = false;
var keyFaster = false;
var keySlower = false;
var hud = {
speed: { value: null, dom: Dom.get('speed_value') },
current_lap_time: { value: null, dom: Dom.get('current_lap_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') }
}
//=========================================================================
// 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 (keyLeft)
playerX = playerX - dx;
else if (keyRight)
playerX = playerX + dx;
playerX = playerX - (dx * speedPercent * playerSegment.curve * centrifugal);
if (keyFaster)
speed = Util.accelerate(speed, accel, dt);
else if (keySlower)
speed = Util.accelerate(speed, breaking, dt);
else
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
speed = Util.limit(speed, 0, maxSpeed); // or exceed maxSpeed
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)) {
lastLapTime = currentLapTime;
currentLapTime = 0;
if (lastLapTime <= Util.toFloat(Dom.storage.fast_lap_time)) {
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 {
currentLapTime += dt;
}
}
updateHud('speed', 5 * Math.round(speed/500));
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);
ctx.clearRect(0, 0, width, height);
Render.background(ctx, background, width, height, BACKGROUND.SKY, skyOffset, resolution * skySpeed * playerY);
Render.background(ctx, background, width, height, BACKGROUND.HILLS, hillOffset, resolution * hillSpeed * playerY);
Render.background(ctx, background, width, height, BACKGROUND.TREES, treeOffset, resolution * treeSpeed * playerY);
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);
}
}
}
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() {
segments = [];
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();
resetSprites();
resetCars();
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) * segmentLength;
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], mode: 'down', action: function() { keyLeft = true; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'down', action: function() { keyRight = true; } },
{ keys: [KEY.UP, KEY.W], mode: 'down', action: function() { keyFaster = true; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'down', action: function() { keySlower = true; } },
{ keys: [KEY.LEFT, KEY.A], mode: 'up', action: function() { keyLeft = false; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'up', action: function() { keyRight = false; } },
{ keys: [KEY.UP, KEY.W], mode: 'up', action: function() { keyFaster = false; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'up', action: function() { keySlower = false; } }
],
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)));
}
});
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(); // only rebuild road when necessary
}
//=========================================================================
// 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;
}
//=========================================================================
</script>
</body>
</html>

Binary file not shown.

View File

@ -0,0 +1,82 @@
[
{
"enter": 1,
"hold" : 1,
"leave": 90,
"curve": 9,
"height": 80,
"width" : 3000
},
{
"enter": 25,
"hold" : 25,
"leave": 25,
"curve": 0,
"height": 0,
"width" : 1000
},
{
"enter": 50,
"hold" : 50,
"leave": 50,
"curve": 2,
"height": 0,
"width" : 2000
},
{
"enter": 50,
"hold" : 50,
"leave": 50,
"curve": 2,
"height": 20,
"width" : 2000
},
{
"enter": 100,
"hold" : 100,
"leave": 100,
"curve": 4,
"height": 40,
"width" : 4000
},
{
"enter": 50,
"hold" : 50,
"leave": 50,
"curve": 0,
"height": 0,
"width" : 1000
},
{
"enter": 50,
"hold" : 50,
"leave": 50,
"curve": 6,
"height": 60,
"width" : 3000
},
{
"enter": 50,
"hold" : 50,
"leave": 50,
"curve": 0,
"height": 60,
"width" : 2000
},
{
"enter": 50,
"hold" : 50,
"leave": 50,
"curve": 0,
"height": 20,
"width" : 3000
},
{
"enter": 100,
"hold" : 100,
"leave": 100,
"curve": 0,
"height": -280,
"width" : 4000
}
]

View File

@ -0,0 +1,17 @@
[
{
"index" : 200,
"texture" : "asset/image/sample/javascript-racer-master/images/sprites/column.png",
"offset" : -0.1
},
{
"index" : 200,
"texture" : "asset/image/sample/javascript-racer-master/images/sprites/cactus.png",
"offset" : 4
},
{
"index" : 150,
"texture" : "asset/image/sample/javascript-racer-master/images/sprites/stump.png",
"offset" : 0
}
]

1
game/lib/Concord Submodule

@ -0,0 +1 @@
Subproject commit 848652f68887db0c4261efe499facbec88959d03

View File

@ -0,0 +1,78 @@
local vm = require("lib.vornmath")
-- https://jakesgordon.com/writing/javascript-racer-v1-straight/
---@class lib.choro.projection
local projection = {}
--- World {x, y, z} - Camera {x, y, z} (Relative to Camera Pos)
---@param world table {x, y, z}
---@param cam table {x, y, z}
---@return table {x, y, z}
function projection.translateToRelativeCamera(world, cam)
return vm.vec3(world) - vm.vec3(cam)
end
--- project relative camera {x, y, z} to z render distance on screen {x, y}
---@param camPos table {x, y, z}
---@param distance number distance to render point
---@return table {x, y}
function projection.projectRelativeCamera(camPos, distance)
return vm.vec2({
camPos[1] * distance / camPos[3],
camPos[2] * distance / camPos[3]
})
end
--- scale things from camera projection {x, y}
---@param camProj table camera projection {x, y, z}
---@param screenScale number projection scale / distance
---@param resolution table {screen width, screen height}
---@return table {x, y}
function projection.scalePosToRelativeProjectCamera(camProj, screenScale, resolution)
local w2 = resolution[1]/2
local h2 = resolution[2]/2
return vm.vec2({
vm.round(w2 + ( screenScale * camProj[1] * w2)),
vm.round(h2 - ( screenScale * camProj[2] * h2)),
})
end
--- scale things from camera projection {w, h}
---@param size table world size {w, h}
---@param screenScale number projection scale / distance
---@param resolution table {screen width, screen height}
---@return table {w, h}
function projection.scaleSizeToRelativeProjectCamera(size, screenScale, resolution)
local w2 = resolution[1]/2
local h2 = resolution[2]/2
return vm.vec2({
vm.round( screenScale * size[1] * w2),
vm.round( screenScale * size[2] * h2)
})
end
--- calculate distance from field of view
---@param fieldOfView number field of view
---@return number distance distance from camera to projection plane
function projection.distanceCamToProjection(fieldOfView)
local d = 1 / math.tan((fieldOfView/2) * math.pi/180);
return d
end
--- apply world to camera projection
---@param world table world position {x, y, z}
---@param cameraPos table camera position {x, y, z}
---@param cameraDepth number distance from camera to projection plane
---@param resolution table render resolution {width, height}
---@param size table object {width, height}
---@return table,table,table {x, y, z}, {x, y}, {w, h}
function projection.projectWorldToCam(world, cameraPos, cameraDepth, resolution, size)
local relativetocamera = projection.translateToRelativeCamera(world, cameraPos)
local scale = cameraDepth / relativetocamera[3];
return
relativetocamera,
projection.scalePosToRelativeProjectCamera(relativetocamera, scale, resolution),
projection.scaleSizeToRelativeProjectCamera(size, scale, resolution)
end
return projection

1
game/lib/classic Submodule

@ -0,0 +1 @@
Subproject commit e5610756c98ac2f8facd7ab90c94e1a097ecd2c6

388
game/lib/json.lua Normal file
View File

@ -0,0 +1,388 @@
--
-- json.lua
--
-- Copyright (c) 2020 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
local json = { _version = "0.1.2" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\",
[ "\"" ] = "\"",
[ "\b" ] = "b",
[ "\f" ] = "f",
[ "\n" ] = "n",
[ "\r" ] = "r",
[ "\t" ] = "t",
}
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(1, 4), 16 )
local n2 = tonumber( s:sub(7, 10), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local res = ""
local j = i + 1
local k = j
while j <= #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
elseif x == 92 then -- `\`: Escape
res = res .. str:sub(k, j - 1)
j = j + 1
local c = str:sub(j, j)
if c == "u" then
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
or str:match("^%x%x%x%x", j + 1)
or decode_error(str, j - 1, "invalid unicode escape in string")
res = res .. parse_unicode_escape(hex)
j = j + #hex
else
if not escape_chars[c] then
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
end
res = res .. escape_char_map_inv[c]
end
k = j + 1
elseif x == 34 then -- `"`: End of string
res = res .. str:sub(k, j - 1)
return res, j + 1
end
j = j + 1
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
return json

33
game/lib/reap/init.lua Normal file
View File

@ -0,0 +1,33 @@
local reap = {}
--- given lua require file path, return the base in lua path notation
--- usage example :
--- ``local BASE_PATH = reap.base_path(...)``
---@param path string ex: lib.reap.test
---@return string base_path ex: lib.reap
---@return number replacement_count ex: 1
function reap.base_path(path)
return path:gsub('%.[^%.]+$', '')
end
--- given lua require file path, return the base in directory notation
--- usage example :
--- ``local BASE_DIR = reap.base_dir(...)``
---@param path string ex: lib.reap.test
---@return string base_dir ex: lib/reap
---@return number replacement_count ex: 1
function reap.base_dir(path)
return reap.base_path(path):gsub("[.]", "/")
end
--- given lua require file path, return in directory notation
--- usage example :
--- ``local DIR = reap.dir(...)``
---@param path string ex: lib.reap.test
---@return string lua_dir ex: lib/reap/test
---@return number replacement_count ex: 1
function reap.dir(path)
return path:gsub("[.]", "/")
end
return reap

112
game/lib/reoof/cache.lua Normal file
View File

@ -0,0 +1,112 @@
local format = string.format
local classic = require("lib.classic.classic")
---@class Cache : lib.classic.class
---@field private fn function
---@field cache table
---@field count integer
---@field name string
local cache = classic:extend()
cache._VERSION = "0.0.0-alpha"
cache._DESCRIPTION = "A simple and straightforward cache made for LÖVE."
cache._URL = "https://github.com/FNicon/reoof"
cache._LICENSE = [[
MIT License
Copyright (c) 2025 FNicon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
local function default_empty()
end
--- create entity
---@param fn? function lambda to load resource
---@param name? string for debug purposes
-- ---@return Cache self
function cache:new(fn, name)
-- local self = setmetatable({}, cache)
self.fn = fn or default_empty
self.cache = {}
self.count = 0
self.name = name or "cache"
end
--- load resource to cache
---@param self Cache
---@param key string assigned_key
---@param ... any
---@return any resource
function cache:load_to(key, ...)
if (self.cache[key] == nil) then
self.cache[key] = self.fn(...)
self.count = self.count + 1
return self.cache[key]
else
return self:load_from(key)
end
end
--- load resource from cache
---@param self Cache
---@param key string assigned_key
---@return any resource
function cache:load_from(key)
if (self.cache[key] == nil) then
print(format("WARNING : key %s not found"), key)
else
return self.cache[key]
end
end
--- release object and set it up to nil
---@param self Cache
function cache:release()
for k, _ in pairs(self.cache) do
self:release(k)
end
self.cache = nil
self.count = nil
self.name = nil
self.fn = nil
setmetatable(self, nil)
self = nil
end
--- release object and set it up to nil
---@param self Cache
---@param key string
function cache:release_from(key)
if (self.cache[key]) then
if (self.cache[key].release) then
self.cache[key]:release()
self.cache[key] = nil
self.count = self.count - 1
else
print(format("WARNING : during release, %s don't have release function"), key)
end
else
error(format("ERROR : during release, %s not found", key))
end
end
return cache

193
game/lib/reoof/pool.lua Normal file
View File

@ -0,0 +1,193 @@
local insert = table.insert
local remove = table.remove
local classic = require("lib.classic.classic")
---@class Pool : lib.classic.class
---@field private fn function
---@field private rfn function
---@field private efn function
---@field active any[]
---@field hidden any[]
---@field max number | nil
---@field name string
local pool = classic:extend()
pool._VERSION = "0.0.0-alpha"
pool._DESCRIPTION = "A simple and straightforward pool made for LÖVE."
pool._URL = "https://github.com/FNicon/reoof"
pool._LICENSE = [[
MIT License
Copyright (c) 2025 FNicon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
--- default equal function
---@param v1 any
---@param v2 any
local function defaultEqual(v1, v2)
return v1 == v2
end
--- Return the first index with the given value (or nil if not found).
---@param array any[]
---@param value any
---@param efn fun(...):boolean
---@return number?
local function indexOf(array, value, efn)
for i, v in pairs(array) do
if efn(v, value) then
return i
end
end
return nil
end
--- initialize pool
---@param fn fun(...):any generate Function
---@param rfn fun(...):any reset Function
---@param name? string for debug purposes
---@param max? number pool max size
---@param efn? fun(...):boolean equal Function
-- ---@return Pool self
function pool:new(fn, rfn, name, max, efn)
-- local self = setmetatable({}, pool)
self.active = {}
self.hidden = {}
self.fn = fn
self.rfn = rfn
self.efn = efn or defaultEqual
self.max = max > 0 and max or nil
self.name = name or "pool"
end
--- put entity to pool
---@param self Pool
---@param entity any
---@return Pool
---@return number hidden_idx index from hidden pool
function pool:put(entity)
local idx = indexOf(self.active, entity, self.efn)
if (idx) then
self:put_to(entity, idx)
return self, #self.hidden
else
print("WARNING : trying to insert entity not from generate function")
return self, -1
end
end
--- put entity to pool
---@param self Pool
---@param entity any
---@param idx number
---@return Pool
---@return number hidden_idx index from hidden pool
function pool:put_to(entity, idx)
if (self.max) then
if (#self.hidden < self.max) then
insert(self.hidden, entity)
else
self:release(entity)
end
else
insert(self.hidden, entity)
end
remove(self.active, idx)
return self, #self.hidden
end
--- get entity from pool
---@param self Pool
---@param ... any
---@return any entity from pool
---@return number active_idx
function pool:get(...)
local entity = self.hidden[#self.hidden]
if (entity) then
insert(self.active, entity)
remove(self.hidden, #self.hidden)
self.rfn(entity, ...)
return entity, #self.active
else
entity = self.fn(...)
insert(self.active, entity)
return entity, #self.active
end
end
--- release function
---@param self Pool
---@param entity? any
function pool:release(entity)
if (entity == nil) then
for _, v in pairs(self.active) do
self:release(v)
end
for _, v in pairs(self.hidden) do
self:release(v)
end
self.active = nil
self.hidden = nil
self.fn = nil
self.rfn = nil
self.efn = nil
self.name = nil
setmetatable(self, nil)
self = nil
else
local idx = indexOf(self.active, entity, self.efn)
if (idx) then
self:release_from("active", idx)
else
idx = indexOf(self.hidden, entity, self.efn)
if (idx) then
self:release_from("hidden", idx)
else
print("WARNING : trying to release object not from generate function")
end
end
end
end
--- release function
---@param self Pool
---@param from_pool "hidden" | "active"
---@param idx number
function pool:release_from(from_pool, idx)
if (from_pool == "hidden") then
if (self.hidden[idx].release) then
self.hidden[idx]:release()
remove(self.hidden, idx)
else
print("WARNING : during release hidden, %d don't have release function", idx)
end
else
if (self.active[idx].release) then
self.active[idx]:release()
remove(self.active, idx)
else
print("WARNING : during release active, %d don't have release function", idx)
end
end
end
return pool

6072
game/lib/vornmath.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,40 @@
local mode = { local wm = require("world_map")
require("src.modes.racing"),
require("src.modes.raising_sim") world = {
["main_menu"] = require("src.world.main_menu")(),
["1_intro"] = require("src.world.1_intro")(),
["2_town_square"] = require("src.world.2_town_square")(),
["race"] = require("src.world.race")(),
["train"] = require("src.world.train")(),
}; };
local actor = require("src.entities.shared.actor") current = wm["2_town_square"]
local player = actor.load("player")
local mode_i = 1 function load_world(world_to_load)
current = world_to_load
world[current]:reload()
end
function love.load() function love.load()
mode[mode_i].load(player) world[current]:load()
end end
function love.update(dt) function love.update(dt)
mode[mode_i].update(dt) world[current]:update(dt)
end end
function love.draw() function love.draw()
mode[mode_i].draw() world[current]:draw()
end end
function love.keyreleased(key, scancode) function love.keyreleased(key, scancode)
if (key == "right") then world[current]:keyreleased(key, scancode)
mode_i = mode_i + 1
if (mode_i > 2) then
mode_i = 1
end
mode[mode_i].load(player)
end
mode[mode_i].keyreleased(key, scancode)
end end
function love.keypressed(key, scancode, isrepeat) function love.keypressed(key, scancode, isrepeat)
mode[mode_i].keypressed(key, scancode, isrepeat) world[current]:keypressed(key, scancode, isrepeat)
end end
function love.mousereleased(x, y, button, istouch, presses) function love.mousereleased(x, y, button, istouch, presses)
mode[mode_i].mousereleased(x, y, button, istouch, presses) world[current]:mousereleased(x, y, button, istouch, presses)
end end

View File

@ -0,0 +1,68 @@
local reap = require("lib.reap")
local BASE = reap.base_path(...)
local world = require("wrapper.Concord.world")
local debug_entity = require("src.world.common.template.debug_entity")
local button = require("src.world.common.template.button")
local video = require("src.world.common.template.video")
local c_video = require("src.world.common.component.video")
local wm = require("world_map")
local wrapper = world:extend()
local function button_func()
load_world(wm["2_town_square"])
end
function wrapper:new()
wrapper.super.new(self, BASE, ".1_intro")
end
function wrapper:load(_args)
wrapper.super.load(self, {
"src/world/common/system/"
}, {
{
assemblage = debug_entity.assembleDebug,
data = {
position = {0, 0},
label = "1_intro"
}
},
{
assemblage = button.assemble,
data = {
collider = {
x = 20,
y = 20,
w = 20,
h = 20
},
func = button_func,
label = "skip"
}
},
{
assemblage = video.assemble,
data = {
path = "asset/video/1_intro.ogv"
}
}
})
end
function wrapper:update(dt)
wrapper.super.update(self, dt)
for k, v in pairs(self.entities) do
local c_v = v[c_video.dict.video]
if c_v ~= nil then
if (not c_v.data.video:isPlaying()) then
load_world(wm["2_town_square"])
end
end
end
end
return wrapper

View File

@ -0,0 +1,69 @@
local components = {}
components.dict = {
resolution = "perspective.resolution",
lanes = "perspective.lanes",
draw_distance = "perspective.draw_distance",
road_width = "perspective.road_width",
fog_density = "perspective.fog_density",
field_of_view = "perspective.field_of_view",
camera_height = "perspective.camera_height",
segment_length = "perspective.segment_length",
segment_count = "perspective.segment_count",
rumble_length = "perspective.rumble_length",
segment_path = "perspective.segment_path",
segment_sprite_map_path = "perspective.segment_sprite_map_path"
}
function components.resolution (c, x, y)
c.data = {love.graphics.getDimensions()}
end
function components.lanes (c, x)
c.data = x
end
function components.draw_distance (c, x)
c.data = x
end
function components.road_width (c, x)
c.data = x
end
function components.fog_density (c, x)
c.data = x
end
function components.field_of_view (c, x)
c.data = x
end
function components.camera_height (c, x)
c.data = x
end
function components.segment_length (c, x)
c.data = x
end
function components.segment_count (c, x)
c.data = x
end
function components.rumble_length (c, x)
c.data = x
end
function components.segment_path (c, x)
c.data = x
end
function components.segment_sprite_map_path (c, x)
c.data = x
end
return components

View File

@ -0,0 +1,13 @@
local vm = require("lib.vornmath")
local components = {}
components.dict = {
pos = "pos3d.pos"
}
function components.pos (c, x, y, z)
c.data = vm.vec3({x, y, z})
end
return components

View File

@ -0,0 +1,37 @@
local reap = require("lib.reap")
local BASE = reap.base_path(...)
local world = require("wrapper.Concord.world")
local debug_entity = require("src.world.common.template.debug_entity")
local road = require("src.world.2_town_square.template.road")
local wm = require("world_map")
local wrapper = world:extend()
function wrapper:new()
wrapper.super.new(self, BASE, ".2_town_square")
end
function wrapper:load(_args)
wrapper.super.load(self, {
"src/world/common/system/",
"src/world/2_town_square/system"
}, {
{
assemblage = debug_entity.assembleDebug,
data = {
position = {0, 0},
label = "2_town_square"
}
},
{
assemblage = road.assemble,
data = road.default_data
},
})
end
return wrapper

View File

@ -0,0 +1,12 @@
local utils = {}
function utils.overlap(x1, w1, x2, w2, percent)
local half = (percent or 1)/2;
local min1 = x1 - (w1*half);
local max1 = x1 + (w1*half);
local min2 = x2 - (w2*half);
local max2 = x2 + (w2*half);
return not ((max1 < min2) or (min1 > max2));
end
return utils

View File

@ -0,0 +1,55 @@
local vm = require("lib.vornmath")
local utils = {}
function utils.easeIn(a,b,percent)
return a + (b-a)*math.pow(percent,2);
end
function utils.easeOut(a,b,percent)
return a + (b-a)*(1-math.pow(1-percent,2));
end
function utils.easeInOut(a,b,percent)
return a + (b-a)*((-math.cos(percent*math.pi)/2) + 0.5);
end
function utils.percentRemaining(n, total)
return (n%total)/total;
end
function utils.interpolate(a, b, percent)
return a + (b-a)*percent
end
function utils.randomInt(min, max)
return vm.round(utils.interpolate(min, max, math.random()));
end
function utils.randomChoice(options)
return options[utils.randomInt(1, #options)]
end
function utils.limit(value, min, max)
return math.max(min, math.min(value, max))
end
function utils.accelerate(v, accel, dt)
return v + (accel * dt)
end
function utils.exponentialFog(distance, density)
return 1 / (math.exp(distance * distance * density))
end
function utils.increase(start, increment, max, is_loop) -- with looping
local result = start + increment
if (result > max) and is_loop then
result = 1
elseif result <= 0 and is_loop then
result = max - 1
end
return result
end
return utils

View File

@ -0,0 +1,89 @@
local ease = require("src.world.2_town_square.pseudo3d.ease")
local utils = {}
local function drawQuad(slice, image, quads, x1, y1, x2, y2, w1, w2, h1, h2, sw, sh)
for i = 1, #quads do
local percent = ease.percentRemaining(i, #quads)
local destY = ease.interpolate(y1, y2, percent)
local destX = ease.interpolate(x1, x2, percent)
local destW = ease.interpolate(w1, w2, percent)
local destH = ease.interpolate(h1, h2, percent)
if (slice == "vertical") then
love.graphics.draw(image,quads[i],
destX, destY, 0, destW / sw, 1
)
elseif (slice == "horizontal") then
love.graphics.draw(image,quads[i],
destX, destY, 0, 1, destH / sh
)
end
end
end
local function drawSection(texture, key, x1, y1, x2, y2, w1, w2, h1, h2, resolution)
local image = texture[key].image
local quads = texture[key].quads
local sw, sh = image:getDimensions()
if (key == "floor") then
drawQuad("vertical", image, quads,
x1, y1,
x2, y2,
w1, w2,
h1, h2,
sw, sh
)
elseif (key == "ceil") then
drawQuad("vertical", image, quads,
x1, -y1 + resolution[2]/2,
x2, -y2 + resolution[2]/2,
w1, w2,
h1, h2,
sw, sh
)
elseif (key == "wallL") then
drawQuad("horizontal", image, quads,
x1, -y1 + h1,
x2, -y2 + h2,
w1, w2,
h1, h2,
sw, sh
)
elseif (key == "wallR") then
drawQuad("horizontal", image, quads,
x1 + w1, -y1,
x2 + w2, -y2,
w1, w2,
h1 + y1,
h2 + y2,
sw, sh
)
end
end
function utils.draw(p1screenpos, p2screenpos, p1screensize, p2screensize, texture, resolution, maxy)
local x1 = p1screenpos[1]
local y1 = p1screenpos[2]
local x2 = p2screenpos[1]
local y2 = p2screenpos[2]
local w1 = p1screensize[1]
local w2 = p2screensize[1]
local h1 = (y1)
local h2 = (y2)
local x1e = x1 + w1
local x2e = x2 + w2
drawSection(texture, "wallL", x1 , y1, x2, y2, w1, w2, h1, h2, resolution)
drawSection(texture, "wallR", x1 , y1, x2, y2, w1, w2, h1, h2, resolution)
drawSection(texture, "floor", x1 , y1, x2, y2, w1, w2, h1, h2, resolution)
drawSection(texture, "ceil", x1 , y1, x2, y2, w1, w2, h1, h2, resolution)
end
return utils

View File

@ -0,0 +1,72 @@
local projection = require("lib.choro.projection")
local UV = require("engine.utils.obj3d.texture.uv")
local quadToUV = UV.quadToUV
local imageToUV = UV.imageToUV
local QuadCache = require("engine.cache.loveapi.quadcache").obj
local utils = {}
function utils.drawstatic(segment, roadWidth, resolution)
-- render roadside sprites
local w2 = resolution[1]/2
for i = 1, #segment.sprites do
local sprite = segment.sprites[i].source;
local spriteoffset = segment.sprites[i].offset;
local spritePath = segment.sprites[i].data.path;
local spriteType = segment.sprites[i].data.type;
local spriteState = segment.sprites[i].data.state;
local quad
local img
if (spriteType == "aseprite") then
img = sprite.image
quad = sprite.frame.quad
else
img = sprite
quad = QuadCache:load(spritePath, 0, 0, 1, 1, img:getWidth(), img:getHeight(), spriteType)
end
local u, v, nu, nv = quadToUV(quad)
local spriteScale = segment.p1.screen.scale;
local spriteX = segment.p1.screen.pos.x + (spriteScale * spriteoffset * roadWidth * w2);
local spriteY = segment.p1.screen.pos.y;
local offsetX
if (spriteoffset < 0) then
offsetX = -1
else
offsetX = 0
end
local offsetY = -1
-- scale for projection AND relative to roadWidth (for tweakUI)
local destW = (img:getWidth() * spriteScale * w2) * (projection.getSpriteScale(resolution[1]/3) * roadWidth);
local destH = (img:getHeight() * spriteScale * w2) * (projection.getSpriteScale(resolution[1]/3) * roadWidth);
local destX = spriteX + (destW * (offsetX or 0));
local destY = spriteY + (destH * (offsetY or 0));
local clipH = 0;
if (segment.clip) then
clipH = math.max(0, destY+destH-segment.clip)
end
if (clipH < destH) then
local _nv = nv - (nv * clipH/destH)
local _destH = destH - clipH
local _quad = quad
if (_nv ~= nv) then
_quad = QuadCache:load(spritePath, u, v, nu, _nv, img:getWidth(), img:getHeight(), spriteType, spriteState)
end
love.graphics.draw(img, _quad, destX, destY, 0, destW, _destH)
end
end
end
return utils

Some files were not shown because too many files have changed in this diff Show More