not really known
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1623 lines
52 KiB

  1. ((function ( $ ) {
  2. "use strict";
  3. $.widget('aerolab.blockrain', {
  4. options: {
  5. autoplay: false, // Let a bot play the game
  6. autoplayRestart: true, // Restart the game automatically once a bot loses
  7. showFieldOnStart: true, // Show a bunch of random blocks on the start screen (it looks nice)
  8. theme: null, // The theme name or a theme object
  9. blockWidth: 10, // How many blocks wide the field is (The standard is 10 blocks)
  10. autoBlockWidth: false, // The blockWidth is dinamically calculated based on the autoBlockSize. Disabled blockWidth. Useful for responsive backgrounds
  11. autoBlockSize: 24, // The max size of a block for autowidth mode
  12. difficulty: 'normal', // Difficulty (normal|nice|evil).
  13. speed: 20, // The speed of the game. The higher, the faster the pieces go.
  14. asdwKeys: true, // Enable ASDW keys
  15. //Highest score
  16. highestScore: 0,
  17. // Copy
  18. playButtonText: 'Play',
  19. gameOverText: 'Game Over',
  20. restartButtonText: 'Play Again',
  21. scoreText: 'Score',
  22. highscoreText: 'High Score', // High score text
  23. // Basic Callbacks
  24. onStart: function(){},
  25. onRestart: function(){},
  26. onGameOver: function(score){},
  27. // When a block is placed
  28. onPlaced: function(){},
  29. // When a line is made. Returns the number of lines, score assigned and total score
  30. onLine: function(lines, scoreIncrement, score){}
  31. },
  32. /**
  33. * Start/Restart Game
  34. */
  35. start: function() {
  36. this._doStart();
  37. this.options.onStart.call(this.element);
  38. },
  39. restart: function() {
  40. this.options.onRestart.call(this.element);
  41. this._doStart();
  42. },
  43. gameover: function() {
  44. this.showGameOverMessage();
  45. this._board.gameover = true;
  46. this.options.onGameOver.call(this.element, this._filled.score);
  47. },
  48. _doStart: function() {
  49. this._filled.clearAll();
  50. this._filled._resetScore();
  51. this._filled._showHighScore(); // shows high score at the beginning of game. Initial high score is 0
  52. this._board.cur = this._board.nextShape();
  53. this._board.started = true;
  54. this._board.gameover = false;
  55. this._board.dropDelay = 5;
  56. this._board.render(true);
  57. this._board.animate();
  58. this._$start.fadeOut(150);
  59. this._$gameover.fadeOut(150);
  60. this._$score.fadeIn(150);
  61. this._$highscore.fadeIn(150);
  62. },
  63. pause: function() {
  64. this._board.paused = true;
  65. },
  66. resume: function() {
  67. this._board.paused = false;
  68. },
  69. autoplay: function(enable) {
  70. if( typeof enable !== 'boolean' ){ enable = true; }
  71. // On autoplay, start the game right away
  72. this.options.autoplay = enable;
  73. if( enable && ! this._board.started ) {
  74. this._doStart();
  75. }
  76. this._setupControls( ! enable );
  77. this._setupTouchControls( ! enable );
  78. },
  79. controls: function(enable) {
  80. if( typeof enable !== 'boolean' ){ enable = true; }
  81. this._setupControls(enable);
  82. },
  83. touchControls: function(enable) {
  84. if( typeof enable !== 'boolean' ){ enable = true; }
  85. this._setupTouchControls(enable);
  86. },
  87. score: function(newScore) {
  88. if( typeof newScore !== 'undefined' && parseInt(newScore) >= 0 ) {
  89. this._filled.score = parseInt(newScore);
  90. this._$scoreText.text(this._filled_score);
  91. }
  92. return this._filled.score;
  93. },
  94. highscore: function() {
  95. this._filled.highscore = parseInt(game.options.highestScore);
  96. this._$highscoreText.text(this._filled_highscore);
  97. return this._filled.highscore;
  98. },
  99. freesquares: function() {
  100. return this._filled.getFreeSpaces();
  101. },
  102. showStartMessage: function() {
  103. this._$start.show();
  104. },
  105. showGameOverMessage: function() {
  106. this._$gameover.show();
  107. },
  108. /**
  109. * Update the sizes of the renderer (this makes the game responsive)
  110. */
  111. updateSizes: function() {
  112. this._PIXEL_WIDTH = this.element.innerWidth();
  113. this._PIXEL_HEIGHT = this.element.innerHeight();
  114. this._BLOCK_WIDTH = this.options.blockWidth;
  115. this._BLOCK_HEIGHT = Math.floor(this.element.innerHeight() / this.element.innerWidth() * this._BLOCK_WIDTH);
  116. this._block_size = Math.floor(this._PIXEL_WIDTH / this._BLOCK_WIDTH);
  117. this._border_width = 2;
  118. // Recalculate the pixel width and height so the canvas always has the best possible size
  119. this._PIXEL_WIDTH = this._block_size * this._BLOCK_WIDTH;
  120. this._PIXEL_HEIGHT = this._block_size * this._BLOCK_HEIGHT;
  121. this._$canvas .attr('width', this._PIXEL_WIDTH)
  122. .attr('height', this._PIXEL_HEIGHT);
  123. },
  124. theme: function(newTheme){
  125. if( typeof newTheme === 'undefined' ) {
  126. return this.options.theme || this._theme;
  127. }
  128. // Setup the theme properly
  129. if( typeof newTheme === 'string' ) {
  130. this.options.theme = newTheme;
  131. this._theme = $.extend(true, {}, BlockrainThemes[newTheme]);
  132. }
  133. else {
  134. this.options.theme = null;
  135. this._theme = newTheme;
  136. }
  137. if( typeof this._theme === 'undefined' || this._theme === null ) {
  138. this._theme = $.extend(true, {}, BlockrainThemes['retro']);
  139. this.options.theme = 'retro';
  140. }
  141. if( isNaN(parseInt(this._theme.strokeWidth)) || typeof parseInt(this._theme.strokeWidth) !== 'number' ) {
  142. this._theme.strokeWidth = 2;
  143. }
  144. // Load the image assets
  145. this._preloadThemeAssets();
  146. if( this._board !== null ) {
  147. if( typeof this._theme.background === 'string' ) {
  148. this._$canvas.css('background-color', this._theme.background);
  149. }
  150. this._board.render();
  151. }
  152. },
  153. // Theme
  154. _theme: {
  155. },
  156. // UI Elements
  157. _$game: null,
  158. _$canvas: null,
  159. _$gameholder: null,
  160. _$start: null,
  161. _$gameover: null,
  162. _$score: null,
  163. _$scoreText: null,
  164. _$highscore: null,
  165. _$highscoreText: null,
  166. // Canvas
  167. _canvas: null,
  168. _ctx: null,
  169. // Initialization
  170. _create: function() {
  171. var game = this;
  172. this.theme(this.options.theme);
  173. this._createHolder();
  174. this._createUI();
  175. this._refreshBlockSizes();
  176. this.updateSizes();
  177. $(window).resize(function(){
  178. //game.updateSizes();
  179. });
  180. this._SetupShapeFactory();
  181. this._SetupFilled();
  182. this._SetupInfo();
  183. this._SetupBoard();
  184. this._info.init();
  185. this._board.init();
  186. var renderLoop = function(){
  187. requestAnimationFrame(renderLoop);
  188. game._board.render();
  189. };
  190. renderLoop();
  191. if( this.options.autoplay ) {
  192. this.autoplay(true);
  193. this._setupTouchControls(false);
  194. } else {
  195. this._setupControls(true);
  196. this._setupTouchControls(false);
  197. }
  198. },
  199. _checkCollisions: function(x, y, blocks, checkDownOnly) {
  200. // x & y should be aspirational values
  201. var i = 0, len = blocks.length, a, b;
  202. for (; i<len; i += 2) {
  203. a = x + blocks[i];
  204. b = y + blocks[i+1];
  205. if (b >= this._BLOCK_HEIGHT || this._filled.check(a, b)) {
  206. return true;
  207. } else if (!checkDownOnly && a < 0 || a >= this._BLOCK_WIDTH) {
  208. return true;
  209. }
  210. }
  211. return false;
  212. },
  213. _board: null,
  214. _info: null,
  215. _filled: null,
  216. /**
  217. * Draws the background
  218. */
  219. _drawBackground: function() {
  220. if( typeof this._theme.background !== 'string' ) {
  221. return;
  222. }
  223. if( this._theme.backgroundGrid instanceof Image ) {
  224. // Not loaded
  225. if( this._theme.backgroundGrid.width === 0 || this._theme.backgroundGrid.height === 0 ){ return; }
  226. this._ctx.globalAlpha = 1.0;
  227. for( var x=0; x<this._BLOCK_WIDTH; x++ ) {
  228. for( var y=0; y<this._BLOCK_HEIGHT; y++ ) {
  229. var cx = x * this._block_size;
  230. var cy = y * this._block_size;
  231. this._ctx.drawImage( this._theme.backgroundGrid,
  232. 0, 0, this._theme.backgroundGrid.width, this._theme.backgroundGrid.height,
  233. cx, cy, this._block_size, this._block_size);
  234. }
  235. }
  236. }
  237. else if( typeof this._theme.backgroundGrid === 'string' ) {
  238. var borderWidth = this._theme.strokeWidth;
  239. var borderDistance = Math.round(this._block_size*0.23);
  240. var squareDistance = Math.round(this._block_size*0.30);
  241. this._ctx.globalAlpha = 1.0;
  242. this._ctx.fillStyle = this._theme.backgroundGrid;
  243. for( var x=0; x<this._BLOCK_WIDTH; x++ ) {
  244. for( var y=0; y<this._BLOCK_HEIGHT; y++ ) {
  245. var cx = x * this._block_size;
  246. var cy = y * this._block_size;
  247. this._ctx.fillRect(cx+borderWidth, cy+borderWidth, this._block_size-borderWidth*2, this._block_size-borderWidth*2);
  248. }
  249. }
  250. }
  251. this._ctx.globalAlpha = 1.0;
  252. },
  253. /**
  254. * Shapes
  255. */
  256. _shapeFactory: null,
  257. _shapes: {
  258. /**
  259. * The shapes have a reference point (the dot) and always rotate left.
  260. * Keep in mind that the blocks should keep in the same relative position when rotating,
  261. * to allow for custom per-block themes.
  262. */
  263. /*
  264. * X
  265. * O XOXX
  266. * X
  267. * X
  268. * . .
  269. */
  270. line: [
  271. [ 0, -1, 0, -2, 0, -3, 0, -4],
  272. [ 2, -2, 1, -2, 0, -2, -1, -2],
  273. [ 0, -4, 0, -3, 0, -2, 0, -1],
  274. [-1, -2, 0, -2, 1, -2, 2, -2]
  275. ],
  276. /*
  277. * XX
  278. * XX
  279. */
  280. square: [
  281. [0, 0, 1, 0, 0, -1, 1, -1],
  282. [1, 0, 1, -1, 0, 0, 0, -1],
  283. [1, -1, 0, -1, 1, 0, 0, 0],
  284. [0, -1, 0, 0, 1, -1, 1, 0]
  285. ],
  286. /*
  287. * X X X
  288. * XOX XO XOX OX
  289. * . .X .X .X
  290. */
  291. arrow: [
  292. [0, -1, 1, -1, 2, -1, 1, -2],
  293. [1, 0, 1, -1, 1, -2, 0, -1],
  294. [2, -1, 1, -1, 0, -1, 1, 0],
  295. [1, -2, 1, -1, 1, 0, 2, -1]
  296. ],
  297. /*
  298. * X X XX
  299. * O XOX O XOX
  300. * .XX . .X X
  301. */
  302. rightHook: [
  303. [2, 0, 1, 0, 1, -1, 1, -2],
  304. [2, -2, 2, -1, 1, -1, 0, -1],
  305. [0, -2, 1, -2, 1, -1, 1, 0],
  306. [0, 0, 0, -1, 1, -1, 2, -1]
  307. ],
  308. /*
  309. * X XX X
  310. * O XOX O XOX
  311. * XX . X .X .
  312. */
  313. leftHook: [
  314. [0, 0, 1, 0, 1, -1, 1, -2],
  315. [2, 0, 2, -1, 1, -1, 0, -1],
  316. [2, -2, 1, -2, 1, -1, 1, 0],
  317. [0, -2, 0, -1, 1, -1, 2, -1]
  318. ],
  319. /*
  320. * X XX
  321. * XO OX
  322. * X .
  323. */
  324. leftZag: [
  325. [0, 0, 0, -1, 1, -1, 1, -2],
  326. [2, -1, 1, -1, 1, -2, 0, -2],
  327. [1, -2, 1, -1, 0, -1, 0, 0],
  328. [0, -2, 1, -2, 1, -1, 2, -1]
  329. ],
  330. /*
  331. * X
  332. * XO OX
  333. * .X XX
  334. */
  335. rightZag: [
  336. [1, 0, 1, -1, 0, -1, 0, -2],
  337. [2, -1, 1, -1, 1, 0, 0, 0],
  338. [0, -2, 0, -1, 1, -1, 1, 0],
  339. [0, 0, 1, 0, 1, -1, 2, -1]
  340. ]
  341. },
  342. _SetupShapeFactory: function(){
  343. var game = this;
  344. if( this._shapeFactory !== null ){ return; }
  345. function Shape(game, orientations, symmetrical, blockType) {
  346. $.extend(this, {
  347. x: 0,
  348. y: 0,
  349. symmetrical: symmetrical,
  350. init: function() {
  351. $.extend(this, {
  352. orientation: 0,
  353. x: Math.floor(game._BLOCK_WIDTH / 2) - 1,
  354. y: -1
  355. });
  356. return this;
  357. },
  358. blockType: blockType,
  359. blockVariation: null,
  360. blocksLen: orientations[0].length,
  361. orientations: orientations,
  362. orientation: 0, // 4 possible
  363. rotate: function(direction) {
  364. var orientation = (this.orientation + (direction === 'left' ? 1 : -1) + 4) % 4;
  365. //TODO - when past limit - auto shift and remember that too!
  366. if (!game._checkCollisions(this.x, this.y, this.getBlocks(orientation))) {
  367. this.orientation = orientation;
  368. game._board.renderChanged = true;
  369. }
  370. },
  371. moveRight: function() {
  372. if (!game._checkCollisions(this.x + 1, this.y, this.getBlocks())) {
  373. this.x++;
  374. game._board.renderChanged = true;
  375. }
  376. },
  377. moveLeft: function() {
  378. if (!game._checkCollisions(this.x - 1, this.y, this.getBlocks())) {
  379. this.x--;
  380. game._board.renderChanged = true;
  381. }
  382. },
  383. drop: function() {
  384. if (!game._checkCollisions(this.x, this.y + 1, this.getBlocks())) {
  385. this.y++;
  386. // Reset the drop count, as we dropped the block sooner
  387. game._board.dropCount = -1;
  388. game._board.animate();
  389. game._board.renderChanged = true;
  390. }
  391. },
  392. getBlocks: function(orientation) { // optional param
  393. return this.orientations[orientation !== undefined ? orientation : this.orientation];
  394. },
  395. draw: function(_x, _y, _orientation) {
  396. var blocks = this.getBlocks(_orientation),
  397. x = _x === undefined ? this.x : _x,
  398. y = _y === undefined ? this.y : _y,
  399. i = 0,
  400. index = 0;
  401. for (; i<this.blocksLen; i += 2) {
  402. game._board.drawBlock(x + blocks[i], y + blocks[i+1], this.blockType, this.blockVariation, index, this.orientation, true);
  403. index++;
  404. }
  405. },
  406. getBounds: function(_blocks) { // _blocks can be an array of blocks, an orientation index, or undefined
  407. var blocks = $.isArray(_blocks) ? _blocks : this.getBlocks(_blocks),
  408. i=0, len=blocks.length, minx=999, maxx=-999, miny=999, maxy=-999;
  409. for (; i<len; i+=2) {
  410. if (blocks[i] < minx) { minx = blocks[i]; }
  411. if (blocks[i] > maxx) { maxx = blocks[i]; }
  412. if (blocks[i+1] < miny) { miny = blocks[i+1]; }
  413. if (blocks[i+1] > maxy) { maxy = blocks[i+1]; }
  414. }
  415. return {
  416. left: minx,
  417. right: maxx,
  418. top: miny,
  419. bottom: maxy,
  420. width: maxx - minx,
  421. height: maxy - miny
  422. };
  423. }
  424. });
  425. return this.init();
  426. };
  427. this._shapeFactory = {
  428. line: function() {
  429. return new Shape(game, game._shapes.line, false, 'line');
  430. },
  431. square: function() {
  432. return new Shape(game, game._shapes.square, false, 'square');
  433. },
  434. arrow: function() {
  435. return new Shape(game, game._shapes.arrow, false, 'arrow');
  436. },
  437. leftHook: function() {
  438. return new Shape(game, game._shapes.leftHook, false, 'leftHook');
  439. },
  440. rightHook: function() {
  441. return new Shape(game, game._shapes.rightHook, false, 'rightHook');
  442. },
  443. leftZag: function() {
  444. return new Shape(game, game._shapes.leftZag, false, 'leftZag');
  445. },
  446. rightZag: function() {
  447. return new Shape(game, game._shapes.rightZag, false, 'rightZag');
  448. }
  449. };
  450. },
  451. _SetupFilled: function() {
  452. var game = this;
  453. if( this._filled !== null ){ return; }
  454. this._filled = {
  455. data: new Array(game._BLOCK_WIDTH * game._BLOCK_HEIGHT),
  456. score: 0,
  457. highscore: game.options.highestScore,
  458. toClear: {},
  459. check: function(x, y) {
  460. return this.data[this.asIndex(x, y)];
  461. },
  462. add: function(x, y, blockType, blockVariation, blockIndex, blockOrientation) {
  463. if (x >= 0 && x < game._BLOCK_WIDTH && y >= 0 && y < game._BLOCK_HEIGHT) {
  464. this.data[this.asIndex(x, y)] = {
  465. blockType: blockType,
  466. blockVariation: blockVariation,
  467. blockIndex: blockIndex,
  468. blockOrientation: blockOrientation
  469. };
  470. }
  471. },
  472. getFreeSpaces: function() {
  473. var count = 0;
  474. for( var i=0; i<this.data.length; i++ ) {
  475. count += (this.data[i] ? 1 : 0);
  476. }
  477. },
  478. asIndex: function(x, y) {
  479. return x + y*game._BLOCK_WIDTH;
  480. },
  481. asX: function(index) {
  482. return index % game._BLOCK_WIDTH;
  483. },
  484. asY: function(index) {
  485. return Math.floor(index / game._BLOCK_WIDTH);
  486. },
  487. clearAll: function() {
  488. delete this.data;
  489. this.data = new Array(game._BLOCK_WIDTH * game._BLOCK_HEIGHT);
  490. },
  491. _popRow: function(row_to_pop) {
  492. for (var i=game._BLOCK_WIDTH*(row_to_pop+1) - 1; i>=0; i--) {
  493. this.data[i] = (i >= game._BLOCK_WIDTH ? this.data[i-game._BLOCK_WIDTH] : undefined);
  494. }
  495. },
  496. checkForClears: function() {
  497. var startLines = game._board.lines;
  498. var rows = [], i, len, count, mod;
  499. for (i=0, len=this.data.length; i<len; i++) {
  500. mod = this.asX(i);
  501. if (mod == 0) count = 0;
  502. if (this.data[i] && typeof this.data[i] !== 'undefined' && typeof this.data[i].blockType === 'string') {
  503. count += 1;
  504. }
  505. if (mod == game._BLOCK_WIDTH - 1 && count == game._BLOCK_WIDTH) {
  506. rows.push(this.asY(i));
  507. }
  508. }
  509. for (i=0, len=rows.length; i<len; i++) {
  510. this._popRow(rows[i]);
  511. game._board.lines++;
  512. if( game._board.lines % 10 == 0 && game._board.dropDelay > 1 ) {
  513. game._board.dropDelay *= 0.9;
  514. }
  515. }
  516. var clearedLines = game._board.lines - startLines;
  517. this._updateScore(clearedLines);
  518. //Updates high score when score is more than the current high score
  519. if (this.score > this.highscore) {
  520. this._updateHighScore();
  521. }
  522. },
  523. _updateScore: function(numLines) {
  524. if( numLines <= 0 ) { return; }
  525. var scores = [0,400,1000,3000,12000];
  526. if( numLines >= scores.length ){ numLines = scores.length-1 }
  527. this.score += scores[numLines];
  528. game._$scoreText.text(this.score);
  529. game.options.onLine.call(game.element, numLines, scores[numLines], this.score);
  530. },
  531. _resetScore: function() {
  532. this.score = 0;
  533. game._$scoreText.text(this.score);
  534. },
  535. _showHighScore: function() {
  536. game._$highscoreText.text(this.highscore);
  537. },
  538. _updateHighScore: function() {
  539. this.highscore = this.score;
  540. game._$highscoreText.text(this.highscore);
  541. },
  542. draw: function() {
  543. for (var i=0, len=this.data.length, row, color; i<len; i++) {
  544. if (this.data[i] !== undefined) {
  545. row = this.asY(i);
  546. var block = this.data[i];
  547. game._board.drawBlock(this.asX(i), row, block.blockType, block.blockVariation, block.blockIndex, block.blockOrientation);
  548. }
  549. }
  550. }
  551. };
  552. },
  553. _SetupInfo: function() {
  554. var game = this;
  555. this._info = {
  556. mode: game.options.difficulty,
  557. modes: [
  558. 'normal',
  559. 'nice',
  560. 'evil'
  561. ],
  562. modesY: 170,
  563. autopilotY: null,
  564. init: function() {
  565. this.mode = game.options.difficulty;
  566. },
  567. setMode: function(mode) {
  568. this.mode = mode;
  569. game._board.nextShape(true);
  570. }
  571. };
  572. },
  573. _SetupBoard: function() {
  574. var game = this;
  575. var info = this._info;
  576. this._board = {
  577. // This sets the tick rate for the game
  578. animateDelay: 1000 / game.options.speed,
  579. animateTimeoutId: null,
  580. cur: null,
  581. lines: 0,
  582. // DropCount increments on each animation frame. After n frames, the piece drops 1 square
  583. // By making dropdelay lower (down to 0), the pieces move faster, up to once per tick (animateDelay).
  584. dropCount: 0,
  585. dropDelay: 5, //5,
  586. holding: {left: null, right: null, drop: null},
  587. holdingThreshold: 200, // How long do you have to hold a key to make commands repeat (in ms)
  588. started: false,
  589. gameover: false,
  590. renderChanged: true,
  591. init: function() {
  592. this.cur = this.nextShape();
  593. if( game.options.showFieldOnStart ) {
  594. game._drawBackground();
  595. game._board.createRandomBoard();
  596. game._board.render();
  597. }
  598. this.showStartMessage();
  599. },
  600. showStartMessage: function() {
  601. game._$start.show();
  602. },
  603. showGameOverMessage: function() {
  604. game._$gameover.show();
  605. },
  606. nextShape: function(_set_next_only) {
  607. var next = this.next,
  608. func, shape, result;
  609. if (info.mode == 'nice' || info.mode == 'evil') {
  610. func = game._niceShapes;
  611. }
  612. else {
  613. func = game._randomShapes();
  614. }
  615. if( game.options.no_preview ) {
  616. this.next = null;
  617. if (_set_next_only) return null;
  618. shape = func(game._filled, game._checkCollisions, game._BLOCK_WIDTH, game._BLOCK_HEIGHT, info.mode);
  619. if (!shape) throw new Error('No shape returned from shape function!', func);
  620. shape.init();
  621. result = shape;
  622. }
  623. else {
  624. shape = func(game._filled, game._checkCollisions, game._BLOCK_WIDTH, game._BLOCK_HEIGHT, info.mode);
  625. if (!shape) throw new Error('No shape returned from shape function!', func);
  626. shape.init();
  627. this.next = shape;
  628. if (_set_next_only) return null;
  629. result = next || this.nextShape();
  630. }
  631. if( game.options.autoplay ) { //fun little hack...
  632. game._niceShapes(game._filled, game._checkCollisions, game._BLOCK_WIDTH, game._BLOCK_HEIGHT, 'normal', result);
  633. result.orientation = result.best_orientation;
  634. result.x = result.best_x;
  635. }
  636. if( typeof game._theme.complexBlocks !== 'undefined' ) {
  637. if( $.isArray(game._theme.complexBlocks[result.blockType]) ) {
  638. result.blockVariation = game._randInt(0, game._theme.complexBlocks[result.blockType].length-1);
  639. } else {
  640. result.blockVariation = null;
  641. }
  642. }
  643. else if( typeof game._theme.blocks !== 'undefined' ) {
  644. if( $.isArray(game._theme.blocks[result.blockType]) ) {
  645. result.blockVariation = game._randInt(0, game._theme.blocks[result.blockType].length-1);
  646. } else {
  647. result.blockVariation = null;
  648. }
  649. }
  650. return result;
  651. },
  652. animate: function() {
  653. var drop = false,
  654. moved = false,
  655. gameOver = false,
  656. now = Date.now();
  657. if( this.animateTimeoutId ){ clearTimeout(this.animateTimeoutId); }
  658. //game.updateSizes();
  659. if( !this.paused && !this.gameover ) {
  660. this.dropCount++;
  661. // Drop by delay or holding
  662. if( (this.dropCount >= this.dropDelay) ||
  663. (game.options.autoplay) ||
  664. (this.holding.drop && (now - this.holding.drop) >= this.holdingThreshold) ) {
  665. drop = true;
  666. moved = true;
  667. this.dropCount = 0;
  668. }
  669. // Move Left by holding
  670. if( this.holding.left && (now - this.holding.left) >= this.holdingThreshold ) {
  671. moved = true;
  672. this.cur.moveLeft();
  673. }
  674. // Move Right by holding
  675. if( this.holding.right && (now - this.holding.right) >= this.holdingThreshold ) {
  676. moved = true;
  677. this.cur.moveRight();
  678. }
  679. // Test for a collision, add the piece to the filled blocks and fetch the next one
  680. if (drop) {
  681. var cur = this.cur, x = cur.x, y = cur.y, blocks = cur.getBlocks();
  682. if (game._checkCollisions(x, y+1, blocks, true)) {
  683. drop = false;
  684. var blockIndex = 0;
  685. for (var i=0; i<cur.blocksLen; i+=2) {
  686. game._filled.add(x + blocks[i], y + blocks[i+1], cur.blockType, cur.blockVariation, blockIndex, cur.orientation);
  687. if (y + blocks[i] < 0) {
  688. gameOver = true;
  689. }
  690. blockIndex++;
  691. }
  692. game._filled.checkForClears();
  693. this.cur = this.nextShape();
  694. this.renderChanged = true;
  695. // Stop holding drop (and any other buttons). Just in case the controls get sticky.
  696. this.holding.left = null;
  697. this.holding.right = null;
  698. this.holding.drop = null;
  699. game.options.onPlaced.call(game.element);
  700. }
  701. }
  702. }
  703. // Drop
  704. if (drop) {
  705. moved = true;
  706. this.cur.y++;
  707. }
  708. if( drop || moved ) {
  709. this.renderChanged = true;
  710. }
  711. if( gameOver ) {
  712. this.gameover = true;
  713. game.gameover();
  714. if( game.options.autoplay && game.options.autoplayRestart ) {
  715. // On autoplay, restart the game automatically
  716. game.restart();
  717. }
  718. this.renderChanged = true;
  719. } else {
  720. // Update the speed
  721. this.animateDelay = 1000 / game.options.speed;
  722. this.animateTimeoutId = window.setTimeout(function() {
  723. game._board.animate();
  724. }, this.animateDelay);
  725. }
  726. },
  727. createRandomBoard: function() {
  728. var start = [], blockTypes = [], i, ilen, j, jlen, blockType;
  729. // Draw a random blockrain screen
  730. blockTypes = Object.keys(game._shapeFactory);
  731. for (i=0, ilen=game._BLOCK_WIDTH; i<ilen; i++) {
  732. for (j=0, jlen=game._randChoice([game._randInt(0, 8), game._randInt(5, 9)]); j<jlen; j++) {
  733. if (!blockType || !game._randInt(0, 3)) blockType = game._randChoice(blockTypes);
  734. // Use a random piece and orientation
  735. // Todo: Use an actual random variation
  736. game._filled.add(i, game._BLOCK_HEIGHT - j, blockType, game._randInt(0,3), null, game._randInt(0,3));
  737. }
  738. }
  739. /*
  740. for (i=0, ilen=WIDTH; i<ilen; i++) {
  741. for (j=0, jlen=randChoice([randInt(0, 8), randInt(5, 9)]); j<jlen; j++) {
  742. if (!blockType || !randInt(0, 3)) blockType = randChoice(blockTypes);
  743. start.push([i, HEIGHT - j, blockType]);
  744. }
  745. }
  746. if( options.showFieldOnStart ) {
  747. drawBackground();
  748. for (i=0, ilen=start.length; i<ilen; i++) {
  749. drawBlock.apply(drawBlock, start[i]);
  750. }
  751. }
  752. */
  753. game._board.render(true);
  754. },
  755. render: function(forceRender) {
  756. if( this.renderChanged || forceRender ) {
  757. this.renderChanged = false;
  758. game._ctx.clearRect(0, 0, game._PIXEL_WIDTH, game._PIXEL_HEIGHT);
  759. game._drawBackground();
  760. game._filled.draw();
  761. this.cur.draw();
  762. }
  763. },
  764. /**
  765. * Draws one block (Each piece is made of 4 blocks)
  766. * The blockType is used to draw any block.
  767. * The falling attribute is needed to apply different styles for falling and placed blocks.
  768. */
  769. drawBlock: function(x, y, blockType, blockVariation, blockIndex, blockRotation, falling) {
  770. // convert x and y to pixel
  771. x = x * game._block_size;
  772. y = y * game._block_size;
  773. falling = typeof falling === 'boolean' ? falling : false;
  774. var borderWidth = game._theme.strokeWidth;
  775. var borderDistance = Math.round(game._block_size*0.23);
  776. var squareDistance = Math.round(game._block_size*0.30);
  777. var color = this.getBlockColor(blockType, blockVariation, blockIndex, falling);
  778. // Draw the main square
  779. game._ctx.globalAlpha = 1.0;
  780. // If it's an image, the block has a specific texture. Use that.
  781. if( color instanceof Image ) {
  782. game._ctx.globalAlpha = 1.0;
  783. // Not loaded
  784. if( color.width === 0 || color.height === 0 ){ return; }
  785. // A square is the same style for all blocks
  786. if( typeof game._theme.blocks !== 'undefined' && game._theme.blocks !== null ) {
  787. game._ctx.drawImage(color, 0, 0, color.width, color.height, x, y, game._block_size, game._block_size);
  788. }
  789. // A custom texture
  790. else if( typeof game._theme.complexBlocks !== 'undefined' && game._theme.complexBlocks !== null ) {
  791. if( typeof blockIndex === 'undefined' || blockIndex === null ){ blockIndex = 0; }
  792. var getCustomBlockImageCoordinates = function(image, blockType, blockIndex) {
  793. // The image is based on the first ("upright") orientation
  794. var positions = game._shapes[blockType][0];
  795. // Find the number of tiles it should have
  796. var minX = Math.min(positions[0], positions[2], positions[4], positions[6]);
  797. var maxX = Math.max(positions[0], positions[2], positions[4], positions[6]);
  798. var minY = Math.min(positions[1], positions[3], positions[5], positions[7]);
  799. var maxY = Math.max(positions[1], positions[3], positions[5], positions[7]);
  800. var rangeX = maxX - minX + 1;
  801. var rangeY = maxY - minY + 1;
  802. // X and Y sizes should match. Should.
  803. var tileSizeX = image.width / rangeX;
  804. var tileSizeY = image.height / rangeY;
  805. return {
  806. x: tileSizeX * (positions[blockIndex*2]-minX),
  807. y: tileSizeY * Math.abs(minY-positions[blockIndex*2+1]),
  808. w: tileSizeX,
  809. h: tileSizeY
  810. };
  811. };
  812. var coords = getCustomBlockImageCoordinates(color, blockType, blockIndex);
  813. game._ctx.save();
  814. game._ctx.translate(x, y);
  815. game._ctx.translate(game._block_size/2, game._block_size/2);
  816. game._ctx.rotate(-Math.PI/2 * blockRotation);
  817. game._ctx.drawImage(color, coords.x, coords.y, coords.w, coords.h,
  818. -game._block_size/2, -game._block_size/2, game._block_size, game._block_size);
  819. game._ctx.restore();
  820. } else {
  821. // ERROR
  822. game._ctx.fillStyle = '#ff0000';
  823. game._ctx.fillRect(x, y, game._block_size, game._block_size);
  824. }
  825. }
  826. else if( typeof color === 'string' )
  827. {
  828. game._ctx.fillStyle = color;
  829. game._ctx.fillRect(x, y, game._block_size, game._block_size);
  830. // Inner Shadow
  831. if( typeof game._theme.innerShadow === 'string' ) {
  832. game._ctx.globalAlpha = 1.0;
  833. game._ctx.strokeStyle = game._theme.innerShadow;
  834. game._ctx.lineWidth = 1.0;
  835. // Draw the borders
  836. game._ctx.strokeRect(x+1, y+1, game._block_size-2, game._block_size-2);
  837. }
  838. // Decoration (borders)
  839. if( typeof game._theme.stroke === 'string' ) {
  840. game._ctx.globalAlpha = 1.0;
  841. game._ctx.fillStyle = game._theme.stroke;
  842. game._ctx.strokeStyle = game._theme.stroke;
  843. game._ctx.lineWidth = borderWidth;
  844. // Draw the borders
  845. game._ctx.strokeRect(x, y, game._block_size, game._block_size);
  846. }
  847. if( typeof game._theme.innerStroke === 'string' ) {
  848. // Draw the inner dashes
  849. game._ctx.fillStyle = game._theme.innerStroke;
  850. game._ctx.fillRect(x+borderDistance, y+borderDistance, game._block_size-borderDistance*2, borderWidth);
  851. // The rects shouldn't overlap, to prevent issues with transparency
  852. game._ctx.fillRect(x+borderDistance, y+borderDistance+borderWidth, borderWidth, game._block_size-borderDistance*2-borderWidth);
  853. }
  854. if( typeof game._theme.innerSquare === 'string' ) {
  855. // Draw the inner square
  856. game._ctx.fillStyle = game._theme.innerSquare;
  857. game._ctx.globalAlpha = 0.2;
  858. game._ctx.fillRect(x+squareDistance, y+squareDistance, game._block_size-squareDistance*2, game._block_size-squareDistance*2);
  859. }
  860. }
  861. // Return the alpha back to 1.0 so we don't create any issues with other drawings.
  862. game._ctx.globalAlpha = 1.0;
  863. },
  864. getBlockColor: function(blockType, blockVariation, blockIndex, falling) {
  865. /**
  866. * The theme allows us to do many things:
  867. * - Use a specific color for the falling block (primary), regardless of the proper color.
  868. * - Use another color for the placed blocks (secondary).
  869. * - Default to the "original" block color in any of those cases by setting primary and/or secondary to null.
  870. * - With primary and secondary as null, all blocks keep their original colors.
  871. */
  872. var getBlockVariation = function(blockTheme, blockVariation) {
  873. if( $.isArray(blockTheme) ) {
  874. if( blockVariation !== null && typeof blockTheme[blockVariation] !== 'undefined' ) {
  875. return blockTheme[blockVariation];
  876. }
  877. else if(blockTheme.length > 0) {
  878. return blockTheme[0];
  879. } else {
  880. return null;
  881. }
  882. } else {
  883. return blockTheme;
  884. }
  885. }
  886. if( typeof falling !== 'boolean' ){ falling = true; }
  887. if( falling ) {
  888. if( typeof game._theme.primary === 'string' && game._theme.primary !== '' ) {
  889. return game._theme.primary;
  890. } else if( typeof game._theme.blocks !== 'undefined' && game._theme.blocks !== null ) {
  891. return getBlockVariation(game._theme.blocks[blockType], blockVariation);
  892. } else {
  893. return getBlockVariation(game._theme.complexBlocks[blockType], blockVariation);
  894. }
  895. } else {
  896. if( typeof game._theme.secondary === 'string' && game._theme.secondary !== '' ) {
  897. return game._theme.secondary;
  898. } else if( typeof game._theme.blocks !== 'undefined' && game._theme.blocks !== null ) {
  899. return getBlockVariation(game._theme.blocks[blockType], blockVariation);
  900. } else {
  901. return getBlockVariation(game._theme.complexBlocks[blockType], blockVariation);
  902. }
  903. }
  904. }
  905. };
  906. game._niceShapes = game._getNiceShapes();
  907. },
  908. // Utility Functions
  909. _randInt: function(a, b) { return a + Math.floor(Math.random() * (1 + b - a)); },
  910. _randSign: function() { return this._randInt(0, 1) * 2 - 1; },
  911. _randChoice: function(choices) { return choices[this._randInt(0, choices.length-1)]; },
  912. /**
  913. * Find base64 encoded images and load them as image objects, which can be used by the canvas renderer
  914. */
  915. _preloadThemeAssets: function() {
  916. var game = this;
  917. var hexColorcheck = new RegExp('^#[A-F0-9+]{3,6}', 'i');
  918. var base64check = new RegExp('^data:image/(png|gif|jpg);base64,', 'i');
  919. var handleAssetLoad = function() {
  920. // Rerender the board as soon as an asset loads
  921. if( game._board ) {
  922. game._board.render(true);
  923. }
  924. };
  925. var loadAsset = function(src) {
  926. var plainSrc = src;
  927. if( ! hexColorcheck.test( plainSrc ) ) {
  928. // It's an image
  929. src = new Image();
  930. src.src = plainSrc;
  931. src.onload = handleAssetLoad;
  932. } else {
  933. // It's a color
  934. src = plainSrc;
  935. }
  936. return src;
  937. };
  938. var startAssetLoad = function(block) {
  939. // Assets can be an array of variation so they can change color/design randomly
  940. if( $.isArray(block) && block.length > 0 ) {
  941. for( var i=0; i<block.length; i++ ) {
  942. block[i] = loadAsset(block[i]);
  943. }
  944. }
  945. else if( typeof block === 'string' ) {
  946. block = loadAsset(block);
  947. }
  948. return block;
  949. };
  950. if( typeof this._theme.complexBlocks !== 'undefined' ){
  951. var keys = Object.keys(this._theme.complexBlocks);
  952. // Load the complexBlocks
  953. for( var i = 0; i < keys.length; i++ ) {
  954. this._theme.complexBlocks[ keys[i] ] = startAssetLoad( this._theme.complexBlocks[ keys[i] ] );
  955. }
  956. }
  957. else if( typeof this._theme.blocks !== 'undefined' ){
  958. var keys = Object.keys(this._theme.blocks);
  959. // Load the blocks
  960. for( var i = 0; i < keys.length; i++ ) {
  961. this._theme.blocks[ keys[i] ] = startAssetLoad( this._theme.blocks[ keys[i] ] );
  962. }
  963. }
  964. // Load the bg
  965. if( typeof this._theme.backgroundGrid !== 'undefined' ){
  966. if( typeof this._theme.backgroundGrid === 'string' ) {
  967. if( ! hexColorcheck.test( this._theme.backgroundGrid ) ) {
  968. var src = this._theme.backgroundGrid;
  969. this._theme.backgroundGrid = new Image();
  970. this._theme.backgroundGrid.src = src;
  971. this._theme.backgroundGrid.onload = handleAssetLoad;
  972. }
  973. }
  974. }
  975. },
  976. _createHolder: function() {
  977. // Create the main holder (it holds all the ui elements, the original element is just the wrapper)
  978. this._$gameholder = $('<div class="blockrain-game-holder"></div>');
  979. this._$gameholder.css('position', 'relative').css('width', '100%').css('height', '100%');
  980. this.element.html('').append(this._$gameholder);
  981. // Create the game canvas and context
  982. this._$canvas = $('<canvas style="display:block; width:100%; height:100%; padding:0; margin:0; border:none;" />');
  983. if( typeof this._theme.background === 'string' ) {
  984. this._$canvas.css('background-color', this._theme.background);
  985. }
  986. this._$gameholder.append(this._$canvas);
  987. this._canvas = this._$canvas.get(0);
  988. this._ctx = this._canvas.getContext('2d');
  989. },
  990. _createUI: function() {
  991. var game = this;
  992. // Score
  993. game._$score = $(
  994. '<div class="blockrain-score-holder" style="position:absolute;">'+
  995. '<div class="blockrain-score">'+
  996. '<div class="blockrain-score-msg">'+ this.options.scoreText +'</div>'+
  997. '<div class="blockrain-score-num">0</div>'+
  998. '</div>'+
  999. '</div>').hide();
  1000. game._$scoreText = game._$score.find('.blockrain-score-num');
  1001. game._$gameholder.append(game._$score);
  1002. // High Score
  1003. game._$highscore = $(
  1004. '<div class="blockrain-Hscore-holder" style="position:absolute;">'+
  1005. '<div class="blockrain-Hscore">'+
  1006. '<div class="blockrain-Hscore-msg">'+ this.options.highscoreText +'</div>'+
  1007. '<div class="blockrain-Hscore-num">0</div>'+
  1008. '</div>'+
  1009. '</div>').hide();
  1010. game._$highscoreText = game._$highscore.find('.blockrain-Hscore-num');
  1011. game._$gameholder.append(game._$highscore);
  1012. // Create the start menu
  1013. game._$start = $(
  1014. '<div class="blockrain-start-holder" style="position:absolute;">'+
  1015. '<div class="blockrain-start">'+
  1016. '<a class="blockrain-btn blockrain-start-btn">'+ this.options.playButtonText +'</a>'+
  1017. '</div>'+
  1018. '</div>').hide();
  1019. game._$gameholder.append(game._$start);
  1020. game._$start.find('.blockrain-start-btn').click(function(event){
  1021. event.preventDefault();
  1022. game.start();
  1023. });
  1024. // Create the game over menu
  1025. game._$gameover = $(
  1026. '<div class="blockrain-game-over-holder" style="position:absolute;">'+
  1027. '<div class="blockrain-game-over">'+
  1028. '<div class="blockrain-game-over-msg">'+ this.options.gameOverText +'</div>'+
  1029. '<a class="blockrain-btn blockrain-game-over-btn">'+ this.options.restartButtonText +'</a>'+
  1030. '</div>'+
  1031. '</div>').hide();
  1032. game._$gameover.find('.blockrain-game-over-btn').click(function(event){
  1033. event.preventDefault();
  1034. game.restart();
  1035. });
  1036. game._$gameholder.append(game._$gameover);
  1037. this._createControls();
  1038. },
  1039. _createControls: function() {
  1040. var game = this;
  1041. game._$touchLeft = $('<a class="blockrain-touch blockrain-touch-left" />').appendTo(game._$gameholder);
  1042. game._$touchRight = $('<a class="blockrain-touch blockrain-touch-right" />').appendTo(game._$gameholder);
  1043. game._$touchRotateRight = $('<a class="blockrain-touch blockrain-touch-rotate-right" />').appendTo(game._$gameholder);
  1044. game._$touchRotateLeft = $('<a class="blockrain-touch blockrain-touch-rotate-left" />').appendTo(game._$gameholder);
  1045. game._$touchDrop = $('<a class="blockrain-touch blockrain-touch-drop" />').appendTo(game._$gameholder);
  1046. },
  1047. _refreshBlockSizes: function() {
  1048. if( this.options.autoBlockWidth ) {
  1049. this.options.blockWidth = Math.ceil( this.element.width() / this.options.autoBlockSize );
  1050. }
  1051. },
  1052. _getNiceShapes: function() {
  1053. /*
  1054. * Things I need for this to work...
  1055. * - ability to test each shape with this._filled data
  1056. * - maybe give empty spots scores? and try to maximize the score?
  1057. */
  1058. var game = this;
  1059. var shapes = {},
  1060. attr;
  1061. for( var attr in this._shapeFactory ) {
  1062. shapes[attr] = this._shapeFactory[attr]();
  1063. }
  1064. function scoreBlocks(possibles, blocks, x, y, filled, width, height) {
  1065. var i, len=blocks.length, score=0, bottoms = {}, tx, ty, overlaps;
  1066. // base score
  1067. for (i=0; i<len; i+=2) {
  1068. score += possibles[game._filled.asIndex(x + blocks[i], y + blocks[i+1])] || 0;
  1069. }
  1070. // overlap score -- //TODO - don't count overlaps if cleared?
  1071. for (i=0; i<len; i+=2) {
  1072. tx = blocks[i];
  1073. ty = blocks[i+1];
  1074. if (bottoms[tx] === undefined || bottoms[tx] < ty) {
  1075. bottoms[tx] = ty;
  1076. }
  1077. }
  1078. overlaps = 0;
  1079. for (tx in bottoms) {
  1080. tx = parseInt(tx);
  1081. for (ty=bottoms[tx]+1, i=0; y+ty<height; ty++, i++) {
  1082. if (!game._filled.check(x + tx, y + ty)) {
  1083. overlaps += i == 0 ? 2 : 1; //TODO-score better
  1084. //if (i == 0) overlaps += 1;
  1085. break;
  1086. }
  1087. }
  1088. }
  1089. score = score - overlaps;
  1090. return score;
  1091. }
  1092. function resetShapes() {
  1093. for (var attr in shapes) {
  1094. shapes[attr].x = 0;
  1095. shapes[attr].y = -1;
  1096. }
  1097. }
  1098. //TODO -- evil mode needs to realize that overlap is bad...
  1099. var func = function(filled, checkCollisions, width, height, mode, _one_shape) {
  1100. if (!_one_shape) resetShapes();
  1101. var possibles = new Array(width * height),
  1102. evil = mode == 'evil',
  1103. x, y, py,
  1104. attr, shape, i, blocks, bounds,
  1105. score, best_shape, best_score = (evil ? 1 : -1) * 999, best_orientation, best_x,
  1106. best_score_for_shape, best_orientation_for_shape, best_x_for_shape;
  1107. for (x=0; x<width; x++) {
  1108. for (y=0; y<=height; y++) {
  1109. if (y == height || filled.check(x, y)) {
  1110. for (py=y-4; py<y; py++) {
  1111. possibles[filled.asIndex(x, py)] = py; //TODO - figure out better scoring?
  1112. }
  1113. break;
  1114. }
  1115. }
  1116. }
  1117. // for each shape...
  1118. var opts = _one_shape === undefined ? shapes : {cur: _one_shape}; //BOO
  1119. for (attr in opts) { //TODO - check in random order to prevent later shapes from winning
  1120. shape = opts[attr];
  1121. best_score_for_shape = -999;
  1122. // for each orientation...
  1123. for (i=0; i<(shape.symmetrical ? 2 : 4); i++) { //TODO - only look at unique orientations
  1124. blocks = shape.getBlocks(i);
  1125. bounds = shape.getBounds(blocks);
  1126. // try each possible position...
  1127. for (x=-bounds.left; x<width - bounds.width; x++) {
  1128. for (y=-1; y<height - bounds.bottom; y++) {
  1129. if( game._checkCollisions(x, y + 1, blocks, true) ) {
  1130. // collision
  1131. score = scoreBlocks(possibles, blocks, x, y, filled, width, height);
  1132. if (score > best_score_for_shape) {
  1133. best_score_for_shape = score;
  1134. best_orientation_for_shape = i;
  1135. best_x_for_shape = x;
  1136. }
  1137. break;
  1138. }
  1139. }
  1140. }
  1141. }
  1142. if ((evil && best_score_for_shape < best_score) ||
  1143. (!evil && best_score_for_shape > best_score)) {
  1144. best_shape = shape;
  1145. best_score = best_score_for_shape;
  1146. best_orientation = best_orientation_for_shape;
  1147. best_x = best_x_for_shape;
  1148. }
  1149. }
  1150. best_shape.best_orientation = best_orientation;
  1151. best_shape.best_x = best_x;
  1152. return best_shape;
  1153. };
  1154. func.no_preview = true;
  1155. return func;
  1156. },
  1157. _randomShapes: function() {
  1158. // Todo: The shapefuncs should be cached.
  1159. var shapeFuncs = [];
  1160. $.each(this._shapeFactory, function(k,v) { shapeFuncs.push(v); });
  1161. return this._randChoice(shapeFuncs);
  1162. },
  1163. /**
  1164. * Controls
  1165. */
  1166. _setupControls: function(enable) {
  1167. var game = this;
  1168. var moveLeft = function(start) {
  1169. if( ! start ) { game._board.holding.left = null; return; }
  1170. if( ! game._board.holding.left ) {
  1171. game._board.cur.moveLeft();
  1172. game._board.holding.left = Date.now();
  1173. game._board.holding.right = null;
  1174. }
  1175. }
  1176. var moveRight = function(start) {
  1177. if( ! start ) { game._board.holding.right = null; return; }
  1178. if( ! game._board.holding.right ) {
  1179. game._board.cur.moveRight();
  1180. game._board.holding.right = Date.now();
  1181. game._board.holding.left = null;
  1182. }
  1183. }
  1184. var drop = function(start) {
  1185. if( ! start ) { game._board.holding.drop = null; return; }
  1186. if( ! game._board.holding.drop ) {
  1187. game._board.cur.drop();
  1188. game._board.holding.drop = Date.now();
  1189. }
  1190. }
  1191. var rotateLeft = function() {
  1192. game._board.cur.rotate('left');
  1193. }
  1194. var rotateRight = function() {
  1195. game._board.cur.rotate('right');
  1196. }
  1197. // Handlers: These are used to be able to bind/unbind controls
  1198. var handleKeyDown = function(evt) {
  1199. if( ! game._board.cur ) { return true; }
  1200. var caught = false;
  1201. caught = true;
  1202. if (game.options.asdwKeys) {
  1203. switch(evt.keyCode) {
  1204. case 65: /*a*/ moveLeft(true); break;
  1205. case 68: /*d*/ moveRight(true); break;
  1206. case 83: /*s*/ drop(true); break;
  1207. case 87: /*w*/ game._board.cur.rotate('right'); break;
  1208. }
  1209. }
  1210. switch(evt.keyCode) {
  1211. case 37: /*left*/ moveLeft(true); break;
  1212. case 39: /*right*/ moveRight(true); break;
  1213. case 40: /*down*/ drop(true); break;
  1214. case 38: /*up*/ game._board.cur.rotate('right'); break;
  1215. case 88: /*x*/ game._board.cur.rotate('right'); break;
  1216. case 90: /*z*/ game._board.cur.rotate('left'); break;
  1217. default: caught = false;
  1218. }
  1219. if (caught) evt.preventDefault();
  1220. return !caught;
  1221. };
  1222. var handleKeyUp = function(evt) {
  1223. if( ! game._board.cur ) { return true; }
  1224. var caught = false;
  1225. caught = true;
  1226. if (game.options.asdwKeys) {
  1227. switch(evt.keyCode) {
  1228. case 65: /*a*/ moveLeft(false); break;
  1229. case 68: /*d*/ moveRight(false); break;
  1230. case 83: /*s*/ drop(false); break;
  1231. }
  1232. }
  1233. switch(evt.keyCode) {
  1234. case 37: /*left*/ moveLeft(false); break;
  1235. case 39: /*right*/ moveRight(false); break;
  1236. case 40: /*down*/ drop(false); break;
  1237. default: caught = false;
  1238. }
  1239. if (caught) evt.preventDefault();
  1240. return !caught;
  1241. };
  1242. function isStopKey(evt) {
  1243. var cfg = {
  1244. stopKeys: {37:1, 38:1, 39:1, 40:1}
  1245. };
  1246. var isStop = (cfg.stopKeys[evt.keyCode] || (cfg.moreStopKeys && cfg.moreStopKeys[evt.keyCode]));
  1247. if (isStop) evt.preventDefault();
  1248. return isStop;
  1249. }
  1250. function getKey(evt) { return 'safekeypress.' + evt.keyCode; }
  1251. function keydown(evt) {
  1252. var key = getKey(evt);
  1253. $.data(this, key, ($.data(this, key) || 0) - 1);
  1254. return handleKeyDown.call(this, evt);
  1255. }
  1256. function keyup(evt) {
  1257. $.data(this, getKey(evt), 0);
  1258. handleKeyUp.call(this, evt);
  1259. return isStopKey(evt);
  1260. }
  1261. // Unbind everything by default
  1262. // Use event namespacing so we don't ruin other keypress events
  1263. $(document) .unbind('keydown.blockrain')
  1264. .unbind('keyup.blockrain');
  1265. if( ! game.options.autoplay ) {
  1266. if( enable ) {
  1267. $(document) .bind('keydown.blockrain', keydown)
  1268. .bind('keyup.blockrain', keyup);
  1269. }
  1270. }
  1271. },
  1272. _setupTouchControls: function(enable) {
  1273. var game = this;
  1274. // Movements can be held for faster movement
  1275. var moveLeft = function(event){
  1276. event.preventDefault();
  1277. game._board.cur.moveLeft();
  1278. game._board.holding.left = Date.now();
  1279. game._board.holding.right = null;
  1280. game._board.holding.drop = null;
  1281. };
  1282. var moveRight = function(event){
  1283. event.preventDefault();
  1284. game._board.cur.moveRight();
  1285. game._board.holding.right = Date.now();
  1286. game._board.holding.left = null;
  1287. game._board.holding.drop = null;
  1288. };
  1289. var drop = function(event){
  1290. event.preventDefault();
  1291. game._board.cur.drop();
  1292. game._board.holding.drop = Date.now();
  1293. };
  1294. var endMoveLeft = function(event){
  1295. event.preventDefault();
  1296. game._board.holding.left = null;
  1297. };
  1298. var endMoveRight = function(event){
  1299. event.preventDefault();
  1300. game._board.holding.right = null;
  1301. };
  1302. var endDrop = function(event){
  1303. event.preventDefault();
  1304. game._board.holding.drop = null;
  1305. };
  1306. // Rotations can't be held
  1307. var rotateLeft = function(event){
  1308. event.preventDefault();
  1309. game._board.cur.rotate('left');
  1310. };
  1311. var rotateRight = function(event){
  1312. event.preventDefault();
  1313. game._board.cur.rotate('right');
  1314. };
  1315. // Unbind everything by default
  1316. game._$touchLeft.unbind('touchstart touchend click');
  1317. game._$touchRight.unbind('touchstart touchend click');
  1318. game._$touchRotateLeft.unbind('touchstart touchend click');
  1319. game._$touchRotateRight.unbind('touchstart touchend click');
  1320. game._$touchDrop.unbind('touchstart touchend click');
  1321. if( ! game.options.autoplay && enable ) {
  1322. game._$touchLeft.show().bind('touchstart click', moveLeft).bind('touchend', endMoveLeft);
  1323. game._$touchRight.show().bind('touchstart click', moveRight).bind('touchend', endMoveRight);
  1324. game._$touchDrop.show().bind('touchstart click', drop).bind('touchend', endDrop);
  1325. game._$touchRotateLeft.show().bind('touchstart click', rotateLeft);
  1326. game._$touchRotateRight.show().bind('touchstart click', rotateRight);
  1327. } else {
  1328. game._$touchLeft.hide();
  1329. game._$touchRight.hide();
  1330. game._$touchRotateLeft.hide();
  1331. game._$touchRotateRight.hide();
  1332. game._$touchDrop.hide();
  1333. }
  1334. $("#left-arrow").bind('touchstart click', function(event) {moveLeft(event); endMoveLeft(event)});
  1335. $("#right-arrow").bind('touchstart click', function(event) {moveRight(event); endMoveRight(event)});
  1336. $("#up-arrow").bind('touchstart click', rotateRight);
  1337. $("#down-arrow").bind('touchstart click', function(event) {drop(event); endDrop(event)});
  1338. }
  1339. });
  1340. })(jQuery));