game.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. var canvas = document.getElementById('game');
  2. var ctx = canvas.getContext('2d');
  3. var beatUrls = [
  4. 'sound/b1.mp3',
  5. 'sound/b2.mp3',
  6. 'sound/b3.mp3',
  7. 'sound/b4.mp3',
  8. 'sound/b5.mp3'
  9. ];
  10. var createSound = function(name, url) {
  11. var deferred = Q.defer();
  12. soundManager.createSound({
  13. id: name,
  14. url: url,
  15. autoLoad: true,
  16. onload: function() {
  17. deferred.resolve(this);
  18. }
  19. });
  20. return deferred.promise;
  21. };
  22. var starColors = [
  23. 'maroon',
  24. 'red',
  25. 'orange',
  26. 'yellow',
  27. 'olive',
  28. 'green',
  29. 'teal',
  30. 'blue',
  31. 'navy',
  32. 'purple'
  33. ];
  34. var paused = false;
  35. var debug = false;
  36. var beats = [];
  37. var beatTime = 0;
  38. var currentBeats = [];
  39. var gotPoint = false;
  40. var switchBeat = function(index) {
  41. var currentBeat = beats[index];
  42. activeTarget.beat = currentBeat;
  43. currentBeats.push(currentBeat);
  44. if(currentBeats.length > 4) {
  45. currentBeats.splice(0, 1);
  46. }
  47. };
  48. // Sound
  49. var soundDeferred = Q.defer();
  50. var soundPromise = soundDeferred.promise;
  51. soundManager.setup({
  52. url: 'swf/',
  53. flashVersion: 9,
  54. onready: function() {
  55. soundDeferred.resolve(this);
  56. }
  57. });
  58. soundPromise = soundPromise.then(function() {
  59. var beatPromises = [];
  60. beatUrls.map(function(elem, index) {
  61. beatPromises.push(createSound('beat'+index, elem));
  62. });
  63. return Q.spread([
  64. Q.all(beatPromises),
  65. createSound('get', 'sound/get.mp3')
  66. ], function(beatz) {
  67. beats = beatz;
  68. })
  69. });
  70. // Input handling
  71. var keysDown = {};
  72. addEventListener('keydown', function(e) {
  73. keysDown[e.keyCode] = true;
  74. }, false);
  75. addEventListener('keyup', function(e) {
  76. if(e.keyCode == 68) {
  77. debug = !debug;
  78. }
  79. if(e.keyCode == 80) {
  80. pause();
  81. }
  82. delete keysDown[e.keyCode];
  83. }, false);
  84. addEventListener('blur', function() {
  85. if(!paused) pause();
  86. });
  87. // Constants
  88. var RADIUS = 8;
  89. var LEAF_SIZE = 8;
  90. var LEVEL_SIZE = 800;
  91. var ALLOWANCE = (LEVEL_SIZE - canvas.width)/2;
  92. var METER_WIDTH = 120;
  93. var METER_HEIGHT = 40;
  94. var METER_SPEED = 10;
  95. var PLAYER_SPEED = 50;
  96. var STAGE_SPEED = 25;
  97. var RIPPLE_SIZE = 10;
  98. var RIPPLE_SPEED = 10;
  99. var DOT_SIZE = 3;
  100. var DOT_DISTANCE = 4 * 5;
  101. // Drawing functions
  102. function drawCircle(point, r, w, color) {
  103. ctx.beginPath();
  104. ctx.arc(point.x, point.y, r, 0, 2*Math.PI, false);
  105. ctx.lineWidth = w;
  106. ctx.strokeStyle = color;
  107. ctx.stroke();
  108. }
  109. function drawLeafV(point, h, w) {
  110. ctx.beginPath();
  111. ctx.moveTo(point.x, point.y);
  112. ctx.quadraticCurveTo(point.x + w, point.y + h/2, point.x, point.y + h);
  113. ctx.quadraticCurveTo(point.x - w, point.y + h/2, point.x, point.y);
  114. ctx.fill();
  115. }
  116. function drawLeafH(point, w, h) {
  117. ctx.beginPath();
  118. ctx.moveTo(point.x, point.y);
  119. ctx.quadraticCurveTo(point.x + w/2, point.y + h, point.x + w, point.y);
  120. ctx.quadraticCurveTo(point.x + w/2, point.y - h, point.x, point.y);
  121. ctx.fill();
  122. }
  123. function drawPlayer(point) {
  124. drawCircle(point, RADIUS, 5, "#FFFFFF");
  125. }
  126. function drawSpectrum() {
  127. var barHeight = 30;
  128. var barWidth = 6;
  129. var barSpacing = 2;
  130. var margin = 5;
  131. ctx.strokeStyle = 'white';
  132. ctx.lineWidth = 2;
  133. ctx.beginPath();
  134. ctx.rect(
  135. margin - ctx.lineWidth,
  136. canvas.height - barHeight - ctx.lineWidth - margin,
  137. activeTarget.spectrum.length * (barWidth+barSpacing) + 2 * ctx.lineWidth - barSpacing,
  138. barHeight + 2 * ctx.lineWidth
  139. );
  140. ctx.stroke();
  141. for(var i = 0; i < activeTarget.spectrum.length; ++i) {
  142. var height = activeTarget.spectrum[i]*20;
  143. ctx.fillStyle = starColors[i];
  144. ctx.fillRect(i * (barWidth + barSpacing) + margin, canvas.height - height - margin, barWidth, height);
  145. }
  146. }
  147. function sign(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; }
  148. function lerp(from, to, p) {
  149. return to * p + from * (1 - p);
  150. }
  151. function ease(v) { return v * v * (3 - 2 * v); }
  152. function easeOutExpo(t, b, c, d) {
  153. return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
  154. }
  155. function color(r,g,b,a) {
  156. return 'rgba('+r+','+g+','+b+','+a.toFixed(5)+')';
  157. }
  158. function distX(a, b) {
  159. var dx = a.x - b.x;
  160. if(dx > LEVEL_SIZE / 2) dx -= LEVEL_SIZE;
  161. if(dx < -LEVEL_SIZE / 2) dx += LEVEL_SIZE;
  162. return dx;
  163. }
  164. function distSq(a, b) {
  165. var c = distX(a, b);
  166. var d = a.y - b.y;
  167. return c * c + d * d;
  168. }
  169. function colliding(a, b) {
  170. return distSq(a, b) <= 4 * RADIUS * RADIUS;
  171. }
  172. // "Entities"
  173. function Meter() {
  174. this.offset = 10;
  175. this.amp = 0;
  176. this.percentage = 0;
  177. this.direction = METER_SPEED;
  178. }
  179. Meter.prototype.draw = function() {
  180. var left = (canvas.width - METER_WIDTH) / 2;
  181. var vmid = this.offset + METER_HEIGHT / 2;
  182. var third = METER_WIDTH / 3;
  183. var peak = METER_HEIGHT / 2;
  184. ctx.fillStyle = 'black';
  185. ctx.rect(left, this.offset, METER_WIDTH, METER_HEIGHT);
  186. ctx.fill();
  187. var cpUp = lerp(vmid, vmid + peak * this.amp, this.percentage);
  188. var cpDown = lerp(vmid, vmid - peak * this.amp, this.percentage);
  189. ctx.lineWidth = 2;
  190. ctx.strokeStyle = 'red';
  191. ctx.beginPath();
  192. var pos = left;
  193. ctx.moveTo(left, vmid);
  194. ctx.quadraticCurveTo(pos+third/2, cpUp, pos+third, vmid);
  195. pos += third;
  196. ctx.quadraticCurveTo(pos+third/2, cpDown, pos+third, vmid);
  197. pos += third;
  198. ctx.quadraticCurveTo(pos+third/2, cpUp, pos+third, vmid);
  199. ctx.stroke();
  200. ctx.lineWidth = 3;
  201. ctx.strokeStyle = 'white';
  202. ctx.beginPath();
  203. ctx.rect(left, this.offset, METER_WIDTH, METER_HEIGHT);
  204. ctx.stroke();
  205. };
  206. Meter.prototype.onUpdate = function(dt) {
  207. if(this.percentage > 1) {
  208. this.direction = -METER_SPEED;
  209. }
  210. if(this.percentage < -1) {
  211. this.direction = METER_SPEED;
  212. }
  213. this.percentage += this.direction * dt;
  214. this.amp = 0.5 + lerp(0, 1, activeTarget.shimmerFactor);
  215. };
  216. function Star(x, y) {
  217. this.x = x;
  218. this.y = y;
  219. this.alpha = 0;
  220. this.randomize();
  221. }
  222. Star.prototype.randomize = function() {
  223. this.type = Math.floor(Math.random()*starColors.length);
  224. this.color = starColors[this.type];
  225. };
  226. Star.prototype.draw = function() {
  227. ctx.fillStyle = this.color;
  228. ctx.globalAlpha = this.alpha;
  229. var adjPosition = translatePoints(this);
  230. ctx.fillRect(adjPosition.x, adjPosition.y, DOT_SIZE, DOT_SIZE);
  231. var top = {x: adjPosition.x + DOT_SIZE/2, y: adjPosition.y - DOT_SIZE/2 };
  232. var bottom = {x: adjPosition.x + DOT_SIZE/2, y: adjPosition.y + DOT_SIZE/2*3};
  233. var left = {x: adjPosition.x - DOT_SIZE/2, y: adjPosition.y + DOT_SIZE/2 };
  234. var right = {x: adjPosition.x + DOT_SIZE/2*3, y: adjPosition.y + DOT_SIZE/2 };
  235. drawLeafH(left, -LEAF_SIZE, LEAF_SIZE/2);
  236. drawLeafH(right, LEAF_SIZE, LEAF_SIZE/2);
  237. drawLeafV(top, -LEAF_SIZE, LEAF_SIZE/2);
  238. drawLeafV(bottom, LEAF_SIZE, LEAF_SIZE/2);
  239. ctx.globalAlpha = 1;
  240. };
  241. Star.prototype.onUpdate = function() {
  242. if(gotPoint) {
  243. this.randomize();
  244. }
  245. if(this.y > canvas.height + DOT_DISTANCE * 2) {
  246. this.y -= canvas.height + DOT_DISTANCE * 4;
  247. }
  248. var newAlpha = 1.5 * activeTarget.spectrum[this.type];
  249. var a = 0.001;
  250. if(newAlpha > this.alpha) {
  251. a = 0.9;
  252. }
  253. this.alpha = Math.min(1, activeTarget.shimmerFactor * (this.alpha * (1-a) + newAlpha * a));
  254. };
  255. function Dot(x, y) {
  256. this.x = x;
  257. this.y = y;
  258. }
  259. Dot.prototype.draw = function() {
  260. ctx.fillStyle = 'gray';
  261. var adjPosition = translatePoints(this);
  262. ctx.fillRect(adjPosition.x, adjPosition.y, DOT_SIZE, DOT_SIZE);
  263. };
  264. Dot.prototype.onUpdate = function() {
  265. if(this.y > canvas.height + DOT_DISTANCE * 2) {
  266. this.y -= canvas.height + DOT_DISTANCE * 4;
  267. }
  268. };
  269. function Target(beat) {
  270. this.reset();
  271. this.beat = beat;
  272. this.alpha = 0;
  273. if(points < 4) {
  274. this.alpha = ease(1-points/4);
  275. }
  276. this.rippleCounter = 0;
  277. }
  278. Target.prototype.reset = function() {
  279. this.y = -20;
  280. this.x = Math.random()*LEVEL_SIZE;
  281. };
  282. Target.prototype.color = function(p) {
  283. return color(0, 255, 0, p*this.alpha);
  284. };
  285. Target.prototype.draw = function() {
  286. var adjPosition = translatePoints(this);
  287. drawCircle(adjPosition, RADIUS, 3, this.color(1));
  288. drawCircle(adjPosition, 2, 3, this.color(1));
  289. // ripples
  290. var p = lerp(1, 0, ease(this.rippleCounter/RIPPLE_SIZE));
  291. drawCircle(adjPosition, RADIUS+7+this.rippleCounter, 1, this.color(p));
  292. drawCircle(adjPosition, RADIUS+4+this.rippleCounter, 1, this.color(p*0.8));
  293. drawCircle(adjPosition, RADIUS+1+this.rippleCounter, 1, this.color(p*0.5));
  294. };
  295. Target.prototype.onUpdate = function(dt, t) {
  296. this.rippleCounter += RIPPLE_SPEED * dt;
  297. if(this.rippleCounter > RIPPLE_SIZE) {
  298. this.rippleCounter = 0;
  299. }
  300. if(gotPoint) {
  301. gotPoint = false;
  302. }
  303. if(colliding(player, this)) {
  304. gotPoint = true;
  305. this.beat.setPan(0);
  306. this.beat.setVolume(100);
  307. this.shouldDelete = true;
  308. return;
  309. }
  310. if(this.y > canvas.height + 20) {
  311. this.reset();
  312. }
  313. var xDist = distX(this, player);
  314. var s = sign(xDist);
  315. var xDistAbs = easeOutExpo(Math.abs(xDist), 0, 100, LEVEL_SIZE/2);
  316. var yDist = this.y - player.y;
  317. var angle = 0;
  318. if(yDist != 0) {
  319. angle = easeOutExpo(Math.abs(Math.atan2(Math.abs(yDist), xDist) - Math.PI/2), 0, 100, Math.PI);
  320. }
  321. else {
  322. angle = s * 100;
  323. }
  324. this.beat.setPan(Math.max(xDistAbs, angle) * s);
  325. var rDist = xDist*xDist + yDist*yDist;
  326. if(rDist >= 250000) {
  327. rDist = 250000;
  328. }
  329. rDist = 250000 - rDist;
  330. rDist /= 2500;
  331. this.beat.setVolume(rDist);
  332. this.shimmerFactor = easeOutExpo(Math.abs(xDist), 1, -1, LEVEL_SIZE/2) * rDist / 100;
  333. if(isNaN(this.shimmerFactor) || this.shimmerFactor < 0) {
  334. this.shimmerFactor = 0;
  335. }
  336. this.spectrum = this.beat.getSpectrum(t*1000);
  337. };
  338. var player = {
  339. x: 0,
  340. y: canvas.height - 20,
  341. dx: PLAYER_SPEED
  342. };
  343. var meter = new Meter();
  344. var points;
  345. var entities;
  346. var activeTarget;
  347. var respawnTarget = function() {
  348. activeTarget = new Target();
  349. entities.push(activeTarget);
  350. };
  351. var reset = function() {
  352. points = 0;
  353. entities = [];
  354. for(var j = 0; j < canvas.height / DOT_DISTANCE + 4; ++j) {
  355. for(var i = 0; i < LEVEL_SIZE / DOT_DISTANCE; ++i) {
  356. var x = i * DOT_DISTANCE;
  357. var y = (j-4) * DOT_DISTANCE;
  358. if(j % 2 == 0 && i % 2 == 0) {
  359. entities.push(new Dot(x, y));
  360. }
  361. if(j % 4 == 1) {
  362. if(i % 4 == 0) {
  363. entities.push(new Star(x, y));
  364. }
  365. else {
  366. entities.push(new Dot(x, y));
  367. }
  368. }
  369. if(j % 4 == 3) {
  370. if(i % 4 == 2) {
  371. entities.push(new Star(x, y));
  372. }
  373. else {
  374. entities.push(new Dot(x, y));
  375. }
  376. }
  377. }
  378. }
  379. respawnTarget();
  380. };
  381. reset();
  382. // Update
  383. var update = function(dt) {
  384. if(paused) return;
  385. var repeat = false;
  386. beatTime += dt;
  387. while(beatTime > 4) {
  388. repeat = true;
  389. beatTime -= 4;
  390. }
  391. if(repeat) {
  392. currentBeats.map(function(beat) {
  393. beat.play();
  394. });
  395. }
  396. if(37 in keysDown) {
  397. player.x -= player.dx * dt;
  398. }
  399. if(39 in keysDown) {
  400. player.x += player.dx * dt;
  401. }
  402. player.x = (player.x + LEVEL_SIZE) % LEVEL_SIZE;
  403. for(var i = entities.length - 1; i >= 0; --i) {
  404. var entity = entities[i];
  405. entity.y += STAGE_SPEED * dt;
  406. entity.onUpdate(dt, beatTime);
  407. if(entity.shouldDelete) {
  408. entities.splice(i, 1);
  409. }
  410. }
  411. meter.onUpdate(dt);
  412. if(gotPoint) {
  413. ++points;
  414. soundManager.play('get');
  415. if(beats.length > 0) {
  416. respawnTarget();
  417. switchBeat(points % beats.length);
  418. }
  419. }
  420. };
  421. // Render
  422. var translatePoints = function(point) {
  423. var x = point.x - player.x + canvas.width/2;
  424. if(x < -ALLOWANCE) x += LEVEL_SIZE;
  425. if(x > LEVEL_SIZE - ALLOWANCE) x -= LEVEL_SIZE;
  426. return {x:x, y:point.y};
  427. };
  428. var render = function() {
  429. ctx.fillStyle = "black";
  430. ctx.fillRect(0, 0, canvas.width, canvas.height);
  431. entities.map(function(elem) {
  432. elem.draw();
  433. });
  434. drawPlayer(translatePoints(player));
  435. meter.draw();
  436. ctx.fillStyle = "white";
  437. ctx.fillText(points+"", 5, 15);
  438. if(debug) {
  439. drawSpectrum();
  440. }
  441. if(paused) {
  442. ctx.textAlign = 'center';
  443. ctx.fillText("Paused (P to unpause)", canvas.width / 2, canvas.height / 2);
  444. ctx.textAlign = 'start';
  445. }
  446. };
  447. var main = function() {
  448. var now = Date.now();
  449. var delta = (now - then);
  450. delta /= 1000;
  451. update(delta);
  452. render();
  453. then = now;
  454. };
  455. var then;
  456. var start = function() {
  457. then = Date.now();
  458. setInterval(main, 10);
  459. };
  460. var pause = function() {
  461. paused = !paused;
  462. if(paused) {
  463. currentBeats.map(function(beat) {beat.pause()});
  464. }
  465. else {
  466. currentBeats.map(function(beat) {beat.resume()});
  467. }
  468. };
  469. ctx.textAlign = 'center';
  470. ctx.fillText("Loading...", canvas.width / 2, canvas.height / 2);
  471. ctx.textAlign = 'start';
  472. Q.all([soundPromise]).done(function() {
  473. switchBeat(0);
  474. start();
  475. });