├── src ├── post.js ├── pre.js ├── env_dev.js ├── shaders │ ├── static.vert │ ├── copy.frag │ ├── glare.frag │ ├── persistence.frag │ ├── laser.frag │ ├── blur1d.frag │ ├── game.frag │ └── player.frag ├── env_prod.js ├── lib │ ├── path.js │ ├── math.js │ ├── audio.js │ ├── webgl.js │ ├── asteroids.font.js │ └── jsfxr.js ├── bullets.js ├── particles.js ├── target.html ├── behaviors.js ├── ufo.js ├── sounds.js ├── state.js ├── spaceship.js ├── input.js ├── asteroids.js ├── setup.js ├── effects.js ├── asteroidsIncoming.js ├── ai.js ├── game.js └── ui.js ├── 160x160.png ├── 400x250.png ├── behind-asteroids.zip ├── screenshots ├── dead.png ├── intro.png ├── ufo.png ├── danger1.png ├── danger2.png ├── continue.png ├── explosion.png ├── gameover.png ├── newplayer.png ├── player24.png ├── playerlose.png ├── tech │ ├── font.png │ ├── game.png │ ├── glare.png │ ├── laser.png │ ├── player.png │ ├── result.png │ ├── player_raw.png │ └── persistence.png └── playerlose2.png ├── .gitignore ├── scripts ├── wrapjs.sh ├── compileglslfiles.sh └── concat.sh ├── .eslintrc ├── package.json └── README.md /src/post.js: -------------------------------------------------------------------------------- 1 | }()); 2 | -------------------------------------------------------------------------------- /src/pre.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | -------------------------------------------------------------------------------- /160x160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/160x160.png -------------------------------------------------------------------------------- /400x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/400x250.png -------------------------------------------------------------------------------- /behind-asteroids.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/behind-asteroids.zip -------------------------------------------------------------------------------- /screenshots/dead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/dead.png -------------------------------------------------------------------------------- /screenshots/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/intro.png -------------------------------------------------------------------------------- /screenshots/ufo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/ufo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target.zip 2 | target/ 3 | node_modules/ 4 | npm-debug.log* 5 | .DS_Store 6 | build/ 7 | -------------------------------------------------------------------------------- /screenshots/danger1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/danger1.png -------------------------------------------------------------------------------- /screenshots/danger2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/danger2.png -------------------------------------------------------------------------------- /screenshots/continue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/continue.png -------------------------------------------------------------------------------- /screenshots/explosion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/explosion.png -------------------------------------------------------------------------------- /screenshots/gameover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/gameover.png -------------------------------------------------------------------------------- /screenshots/newplayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/newplayer.png -------------------------------------------------------------------------------- /screenshots/player24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/player24.png -------------------------------------------------------------------------------- /screenshots/playerlose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/playerlose.png -------------------------------------------------------------------------------- /screenshots/tech/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/tech/font.png -------------------------------------------------------------------------------- /screenshots/tech/game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/tech/game.png -------------------------------------------------------------------------------- /screenshots/tech/glare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/tech/glare.png -------------------------------------------------------------------------------- /screenshots/tech/laser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/tech/laser.png -------------------------------------------------------------------------------- /screenshots/playerlose2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/playerlose2.png -------------------------------------------------------------------------------- /screenshots/tech/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/tech/player.png -------------------------------------------------------------------------------- /screenshots/tech/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/tech/result.png -------------------------------------------------------------------------------- /screenshots/tech/player_raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/tech/player_raw.png -------------------------------------------------------------------------------- /screenshots/tech/persistence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gre/behind-asteroids/HEAD/screenshots/tech/persistence.png -------------------------------------------------------------------------------- /src/env_dev.js: -------------------------------------------------------------------------------- 1 | var DEBUG = true; // eslint-disable-line no-unused-vars 2 | var MOBILE = "ontouchstart" in document; // eslint-disable-line no-unused-vars 3 | -------------------------------------------------------------------------------- /src/shaders/static.vert: -------------------------------------------------------------------------------- 1 | attribute vec2 p; 2 | varying vec2 uv; 3 | 4 | void main() { 5 | gl_Position = vec4(p,0.0,1.0); 6 | uv = 0.5 * (p+1.0); 7 | } 8 | -------------------------------------------------------------------------------- /scripts/wrapjs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # usage: cat glslfile | ./wrapjs.sh varname 4 | 5 | echo -n "var $1 ='" 6 | perl -p -e 's/\n/\\n/'; 7 | echo -ne "';" 8 | -------------------------------------------------------------------------------- /src/env_prod.js: -------------------------------------------------------------------------------- 1 | var DEBUG = false; // eslint-disable-line no-unused-vars 2 | var MOBILE = "ontouchstart" in document; // eslint-disable-line no-unused-vars 3 | -------------------------------------------------------------------------------- /src/shaders/copy.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 uv; 4 | uniform sampler2D t; 5 | 6 | void main() { 7 | gl_FragColor = texture2D(t, uv); 8 | } 9 | -------------------------------------------------------------------------------- /src/shaders/glare.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 uv; 4 | uniform sampler2D t; 5 | 6 | void main() { 7 | gl_FragColor = vec4(step(0.9, texture2D(t, uv).r)); 8 | } 9 | -------------------------------------------------------------------------------- /src/shaders/persistence.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 uv; 4 | uniform sampler2D t; 5 | uniform sampler2D r; 6 | 7 | void main() { 8 | vec3 b = texture2D(r, uv).rgb; 9 | gl_FragColor = vec4( 10 | b * (0.82 - 0.3 * b.r * b.r) + 11 | texture2D(t, uv).rgb, 12 | 1.0); 13 | } 14 | -------------------------------------------------------------------------------- /src/shaders/laser.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 uv; 4 | uniform sampler2D t; 5 | 6 | void main() { 7 | vec3 c = texture2D(t, uv).rgb; 8 | vec2 off = 0.003 * vec2( 9 | cos(47.0 * uv.y), 10 | sin(67.0 * uv.x) 11 | ); 12 | gl_FragColor = vec4( 13 | c.r + c.g + c.b + texture2D(t, uv+off).b 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/path.js: -------------------------------------------------------------------------------- 1 | /* global ctx */ 2 | 3 | function path (pts, noclose) { // eslint-disable-line no-unused-vars 4 | ctx.beginPath(); 5 | var mv = 1; 6 | for (var i = 0; pts && i&2; 4 | exit 1; 5 | fi; 6 | if [ "$1" == "$2" ]; then 7 | echo "fromDir and toDir must be different" >&2; 8 | exit 2; 9 | fi; 10 | if [ ! -d "$1" ]; then 11 | echo "fromDir must be a directory" >&2; 12 | exit 3; 13 | fi; 14 | if [ ! -d "$2" ]; then 15 | echo "toDir must be a directory" >&2; 16 | exit 4; 17 | fi; 18 | 19 | for glsl in $1/*.frag $1/*.vert; do 20 | name=`basename $glsl`; 21 | cat $glsl | glslmin > $2/$name; 22 | done; 23 | -------------------------------------------------------------------------------- /src/bullets.js: -------------------------------------------------------------------------------- 1 | /* global 2 | ctx bullets 3 | */ 4 | 5 | function shoot (obj, vel, ang) { 6 | var ax = Math.cos(ang); 7 | var ay = Math.sin(ang); 8 | bullets.push([ 9 | obj[0] + 14 * ax, 10 | obj[1] + 14 * ay, 11 | obj[2] + vel * ax, 12 | obj[3] + vel * ay, 13 | 1000, 14 | 0 15 | ]); 16 | } 17 | 18 | // RENDERING 19 | 20 | 21 | function drawBullet () { 22 | ctx.globalAlpha = 1 - Math.random()*Math.random(); 23 | ctx.fillStyle = "#00f"; 24 | ctx.beginPath(); 25 | ctx.arc(0, 0, 2+2.5*Math.random(), 0, 2*Math.PI); 26 | ctx.fill(); 27 | } 28 | -------------------------------------------------------------------------------- /src/shaders/blur1d.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 uv; 4 | uniform sampler2D t; 5 | uniform vec2 dim; 6 | uniform vec2 dir; 7 | 8 | void main() { 9 | vec4 color = vec4(0.0); 10 | vec2 off1 = vec2(1.3846153846) * dir; 11 | vec2 off2 = vec2(3.2307692308) * dir; 12 | color += texture2D(t, uv) * 0.2270270270; 13 | color += texture2D(t, uv + (off1 / dim)) * 0.3162162162; 14 | color += texture2D(t, uv - (off1 / dim)) * 0.3162162162; 15 | color += texture2D(t, uv + (off2 / dim)) * 0.0702702703; 16 | color += texture2D(t, uv - (off2 / dim)) * 0.0702702703; 17 | gl_FragColor = color; 18 | } 19 | -------------------------------------------------------------------------------- /src/particles.js: -------------------------------------------------------------------------------- 1 | /* global 2 | particles play Aexplosion1 Aexplosion2 ctx 3 | */ 4 | function explose (o) { 5 | play(Math.random()<0.5 ? Aexplosion1 : Aexplosion2); 6 | var n = Math.floor(19 + 9 * Math.random()); 7 | for (var i = 0; i < n; ++i) { 8 | var l = 30 * Math.random() - 10; 9 | var a = (Math.random() + 2 * Math.PI * i) / n; 10 | particles.push([ 11 | o[0] + l * Math.cos(a), 12 | o[1] + l * Math.sin(a), 13 | a, 14 | 0.06, 15 | Math.random()<0.3 ? 0 : 1000 16 | ]); 17 | } 18 | } 19 | 20 | // RENDERING 21 | 22 | 23 | function drawParticle () { 24 | ctx.globalAlpha = 0.8; 25 | ctx.fillStyle = "#f00"; 26 | ctx.beginPath(); 27 | ctx.arc(0, 0, 1, 0, 2*Math.PI); 28 | ctx.fill(); 29 | } 30 | -------------------------------------------------------------------------------- /src/target.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Behind Asteroids 4 | 5 | 6 | 7 | 8 | 9 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/lib/math.js: -------------------------------------------------------------------------------- 1 | 2 | // normalize radian angle between -PI and PI (assuming it is not too far) 3 | function normAngle (a) { 4 | return a < -Math.PI ? a + 2*Math.PI : 5 | a>Math.PI ? a - 2*Math.PI : a; 6 | } 7 | 8 | function smoothstep (min, max, value) { 9 | var x = Math.max(0, Math.min(1, (value-min)/(max-min))); 10 | return x*x*(3 - 2*x); 11 | } 12 | 13 | function scoreTxt (s) { 14 | return (s<=9?"0":"")+s; 15 | } 16 | 17 | function dist (a, b) { 18 | var x = a[0]-b[0]; 19 | var y = a[1]-b[1]; 20 | return Math.sqrt(x * x + y * y); 21 | } 22 | 23 | function length (v) { 24 | return Math.sqrt(v[0]*v[0]+v[1]*v[1]); 25 | } 26 | 27 | function circleCollides (a, b, r) { 28 | var x = a[0] - b[0]; 29 | var y = a[1] - b[1]; 30 | return x*x+y*y < r*r; 31 | } 32 | -------------------------------------------------------------------------------- /src/behaviors.js: -------------------------------------------------------------------------------- 1 | /* global dt W H */ 2 | 3 | function euclidPhysics (obj) { 4 | obj[0] += obj[2] * dt; 5 | obj[1] += obj[3] * dt; 6 | } 7 | 8 | function polarPhysics (obj) { 9 | var x = Math.cos(obj[2]); 10 | var y = Math.sin(obj[2]); 11 | var s = dt * obj[3]; 12 | obj[0] += s * x; 13 | obj[1] += s * y; 14 | } 15 | 16 | function destroyOutOfBox (obj, i, arr) { 17 | if (obj[0] < -100 || obj[1] < -100 || obj[0] > W+100 || obj[1] > H+100) { 18 | arr.splice(i, 1); 19 | } 20 | } 21 | 22 | function applyLife (obj, i, arr) { 23 | if ((obj[4] -= dt) < 0) { 24 | arr.splice(i, 1); 25 | } 26 | } 27 | 28 | function loopOutOfBox (obj) { 29 | if (obj[0] < 0) { 30 | obj[0] += W; 31 | } 32 | else if (obj[0] > W) { 33 | obj[0] -= W; 34 | } 35 | if (obj[1] < 0) { 36 | obj[1] += H; 37 | } 38 | else if (obj[1] > H) { 39 | obj[1] -= H; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /scripts/concat.sh: -------------------------------------------------------------------------------- 1 | 2 | cat src/pre.js 3 | 4 | if [ "$NODE_ENV" == "production" ]; then 5 | cat src/env_prod.js 6 | else 7 | cat src/env_dev.js 8 | fi; 9 | 10 | # libs 11 | 12 | cat src/lib/math.js 13 | cat src/lib/path.js 14 | cat src/lib/asteroids.font.js 15 | cat src/lib/webgl.js 16 | cat src/lib/jsfxr.js 17 | cat src/lib/audio.js 18 | 19 | # shaders 20 | 21 | cd build; 22 | for glsl in *.frag *.vert; do 23 | name=`echo $glsl | tr '.' '_' | tr '[:lower:]' '[:upper:]'` 24 | cat $glsl | ../scripts/wrapjs.sh $name 25 | echo 26 | done 27 | cd ..; 28 | 29 | # game 30 | 31 | cat src/setup.js 32 | cat src/state.js 33 | cat src/sounds.js 34 | cat src/input.js 35 | cat src/behaviors.js 36 | cat src/ai.js 37 | cat src/asteroids.js 38 | cat src/asteroidsIncoming.js 39 | cat src/bullets.js 40 | cat src/particles.js 41 | cat src/spaceship.js 42 | cat src/ufo.js 43 | cat src/ui.js 44 | cat src/effects.js 45 | cat src/game.js 46 | 47 | cat src/post.js 48 | -------------------------------------------------------------------------------- /src/lib/audio.js: -------------------------------------------------------------------------------- 1 | /* global jsfxr */ 2 | 3 | var audioCtx, audioDest, audio, play; // eslint-disable-line 4 | 5 | var AudioContext = window.AudioContext || window.webkitAudioContext; 6 | 7 | if (AudioContext) { 8 | audioCtx = new AudioContext(); 9 | audioDest = audioCtx.createDynamicsCompressor(); 10 | var gain = audioCtx.createGain(); 11 | gain.gain.value = 0.1; 12 | audioDest.connect(gain); 13 | gain.connect(audioCtx.destination); 14 | 15 | audio = function (conf) { // eslint-disable-line no-unused-vars 16 | var o = []; 17 | jsfxr(conf, audioCtx, function (buf) { 18 | o.push(buf); 19 | }); 20 | return o; 21 | }; 22 | play = function (o) { // eslint-disable-line no-unused-vars 23 | if (!o[0]) return; 24 | var source = audioCtx.createBufferSource(); 25 | source.buffer = o[0]; 26 | source.start(0); 27 | source.connect(audioDest); 28 | setTimeout(function () { 29 | source.disconnect(audioDest); 30 | }, o[0].duration * 1000 + 300); 31 | }; 32 | } 33 | else { 34 | audio = play = function(){}; 35 | } 36 | -------------------------------------------------------------------------------- /src/ufo.js: -------------------------------------------------------------------------------- 1 | /* global 2 | dt dying spaceship shoot ctx path 3 | */ 4 | 5 | function applyUFOlogic (o) { 6 | o[4] -= dt; 7 | if (o[4]<0) { 8 | o[4] = 500 + 300 * Math.random(); 9 | if (!dying) { 10 | var target = Math.atan2(spaceship[1] - o[1], spaceship[0] - o[0]); 11 | if (!o[2] || Math.random()<0.2) { 12 | var randomAngle = 2*Math.PI*Math.random(); 13 | o[2] = 0.08 * Math.cos(randomAngle); 14 | o[3] = 0.08 * Math.sin(randomAngle); 15 | } 16 | shoot(o, 0.3+0.1*Math.random(), target + 0.6 * Math.random() - 0.3); 17 | } 18 | } 19 | } 20 | 21 | // RENDERING 22 | 23 | var UFOa = [ 24 | [8,0], 25 | [7,5], 26 | [0,9], 27 | [7,14] 28 | ]; 29 | var UFOb = [ 30 | [15,14], 31 | [22,9], 32 | [15,5], 33 | [14,0] 34 | ]; 35 | 36 | var UFO = 37 | UFOa 38 | .concat(UFOb) 39 | .concat(UFOa) 40 | .concat([,]) 41 | .concat(UFOb) 42 | .concat([ 43 | , 44 | [7,5], 45 | [15,5], 46 | , 47 | [0,9], 48 | [22,9] 49 | ]); 50 | 51 | function drawUFO () { 52 | ctx.globalAlpha = 0.4; 53 | ctx.strokeStyle = "#f00"; 54 | path(UFO); 55 | ctx.stroke(); 56 | } 57 | -------------------------------------------------------------------------------- /src/shaders/game.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 uv; 4 | uniform sampler2D G; // game 5 | uniform sampler2D R; // persistence 6 | uniform sampler2D B; // blur 7 | uniform sampler2D L; // glare 8 | uniform sampler2D E; // env (player) 9 | uniform float s; // starting 10 | uniform float F; // fail factor (red effect) 11 | uniform vec2 k; 12 | 13 | float squircleDist (vec2 a, vec2 b) { 14 | float p = 10.0; 15 | vec2 c = a-b; 16 | return pow(abs(pow(abs(c.x), p)+pow(abs(c.y), p)), 1.0/p); 17 | } 18 | 19 | void main() { 20 | vec2 UV = uv + k; 21 | vec2 pos = (UV/0.98)-0.01; 22 | float d = squircleDist(UV, vec2(0.5)); 23 | float dd = smoothstep(0.45, 0.51, d); 24 | pos = mix(pos, vec2(0.5), 0.2 * (0.6 - d) - 0.02 * d); 25 | 26 | vec3 gc = texture2D(G, pos).rgb; 27 | 28 | gl_FragColor = 29 | step(0.0, UV.x) * 30 | step(UV.x, 1.0) * 31 | step(0.0, UV.y) * 32 | step(UV.y, 1.0) * 33 | vec4(( 34 | vec3(0.03 + 0.1 * F, 0.04, 0.05) + 35 | mix(vec3(0.05, 0.1, 0.15) - gc, 2.0 * gc, s) + 36 | s * ( 37 | texture2D(L, pos).rgb + 38 | vec3(0.3 + F, 0.6, 1.0) * ( 39 | texture2D(R, pos).rgb + 40 | 3.0 * texture2D(B, pos).rgb 41 | ) + 42 | 0.5 * texture2D(E, pos).rgb 43 | ) 44 | ) 45 | * mix(1.0, smoothstep(1.0, 0.0, dd), 0.6), 1.0); 46 | } 47 | -------------------------------------------------------------------------------- /src/sounds.js: -------------------------------------------------------------------------------- 1 | /* global audio */ 2 | 3 | var Ashot = audio([0,0.06,0.18,,0.33,0.5,0.23,-0.04,-0.24,,,-0.02,,0.37,-0.22,,,,0.8,,,,,0.3]), 4 | 5 | Amusic1 = audio([,,0.12,,0.13,0.16,,,,,,,,,,,,,0.7,,,,,0.5]), 6 | Amusic2 = audio([,,0.12,,0.13,0.165,,,,,,,,,,,,,0.7,,,,,0.5]), 7 | 8 | Aexplosion1 = audio([3,,0.35,0.5369,0.5,0.15,,-0.02,,,,-0.7444,0.78,,,0.7619,,,0.1,,,,,0.5]), 9 | Aexplosion2 = audio([3,,0.38,0.5369,0.52,0.18,,-0.02,,,,-0.7444,0.78,,,0.7619,,,0.1,,,,,0.5]), 10 | 11 | Asend = audio([2,0.07,0.04,,0.24,0.25,,0.34,-0.1999,,,-0.02,,0.3187,,,-0.14,0.04,0.85,,0.28,0.63,,0.5]), 12 | AsendFail = audio([1,,0.04,,0.45,0.14,0.06,-0.06,0.02,0.87,0.95,-0.02,,0.319,,,-0.14,0.04,0.5,,,,,0.4]), 13 | 14 | Alost = audio([0,0.11,0.37,,0.92,0.15,,-0.06,-0.04,0.3,0.14,0.1,,0.5047,,,,,0.16,-0.02,,0.5,,1]), 15 | Aleave = audio([0,0.11,0.36,,0.66,0.19,,0.06,-0.06,0.05,0.8,-0.12,0.3,0.19,-0.06,,,-0.02,0.23,-0.02,,0.4,,0.4]), 16 | Acoin = audio([0,,0.094,0.29,0.42,0.563,,,,,,0.4399,0.5658,,,,,,1,,,,,0.5]), 17 | Amsg = audio([2,0.07,0.1,,0.2,0.75,0.35,-0.1,0.12,,,-0.02,,,,,-0.06,-0.0377,0.26,,,0.8,,0.7]), 18 | Aufo = audio([2,0.05,0.74,,0.33,0.5,,,,0.46,0.29,,,,,,,,1,,,,,0.3]), 19 | Alife = audio([0,0.12,0.8,0.48,0.77,0.92,,-0.12,-0.0999,,,-0.4,0.2,0.34,,0.65,,,0.93,-0.02,,,,0.38]), 20 | Ajump = audio([3,,0.12,0.56,0.27,0.07,,-0.12,0.02,,,-0.02,0.68,,,,-0.04,-0.022,0.06,,,0.06,,0.5]); 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gre-js13k-2014", 3 | "version": "0.0.0", 4 | "description": "", 5 | "scripts": { 6 | "clean": "rm -rf build/; mkdir -p build target", 7 | "compileglsl": "./scripts/compileglslfiles.sh src/shaders build", 8 | "concat": "./scripts/concat.sh > build/build.js", 9 | "minify": "uglifyjs build/build.js -c --screw-ie8 -m -o build/build.min.js", 10 | "nominify": "cp build/build.js build/build.min.js", 11 | "gen": "cp src/target.html target/index.html && cp build/build.min.js target/b.js", 12 | "build": "export NODE_ENV=production; npm run clean && npm run compileglsl && npm run concat && npm run minify && npm run gen && npm run zip", 13 | "build-nominify": "npm run clean && npm run compileglsl && npm run concat && npm run nominify && npm run gen", 14 | "watch": "npm run build-nominify; wr 'npm run build-nominify' src/ scripts/", 15 | "liveserver": "mkdir -p target; cd target; live-server --no-browser", 16 | "zip": "cd target; zip -r ../target.zip .; cd ..; wc -c target.zip", 17 | "start": "npm run watch & npm run liveserver" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/gre/js13k-2014.git" 22 | }, 23 | "author": "", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/gre/js13k-2014/issues" 27 | }, 28 | "homepage": "https://github.com/gre/js13k-2014", 29 | "devDependencies": { 30 | "browserify": "^11.0.1", 31 | "eslint": "^1.3.0", 32 | "glslmin": "0.0.0", 33 | "live-server": "^0.8.1", 34 | "uglify-js": "^2.4.24", 35 | "uglifycss": "^0.0.17", 36 | "wr": "^1.3.1" 37 | }, 38 | "dependencies": { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/state.js: -------------------------------------------------------------------------------- 1 | /* global W H */ 2 | 3 | // N.B: constants don't live here 4 | 5 | var t = 0, dt, 6 | 7 | spaceship = [ W/2, H/2, 0, 0, 0 ], // [x, y, velx, vely, rot] 8 | asteroids = [], // array of [x, y, rot, vel, shape, lvl] 9 | ufos = [], // array of [x, y, vx, vy, timeBeforeShot] 10 | bullets = [], // array of [x, y, velx, vely, life, isAlien] 11 | incomingObjects = [], // array of: [pos, vel, ang, force, rotVel, shape, lvl, key, rotAmp, rotAmpValid, explodeTime] 12 | particles = [], // array of [x, y, rot, vel, life] 13 | 14 | dying = 0, 15 | resurrectionTime = 0, 16 | best = 0, 17 | score = 0, // current asteroids player score 18 | scoreForLife, // will track the next score to win a life (10000, 20000, ...) 19 | playingSince = -10000, 20 | deads = 0, 21 | player = 0, 22 | lifes = 0, 23 | 24 | AIshoot = 0, AIboost = 0, AIrotate = 0, AIexcitement = 0, 25 | AIboostSmoothed = 0, 26 | 27 | shaking = [0,0], 28 | jumping = 0, 29 | jumpingFreq = 0, 30 | jumpingPhase = 0, 31 | jumpingFreqSmoothed = 0, 32 | jumpingAmp = 0, 33 | jumpingAmpSmoothed = 0, 34 | killSmoothed = 0, 35 | 36 | musicPhase = 0, 37 | musicTick = 0, 38 | musicPaused = 0, 39 | ufoMusicTime = 0, 40 | 41 | excitementSmoothed = 0, 42 | neverPlayed = 1, 43 | neverUFOs = 1, 44 | combos = 0, 45 | combosTarget, 46 | gameOver, 47 | awaitingContinue = localStorage.ba_pl && parseInt(localStorage.ba_pl), 48 | // achievements: [nbAsteroids, nbKills, nbUfos] 49 | achievements, 50 | 51 | lastScoreIncrement = 0, 52 | lastJump = 0, 53 | lastBulletShoot = 0, 54 | lastExtraLife = 0, 55 | lastLoseShot = 0, 56 | 57 | // Input state : updated by user events, handled & emptied by the update loop 58 | keys = {}, 59 | tap, 60 | 61 | // variables related to setup 62 | gameScale; 63 | 64 | 65 | function helpVisible () { 66 | return neverPlayed && 67 | incomingObjects[0] && 68 | playingSince>8000; 69 | } 70 | -------------------------------------------------------------------------------- /src/spaceship.js: -------------------------------------------------------------------------------- 1 | /* global 2 | ctx t path lifes play Alost AIboostSmoothed dying:true deads:true achievements killSmoothed:true 3 | */ 4 | 5 | function spaceshipDie() { 6 | if (dying) return; 7 | dying = t; 8 | if (lifes == 1) { 9 | play(Alost); 10 | } 11 | deads ++; 12 | achievements[1] ++; 13 | killSmoothed ++; 14 | } 15 | 16 | /* 17 | function resetSpaceship () { 18 | var x = W * (0.25 + 0.5 * Math.random()); 19 | var y = H * (0.25 + 0.5 * Math.random()); 20 | spaceship = [x, y, 0, 0]; 21 | } 22 | */ 23 | 24 | // RENDERING 25 | 26 | function drawSpaceship (o) { 27 | ctx.strokeStyle = "#f00"; 28 | ctx.globalAlpha = 0.4; 29 | ctx.rotate(o[4]); 30 | if (dying) { 31 | ctx.lineWidth = 2; 32 | var delta = (t-dying)/200; 33 | 34 | path([ 35 | [-6, -6 - 0.5*delta], 36 | [3, -3 - 0.9*delta] 37 | ]); 38 | ctx.stroke(); 39 | 40 | if (delta < 8) { 41 | path([ 42 | [3 + 0.4*delta, -3 - 0.8*delta], 43 | [12 + 0.4*delta, 0 - 0.5*delta] 44 | ]); 45 | ctx.stroke(); 46 | } 47 | 48 | path([ 49 | [12, 0+0.4*delta], 50 | [3, 3+delta] 51 | ]); 52 | ctx.stroke(); 53 | 54 | if (delta < 9) { 55 | path([ 56 | [1, 5 + delta], 57 | [-6, 6 + delta] 58 | ]); 59 | ctx.stroke(); 60 | } 61 | 62 | if (delta < 7) { 63 | path([ 64 | [-6 - delta, -6], 65 | [-6 - delta, 6] 66 | ]); 67 | ctx.stroke(); 68 | } 69 | } 70 | else { 71 | path([ 72 | [-6, -6], 73 | [ 12, 0], 74 | [ -6, 6], 75 | [ -5, 0] 76 | ]); 77 | ctx.stroke(); 78 | if (AIboostSmoothed>0.2) { 79 | path([ 80 | [-7, 2*Math.random()-1], 81 | [-7 - 5*AIboostSmoothed, 4*Math.random()-2] 82 | ]); 83 | ctx.stroke(); 84 | } 85 | if (AIboostSmoothed<-0.2) { 86 | path([ 87 | [2, -5], 88 | [2 - 5 * AIboostSmoothed, -7], 89 | , 90 | [2, 5], 91 | [2 - 5 * AIboostSmoothed, 7] 92 | ]); 93 | ctx.stroke(); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/input.js: -------------------------------------------------------------------------------- 1 | /* global 2 | keys 3 | tap:true 4 | MOBILE 5 | c 6 | d 7 | gameOver 8 | W 9 | gameScale 10 | achievements:true 11 | player:true 12 | playingSince:true 13 | awaitingContinue:true 14 | */ 15 | 16 | for (var i=0; i<99; ++i) keys[i] = 0; 17 | 18 | var fullScreenRequested = 0; 19 | function onTap (e) { 20 | if (MOBILE && !fullScreenRequested && d.webkitRequestFullScreen){ 21 | d.webkitRequestFullScreen(); 22 | fullScreenRequested = 1; 23 | } 24 | 25 | var r = c.getBoundingClientRect(), 26 | x = (e.clientX - r.left) / gameScale, 27 | y = (e.clientY - r.top) / gameScale; 28 | if (gameOver) { 29 | if(280 < y && y < 400) { 30 | if (W/2 - 180 < x && x < W/2 - 20) { 31 | open("https://twitter.com/intent/tweet?via=greweb&url="+ 32 | encodeURIComponent(location.href)+ 33 | "&text="+ 34 | encodeURIComponent( 35 | "Reached Level "+player+ 36 | " ("+(player*25)+"¢) with "+ 37 | achievements[0]+"⬠ "+ 38 | achievements[1]+"ᐃ "+ 39 | achievements[2]+"🝞" 40 | )); 41 | } 42 | else if (W/2 + 20 < x && x < W/2 + 180) { 43 | location.reload(); 44 | } 45 | } 46 | } 47 | else if (awaitingContinue) { 48 | if (playingSince>0 && 170 1) { 44 | var nb = Math.round(2+1.5*Math.random()); 45 | for (var k=0; k 1) gameScale = 1; 53 | d.style.webkitTransform = d.style.transform = "scale("+gameScale+")"; 54 | d.style.top = Math.max(10, Math.floor((wh - (FH+20)*gameScale)/2))+"px"; 55 | d.style.left = Math.max(0, Math.floor((ww - FW*gameScale)/2))+"px"; 56 | } 57 | 58 | 59 | // WebGL setup 60 | 61 | gl.viewport(0, 0, W, H); 62 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); 63 | 64 | var buffer = gl.createBuffer(); 65 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 66 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 67 | -1.0, -1.0, 68 | 1.0, -1.0, 69 | -1.0, 1.0, 70 | -1.0, 1.0, 71 | 1.0, -1.0, 72 | 1.0, 1.0 73 | ]), gl.STATIC_DRAW); 74 | 75 | var blur1dShader = glCreateShader(STATIC_VERT, BLUR1D_FRAG); 76 | gl.uniform2f(glUniformLocation(blur1dShader, "dim"), W, H); 77 | var copyShader = glCreateShader(STATIC_VERT, COPY_FRAG); 78 | var laserShader = glCreateShader( STATIC_VERT, LASER_FRAG); 79 | var persistenceShader = glCreateShader(STATIC_VERT, PERSISTENCE_FRAG); 80 | var glareShader = glCreateShader(STATIC_VERT, GLARE_FRAG); 81 | var playerShader = glCreateShader(STATIC_VERT, PLAYER_FRAG); 82 | gl.uniform1f(glUniformLocation(playerShader, "S"), SEED); 83 | var gameShader = glCreateShader(STATIC_VERT, GAME_FRAG); 84 | 85 | var persistenceFbo = glCreateFBO(); 86 | var playerFbo = glCreateFBO(); 87 | var glareFbo = glCreateFBO(); 88 | var laserFbo = glCreateFBO(); 89 | var fbo1 = glCreateFBO(); 90 | var fbo2 = glCreateFBO(); 91 | 92 | var textureGame = glCreateTexture(); 93 | -------------------------------------------------------------------------------- /src/shaders/player.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec2 uv; 4 | 5 | uniform float pt; // playing since time 6 | uniform float pl; // player number 7 | uniform float S; // Seed 8 | uniform float ex; // excitement 9 | uniform float J; // jump 10 | uniform float P; // playing 11 | 12 | float disc (vec2 c, vec2 r) { 13 | return step(length((uv - c) / r), 1.0); 14 | } 15 | float squircle (vec2 c, vec2 r, float p) { 16 | vec2 v = (uv - c) / r; 17 | return step(pow(abs(v.x), p) + pow(abs(v.y), p), 1.0); 18 | } 19 | 20 | vec3 env () { 21 | return 0.1 + 22 | 0.3 * vec3(1.0, 0.9, 0.7) * smoothstep(0.4, 0.1, distance(uv, vec2(0.2, 1.2))) + 23 | 0.4 * vec3(0.8, 0.6, 1.0) * smoothstep(0.5, 0.2, distance(uv, vec2(1.3, 0.7))); 24 | } 25 | 26 | vec4 player (float p, float dx) { 27 | vec4 c = vec4(0.0); 28 | 29 | vec2 e = vec2( 30 | min(ex, 1.0), 31 | mix(min(ex, 1.0), min(ex-1.0, 1.0), 0.5)); 32 | 33 | // variable params 34 | vec4 skin = 0.2 + 0.4 * pow(abs(cos(4.*p+S)), 2.0) * vec4(1.0, 0.7, 0.3, 1.0); 35 | vec4 hair = vec4(0.5, 0.3, 0.3, 1.0); 36 | vec4 sweater = vec4( 37 | 0.3 * (1.0 + cos(3.*p + 6.*S)), 38 | 0.2 * (1.0 + cos(7.*p + 7.*S)), 39 | 0.1+0.2 * (1.0 + sin(7.*p + 8.*S)), 40 | 1.0); 41 | float feminity = step(sin(9.0*p+S), 0.0); 42 | float hairSize = 0.02 + 0.02 * feminity * cos(p+S); 43 | float walk = step(dx, -0.01) + step(0.01, dx); 44 | float play = (1.0 - walk) * step(0.0, pt); 45 | vec2 pos = vec2(0.5) + 46 | // jumping cycle 47 | J * vec2(0.0, 0.2) + 48 | // walking cycle 49 | walk * vec2( 50 | 0.03 * cos(4.0*pt + sin(pt)), 51 | 0.05 * abs(sin(3.0*pt))) + 52 | // playing cycle 53 | e * play * (1.0 - P) * vec2( 54 | 0.05 * cos(pt * (1.0 + 0.1 * sin(pt))), 55 | 0.05 * abs(sin(pt))); 56 | vec2 pos2 = mix(pos, vec2(0.5), 0.5); 57 | pos.x += dx; 58 | pos2.x += dx; 59 | 60 | // face skin 61 | c += skin * disc(pos, vec2(0.06, 0.1)); 62 | // mouth 63 | c *= 1.0 - (0.5 + 0.5 * feminity) * disc(pos - vec2(0.0, 0.04), vec2(0.03, 0.01)); 64 | // left eye 65 | c *= 1.0 - disc(pos + vec2(0.03, 0.03), vec2(0.02, 0.01)); 66 | // right eye 67 | c *= 1.0 - disc(pos + vec2(-0.03, 0.03), vec2(0.02, 0.01)); 68 | // nose 69 | c *= 1.0 - 0.6 * disc(pos, vec2(0.01, 0.02)); 70 | // hair (also contrib to face skin color) 71 | c += hair * disc(pos + vec2(0.0, hairSize), vec2(0.07, 0.1 + hairSize)); 72 | // left hand 73 | c += play * (hair + skin) * disc(pos2 - vec2( 74 | -0.2 + 0.01 * cos(5.0*pt), 75 | 0.45 - 0.1 * e.y * step(0.0, pt) * P * pow(abs(sin(8.0 * pt * (1.0 + 0.2 * cos(pt)))), 4.0) 76 | ), vec2(0.055, 0.05)); 77 | // right hand 78 | c += play * (hair + skin) * disc(pos2 - vec2( 79 | 0.2 + 0.01 * cos(5.0*pt), 80 | 0.45 - 0.1 * e.x * step(2.0, pt) * P * pow(abs(cos(7.0 * pt)), 4.0) 81 | ), vec2(0.055, 0.05)); 82 | // neck 83 | c += step(c.a, 0.0) * (hair + skin) * 84 | squircle(pos - vec2(0.0, 0.10 + 0.02 * feminity), 85 | vec2(0.05 - 0.01 * feminity, 0.03), 4.0); 86 | // sweater 87 | vec2 sr = vec2( 88 | 0.16 + 0.04 * sin(9.*p), 89 | 0.27 + 0.02 * cos(9.*p)); 90 | c += step(c.r+c.g+c.b, 0.0) * sweater * step(1.0, 91 | squircle(pos - vec2(0.0, 0.35), sr * (1.0 - 0.1 * feminity), 4.0) + 92 | disc(pos - vec2(0.0, 0.35), sr)); 93 | return c; 94 | } 95 | 96 | void main() { 97 | float light = 0.6 + 0.4 * smoothstep(2.0, 0.0, distance(pt, -2.0)); 98 | vec4 c = vec4(0.0); 99 | // main player 100 | c += (1.0 - smoothstep(-0.0, -5.0, pt)) * 101 | player(pl+step(pt, 0.0), -0.6 * smoothstep(-1., -5., pt)); 102 | // prev player 103 | c += step (1.0, pl) * 104 | player(pl+step(pt, 0.0)-1.0, 2.0 *smoothstep(-4., -1., pt)); 105 | c *= 1.0 - 1.3 * distance(uv, vec2(0.5)); 106 | gl_FragColor = vec4(light * mix(env(), c.rgb, clamp(c.a, 0.0, 1.0)), 1.0); 107 | } 108 | -------------------------------------------------------------------------------- /src/lib/asteroids.font.js: -------------------------------------------------------------------------------- 1 | /* global ctx, path */ 2 | 3 | // implementing by hand Asteroid's font. see http://www.dafont.com/hyperspace.font 4 | 5 | var FONT0 = [ // 0 6 | [0, 0], 7 | [2, 0], 8 | [2, 2], 9 | [0, 2], 10 | [0, 0] 11 | ]; 12 | var FONT5 = [ // 5 13 | [2, 0], 14 | [0, 0], 15 | [0, 1], 16 | [2, 1], 17 | [2, 2], 18 | [0, 2] 19 | ]; 20 | var FONT = [ 21 | FONT0, 22 | [ // 1 23 | [1, 0], 24 | [1, 2] 25 | ], 26 | [ // 2 27 | [0, 0], 28 | [2, 0], 29 | [2, 1], 30 | [0, 1], 31 | [0, 2], 32 | [2, 2] 33 | ], 34 | [ // 3 35 | [0, 0], 36 | [2, 0], 37 | [2, 2], 38 | [0, 2], 39 | , 40 | [0, 1], 41 | [2, 1] 42 | ], 43 | [ // 4 44 | [0, 0], 45 | [0, 1], 46 | [2, 1], 47 | , 48 | [2, 0], 49 | [2, 2] 50 | ], 51 | FONT5, 52 | [ // 6 53 | [0, 0], 54 | [0, 2], 55 | [2, 2], 56 | [2, 1], 57 | [0, 1] 58 | ], 59 | [ // 7 60 | [0, 0], 61 | [2, 0], 62 | [2, 2] 63 | ], 64 | [ // 8 65 | [0, 0], 66 | [2, 0], 67 | [2, 2], 68 | [0, 2], 69 | [0, 0], 70 | , 71 | [0, 1], 72 | [2, 1] 73 | ], 74 | [ // 9 75 | [2, 2], 76 | [2, 0], 77 | [0, 0], 78 | [0, 1], 79 | [2, 1] 80 | ] 81 | ]; 82 | [ 83 | [// A 84 | [0,2], 85 | [0,2/3], 86 | [1,0], 87 | [2,2/3], 88 | [2,2], 89 | , 90 | [0,4/3], 91 | [2,4/3] 92 | ], 93 | [ // B 94 | [0, 1], 95 | [0, 0], 96 | [4/3,0], 97 | [2,1/3], 98 | [2,2/3], 99 | [4/3,1], 100 | [0,1], 101 | [0,2], 102 | [4/3,2], 103 | [2,5/3], 104 | [2,4/3], 105 | [4/3,1] 106 | ], 107 | [// C 108 | [2,0], 109 | [0,0], 110 | [0,2], 111 | [2,2] 112 | ], 113 | [// D 114 | [0,0], 115 | [1,0], 116 | [2,2/3], 117 | [2,4/3], 118 | [1,2], 119 | [0,2], 120 | [0,0] 121 | ], 122 | [// E 123 | [2,0], 124 | [0,0], 125 | [0,2], 126 | [2,2], 127 | , 128 | [0,1], 129 | [1.5,1] 130 | ], 131 | [// F 132 | [2,0], 133 | [0,0], 134 | [0,2], 135 | , 136 | [0,1], 137 | [2,1] 138 | ], 139 | [// G 140 | [2,2/3], 141 | [2,0], 142 | [0,0], 143 | [0,2], 144 | [2,2], 145 | [2,4/3], 146 | [1,4/3] 147 | ], 148 | [// H 149 | [0,0], 150 | [0,2], 151 | , 152 | [2,0], 153 | [2,2], 154 | , 155 | [0,1], 156 | [2,1] 157 | ], 158 | [// I 159 | [0,0], 160 | [2,0], 161 | , 162 | [1,0], 163 | [1,2], 164 | , 165 | [0,2], 166 | [2,2] 167 | ], 168 | [// J 169 | [2,0], 170 | [2,2], 171 | [1,2], 172 | [0,4/3] 173 | ], 174 | [// K 175 | [0,0], 176 | [0,2], 177 | , 178 | [2,0], 179 | [0,1], 180 | [2,2] 181 | ], 182 | [// L 183 | [0,0], 184 | [0,2], 185 | [2,2] 186 | ], 187 | [// M 188 | [0,2], 189 | [0,0], 190 | [1,2/3], 191 | [2,0], 192 | [2,2] 193 | ], 194 | [// N 195 | [0,2], 196 | [0,0], 197 | [2,2], 198 | [2,0] 199 | ], 200 | FONT0,// O 201 | [// P 202 | [0,2], 203 | [0,0], 204 | [2,0], 205 | [2,1], 206 | [0,1] 207 | ], 208 | [// Q 209 | [0,0], 210 | [2,0], 211 | [2,4/3], 212 | [1,2], 213 | [0,2], 214 | [0,0], 215 | , 216 | [2,2], 217 | [1,4/3] 218 | ], 219 | [// R 220 | [0,2], 221 | [0,0], 222 | [2,0], 223 | [2,1], 224 | [0,1], 225 | [2,2] 226 | ], 227 | FONT5,// S 228 | [// T 229 | [0,0], 230 | [2,0], 231 | , 232 | [1,0], 233 | [1,2] 234 | ], 235 | [// U 236 | [0,0], 237 | [0,2], 238 | [2,2], 239 | [2,0] 240 | ], 241 | [// V 242 | [0,0], 243 | [1,2], 244 | [2,0] 245 | ], 246 | [// W 247 | [0,0], 248 | [0,2], 249 | [1,4/3], 250 | [2,2], 251 | [2,0] 252 | ], 253 | [// X 254 | [0,0], 255 | [2,2], 256 | , 257 | [2,0], 258 | [0,2] 259 | ], 260 | [// Y 261 | [0,0], 262 | [1,2/3], 263 | [2,0], 264 | , 265 | [1,2/3], 266 | [1,2] 267 | ], 268 | [// Z 269 | [0,0], 270 | [2,0], 271 | [0,2], 272 | [2,2] 273 | ] 274 | ].forEach(function (c, i) { 275 | FONT[String.fromCharCode(65+i)] = c; 276 | }); 277 | 278 | var dot = FONT["."] = [ 279 | [1, 1.8], 280 | [1, 2] 281 | ]; 282 | 283 | FONT[":"] = [ 284 | [1, 0], 285 | [1, 0.2], 286 | , 287 | [1, 1.8], 288 | [1, 2] 289 | ]; 290 | 291 | FONT["'"] = [ 292 | [1, 0], 293 | [1, 2/3] 294 | ]; 295 | 296 | FONT["ᐃ"] = [ 297 | [ 1, 0 ], 298 | [ 1.8, 2 ], 299 | [ 1, 1.6 ], 300 | [ 0.2, 2 ], 301 | [ 1, 0 ] 302 | /* 303 | [-4, -4], 304 | [ 10, 0], 305 | [ -4, 4], 306 | [ -3, 0] 307 | */ 308 | ]; 309 | 310 | FONT["!"] = [ 311 | [1, 0], 312 | [1, 1.5], 313 | , 314 | ].concat(dot); 315 | FONT["?"] = [ 316 | [0, 0], 317 | [2, 0], 318 | [2, 1], 319 | [1, 1], 320 | [1, 1.5], 321 | , 322 | ].concat(dot); 323 | FONT["x"] = [ 324 | [0,1], 325 | [2,2], 326 | , 327 | [2,1], 328 | [0,2] 329 | ]; 330 | FONT["¢"] = [ 331 | [1,0], 332 | [1,2], 333 | , 334 | [1.5,0.5], 335 | [0.5,0.5], 336 | [0.5,1.5], 337 | [1.5,1.5] 338 | ]; 339 | 340 | // oO ASTEROIDS font with fontSize and align (-1:right, 0:center, 1:left) 341 | // will side effect some ctx.translate() (that you could benefit to make text follow) 342 | function font (txt, fontSize, align) { // eslint-disable-line 343 | var l = fontSize*11*txt.length; 344 | ctx.translate(align ? (align>0 ? 0 : -l) : -l/2, 0); 345 | for (var i=0; i4 ? 0.5 * smoothstep(-1, 1, Math.cos(0.01*t)) : 0); 137 | gl.uniform2f(glUniformLocation(gameShader, "k"), shaking[0], shaking[1]); 138 | gl.drawArrays(gl.TRIANGLES, 0, 6); 139 | } 140 | -------------------------------------------------------------------------------- /src/asteroidsIncoming.js: -------------------------------------------------------------------------------- 1 | /* global 2 | GAME_INC_PADDING W H t dt borderLength spaceship incomingObjects player 3 | playingSince randomAsteroidShape lifes dying ctx path MOBILE font helpVisible 4 | */ 5 | 6 | function incPosition (o) { 7 | var i = o[0] % borderLength; 8 | var x, y; 9 | var w = W + GAME_INC_PADDING; 10 | var h = H + GAME_INC_PADDING; 11 | if (i diffMax) diffMin *= Math.random(); 103 | 104 | 105 | var pRotAmp = diffMin + Math.random() * (diffMax-diffMin); 106 | var pRotAmpRatio = diffMin + Math.random() * (diffMax-diffMin); 107 | var pRotSpeed = diffMin + Math.random() * (diffMax-diffMin); 108 | 109 | var lvl = Math.floor(2 + 3 * Math.random() * Math.random() + 4 * Math.random() * Math.random() * Math.random()); 110 | var ampRot = player<2 ? 0 : Math.PI * (0.8 * Math.random() + 0.05 * lvl) * pRotAmp; 111 | if (ampRot < 0.2) ampRot = 0; 112 | var ampRotRatio = 113 | player > 2 && 114 | ampRot > Math.exp(-player/4) && 115 | Math.random() > 0.5 + 0.4 * ((player-3)%8)/8 - 0.5 * (1 - Math.exp(-player/10)) ? 116 | 0.9 - 0.5 * pRotAmpRatio - 0.2 * pRotAmp : 117 | 1; 118 | 119 | if (player == 2) { 120 | ampRot = 0.2 + Math.random(); 121 | } 122 | 123 | if (player == 3) { 124 | ampRot = 0.2 + Math.random(); 125 | ampRotRatio = 0.5 + 0.4 * Math.random(); 126 | } 127 | 128 | incomingObjects.push([ 129 | pos, 130 | // velocity 131 | 0.1 + 0.002 * player, 132 | // initial angle 133 | 2*Math.PI*Math.random(), 134 | // initial force 135 | 10 + 40*Math.random(), 136 | // rot velocity 137 | 0.002 + 0.001 * (Math.random() + 0.5 * lvl * Math.random() + Math.random() * player / 30) * pRotSpeed - 0.001 * pRotAmp, 138 | // shape 139 | randomAsteroidShape(lvl), 140 | // level 141 | lvl, 142 | // key 143 | key, 144 | // amplitude rotation 145 | ampRot, 146 | // amplitude rotation valid ratio 147 | ampRotRatio, 148 | // explode time 149 | 0 150 | ]); 151 | return 1; 152 | } 153 | 154 | function applyIncLogic (o) { 155 | if (!o[10]) { 156 | o[0] += o[1] * dt; 157 | o[2] += o[4] * dt; 158 | o[3] = o[3] < 10 ? 60 : o[3] - 0.02 * dt; 159 | } 160 | } 161 | 162 | // RENDERING 163 | 164 | function drawInc (o) { 165 | var rotC = incRotationCenter(o); 166 | var phase = Math.cos(o[2]); 167 | var rot = phase * o[8] + rotC; 168 | var w = 10 * o[6]; 169 | var valid = Math.abs(phase) < o[9]; 170 | 171 | if (playingSince>0 && lifes && !dying && !o[10]) { 172 | ctx.lineWidth = 1+o[3]/60; 173 | ctx.strokeStyle = valid ? "#7cf" : "#f66"; 174 | 175 | if (o[8] > 0.1) { 176 | ctx.save(); 177 | ctx.rotate(rotC); 178 | ctx.strokeStyle = "#f66"; 179 | ctx.beginPath(); 180 | ctx.arc(0, 0, w+10, -o[8], -o[8]*o[9]); 181 | ctx.stroke(); 182 | ctx.beginPath(); 183 | ctx.arc(0, 0, w+10, o[8]*o[9], o[8]); 184 | ctx.stroke(); 185 | ctx.strokeStyle = "#7cf"; 186 | ctx.beginPath(); 187 | ctx.arc(0, 0, w+10, -o[8] * o[9], o[8] * o[9]); 188 | ctx.stroke(); 189 | path([ 190 | [w+8, 0], 191 | [w+12, 0] 192 | ]); 193 | ctx.stroke(); 194 | ctx.restore(); 195 | } 196 | 197 | ctx.save(); 198 | ctx.rotate(rot); 199 | ctx.save(); 200 | var mx = 60 + w; 201 | var x = o[3] + w; 202 | ctx.globalAlpha = 0.2; 203 | path([ 204 | [0,0], 205 | [mx,0] 206 | ]); 207 | ctx.stroke(); 208 | ctx.restore(); 209 | path([ 210 | [0,0], 211 | [x,0] 212 | ]); 213 | ctx.stroke(); 214 | var r = 6; 215 | path([ 216 | [ mx - r, r ], 217 | [ mx, 0], 218 | [ mx - r, -r ] 219 | ], 1); 220 | ctx.stroke(); 221 | ctx.restore(); 222 | } 223 | else { 224 | ctx.strokeStyle = o[10] ? "#f66" : "#999"; 225 | } 226 | 227 | ctx.save(); 228 | path(o[5]); 229 | ctx.fillStyle = "#000"; 230 | ctx.fill(); 231 | ctx.stroke(); 232 | ctx.restore(); 233 | 234 | var sum = [0, 0]; 235 | o[5].forEach(function (p) { 236 | sum[0] += p[0]; 237 | sum[1] += p[1]; 238 | }); 239 | 240 | if (!MOBILE && playingSince>0) { 241 | if (helpVisible()) { 242 | ctx.strokeStyle = "#f7c"; 243 | } 244 | ctx.translate(sum[0]/o[5].length+1, sum[1]/o[5].length-5); 245 | font(String.fromCharCode(o[7]), 1); 246 | } 247 | } 248 | 249 | function drawIncHelp () { 250 | if (!helpVisible()) return; 251 | ctx.strokeStyle = "#f7c"; 252 | ctx.lineWidth = 4; 253 | incomingObjects.forEach(function (o) { 254 | var p = incPosition(o); 255 | ctx.beginPath(); 256 | ctx.arc(p[0], p[1], 80 + 40 * Math.cos(0.005 * t), 0, 2*Math.PI); 257 | ctx.stroke(); 258 | }); 259 | } 260 | -------------------------------------------------------------------------------- /src/ai.js: -------------------------------------------------------------------------------- 1 | /* global 2 | DEBUG 3 | AIrotate: true 4 | AIboost: true 5 | AIshoot: true 6 | AIexcitement: true 7 | spaceship 8 | t dt 9 | asteroids 10 | bullets 11 | W H 12 | dist normAngle 13 | ufos 14 | playingSince 15 | ctx 16 | */ 17 | 18 | /* 19 | if (DEBUG) { 20 | /* eslint-disable no-inner-declarations 21 | var AIdebug = [], AIdebugCircle = []; 22 | function drawAIDebug () { 23 | AIdebug.forEach(function (debug, i) { 24 | ctx.save(); 25 | ctx.lineWidth = 2; 26 | ctx.fillStyle = ctx.strokeStyle = "hsl("+Math.floor(360*i/AIdebug.length)+",80%,50%)"; 27 | ctx.beginPath(); 28 | ctx.moveTo(debug[0], debug[1]); 29 | ctx.lineTo(debug[2], debug[3]); 30 | ctx.stroke(); 31 | ctx.beginPath(); 32 | ctx.arc(debug[0], debug[1], 2, 0, 2*Math.PI); 33 | ctx.fill(); 34 | ctx.restore(); 35 | }); 36 | AIdebugCircle.forEach(function (debug, i) { 37 | ctx.save(); 38 | ctx.lineWidth = 2; 39 | ctx.fillStyle = ctx.strokeStyle = "hsl("+Math.floor(360*i/AIdebugCircle.length)+",80%,50%)"; 40 | ctx.beginPath(); 41 | ctx.arc(debug[0], debug[1], Math.max(0, debug[2] * debug[3]), 0, 2*Math.PI); 42 | ctx.stroke(); 43 | ctx.lineWidth = 1; 44 | ctx.beginPath(); 45 | ctx.arc(debug[0], debug[1], debug[3], 0, 2*Math.PI); 46 | ctx.stroke(); 47 | ctx.textAlign = "center"; 48 | ctx.textBaseline = "bottom"; 49 | ctx.fillText(debug[2].toFixed(2), debug[0], debug[1]-debug[3]-2); 50 | ctx.restore(); 51 | }); 52 | } 53 | function clearDebug () { 54 | AIdebug = []; 55 | AIdebugCircle = []; 56 | } 57 | function addDebugCircle (p, value, radius) { 58 | AIdebugCircle.push([ p[0], p[1], value, radius ]); 59 | } 60 | function addDebug (p, v) { 61 | var d = 200; 62 | AIdebug.push([ p[0], p[1], p[0]+(v?d*v[0]:0), p[1]+(v?d*v[1]:0) ]); 63 | } 64 | function addPolarDebug (p, ang, vel) { 65 | var v = [ 66 | vel * Math.cos(ang), 67 | vel * Math.sin(ang) 68 | ]; 69 | addDebug(p, v); 70 | } 71 | /* eslint-enable 72 | } 73 | */ 74 | 75 | var closestAsteroidMemory, targetShootMemory, closestAsteroidMemoryT, targetShootMemoryT; 76 | 77 | // AI states 78 | function aiLogic (smart) { // set the 3 AI inputs (rotate, shoot, boost) 79 | var i; 80 | 81 | // DEBUG && clearDebug(); 82 | 83 | // first part is data extraction / analysis 84 | 85 | //var ax = Math.cos(spaceship[4]); 86 | //var ay = Math.sin(spaceship[4]); 87 | var vel = Math.sqrt(spaceship[2]*spaceship[2]+spaceship[3]*spaceship[3]); 88 | var velAng = Math.atan2(spaceship[3], spaceship[2]); 89 | 90 | //var spaceshipVel = [ ax * vel, ay * vel ]; 91 | 92 | 93 | // utilities 94 | 95 | function orient (ang) { 96 | var stableAng = normAngle(ang - spaceship[4]); 97 | AIrotate = stableAng < 0 ? -1 : 1; 98 | return stableAng; 99 | } 100 | 101 | function move (ang, vel) { 102 | var stableAng = normAngle(ang - spaceship[4]); 103 | var abs = Math.abs(stableAng); 104 | if (abs > Math.PI/2) { 105 | if (vel) AIboost = abs>Math.PI/2-0.4 ? vel>0?-1:1 : 0; 106 | AIrotate = stableAng > 0 ? -1 : 1; 107 | } 108 | else { 109 | if (vel) AIboost = abs<0.4 ? vel<0?-1:1 : 0; 110 | AIrotate = stableAng < 0 ? -1 : 1; 111 | } 112 | } 113 | 114 | // take actions to move and stabilize to a point 115 | function moveToPoint (p, minDist) { 116 | var dx = p[0]-spaceship[0]; 117 | var dy = p[1]-spaceship[1]; 118 | if (dx*dx+dy*dy 0.003 * dist && Math.abs(normAngle(ang - velAng)) acceptDist) return; 175 | //DEBUG && addDebug(p, v); 176 | moveAwayFromPoint(p, v); 177 | } 178 | 179 | function predictShootIntersection (bulletVel, pos, target, targetVel) { 180 | // http://gamedev.stackexchange.com/a/25292 181 | var totarget = [ 182 | target[0] - pos[0], 183 | target[1] - pos[1] 184 | ]; 185 | var a = dot(targetVel, targetVel) - bulletVel * bulletVel; 186 | var b = 2 * dot(targetVel, totarget); 187 | var c = dot(totarget, totarget); 188 | var p = -b / (2 * a); 189 | var q = Math.sqrt((b * b) - 4 * a * c) / (2 * a); 190 | var t1 = p - q; 191 | var t2 = p + q; 192 | var t = t1 > t2 && t2 > 0 ? t2 : t1; 193 | 194 | return [t, [ 195 | target[0] + targetVel[0] * t, 196 | target[1] + targetVel[1] * t 197 | ]]; 198 | } 199 | 200 | var middle = [W/2,H/2]; 201 | 202 | var closestAsteroid, targetShoot, danger = 0; 203 | var closestAsteroidScore = 0.3, targetShootScore = 0.1; 204 | var incomingBullet, incomingBulletScore = 0; 205 | 206 | for (i = 0; i < asteroids.length; ++i) { 207 | var ast = asteroids[i]; 208 | // FIXME: take velocity of spaceship into account? 209 | var v = [ 210 | ast[3] * Math.cos(ast[2]), 211 | ast[3] * Math.sin(ast[2]) 212 | ]; 213 | var timeBeforeImpact = dot([ spaceship[0]-ast[0], spaceship[1]-ast[1] ],v)/dot(v,v); 214 | var impact = [ 215 | ast[0] + timeBeforeImpact * v[0], 216 | ast[1] + timeBeforeImpact * v[1] 217 | ]; 218 | var distToImpact = dist(spaceship, impact); 219 | var distWithSize = distToImpact - 10 - 10 * ast[5]; 220 | 221 | var score = 222 | Math.exp(-distWithSize/40) + 223 | Math.exp(-distWithSize/120) + 224 | timeBeforeImpact > 0 ? Math.exp(-timeBeforeImpact/1000) : 0; 225 | 226 | if (score > closestAsteroidScore) { 227 | closestAsteroidScore = score; 228 | closestAsteroid = ast; 229 | danger ++; 230 | } 231 | 232 | score = 233 | Math.exp(-(ast[5]-1)) * 234 | Math.exp(-distWithSize/200); 235 | 236 | if (score > targetShootScore) { 237 | var res = predictShootIntersection(0.3, spaceship, ast, v); 238 | var t = res[0]; 239 | var p = res[1]; 240 | if (0 incomingBulletScore) { 263 | incomingBulletScore = score; 264 | incomingBullet = impact; 265 | } 266 | } 267 | 268 | for (i = 0; i < ufos.length; ++i) { 269 | var u = ufos[i]; 270 | res = predictShootIntersection(0.3, spaceship, u, u.slice(2)); 271 | t = res[0]; 272 | p = res[1]; 273 | targetShoot = p; 274 | } 275 | 276 | AIexcitement = 277 | (1 - Math.exp(-asteroids.length/10)) + // total asteroids 278 | (1 - Math.exp(-danger/3)) // danger 279 | ; 280 | 281 | // Now we implement the spaceship reaction 282 | // From the least to the most important reactions 283 | 284 | // Dump random changes 285 | 286 | AIshoot = playingSince > 3000 && Math.random() < 0.001*dt*(1-smart); 287 | 288 | AIrotate = (playingSince > 1000 && Math.random()<0.002*dt) ? 289 | (Math.random()<0.6 ? 0 : Math.random() < 0.5 ? -1 : 1) : AIrotate; 290 | 291 | AIboost = (playingSince > 2000 && Math.random()<0.004*dt) ? 292 | (Math.random()<0.7 ? 0 : Math.random() < 0.5 ? -1 : 1) : AIboost; 293 | 294 | // Stay in center area 295 | 296 | if (0.1 + smart > Math.random()) moveToPoint(middle, 30); 297 | 298 | // Shot the target 299 | 300 | if (smart > Math.random()) { 301 | if (targetShoot) { 302 | AIshoot = 303 | Math.abs(orient(Math.atan2( 304 | targetShoot[1] - spaceship[1], 305 | targetShoot[0] - spaceship[0]))) < 0.1 && 306 | Math.random() < 0.04 * dt; 307 | targetShootMemory = targetShoot; 308 | targetShootMemoryT = t; 309 | } 310 | else { 311 | AIshoot = 0; 312 | } 313 | } 314 | 315 | // Avoid dangers 316 | if (smart > Math.random()) { 317 | if (closestAsteroid) { 318 | moveAwayFromAsteroid(closestAsteroid); 319 | closestAsteroidMemory = closestAsteroid; 320 | closestAsteroidMemoryT = closestAsteroid; 321 | } 322 | 323 | if (incomingBullet) moveAwayFromPoint(incomingBullet); 324 | } 325 | 326 | //DEBUG && targetShoot && addPolarDebug(targetShoot, 0, 0); 327 | //DEBUG && closestAsteroid && addPolarDebug(closestAsteroid, closestAsteroid[2], closestAsteroid[3]); 328 | } 329 | -------------------------------------------------------------------------------- /src/game.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-enable no-unused-vars */ 3 | 4 | randomAsteroids(); 5 | requestAnimationFrame(render); 6 | 7 | if (DEBUG) { 8 | /* eslint-disable */ 9 | /* 10 | // DEBUG the game over screen 11 | setTimeout(function () { 12 | playingSince = -1; 13 | awaitingContinue = 0; 14 | player = 42; 15 | achievements = [123, 45, 6]; 16 | gameOver = 1; 17 | }, 1000); 18 | */ 19 | // Debug the levels 20 | addEventListener("resize", function () { 21 | playingSince = -1; 22 | awaitingContinue = 0; 23 | player ++; 24 | incomingObjects = []; 25 | console.log("player=", player); 26 | /* 27 | ufos.push([ 28 | 0, 0, 0, 0, 0 29 | ]); 30 | */ 31 | 32 | }); 33 | 34 | /* 35 | setTimeout(function () { 36 | killSmoothed ++; 37 | }, 100); 38 | setTimeout(function () { 39 | killSmoothed ++; 40 | }, 2000); 41 | */ 42 | 43 | // Debug the incomingObjects 44 | 45 | /* 46 | setInterval(function () { 47 | createInc(); 48 | if (incomingObjects[0]) sendAsteroid(incomingObjects[0]); 49 | incomingObjects.splice(0, 1); 50 | }, 800); 51 | */ 52 | 53 | /* eslint-enable */ 54 | } 55 | 56 | // Game Render Loop 57 | 58 | var _lastT, _lastCheckSize = -9999; 59 | function render (_t) { 60 | requestAnimationFrame(render); 61 | if (!_lastT) _lastT = _t; 62 | dt = Math.min(100, _t-_lastT); 63 | _lastT = _t; 64 | 65 | if (t-_lastCheckSize>200) checkSize(); 66 | 67 | t += dt; // accumulate the game time (that is not the same as _t) 68 | 69 | // UPDATE game 70 | update(); 71 | 72 | // RENDER game 73 | 74 | // UI Rendering 75 | 76 | ctx = uiCtx; 77 | 78 | ctx.save(); 79 | 80 | ctx.scale(uiScale, uiScale); 81 | 82 | ctx.save(); 83 | ctx.clearRect(0, 0, FW, FH); 84 | 85 | drawUI(); 86 | 87 | ctx.translate(GAME_MARGIN, GAME_Y_MARGIN); 88 | 89 | //if (DEBUG) drawAIDebug(); 90 | 91 | incomingObjects.forEach(function (inc) { 92 | ctx.save(); 93 | translateTo(incPosition(inc)); 94 | drawInc(inc); 95 | ctx.restore(); 96 | }); 97 | 98 | drawIncHelp(); 99 | 100 | ctx.restore(); 101 | 102 | ctx.restore(); 103 | 104 | // Game rendering 105 | 106 | ctx = gameCtx; 107 | 108 | ctx.save(); 109 | 110 | drawGame(); 111 | 112 | ctx.restore(); 113 | 114 | // WEBGL after effects 115 | 116 | drawPostProcessing(); 117 | } 118 | 119 | // Game Update Loop 120 | 121 | function update () { 122 | playingSince += dt; 123 | 124 | if (t-ufoMusicTime>1200) { 125 | ufoMusicTime = t; 126 | if (ufos[0]) 127 | play(Aufo); 128 | } 129 | 130 | if(!gameOver && !awaitingContinue) { 131 | 132 | if (playingSince > 0 && !achievements) { 133 | achievements = [0,0,0]; 134 | } 135 | 136 | var i; 137 | var nbSpaceshipBullets = 0; 138 | 139 | if (!dying && playingSince>0 && t-musicPaused>5000 && player > 2 && !ufos.length) { 140 | 141 | combosTarget = Math.floor(30 - 25 * Math.exp(-(player-3)/15)); 142 | var musicFreq = 3*combos/combosTarget; 143 | if (combos > combosTarget) { 144 | musicPaused = t; 145 | neverUFOs = combos = 0; 146 | ufos.push([ 147 | W * Math.random(), 148 | H * Math.random(), 149 | 0, 150 | 0, 151 | 0 152 | ]); 153 | achievements[2] ++; 154 | } 155 | 156 | musicPhase += musicFreq*2*Math.PI*dt/1000; 157 | if ((Math.sin(musicPhase) > 0) !== musicTick) { 158 | musicTick = !musicTick; 159 | play(musicTick ? Amusic1 : Amusic2); 160 | } 161 | } 162 | 163 | // randomly send some asteroids 164 | /* 165 | if (Math.random() < 0.001 * dt) 166 | randomInGameAsteroid(); 167 | */ 168 | 169 | // player lifecycle 170 | 171 | if (lifes == 0 && playingSince > 0) { 172 | // player enter 173 | resurrectionTime = t; 174 | lifes = 4; 175 | player++; 176 | score = 0; 177 | scoreForLife = 10000; 178 | jumpingAmp = 0; 179 | jumpingFreq = 0; 180 | asteroids = []; 181 | ufos = []; 182 | play(Acoin); 183 | if (player > 1) { 184 | localStorage.ba_pl = player; 185 | localStorage.ba_ach = achievements; 186 | } 187 | } 188 | 189 | // inc lifecycle 190 | 191 | if (playingSince > 1000 && !dying) { 192 | for (i = 0; i < incomingObjects.length; i++) { 193 | var o = incomingObjects[i]; 194 | if (!o[10]) { 195 | var p = incPosition(o); 196 | var matchingTap = tap && circleCollides(tap, p, (MOBILE ? 60 : 20) + 10 * o[6]); 197 | if (keys[o[7]] || matchingTap) { 198 | // send an asteroid 199 | neverPlayed = tap = keys[o[7]] = 0; 200 | if (sendAsteroid(o)) { 201 | achievements[0] ++; 202 | if (player > 3) combos ++; 203 | incomingObjects.splice(i--, 1); 204 | } 205 | else { 206 | // failed to aim (red aiming) 207 | score += 5000; 208 | combos = 0; 209 | lastLoseShot = o[10] = t; 210 | } 211 | } 212 | } 213 | else { 214 | if (t-o[10] > 1000) 215 | incomingObjects.splice(i--, 1); 216 | } 217 | } 218 | tap = 0; 219 | 220 | while(maybeCreateInc()); 221 | } 222 | 223 | // spaceship lifecycle 224 | 225 | if (dying && t-dying > 2000 + (lifes>1 ? 0 : 2000)) { 226 | dying = 0; 227 | spaceship = [ W/2, H/2, 0, 0, 0 ]; 228 | if (--lifes) { 229 | resurrectionTime = t; 230 | } 231 | else { 232 | // Player lost. game over 233 | playingSince = -5000; 234 | randomAsteroids(); 235 | ufos = []; 236 | setTimeout(function(){ play(Aleave); }, 1000); 237 | } 238 | } 239 | 240 | // score lifecycle 241 | 242 | if (score >= scoreForLife) { 243 | lastExtraLife = t; 244 | lifes ++; 245 | scoreForLife += 10000; 246 | play(Alife); 247 | if (lifes > 5) { 248 | gameOver = 1; 249 | incomingObjects = []; 250 | ufos = []; 251 | randomAsteroids(); 252 | localStorage.ba_pl=0; 253 | } 254 | } 255 | 256 | if (!dying && playingSince>0 && t - lastScoreIncrement > 100) { 257 | score += 10; 258 | lastScoreIncrement = t; 259 | } 260 | best = Math.max(best, score); 261 | 262 | // collision 263 | 264 | bullets.forEach(function (bull, i) { 265 | if (!bull[5]) nbSpaceshipBullets ++; 266 | var j; 267 | 268 | if (bull[4]<900) { 269 | // bullet-spaceship collision 270 | if (!dying && circleCollides(bull, spaceship, 20)) { 271 | explose(bull); 272 | bullets.splice(i, 1); 273 | spaceshipDie(); 274 | return; 275 | } 276 | 277 | // bullet-ufo collision 278 | for (j = 0; j < ufos.length; ++j) { 279 | var ufo = ufos[j]; 280 | if (circleCollides(bull, ufo, 20)) { 281 | explose(bull); 282 | bullets.splice(i, 1); 283 | ufos.splice(j, 1); 284 | return; 285 | } 286 | } 287 | } 288 | 289 | for (j = 0; j < asteroids.length; ++j) { 290 | var aster = asteroids[j]; 291 | var lvl = aster[5]; 292 | // bullet-asteroid collision 293 | if (circleCollides(bull, aster, 10 * lvl)) { 294 | explose(bull); 295 | bullets.splice(i, 1); 296 | explodeAsteroid(j); 297 | score += 50 * Math.floor(0.4 * (6 - lvl) * (6 - lvl)); 298 | return; 299 | } 300 | } 301 | }); 302 | 303 | if (!dying && playingSince > 0) asteroids.forEach(function (aster, j) { 304 | // asteroid-spaceship collision 305 | if (circleCollides(aster, spaceship, 10 + 10 * aster[5])) { 306 | if (t - resurrectionTime < 200) { 307 | // if spaceship just resurect, will explode the asteroid 308 | explodeAsteroid(j); 309 | } 310 | else { 311 | // otherwise, player die 312 | explose(spaceship); 313 | spaceshipDie(); 314 | } 315 | } 316 | }); 317 | 318 | // run spaceship AI 319 | AIexcitement = 0; 320 | if (!dying && playingSince > 0) { 321 | var ax = Math.cos(spaceship[4]); 322 | var ay = Math.sin(spaceship[4]); 323 | 324 | // ai logic (determine the 3 inputs) 325 | aiLogic(1-Math.exp(-(player-0.8)/14)); 326 | 327 | // apply ai inputs with game logic 328 | 329 | var rotSpeed = 0.004 + 0.003 * (1-Math.exp(-player/40)); 330 | var accSpeed = 0.0003 - 0.0002 * Math.exp(-(player-1)/5) + 0.00001 * player; 331 | var shotRate = 100 + 1000 * Math.exp(-(player-1)/8) + 300 * Math.exp(-player/20); 332 | 333 | spaceship[2] += AIboost * dt * accSpeed * ax; 334 | spaceship[3] += AIboost * dt * accSpeed * ay; 335 | spaceship[4] = normAngle(spaceship[4] + AIrotate * dt * rotSpeed); 336 | if (nbSpaceshipBullets < 3) { 337 | if (AIshoot && t-lastBulletShoot > shotRate) { 338 | lastBulletShoot = t; 339 | play(Ashot); 340 | shoot(spaceship, 0.3, spaceship[4]); 341 | } 342 | } 343 | } 344 | } 345 | 346 | euclidPhysics(spaceship); 347 | asteroids.forEach(polarPhysics); 348 | ufos.forEach(euclidPhysics); 349 | bullets.forEach(euclidPhysics); 350 | particles.forEach(polarPhysics); 351 | 352 | ufos.forEach(applyUFOlogic); 353 | incomingObjects.forEach(applyIncLogic); 354 | 355 | particles.forEach(applyLife); 356 | loopOutOfBox(spaceship); 357 | asteroids.forEach(playingSince > 0 && !awaitingContinue && !gameOver ? destroyOutOfBox : loopOutOfBox); 358 | ufos.forEach(loopOutOfBox); 359 | bullets.forEach(applyLife); 360 | bullets.forEach(loopOutOfBox); 361 | 362 | excitementSmoothed += 0.04 * (AIexcitement - excitementSmoothed); 363 | AIboostSmoothed += 0.04 * (AIboost - AIboostSmoothed); 364 | 365 | // handling jumping / shaking 366 | killSmoothed -= dt * 0.0003 * killSmoothed; 367 | jumpingAmpSmoothed += 0.04 * (jumpingAmp - jumpingAmpSmoothed); 368 | jumpingFreqSmoothed += 0.04 * (jumpingFreq - jumpingFreqSmoothed); 369 | if (killSmoothed > 1.3) { 370 | if (jumpingAmp < 0.5) { 371 | jumpingFreq = 1 + Math.random(); 372 | jumpingAmp ++; 373 | } 374 | } 375 | if (killSmoothed < 0.8) { 376 | jumpingAmp = 0; 377 | } 378 | var prevPhase = jumpingPhase; 379 | jumpingPhase += jumpingFreq *2*Math.PI*dt/1000; 380 | jumping = jumpingAmpSmoothed * Math.pow(Math.cos(jumpingPhase), 2.0); 381 | if (Math.cos(prevPhase) < 0 && 0 < Math.cos(jumpingPhase)) { 382 | jumpingFreq = 1 + 3 * Math.random() * Math.random(); 383 | } 384 | if (jumpingAmp < 0.5) { 385 | jumpingAmpSmoothed += 0.04 * (jumpingAmp - jumpingAmpSmoothed); 386 | } 387 | 388 | var shake = jumpingAmp * Math.pow(smoothstep(0.2, 0.0, jumping), 0.5); 389 | if (shake > 0.5 && t-lastJump>100) { 390 | play(Ajump); 391 | lastJump = t; 392 | } 393 | shaking = [ 394 | 30 * shake * (Math.random()-0.5) / FW, 395 | 30 * shake * (Math.random()-0.5) / FH 396 | ]; 397 | } 398 | 399 | 400 | // Game DRAWING 401 | 402 | function drawGame () { 403 | ctx.save(); 404 | ctx.fillStyle = "#000"; 405 | ctx.fillRect(0, 0, W, H); 406 | ctx.restore(); 407 | 408 | renderCollection(asteroids, drawAsteroid); 409 | renderCollection(ufos, drawUFO); 410 | renderCollection(bullets, drawBullet); 411 | renderCollection(particles, drawParticle); 412 | 413 | if (playingSince > 0 && !awaitingContinue && !gameOver) { 414 | ctx.save(); 415 | translateTo(spaceship); 416 | drawSpaceship(spaceship); 417 | ctx.restore(); 418 | } 419 | 420 | drawGameUI(); 421 | 422 | drawGlitch(); 423 | } 424 | 425 | 426 | function translateTo (p) { 427 | ctx.translate(p[0], p[1]); 428 | } 429 | 430 | function renderCollection (coll, draw) { 431 | for (var i=0; i ["Not bad for a 35 years old system. 35 years old. Things don't get better than this. They don't get better than this. Look at this picture, you know what, even HD can't be this clear, crystal clear, razor sharp, wonderful vector graphics. Sorry there is nothing like it. *nothing like it*. What a terrific game."](https://youtu.be/i-x_gPxqEMw?t=4m14s) 23 | 24 | ### Special thanks 25 | 26 | - [mrspeaker](http://twitter.com/mrspeaker) for his support, testing and English help. 27 | 28 | ## Game versions 29 | 30 | The game both works for mobile and desktop but the gameplay varies. 31 | The desktop version is a [touch typing](https://en.wikipedia.org/wiki/Touch_typing) game 32 | where the mobile version is a simple touch game. If you are not good at typing on keyboards, 33 | just prefer the mobile version. 34 | 35 | On mobile (especially for iOS Safari), please use **Add to Home screen** for better experience. 36 | 37 | --- 38 | 39 | # The Game 40 | 41 | Behind Asteroids is a game about throwing asteroids to people playing "Asteroids" 42 | on an arcade machine. Like in Asteroids game, player have 3 extra lifes. 43 | The goal is to make the player lose and try to earn as much coins as possible. 44 | When a player lose, another come and put a new coin in the arcade. 45 | 46 | 47 | 48 | 49 | 50 | 51 | There are different game mechanism involved, they get introduced in first levels 52 | and get harder and harder to use: 53 | 54 | - The Asteroids have an aiming centered in the spaceship that varies the throw velocity 55 | - The Asteroids aiming rotates (Player >2) 56 | - The "RED" area in the aiming that make you fail the throw (Player >3) 57 | - The UFO bonus that you get after sending asteroids without failing to throw an asteroid (Player >4) 58 | 59 | 60 | 61 | 62 | 63 | 64 | ## Game Over 65 | 66 | Everytime the player is reaching 10'000 points, he wins a new extra life, 67 | You lose if player reaches 5 lifes. 68 | 69 | 70 | 71 | 72 | ## Continue 73 | 74 | Game is saved every time a player entered and can be continued later. 75 | 76 | 77 | 78 | --- 79 | 80 | # Tech overview 81 | 82 | - Canvas2D for the game primitives drawing 83 | - [WebGL](src/lib/webgl.js) for post processing effects (7 fragment shaders) 84 | - [Web Audio API](src/lib/audio.js) + [jsfxr](src/lib/jsfxr.js) ([14 sounds](src/sounds.js)) 85 | - [Asteroid fonts implemented "by hand"](src/lib/asteroids.font.js) 86 | - *... (more to describe later)* 87 | 88 | ## Making of the post-processing effects pipeline 89 | 90 | > Here is an non exhaustive summary of what's going on with the WebGL post-processing effects. 91 | 92 | Because this is a 2D game, a subset of WebGL is used here: we just use 2 triangles that cover the whole surface in order to just focus in writing fragment shaders. 93 | 94 | ### primitives are down on a simple [2D Canvas](http://www.w3.org/TR/2dcontext/) 95 | with classical Canvas 2D code but also 96 | using the 3 color channels (RED, GREEN, BLUE) independently to split objects into different classes... 97 | 98 | ![](screenshots/tech/game.png) 99 | 100 | ### A [laser shader](src/shaders/laser.frag) draws it to monochrome 101 | 102 | It sums up the 3 color channels. The **BLUE** channel, used for the bullets, gets accentuated in a factor that depends on the screen position. This intends to recreate the various intensity of a vector monitor. 103 | 104 | The result of this shader is also blurred: 105 | 106 | ![](screenshots/tech/laser.png) 107 | 108 | ### the [player shader](src/shaders/player.frag) is rendered 109 | The player and it environment (that will be reflected in the screen) is procedurally generated in a shader. 110 | 111 | The shader code is a bit crazy right now probably because of all animations, but the drawing is not so complex: this is just about drawing ovale and [squircle]() shapes and also some gradients for the lightning. 112 | 113 | ![](screenshots/tech/player_raw.png) 114 | 115 | We don't directly use this image in the game, it is visually not very realist, but if we **blur it a lot (and even more on X axis)** to recreate a reflection style, it becomes quite interesting: 116 | 117 | ![](screenshots/tech/player.png) 118 | 119 | The objective is to find an equilibrium between seeing it a bit in background but not too much. Also note that the hands are moving during a game, this is very subtile to see but it is part of the environment. 120 | 121 | ### a [Glare shader](src/shaders/glare.frag) effects is added 122 | 123 | Glare is obtained by applying a large directional blur. 124 | It is only applied on bright objects (basically just bullets). 125 | 126 | ![](screenshots/tech/glare.png) 127 | 128 | 129 | ### Result with some [persistence](src/shaders/persistence.frag) 130 | 131 | ![](screenshots/tech/persistence.png) 132 | 133 | The final [Game shader](src/shaders/game.frag) combines 5 textures: 134 | 135 | ```glsl 136 | uniform sampler2D G; // game 137 | uniform sampler2D R; // persistence 138 | uniform sampler2D B; // blur 139 | uniform sampler2D L; // glare 140 | uniform sampler2D E; // env (player) 141 | ``` 142 | 143 | The blur texture is used as a way to make the glowing effect (multiplying with a blue color). 144 | The persistence texture stores the previous blur texture to accumulate motion blur over time. 145 | 146 | ### Finally, we just put the UI canvas on top of the game canvas 147 | 148 | ![](screenshots/player24.png) 149 | 150 | ## Font drawing 151 | 152 | ![](screenshots/tech/font.png) 153 | 154 | [Code is here](src/lib/asteroids.font.js) 155 | 156 | ## Feedbacks on developing a JS13K game 157 | 158 | ### Don't start with JS*K tricks 159 | 160 | My first point is about NOT doing any JavaScript tricks to save more bytes until you are at the last days of the competitions and if it happens you actually are >13k (really, 13K zipped is plenty of room for making a game even if not "bytes optimized"). 161 | 162 | So, you want your game to run first. 163 | And even if you have a first version, you might improve it, so keeping your code readable and maintainable is very important. 164 | 165 | ### Don't fear beginning with libraries! 166 | 167 | Unlike some recommendations I've seen previously about making JS13K games, 168 | I think you can afford starting with some libraries. 169 | Just keep in mind to not be too much tied to these libraries so you can eventually remove them. 170 | 171 | My point is, the process of making a game is very long and you want to be 172 | as productive as possible to prototype and add game features. 173 | 174 | In my game I've used [stack.gl](http://stack.gl) libraries for making the post processing effects, and I only port my code back to raw WebGL when I was really sure it was done. 175 | I was very productive working on these effects and was not stuck by crazy code. 176 | 177 | When I was sure of the post-processing pipeline, I've then replaced usage of these libraries by [tiny utility functions](src/lib/webgl.js) specific for my needs. 178 | 179 | ## Make your game visually debuggable 180 | 181 | When developing a game, especially an AI, it is important to debug. 182 | And by debug I mean displaying hidden game logic. 183 | The problem of `console.log`ging things is it is difficult to picture it with the game for a given instant. 184 | 185 | You want to see vectors and AI decisions to be able to tweak game parameters and improve the game. 186 | 187 | [See in this video one display I've used when debugging the AI](https://www.youtube.com/watch?v=1F5XWY4fGaY) 188 | 189 | ## I have avoided "OO-style" to functional style 190 | 191 | There is no "Objects" in my game, I've gone away from the classical prototype / OO way of doing games. 192 | 193 | What I've used is just arrays. This is both a technical choice (going more FP) and a way to save more bytes (a minifier can't rename fields of objects, `[0], [1], ...` are obviously saving bytes especially when zipped). 194 | 195 | ### Array as data structure 196 | 197 | So instead of objects, I've used array like a tuple. 198 | For instance, the `spaceship` state is `[x, y, velx, vely, rot]` 199 | and `asteroids` state is an array of `[x, y, rot, vel, shape, lvl]`. 200 | 201 | All my game state is in [state.js](src/state.js) and "tuple types" are all documented. 202 | 203 | Taking this approach, you better have to design your game state first so you don't change this over time (this is the cons of this approach, indexes are not really readable and maintainable). 204 | 205 | Also you should try to make your tuple looking like the same so you can share some code for different types (`x, y` is always the 2 first values in my tuples). 206 | 207 | ### Embrace Functions 208 | 209 | Instead of object methods, I just have a lot of functions. 210 | For instance, I have `drawAsteroid(asteroid)`. 211 | 212 | Some functions are generic so you can re-use them for different needs. 213 | I've found a very nice way of implementing **"behaviors"** of objects: 214 | 215 | ```js 216 | // code from the update loop 217 | euclidPhysics(spaceship); 218 | asteroids.forEach(polarPhysics); 219 | ufos.forEach(euclidPhysics); 220 | bullets.forEach(euclidPhysics); 221 | particles.forEach(polarPhysics); 222 | 223 | ufos.forEach(applyUFOlogic); 224 | incomingObjects.forEach(applyIncLogic); 225 | 226 | particles.forEach(applyLife); 227 | loopOutOfBox(spaceship); 228 | asteroids.forEach( 229 | // conditional behavior !! 230 | playingSince > 0 && !awaitingContinue && !gameOver ? 231 | destroyOutOfBox : loopOutOfBox); 232 | ufos.forEach(loopOutOfBox); 233 | bullets.forEach(applyLife); 234 | bullets.forEach(loopOutOfBox); 235 | ``` 236 | 237 | Also, it is easy to pass function as a value: 238 | ```js 239 | // code from the render loop 240 | renderCollection(asteroids, drawAsteroid); 241 | renderCollection(ufos, drawUFO); 242 | renderCollection(bullets, drawBullet); 243 | renderCollection(particles, drawParticle); 244 | ``` 245 | 246 | ## Build system 247 | 248 | The build system is dedicated to JS13K and made with a few simple [scripts](package.json). 249 | It is able to copy assets, [concat all files](scripts/concat.sh), [minify the GLSL code](scripts/compileglslfiles.sh), minify the JavaScript, zip the result and give size information. 250 | 251 | Things are a bit specific to my need but remain very simple, modular and powerful, you could easily fork it. 252 | 253 | ### dev 254 | 255 | ``` 256 | npm run liveserver 257 | ``` 258 | 259 | ``` 260 | npm run watch 261 | ``` 262 | 263 | ### prod 264 | 265 | ``` 266 | npm run build 267 | ``` 268 | -------------------------------------------------------------------------------- /src/ui.js: -------------------------------------------------------------------------------- 1 | /* global 2 | ctx path font gameOver W H player playingSince:true awaitingContinue scoreTxt 3 | lifes dying MOBILE best score t UFO neverPlayed lastExtraLife neverUFOs dt play 4 | Amsg GAME_MARGIN FW FH combos achievements musicTick helpVisible */ 5 | 6 | // IN GAME UI 7 | 8 | function button (t1, t2) { 9 | ctx.globalAlpha = 1; 10 | path([ 11 | [0, 0], 12 | [160, 0], 13 | [160, 120], 14 | [0, 120] 15 | ]); 16 | ctx.translate(80, 30); 17 | ctx.stroke(); 18 | ctx.fillStyle = "#000"; 19 | ctx.fill(); 20 | ctx.save(); 21 | font(t1, 2); 22 | ctx.restore(); 23 | ctx.save(); 24 | ctx.translate(0, 40); 25 | font(t2, 2); 26 | ctx.restore(); 27 | } 28 | 29 | function drawGameUI () { 30 | ctx.save(); 31 | ctx.fillStyle = ctx.strokeStyle = "#0f0"; 32 | ctx.globalAlpha = 0.3; 33 | 34 | if (gameOver) { 35 | ctx.save(); 36 | ctx.strokeStyle = "#0f0"; 37 | ctx.globalAlpha = 0.3; 38 | ctx.save(); 39 | ctx.translate((W-340)/2, H/8); 40 | font("YOU EARNED ", 2, 1); 41 | ctx.globalAlpha = 0.5; 42 | font((player*25)+"¢", 2, 1); 43 | ctx.restore(); 44 | ctx.save(); 45 | ctx.translate(W/2, H/4); 46 | font("FROM "+player+" PLAYERS", 2); 47 | ctx.restore(); 48 | ctx.save(); 49 | ctx.globalAlpha = 0.5; 50 | ctx.translate((W-200)/2, H/2); 51 | drawAchievements(2); 52 | ctx.restore(); 53 | 54 | ctx.save(); 55 | ctx.translate(W/2 - 180, H - 160); 56 | button("TWEET", "SCORE"); 57 | ctx.restore(); 58 | 59 | ctx.save(); 60 | ctx.translate(W/2 + 20, H - 160); 61 | button("PLAY", "AGAIN"); 62 | ctx.restore(); 63 | 64 | ctx.restore(); 65 | } 66 | else if (playingSince < 0 || awaitingContinue) { 67 | ctx.save(); 68 | ctx.translate(W-50, 20); 69 | font(scoreTxt(0), 1.5, -1); 70 | ctx.restore(); 71 | 72 | ctx.save(); 73 | ctx.translate(50, 70); 74 | if ((!awaitingContinue || playingSince>0) && t%1000<500) 75 | font("PLAYER "+(awaitingContinue||player+1), 2, 1); 76 | ctx.restore(); 77 | 78 | ctx.save(); 79 | ctx.translate(W/2 - 160, 0.7*H); 80 | path([ 81 | [0,2], 82 | [0,18] 83 | ]); 84 | ctx.stroke(); 85 | ctx.translate(40,0); 86 | font("COIN", 2, 1); 87 | ctx.translate(40,0); 88 | path([ 89 | [0,2], 90 | [0,18] 91 | ]); 92 | ctx.stroke(); 93 | ctx.translate(40,0); 94 | font("PLAY", 2, 1); 95 | ctx.restore(); 96 | } 97 | else { 98 | for (var i=1; i 0) { 120 | ctx.save(); 121 | ctx.globalAlpha = 1; 122 | ctx.translate(W/2, 140); 123 | font("CONTINUE ?", 3); 124 | ctx.restore(); 125 | ctx.save(); 126 | ctx.globalAlpha = 1; 127 | ctx.translate(W/4, 210); 128 | font("YES", MOBILE ? 4 : 6); 129 | ctx.restore(); 130 | ctx.save(); 131 | ctx.globalAlpha = 1; 132 | ctx.translate(3*W/4, 210); 133 | font("NO", MOBILE ? 4 : 6); 134 | ctx.restore(); 135 | } 136 | ctx.save(); 137 | ctx.translate(W/2, H-14); 138 | font("2015 GREWEB INC", .6); 139 | ctx.restore(); 140 | 141 | if (!gameOver) { 142 | ctx.save(); 143 | ctx.translate(W/2, 20); 144 | font(scoreTxt(best), .6); 145 | ctx.restore(); 146 | 147 | ctx.save(); 148 | ctx.translate(50, 20); 149 | font(scoreTxt(score), 1.5, 1); 150 | ctx.restore(); 151 | } 152 | 153 | if (gameOver || playingSince<0 && t%1000<800) { 154 | ctx.save(); 155 | ctx.translate(W-20, H-24); 156 | font(MOBILE ? "MOBILE" : "DESKTOP", .6, -1); 157 | ctx.restore(); 158 | ctx.save(); 159 | ctx.translate(W-20, H-14); 160 | font("VERSION", .6, -1); 161 | ctx.restore(); 162 | } 163 | 164 | ctx.restore(); 165 | } 166 | 167 | function drawGlitch () { 168 | ctx.save(); 169 | ctx.fillStyle = 170 | ctx.strokeStyle = "#f00"; 171 | ctx.globalAlpha = 0.03; 172 | ctx.translate(W/2, H/2); 173 | ctx.beginPath(); 174 | ctx.arc(0, 0, 4, 0, 2*Math.PI); 175 | ctx.fill(); 176 | ctx.stroke(); 177 | ctx.beginPath(); 178 | ctx.arc(0, 0, 12, 0, 2*Math.PI); 179 | ctx.stroke(); 180 | ctx.beginPath(); 181 | ctx.arc(0, 0, 12, 4, 6); 182 | ctx.stroke(); 183 | ctx.beginPath(); 184 | ctx.arc(0, 0, 12, 1, 2); 185 | ctx.stroke(); 186 | ctx.restore(); 187 | } 188 | 189 | 190 | // EXTERNAL UI 191 | 192 | var badgesIcons = [ 193 | [ 194 | [-11, -11], 195 | [4, -13], 196 | [6, -6], 197 | [14, 0], 198 | [14, 8], 199 | [6, 8], 200 | [-6, 14], 201 | [-14, 0] 202 | ], 203 | [ 204 | [-8, 13], 205 | [0, -13], 206 | [8, 13], 207 | [0, 11], 208 | [-8, 13], 209 | , 210 | [-10, -2], 211 | [10, 2], 212 | , 213 | [10, -2], 214 | [-10, 2], 215 | , 216 | ], 217 | UFO.map(function (p) { 218 | return p ? [p[0]-11,p[1]-7] : p; 219 | }) 220 | ]; 221 | 222 | var lastStatement, lastStatementTime = 0; 223 | 224 | var lastMessage2; 225 | 226 | function drawUI () { 227 | var currentMessage = "", 228 | currentMessage2 = "", 229 | currentMessageClr = "#f7c", 230 | currentMessageClr2 = "#7fc"; 231 | 232 | function announcePlayer (player) { 233 | currentMessage = "PLAYER "+player; 234 | currentMessage2 = [ 235 | "GENIOUS PLAYER!!", 236 | "EXPERIENCED PLAYER!!", 237 | "GOOD PLAYER. GET READY", 238 | "NICE PLAYER.", 239 | "BEGINNER.", 240 | "VERY BEGINNER. EASY KILL" 241 | ][Math.floor(Math.exp((-player)/8)*6)]; 242 | } 243 | 244 | if (gameOver) { 245 | currentMessage = "PLAYER MASTERED THE GAME"; 246 | currentMessage2 = "REACHED ᐃᐃᐃᐃᐃ"; 247 | } 248 | else if (!player) { 249 | if (playingSince<-7000) { 250 | currentMessage = "BEHIND ASTEROIDS"; 251 | currentMessage2 = "THE DARK SIDE"; 252 | } 253 | else if (playingSince<-3500) { 254 | currentMessageClr = currentMessageClr2 = "#7cf"; 255 | currentMessage = "SEND ASTEROIDS TO MAKE"; 256 | currentMessage2 = "PLAYERS WASTE THEIR MONEY"; 257 | } 258 | else if (!awaitingContinue) { 259 | var nb = Math.min(25, Math.floor((playingSince+3500)/80)); 260 | for (var i=0; i-2000) 263 | currentMessage2 = "A NEW PLAYER!"; 264 | } 265 | else { 266 | if (playingSince<0) playingSince = 0; // jump to skip the "player coming" 267 | announcePlayer(awaitingContinue); 268 | } 269 | } 270 | else if (dying) { 271 | if (lifes==1) { 272 | currentMessageClr2 = "#f66"; 273 | currentMessage = "GOOD JOB !!!"; 274 | currentMessage2 = "THE DUDE IS BROKE"; 275 | } 276 | else if (lifes==2) { 277 | currentMessageClr2 = "#f66"; 278 | currentMessage = "OK..."; 279 | currentMessage2 = "ONE MORE TIME !"; 280 | } 281 | else { 282 | if (lastStatement && t - lastStatementTime > 3000) { // lastStatementTime is not used here 283 | currentMessage = lastStatement; 284 | } 285 | else { 286 | currentMessage = ["!!!", "GREAT!", "COOL!", "OMG!", "AHAH!", "RUDE!", "EPIC!", "WICKED!", "SHAME!", "HEHEHE!", "BWAHAHA!"]; 287 | lastStatement = currentMessage = currentMessage[Math.floor(Math.random() * currentMessage.length)]; 288 | lastStatementTime = 0; 289 | } 290 | } 291 | } 292 | else { 293 | if (playingSince<0) { 294 | currentMessage = "INCOMING NEW PLAYER..."; 295 | currentMessage2 = "25¢ 25¢ 25¢ 25¢ 25¢"; 296 | } 297 | else if (playingSince<6000 && lifes==4) { 298 | announcePlayer(player); 299 | } 300 | else { 301 | currentMessageClr2 = "#f66"; 302 | if (lastStatement && t - lastStatementTime < 3000) { 303 | currentMessage2 = lastStatement; 304 | } 305 | else { 306 | if (neverPlayed) { 307 | if (helpVisible()) { 308 | currentMessageClr = currentMessageClr2 = "#f7c"; 309 | currentMessage = MOBILE ? "TAP ON ASTEROIDS" : "PRESS ASTEROIDS LETTER"; 310 | currentMessage2 = "TO SEND THEM TO THE GAME"; 311 | } 312 | } 313 | else if (lifes > 4 && t - lastExtraLife > 5000) { 314 | currentMessageClr = currentMessageClr2 = "#f66"; 315 | currentMessage = "DON'T LET PLAYER"; 316 | currentMessage2 = "REACH ᐃᐃᐃᐃᐃ !!!"; 317 | } 318 | else if (score > 10000 && t - lastExtraLife < 4500) { 319 | currentMessageClr = currentMessageClr2 = "#f66"; 320 | currentMessage = "OH NO! PLAYER JUST"; 321 | currentMessage2 = "WON AN EXTRA LIFE!"; 322 | } 323 | else if (player==2 && 5000 5) { 340 | lastStatement = 0; 341 | if (Math.random() < 0.0001 * dt && t - lastStatementTime > 8000) { 342 | currentMessage2 = [ 343 | "COME ON! KILL IT!", 344 | "JUST DO IT!", 345 | "I WANT ¢¢¢", 346 | "GIVE ME SOME ¢¢¢", 347 | "DO IT!", 348 | "DESTROY IT!" 349 | ]; 350 | lastStatement = currentMessage2 = currentMessage2[Math.floor(Math.random() * currentMessage2.length)]; 351 | lastStatementTime = t; 352 | } 353 | } 354 | } 355 | } 356 | } 357 | 358 | if (currentMessage2 && lastMessage2 !== currentMessage2 && 359 | (currentMessageClr2 == "#f66" || currentMessageClr2 == "#f7c")) { 360 | play(Amsg); 361 | } 362 | 363 | ctx.save(); 364 | ctx.translate(GAME_MARGIN, MOBILE ? 40 : 2); 365 | ctx.lineWidth = (t%600>300) ? 2 : 1; 366 | ctx.save(); 367 | ctx.strokeStyle = currentMessageClr; 368 | font(currentMessage, MOBILE ? 1.5 : 2, 1); 369 | ctx.restore(); 370 | ctx.save(); 371 | ctx.strokeStyle = currentMessageClr2; 372 | ctx.translate(0, MOBILE ? 30 : 40); 373 | font(lastMessage2 = currentMessage2, MOBILE ? 1.5 : 2, 1); 374 | ctx.restore(); 375 | ctx.restore(); 376 | 377 | if (gameOver) return; 378 | 379 | ctx.save(); 380 | ctx.translate(FW - GAME_MARGIN, 2); 381 | ctx.lineWidth = 2; 382 | ctx.strokeStyle = "#7cf"; 383 | font(((playingSince>0&&awaitingContinue||player)*25)+"¢", 2, -1); 384 | ctx.restore(); 385 | 386 | 387 | ctx.save(); 388 | ctx.globalAlpha = musicTick ? 1 : 0.6; 389 | ctx.strokeStyle = "#7cf"; 390 | ctx.translate(FW - GAME_MARGIN, FH - 30); 391 | if (combos) font(combos+"x", 1.5, -1); 392 | ctx.restore(); 393 | 394 | /* 395 | if (combos && combosTarget-combos < 9) { 396 | ctx.save(); 397 | ctx.strokeStyle = "#7cf"; 398 | ctx.globalAlpha = musicTick ? 1 : 0.5; 399 | ctx.translate(FW - GAME_MARGIN, FH - 50); 400 | font((1+combosTarget-combos)+" ", 1, -1); 401 | ctx.translate(0, 0); 402 | path(UFO); 403 | ctx.stroke(); 404 | ctx.restore(); 405 | } 406 | */ 407 | 408 | if (achievements) { 409 | ctx.save(); 410 | ctx.translate(GAME_MARGIN + 50, FH - 20); 411 | ctx.strokeStyle = "#fc7"; 412 | drawAchievements(1); 413 | ctx.restore(); 414 | } 415 | } 416 | 417 | function drawAchievements (fontSize) { 418 | for (var j = 0; j < 3; j++) { 419 | var badge = achievements[j]; 420 | if (badge) { 421 | ctx.save(); 422 | ctx.translate(100 * j, 0); 423 | path(badgesIcons[j]); 424 | ctx.stroke(); 425 | ctx.translate(0, -20 - 10 * fontSize); 426 | font(""+badge, fontSize); 427 | ctx.restore(); 428 | } 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /src/lib/jsfxr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SfxrParams 3 | * 4 | * Copyright 2010 Thomas Vian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * @author Thomas Vian 19 | */ 20 | 21 | /* eslint-disable */ 22 | 23 | /** @constructor */ 24 | function SfxrParams() { 25 | //-------------------------------------------------------------------------- 26 | // 27 | // Settings String Methods 28 | // 29 | //-------------------------------------------------------------------------- 30 | 31 | /** 32 | * Parses a settings array into the parameters 33 | * @param array Array of the settings values, where elements 0 - 23 are 34 | * a: waveType 35 | * b: attackTime 36 | * c: sustainTime 37 | * d: sustainPunch 38 | * e: decayTime 39 | * f: startFrequency 40 | * g: minFrequency 41 | * h: slide 42 | * i: deltaSlide 43 | * j: vibratoDepth 44 | * k: vibratoSpeed 45 | * l: changeAmount 46 | * m: changeSpeed 47 | * n: squareDuty 48 | * o: dutySweep 49 | * p: repeatSpeed 50 | * q: phaserOffset 51 | * r: phaserSweep 52 | * s: lpFilterCutoff 53 | * t: lpFilterCutoffSweep 54 | * u: lpFilterResonance 55 | * v: hpFilterCutoff 56 | * w: hpFilterCutoffSweep 57 | * x: masterVolume 58 | * @return If the string successfully parsed 59 | */ 60 | this.ss = function(values) 61 | { 62 | for ( var i = 0; i < 24; i++ ) 63 | { 64 | this[String.fromCharCode( 97 + i )] = values[i] || 0; 65 | } 66 | 67 | // I moved this here from the r(true) function 68 | if (this['c'] < .01) { 69 | this['c'] = .01; 70 | } 71 | 72 | var totalTime = this['b'] + this['c'] + this['e']; 73 | if (totalTime < .18) { 74 | var multiplier = .18 / totalTime; 75 | this['b'] *= multiplier; 76 | this['c'] *= multiplier; 77 | this['e'] *= multiplier; 78 | } 79 | } 80 | } 81 | 82 | /** 83 | * SfxrSynth 84 | * 85 | * Copyright 2010 Thomas Vian 86 | * 87 | * Licensed under the Apache License, Version 2.0 (the "License"); 88 | * you may not use this file except in compliance with the License. 89 | * You may obtain a copy of the License at 90 | * 91 | * http://www.apache.org/licenses/LICENSE-2.0 92 | * 93 | * Unless required by applicable law or agreed to in writing, software 94 | * distributed under the License is distributed on an "AS IS" BASIS, 95 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 96 | * See the License for the specific language governing permissions and 97 | * limitations under the License. 98 | * 99 | * @author Thomas Vian 100 | */ 101 | /** @constructor */ 102 | function SfxrSynth() { 103 | // All variables are kept alive through function closures 104 | 105 | //-------------------------------------------------------------------------- 106 | // 107 | // Sound Parameters 108 | // 109 | //-------------------------------------------------------------------------- 110 | 111 | this._p = new SfxrParams(); // Params instance 112 | 113 | //-------------------------------------------------------------------------- 114 | // 115 | // Synth Variables 116 | // 117 | //-------------------------------------------------------------------------- 118 | 119 | var _envelopeLength0, // Length of the attack stage 120 | _envelopeLength1, // Length of the sustain stage 121 | _envelopeLength2, // Length of the decay stage 122 | 123 | _period, // Period of the wave 124 | _maxPeriod, // Maximum period before sound stops (from minFrequency) 125 | 126 | _slide, // Note slide 127 | _deltaSlide, // Change in slide 128 | 129 | _changeAmount, // Amount to change the note by 130 | _changeTime, // Counter for the note change 131 | _changeLimit, // Once the time reaches this limit, the note changes 132 | 133 | _squareDuty, // Offset of center switching point in the square wave 134 | _dutySweep; // Amount to change the duty by 135 | 136 | //-------------------------------------------------------------------------- 137 | // 138 | // Synth Methods 139 | // 140 | //-------------------------------------------------------------------------- 141 | 142 | /** 143 | * rs the runing variables from the params 144 | * Used once at the start (total r) and for the repeat effect (partial r) 145 | */ 146 | this.r = function() { 147 | // Shorter reference 148 | var p = this._p; 149 | 150 | _period = 100 / (p['f'] * p['f'] + .001); 151 | _maxPeriod = 100 / (p['g'] * p['g'] + .001); 152 | 153 | _slide = 1 - p['h'] * p['h'] * p['h'] * .01; 154 | _deltaSlide = -p['i'] * p['i'] * p['i'] * .000001; 155 | 156 | if (!p['a']) { 157 | _squareDuty = .5 - p['n'] / 2; 158 | _dutySweep = -p['o'] * .00005; 159 | } 160 | 161 | _changeAmount = 1 + p['l'] * p['l'] * (p['l'] > 0 ? -.9 : 10); 162 | _changeTime = 0; 163 | _changeLimit = p['m'] == 1 ? 0 : (1 - p['m']) * (1 - p['m']) * 20000 + 32; 164 | } 165 | 166 | // I split the r() function into two functions for better readability 167 | this.tr = function() { 168 | this.r(); 169 | 170 | // Shorter reference 171 | var p = this._p; 172 | 173 | // Calculating the length is all that remained here, everything else moved somewhere 174 | _envelopeLength0 = p['b'] * p['b'] * 100000; 175 | _envelopeLength1 = p['c'] * p['c'] * 100000; 176 | _envelopeLength2 = p['e'] * p['e'] * 100000 + 12; 177 | // Full length of the volume envelop (and therefore sound) 178 | // Make sure the length can be divided by 3 so we will not need the padding "==" after base64 encode 179 | return ((_envelopeLength0 + _envelopeLength1 + _envelopeLength2) / 3 | 0) * 3; 180 | } 181 | 182 | /** 183 | * Writes the wave to the supplied buffer ByteArray 184 | * @param buffer A ByteArray to write the wave to 185 | * @return If the wave is finished 186 | */ 187 | this.sw = function(buffer, length) { 188 | // Shorter reference 189 | var p = this._p; 190 | 191 | // If the filters are active 192 | var _filters = p['s'] != 1 || p['v'], 193 | // Cutoff multiplier which adjusts the amount the wave position can move 194 | _hpFilterCutoff = p['v'] * p['v'] * .1, 195 | // Speed of the high-pass cutoff multiplier 196 | _hpFilterDeltaCutoff = 1 + p['w'] * .0003, 197 | // Cutoff multiplier which adjusts the amount the wave position can move 198 | _lpFilterCutoff = p['s'] * p['s'] * p['s'] * .1, 199 | // Speed of the low-pass cutoff multiplier 200 | _lpFilterDeltaCutoff = 1 + p['t'] * .0001, 201 | // If the low pass filter is active 202 | _lpFilterOn = p['s'] != 1, 203 | // masterVolume * masterVolume (for quick calculations) 204 | _masterVolume = p['x'] * p['x'], 205 | // Minimum frequency before stopping 206 | _minFreqency = p['g'], 207 | // If the phaser is active 208 | _phaser = p['q'] || p['r'], 209 | // Change in phase offset 210 | _phaserDeltaOffset = p['r'] * p['r'] * p['r'] * .2, 211 | // Phase offset for phaser effect 212 | _phaserOffset = p['q'] * p['q'] * (p['q'] < 0 ? -1020 : 1020), 213 | // Once the time reaches this limit, some of the iables are r 214 | _repeatLimit = p['p'] ? ((1 - p['p']) * (1 - p['p']) * 20000 | 0) + 32 : 0, 215 | // The punch factor (louder at begining of sustain) 216 | _sustainPunch = p['d'], 217 | // Amount to change the period of the wave by at the peak of the vibrato wave 218 | _vibratoAmplitude = p['j'] / 2, 219 | // Speed at which the vibrato phase moves 220 | _vibratoSpeed = p['k'] * p['k'] * .01, 221 | // The type of wave to generate 222 | _waveType = p['a']; 223 | 224 | var _envelopeLength = _envelopeLength0, // Length of the current envelope stage 225 | _envelopeOverLength0 = 1 / _envelopeLength0, // (for quick calculations) 226 | _envelopeOverLength1 = 1 / _envelopeLength1, // (for quick calculations) 227 | _envelopeOverLength2 = 1 / _envelopeLength2; // (for quick calculations) 228 | 229 | // Damping muliplier which restricts how fast the wave position can move 230 | var _lpFilterDamping = 5 / (1 + p['u'] * p['u'] * 20) * (.01 + _lpFilterCutoff); 231 | if (_lpFilterDamping > .8) { 232 | _lpFilterDamping = .8; 233 | } 234 | _lpFilterDamping = 1 - _lpFilterDamping; 235 | 236 | var _finished = false, // If the sound has finished 237 | _envelopeStage = 0, // Current stage of the envelope (attack, sustain, decay, end) 238 | _envelopeTime = 0, // Current time through current enelope stage 239 | _envelopeVolume = 0, // Current volume of the envelope 240 | _hpFilterPos = 0, // Adjusted wave position after high-pass filter 241 | _lpFilterDeltaPos = 0, // Change in low-pass wave position, as allowed by the cutoff and damping 242 | _lpFilterOldPos, // Previous low-pass wave position 243 | _lpFilterPos = 0, // Adjusted wave position after low-pass filter 244 | _periodTemp, // Period modified by vibrato 245 | _phase = 0, // Phase through the wave 246 | _phaserInt, // Integer phaser offset, for bit maths 247 | _phaserPos = 0, // Position through the phaser buffer 248 | _pos, // Phase expresed as a Number from 0-1, used for fast sin approx 249 | _repeatTime = 0, // Counter for the repeats 250 | _sample, // Sub-sample calculated 8 times per actual sample, averaged out to get the super sample 251 | _superSample, // Actual sample writen to the wave 252 | _vibratoPhase = 0; // Phase through the vibrato sine wave 253 | 254 | // Buffer of wave values used to create the out of phase second wave 255 | var _phaserBuffer = new Array(1024), 256 | // Buffer of random values used to generate noise 257 | _noiseBuffer = new Array(32); 258 | for (var i = _phaserBuffer.length; i--; ) { 259 | _phaserBuffer[i] = 0; 260 | } 261 | for (var i = _noiseBuffer.length; i--; ) { 262 | _noiseBuffer[i] = Math.random() * 2 - 1; 263 | } 264 | 265 | for (var i = 0; i < length; i++) { 266 | if (_finished) { 267 | return i; 268 | } 269 | 270 | // Repeats every _repeatLimit times, partially rting the sound parameters 271 | if (_repeatLimit) { 272 | if (++_repeatTime >= _repeatLimit) { 273 | _repeatTime = 0; 274 | this.r(); 275 | } 276 | } 277 | 278 | // If _changeLimit is reached, shifts the pitch 279 | if (_changeLimit) { 280 | if (++_changeTime >= _changeLimit) { 281 | _changeLimit = 0; 282 | _period *= _changeAmount; 283 | } 284 | } 285 | 286 | // Acccelerate and apply slide 287 | _slide += _deltaSlide; 288 | _period *= _slide; 289 | 290 | // Checks for frequency getting too low, and stops the sound if a minFrequency was set 291 | if (_period > _maxPeriod) { 292 | _period = _maxPeriod; 293 | if (_minFreqency > 0) { 294 | _finished = true; 295 | } 296 | } 297 | 298 | _periodTemp = _period; 299 | 300 | // Applies the vibrato effect 301 | if (_vibratoAmplitude > 0) { 302 | _vibratoPhase += _vibratoSpeed; 303 | _periodTemp *= 1 + Math.sin(_vibratoPhase) * _vibratoAmplitude; 304 | } 305 | 306 | _periodTemp |= 0; 307 | if (_periodTemp < 8) { 308 | _periodTemp = 8; 309 | } 310 | 311 | // Sweeps the square duty 312 | if (!_waveType) { 313 | _squareDuty += _dutySweep; 314 | if (_squareDuty < 0) { 315 | _squareDuty = 0; 316 | } else if (_squareDuty > .5) { 317 | _squareDuty = .5; 318 | } 319 | } 320 | 321 | // Moves through the different stages of the volume envelope 322 | if (++_envelopeTime > _envelopeLength) { 323 | _envelopeTime = 0; 324 | 325 | switch (++_envelopeStage) { 326 | case 1: 327 | _envelopeLength = _envelopeLength1; 328 | break; 329 | case 2: 330 | _envelopeLength = _envelopeLength2; 331 | } 332 | } 333 | 334 | // Sets the volume based on the position in the envelope 335 | switch (_envelopeStage) { 336 | case 0: 337 | _envelopeVolume = _envelopeTime * _envelopeOverLength0; 338 | break; 339 | case 1: 340 | _envelopeVolume = 1 + (1 - _envelopeTime * _envelopeOverLength1) * 2 * _sustainPunch; 341 | break; 342 | case 2: 343 | _envelopeVolume = 1 - _envelopeTime * _envelopeOverLength2; 344 | break; 345 | case 3: 346 | _envelopeVolume = 0; 347 | _finished = true; 348 | } 349 | 350 | // Moves the phaser offset 351 | if (_phaser) { 352 | _phaserOffset += _phaserDeltaOffset; 353 | _phaserInt = _phaserOffset | 0; 354 | if (_phaserInt < 0) { 355 | _phaserInt = -_phaserInt; 356 | } else if (_phaserInt > 1023) { 357 | _phaserInt = 1023; 358 | } 359 | } 360 | 361 | // Moves the high-pass filter cutoff 362 | if (_filters && _hpFilterDeltaCutoff) { 363 | _hpFilterCutoff *= _hpFilterDeltaCutoff; 364 | if (_hpFilterCutoff < .00001) { 365 | _hpFilterCutoff = .00001; 366 | } else if (_hpFilterCutoff > .1) { 367 | _hpFilterCutoff = .1; 368 | } 369 | } 370 | 371 | _superSample = 0; 372 | for (var j = 8; j--; ) { 373 | // Cycles through the period 374 | _phase++; 375 | if (_phase >= _periodTemp) { 376 | _phase %= _periodTemp; 377 | 378 | // Generates new random noise for this period 379 | if (_waveType == 3) { 380 | for (var n = _noiseBuffer.length; n--; ) { 381 | _noiseBuffer[n] = Math.random() * 2 - 1; 382 | } 383 | } 384 | } 385 | 386 | // Gets the sample from the oscillator 387 | switch (_waveType) { 388 | case 0: // Square wave 389 | _sample = ((_phase / _periodTemp) < _squareDuty) ? .5 : -.5; 390 | break; 391 | case 1: // Saw wave 392 | _sample = 1 - _phase / _periodTemp * 2; 393 | break; 394 | case 2: // Sine wave (fast and accurate approx) 395 | _pos = _phase / _periodTemp; 396 | _pos = (_pos > .5 ? _pos - 1 : _pos) * 6.28318531; 397 | _sample = 1.27323954 * _pos + .405284735 * _pos * _pos * (_pos < 0 ? 1 : -1); 398 | _sample = .225 * ((_sample < 0 ? -1 : 1) * _sample * _sample - _sample) + _sample; 399 | break; 400 | case 3: // Noise 401 | _sample = _noiseBuffer[Math.abs(_phase * 32 / _periodTemp | 0)]; 402 | } 403 | 404 | // Applies the low and high pass filters 405 | if (_filters) { 406 | _lpFilterOldPos = _lpFilterPos; 407 | _lpFilterCutoff *= _lpFilterDeltaCutoff; 408 | if (_lpFilterCutoff < 0) { 409 | _lpFilterCutoff = 0; 410 | } else if (_lpFilterCutoff > .1) { 411 | _lpFilterCutoff = .1; 412 | } 413 | 414 | if (_lpFilterOn) { 415 | _lpFilterDeltaPos += (_sample - _lpFilterPos) * _lpFilterCutoff; 416 | _lpFilterDeltaPos *= _lpFilterDamping; 417 | } else { 418 | _lpFilterPos = _sample; 419 | _lpFilterDeltaPos = 0; 420 | } 421 | 422 | _lpFilterPos += _lpFilterDeltaPos; 423 | 424 | _hpFilterPos += _lpFilterPos - _lpFilterOldPos; 425 | _hpFilterPos *= 1 - _hpFilterCutoff; 426 | _sample = _hpFilterPos; 427 | } 428 | 429 | // Applies the phaser effect 430 | if (_phaser) { 431 | _phaserBuffer[_phaserPos % 1024] = _sample; 432 | _sample += _phaserBuffer[(_phaserPos - _phaserInt + 1024) % 1024]; 433 | _phaserPos++; 434 | } 435 | 436 | _superSample += _sample; 437 | } 438 | 439 | // Averages out the super samples and applies volumes 440 | _superSample *= .125 * _envelopeVolume * _masterVolume; 441 | 442 | // Clipping if too loud 443 | buffer[i] = _superSample >= 1 ? 32767 : _superSample <= -1 ? -32768 : _superSample * 32767 | 0; 444 | } 445 | 446 | return length; 447 | } 448 | } 449 | 450 | // Adapted from http://codebase.es/riffwave/ 451 | var synth = new SfxrSynth(); 452 | // Export for the Closure Compiler 453 | function jsfxr (settings, audioCtx, cb) { 454 | // Initialize SfxrParams 455 | synth._p.ss(settings); 456 | // Synthesize Wave 457 | var envelopeFullLength = synth.tr(); 458 | var data = new Uint8Array(((envelopeFullLength + 1) / 2 | 0) * 4 + 44); 459 | 460 | var used = synth.sw(new Uint16Array(data.buffer, 44), envelopeFullLength) * 2; 461 | 462 | var dv = new Uint32Array(data.buffer, 0, 44); 463 | // Initialize header 464 | dv[0] = 0x46464952; // "RIFF" 465 | dv[1] = used + 36; // put total size here 466 | dv[2] = 0x45564157; // "WAVE" 467 | dv[3] = 0x20746D66; // "fmt " 468 | dv[4] = 0x00000010; // size of the following 469 | dv[5] = 0x00010001; // Mono: 1 channel, PCM format 470 | dv[6] = 0x0000AC44; // 44,100 samples per second 471 | dv[7] = 0x00015888; // byte rate: two bytes per sample 472 | dv[8] = 0x00100002; // 16 bits per sample, aligned on every two bytes 473 | dv[9] = 0x61746164; // "data" 474 | dv[10] = used; // put number of samples here 475 | 476 | // Base64 encoding written by me, @maettig 477 | used += 44; 478 | var i = 0, 479 | base64Characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 480 | output = 'data:audio/wav;base64,'; 481 | for (; i < used; i += 3) 482 | { 483 | var a = data[i] << 16 | data[i + 1] << 8 | data[i + 2]; 484 | output += base64Characters[a >> 18] + base64Characters[a >> 12 & 63] + base64Characters[a >> 6 & 63] + base64Characters[a & 63]; 485 | } 486 | 487 | audioCtx && audioCtx.decodeAudioData(data.buffer, cb); 488 | 489 | return output; 490 | } 491 | --------------------------------------------------------------------------------