libraries_setup #3
6
.gitmodules
vendored
Normal 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/
|
||||||
BIN
game/asset/audio/bgm/Ensemble.mp3
Normal file
BIN
game/asset/audio/sfx/book_flip.1.ogg
Normal file
BIN
game/asset/audio/sfx/book_flip.10.ogg
Normal file
BIN
game/asset/audio/sfx/book_flip.2.ogg
Normal file
BIN
game/asset/audio/sfx/book_flip.3.ogg
Normal file
BIN
game/asset/audio/sfx/book_flip.4.ogg
Normal file
BIN
game/asset/audio/sfx/book_flip.5.ogg
Normal file
BIN
game/asset/audio/sfx/book_flip.6.ogg
Normal file
BIN
game/asset/audio/sfx/book_flip.7.ogg
Normal file
BIN
game/asset/audio/sfx/book_flip.8.ogg
Normal file
BIN
game/asset/audio/sfx/book_flip.9.ogg
Normal file
BIN
game/asset/image/book/StoryMode.png
Normal file
|
After Width: | Height: | Size: 280 B |
BIN
game/asset/image/book/StoryModeBook.png
Normal file
|
After Width: | Height: | Size: 496 B |
BIN
game/asset/image/book/StoryModeBook11.png
Normal file
|
After Width: | Height: | Size: 557 B |
BIN
game/asset/image/book/StoryModeBook16.png
Normal file
|
After Width: | Height: | Size: 643 B |
BIN
game/asset/image/book/StoryModeBook2.png
Normal file
|
After Width: | Height: | Size: 575 B |
BIN
game/asset/image/book/StoryModeBook3.png
Normal file
|
After Width: | Height: | Size: 639 B |
BIN
game/asset/image/book/StoryModeBook4.png
Normal file
|
After Width: | Height: | Size: 643 B |
BIN
game/asset/image/book/StoryModeBook5.png
Normal file
|
After Width: | Height: | Size: 739 B |
BIN
game/asset/image/book/StoryModeBook6.png
Normal file
|
After Width: | Height: | Size: 719 B |
BIN
game/asset/image/book/StoryModeBook7.png
Normal file
|
After Width: | Height: | Size: 653 B |
BIN
game/asset/image/book/StoryModeBook8.png
Normal file
|
After Width: | Height: | Size: 609 B |
BIN
game/asset/image/book/StoryModeBook9.png
Normal file
|
After Width: | Height: | Size: 557 B |
22
game/asset/image/sample/javascript-racer-master/LICENSE
Normal 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.
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
111
game/asset/image/sample/javascript-racer-master/README.md
Normal 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!
|
||||||
|
|
||||||
36
game/asset/image/sample/javascript-racer-master/Rakefile
Normal 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
|
||||||
28
game/asset/image/sample/javascript-racer-master/common.css
Normal 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); }
|
||||||
|
|
||||||
414
game/asset/image/sample/javascript-racer-master/common.js
Normal 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];
|
||||||
|
|
||||||
@ -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 }
|
||||||
|
};
|
||||||
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
BIN
game/asset/image/sample/javascript-racer-master/images/mute.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
@ -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 }
|
||||||
|
};
|
||||||
|
After Width: | Height: | Size: 267 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 647 B |
|
After Width: | Height: | Size: 908 B |
|
After Width: | Height: | Size: 752 B |
|
After Width: | Height: | Size: 790 B |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 745 B |
|
After Width: | Height: | Size: 742 B |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 751 B |
|
After Width: | Height: | Size: 786 B |
|
After Width: | Height: | Size: 758 B |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 973 B |
18
game/asset/image/sample/javascript-racer-master/index.html
Normal 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>
|
||||||
BIN
game/asset/image/sample/javascript-racer-master/music/racer.mp3
Normal file
BIN
game/asset/image/sample/javascript-racer-master/music/racer.ogg
Normal file
143
game/asset/image/sample/javascript-racer-master/stats.js
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
314
game/asset/image/sample/javascript-racer-master/v1.straight.html
Normal 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 <canvas> 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>
|
||||||
|
|
||||||
387
game/asset/image/sample/javascript-racer-master/v2.curves.html
Normal 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 <canvas> 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>
|
||||||
|
|
||||||
419
game/asset/image/sample/javascript-racer-master/v3.hills.html
Normal 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 <canvas> 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>
|
||||||
688
game/asset/image/sample/javascript-racer-master/v4.final.html
Normal 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 <canvas> 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>
|
||||||
BIN
game/asset/video/1_intro.ogv
Normal file
82
game/data/map/segment.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
17
game/data/map/sprites.json
Normal 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
|
||||||
78
game/lib/choro/projection.lua
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
|
||||||
|
|||||||
68
game/src/world/1_intro/init.lua
Normal 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
|
||||||
69
game/src/world/2_town_square/component/perspective.lua
Normal 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
|
||||||
13
game/src/world/2_town_square/component/pos3d.lua
Normal 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
|
||||||
37
game/src/world/2_town_square/init.lua
Normal 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
|
||||||
12
game/src/world/2_town_square/pseudo3d/collide.lua
Normal 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
|
||||||
55
game/src/world/2_town_square/pseudo3d/ease.lua
Normal 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
|
||||||
89
game/src/world/2_town_square/pseudo3d/render/road.lua
Normal 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
|
||||||
72
game/src/world/2_town_square/pseudo3d/render/sprite.lua
Normal 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
|
||||||