├── web ├── favicon.ico ├── og-image.png ├── background.jpg └── index.html ├── src ├── chrome │ ├── icons │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 48.png │ │ └── 512.png │ ├── _locales │ │ ├── jp │ │ │ └── messages.json │ │ ├── da │ │ │ └── messages.json │ │ ├── af │ │ │ └── messages.json │ │ ├── id │ │ │ └── messages.json │ │ ├── ru │ │ │ └── messages.json │ │ ├── pt │ │ │ └── messages.json │ │ ├── fr │ │ │ └── messages.json │ │ ├── es │ │ │ └── messages.json │ │ └── en │ │ │ └── messages.json │ ├── background.js │ └── manifest.json ├── scripts │ ├── getisometricpos.js │ ├── touchlist.js │ ├── random.js │ ├── shadecolor.js │ ├── storage.js │ ├── stats.js │ ├── i18n.js │ ├── levels │ │ ├── levels.json │ │ ├── index.js │ │ ├── encodelevel.js │ │ └── levels.js │ ├── drawCube.js │ ├── sfx.js │ ├── modal.js │ ├── logo.js │ ├── index.js │ ├── sprites.js │ ├── jsfxr.js │ └── game.js ├── templates │ ├── savedialog.js │ └── bootstrap.js ├── index.html ├── images │ ├── icon-menu.svg │ ├── icon-save.svg │ ├── icon-restart.svg │ └── icon-screenshot.svg ├── style │ └── style.css └── i18n.json ├── resources ├── icons │ └── source.png ├── screenshots │ ├── 1280 │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 3-or8.png │ │ └── 4-or8.png │ ├── 1.png │ ├── 2.png │ ├── 3.png │ └── 4.png ├── promos │ ├── tile-1400.png │ ├── tile-440.png │ ├── tile-920.png │ └── tile-800x150.png └── mkicons.sh ├── .gitignore ├── .editorconfig ├── README.md └── package.json /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/web/favicon.ico -------------------------------------------------------------------------------- /web/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/web/og-image.png -------------------------------------------------------------------------------- /web/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/web/background.jpg -------------------------------------------------------------------------------- /src/chrome/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/src/chrome/icons/128.png -------------------------------------------------------------------------------- /src/chrome/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/src/chrome/icons/16.png -------------------------------------------------------------------------------- /src/chrome/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/src/chrome/icons/48.png -------------------------------------------------------------------------------- /src/chrome/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/src/chrome/icons/512.png -------------------------------------------------------------------------------- /resources/icons/source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/icons/source.png -------------------------------------------------------------------------------- /resources/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/screenshots/1.png -------------------------------------------------------------------------------- /resources/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/screenshots/2.png -------------------------------------------------------------------------------- /resources/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/screenshots/3.png -------------------------------------------------------------------------------- /resources/screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/screenshots/4.png -------------------------------------------------------------------------------- /resources/promos/tile-1400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/promos/tile-1400.png -------------------------------------------------------------------------------- /resources/promos/tile-440.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/promos/tile-440.png -------------------------------------------------------------------------------- /resources/promos/tile-920.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/promos/tile-920.png -------------------------------------------------------------------------------- /resources/screenshots/1280/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/screenshots/1280/1.png -------------------------------------------------------------------------------- /resources/screenshots/1280/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/screenshots/1280/2.png -------------------------------------------------------------------------------- /resources/screenshots/1280/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/screenshots/1280/3.png -------------------------------------------------------------------------------- /resources/screenshots/1280/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/screenshots/1280/4.png -------------------------------------------------------------------------------- /resources/promos/tile-800x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/promos/tile-800x150.png -------------------------------------------------------------------------------- /resources/screenshots/1280/3-or8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/screenshots/1280/3-or8.png -------------------------------------------------------------------------------- /resources/screenshots/1280/4-or8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AshKyd/roadblocks/HEAD/resources/screenshots/1280/4-or8.png -------------------------------------------------------------------------------- /src/chrome/_locales/jp/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Road Blocks" 4 | }, 5 | "appDesc": { 6 | "message": "この道路建設パズルゲーム内のマップを接続します。" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/scripts/getisometricpos.js: -------------------------------------------------------------------------------- 1 | module.exports = function(x, y, tileWidth){ 2 | return [ 3 | (x - y) * (tileWidth / 2), 4 | (x + y) * (tileWidth / 4) 5 | ]; 6 | }; 7 | -------------------------------------------------------------------------------- /src/chrome/_locales/da/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Road Blocks" 4 | }, 5 | "appDesc": { 6 | "message": "Slut kortet i denne vejbyggeri puslespil." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/chrome/_locales/af/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Road Blocks" 4 | }, 5 | "appDesc": { 6 | "message": "Verbind die kaart in hierdie pad gebou puzzelspel." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/chrome/_locales/id/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Road Blocks" 4 | }, 5 | "appDesc": { 6 | "message": "Hubungkan peta dalam pembangunan jalan permainan puzzle." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/chrome/_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Road Blocks" 4 | }, 5 | "appDesc": { 6 | "message": "Подключите карту в этой игре дорожно-строительной головоломки." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/chrome/_locales/pt/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Road Blocks" 4 | }, 5 | "appDesc": { 6 | "message": "Conecte o mapa neste quebra-cabeça jogo de construção de estrada." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/chrome/background.js: -------------------------------------------------------------------------------- 1 | chrome.app.runtime.onLaunched.addListener(function() { 2 | chrome.app.window.create('index.html', { 3 | minWidth: 500, 4 | minHeight:400, 5 | state: 'fullscreen', 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/chrome/_locales/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Road Blocks" 4 | }, 5 | "appDesc": { 6 | "message": "Branchez la carte dans ce jeu de puzzle de la construction de routes." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/chrome/_locales/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Road Blocks" 4 | }, 5 | "appDesc": { 6 | "message": "Conecte el mapa en este juego de rompecabezas de la construcción de carreteras." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/scripts/touchlist.js: -------------------------------------------------------------------------------- 1 | module.exports = function(e){ 2 | var originalTouch = e.touches || [e]; 3 | var returnObj = { 4 | clientX: originalTouch[0].clientX, 5 | clientY: originalTouch[0].clientY, 6 | is: e.touches 7 | }; 8 | return returnObj; 9 | }; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | dist.zip 4 | dist-chrome 5 | levels-compiled.json 6 | firefox.zip 7 | chrome.zip 8 | /firefox 9 | /chrome 10 | .levels.json 11 | src/phonegap/www/res/screen 12 | src/phonegap/www/res/icon 13 | src/firefox/icons 14 | src/phonegap/www/dist 15 | .DS_Store 16 | .cache 17 | -------------------------------------------------------------------------------- /src/scripts/random.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Seeded random function 3 | * @param {Number} seed Seed. Increment this yourself. 4 | * @return {Number} Randomish value between 0 and 1. 5 | */ 6 | module.exports = function(seed) { 7 | var x = Math.sin(seed) * 10000; 8 | return x - Math.floor(x); 9 | }; 10 | -------------------------------------------------------------------------------- /src/templates/savedialog.js: -------------------------------------------------------------------------------- 1 | module.exports = `
2 | 3 | 4 |
5 |
6 | 7 | 8 |
9 | `; 10 | -------------------------------------------------------------------------------- /src/chrome/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Road Blocks", 4 | "description": "The title of the application, displayed in the web store." 5 | }, 6 | "appDesc": { 7 | "message": "Connect the map in this road building puzzle game.", 8 | "description":"The description of the application, displayed in the web store." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | # Sublime Text: Install EditorConfig with Package Control and restart Sublime. 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Unix-style newlines with a newline ending every file 8 | [*] 9 | end_of_line = lf 10 | indent_style = space 11 | indent_size = 4 12 | charset = utf-8 13 | 14 | trim_trailing_whitespace = true 15 | insert_final_newline = false 16 | 17 | [{package.json}] 18 | indent_style = space 19 | indent_size = 2 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Work in progress for JS13k. 2 | 3 | * Title unknown. 4 | * The theme is precarious. 5 | 6 | Developing 7 | ---------------- 8 | 9 | First you need to ensure you've got your tools installed: 10 | 11 | * Ensure you're in a Unixlike environment (Windows is unlikely to work). 12 | * `npm install -g browserify uglify-js beefy` 13 | * Run `npm watch` to get started. 14 | 15 | The watch task uses the node `watch` module for platform agnosticism, and runs a 16 | `beefy` server so you don't get out-of-date Browserify builds. 17 | -------------------------------------------------------------------------------- /src/scripts/shadecolor.js: -------------------------------------------------------------------------------- 1 | // Nicked from http://stackoverflow.com/questions/5560248 2 | module.exports = function(color, percent) { 3 | color = color.substr(1); 4 | var num = parseInt(color, 16), 5 | amt = Math.round(2.55 * percent), 6 | R = (num >> 16) + amt, 7 | G = (num >> 8 & 0x00FF) + amt, 8 | B = (num & 0x0000FF) + amt; 9 | return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1); 10 | }; 11 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ROADBLOCKS 6 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/templates/bootstrap.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 |
3 | 4 |
5 |
6 |
7 | 8 | 9 | 10 |
11 |
12 | 13 |
14 | `; 15 | -------------------------------------------------------------------------------- /src/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "background": { 4 | "scripts": [ "background.js" ] 5 | } 6 | }, 7 | "default_locale": "en", 8 | "description": "Connect the map in this road building puzzle game.", 9 | "icons": { 10 | "16": "icons/16.png", 11 | "48": "icons/48.png", 12 | "128": "icons/128.png", 13 | "512": "icons/512.png" 14 | }, 15 | "manifest_version": 2, 16 | "name": "Road Blocks", 17 | "version": "1.3.0", 18 | "permissions": [ 19 | "fullscreen", 20 | "storage" 21 | ], 22 | "offline_enabled": true 23 | } 24 | -------------------------------------------------------------------------------- /src/scripts/storage.js: -------------------------------------------------------------------------------- 1 | var chromeStorage; 2 | try{ 3 | chromeStorage = chrome.storage.sync; 4 | }catch(e){} 5 | 6 | module.exports = { 7 | set: function(key, value){ 8 | module.exports.state[key] = value; 9 | if(chromeStorage){ 10 | chromeStorage.set({state: JSON.stringify(module.exports.state)}); 11 | } 12 | }, 13 | state: {} 14 | }; 15 | 16 | if(chromeStorage){ 17 | chromeStorage.get('state', function(items){ 18 | if(items.state){ 19 | try{ 20 | module.exports.state = JSON.parse(items.state); 21 | }catch(e){ 22 | console.log('failed to parse saved data.'); 23 | } 24 | } 25 | }); 26 | } else { 27 | module.exports.state = localStorage; 28 | } 29 | -------------------------------------------------------------------------------- /src/scripts/stats.js: -------------------------------------------------------------------------------- 1 | var Stats = require('stats.js'); 2 | var statsTypes = [0]; 3 | 4 | module.exports = function(){ 5 | var _this = this; 6 | this.counters = []; 7 | statsTypes.forEach(function(i){ 8 | var stats = new Stats(); 9 | stats.setMode(i); 10 | stats.domElement.style.position = 'absolute'; 11 | stats.domElement.style.left = (i * 90) + 'px'; 12 | stats.domElement.style.top = '0px'; 13 | document.body.appendChild(stats.domElement); 14 | _this.counters[i] = stats; 15 | }); 16 | }; 17 | 18 | module.exports.prototype = { 19 | begin: function(){ 20 | var _this = this; 21 | statsTypes.forEach(function(i){ 22 | _this.counters[i].begin(); 23 | }); 24 | }, 25 | end: function(){ 26 | var _this = this; 27 | statsTypes.forEach(function(i){ 28 | _this.counters[i].end(); 29 | }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roadblocks", 3 | "version": "1.3.0", 4 | "description": "I don't know yet", 5 | "main": "src/scripts/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:AshKyd/roadblocks.git" 9 | }, 10 | "author": "Ash Kyd ", 11 | "license": "BSD", 12 | "bugs": { 13 | "url": "https://github.com/AshKyd/roadblocks/issues" 14 | }, 15 | "devDependencies": { 16 | "stats.js": "~1.0.0", 17 | "i18n": "~0.5.0", 18 | "i18next": "~2.0.0-alpha.17", 19 | "parcel": "^1.11.0" 20 | }, 21 | "scripts": { 22 | "start": "parcel src/index.html", 23 | "build": "npm run clean;parcel build src/index.html -d dist/game/ --public-url /game/;cp web/* dist/", 24 | "clean": "rm -rf dist chrome chrome.zip;mkdir -p dist;", 25 | "encode-levels": "node src/scripts/levels/levels.js > src/scripts/levels/levels.json", 26 | "build-chrome": "npm run clean;npm run build & cp src/chrome/ chrome/ -R;wait;cp dist/* chrome/ -R;cd chrome;zip ../chrome.zip -X -r *" 27 | }, 28 | "browserify": { 29 | "transform": [ 30 | "cssify", 31 | "brfs" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/scripts/i18n.js: -------------------------------------------------------------------------------- 1 | var translations = require('../i18n'); 2 | 3 | var translationMap, langs, preferredLang; 4 | 5 | var i18n = module.exports = { 6 | t: function(str){ 7 | if(translationMap[str]){ 8 | return translationMap[str][langs[preferredLang]]; 9 | } else { 10 | return str; 11 | } 12 | }, 13 | setup: function(langOverride){ 14 | langs = {}; 15 | 16 | [ 17 | "en", 18 | // "es", 19 | // "fr", 20 | // "pt", 21 | // "ru", 22 | // "da", 23 | // "af", 24 | // "id", 25 | // "jp", 26 | ].forEach(function(lang, i){ 27 | langs[lang] = i; 28 | }); 29 | 30 | translationMap = {}; 31 | translations.forEach(function(row){ 32 | translationMap[row[0]] = row; 33 | }); 34 | 35 | preferredLang = 'en'; 36 | (langOverride || navigator.languages).some(function(lang){ 37 | lang = lang.split('-')[0]; 38 | if(langs[lang]){ 39 | preferredLang = lang; 40 | return true; 41 | } 42 | }); 43 | } 44 | }; 45 | 46 | module.exports.setup(); 47 | -------------------------------------------------------------------------------- /src/scripts/levels/levels.json: -------------------------------------------------------------------------------- 1 | {"Puzzle":[["Roads 101","i76c67-255s-1knunumjd",["Connect from left to right by dragging tiles from the top.","Left to right","roady-base"]],["Town planning","i76c67-65184-1p97t4hl2u15",["Now you've got the hang of it, give it a go with all the tiles.","Your turn","roadx"]],["Reverse the flow","azbmn-1q0ank-1g2hikr6f",["Use the helipad to reverse the order of your tiles.","Stack tiles for later","helipad"]],["The Block Forest","1v2s8v-1qj324h32y-1q6ak2ac1xh4cznx3p",["Building road past special tiles like forests or the helipad gives you extra points.","Bonus points","forest"]],["★ Bulldozer Beach","i7b1xa-1q2p0rg4a-1gzj2ngl1pkgajre1jbq4o181xumsg041xxuxo8u1uup921p1l2ks6zg1ouj8oml2bh9tx",["Long press to bulldoze a tile you no longer need.","Bulldoze","dump"],["Congratulations. You've unlocked Free Map mode from the main menu.","Free Map","dump"]],["Palm Island","1vo7ux-gq8p29-1gzk9imq1sdea13u1kz9p2ka1ou7c1b81yeb9toa1tc4t3m1qvtx"],["Loopy Lagoon","juufym-23tbv4l816u-1gzkuybk1kz2judm1jbq4o181xumsg041xxuxo8u1uuph0c55kucn4l"],["Mini Monaco","lj1jeg-1bhgx297at7-1ztkxtp01f53i2vw1dpysny21opahvwp1xhbz7hg1v6adci81l9iuyio1b1oewh41ntzxghi34o8"],["Dual Carriageway","n5zga6-o04u8y-22kz99uk1hatzygi1l5e01zw1k8szvjg1plhv9je1tiqqs261l67rhqd1fgoxwyr"],["Little condo by the sea","1wed7t-1ahr1tavxbe5-1r3foqhe1sde83nn1kzfn8fs22lx2t6o1f3q5hvo1sywaee81l1l2z8n1fo32lgc281qh6l61vv32wmm1l2qqjt01tfxkw0o1a1sx1ai1tpd1b821l8p2xtc1sqlgat51mfusy7k2ppd"]],"Free":[[null,"6exf2-a-"]]} 2 | -------------------------------------------------------------------------------- /src/scripts/drawCube.js: -------------------------------------------------------------------------------- 1 | var cCache = {}; 2 | 3 | var shadeColor = require('./shadecolor'); 4 | 5 | function drawPoly(ctx, a, b, c, d, e, f, g, h, fill, stroke){ 6 | ctx.beginPath(); 7 | ctx.moveTo(a, b); 8 | ctx.lineTo(c, d); 9 | ctx.lineTo(e, f); 10 | ctx.lineTo(g, h); 11 | ctx.closePath(); 12 | ctx.fillStyle = fill; 13 | ctx.strokeStyle = stroke; 14 | ctx.stroke(); 15 | ctx.fill(); 16 | } 17 | 18 | // Draw a cube to the specified specs 19 | module.exports = function(ctx, x, y, wx, wy, h, color, alpha) { 20 | ctx.globalAlpha = alpha || 1; 21 | ctx.lineWidth = 1; 22 | drawPoly( 23 | ctx, 24 | x, // x & y start coords 25 | y, 26 | x - wx, // first lineTo x & y 27 | y - wx * 0.5, 28 | x - wx, // Second lineTo x & y 29 | y - h - wx * 0.5, 30 | x, // Third lineTo x &y 31 | y - h * 1, 32 | color, // fill 33 | shadeColor(color, -5) //stroke 34 | ); 35 | 36 | drawPoly( 37 | ctx, 38 | x, 39 | y, 40 | x + wy, 41 | y - wy * 0.5, 42 | x + wy, 43 | y - h - wy * 0.5, 44 | x, 45 | y - h * 1, 46 | shadeColor(color, 10), 47 | shadeColor(color, 25) 48 | ); 49 | 50 | drawPoly( 51 | ctx, 52 | x, 53 | y - h, 54 | x - wx, 55 | y - h - wx * 0.5, 56 | x - wx + wy, 57 | y - h - (wx * 0.5 + wy * 0.5), 58 | x + wy, 59 | y - h - wy * 0.5, 60 | shadeColor(color, 20), 61 | shadeColor(color, 30) 62 | ); 63 | ctx.globalAlpha = 1; 64 | }; 65 | -------------------------------------------------------------------------------- /src/images/icon-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/scripts/sfx.js: -------------------------------------------------------------------------------- 1 | var ac = window.AudioContext || window.webkitAudioContext; 2 | module.exports = (function(){ 3 | if(!ac){ 4 | return function(){}; 5 | } 6 | var context = new ac(); 7 | 8 | var jsfxr = require('./jsfxr'); 9 | var audioCache = {}; 10 | [ 11 | ['select', [2,,0.11015387261286379,0.4,0.12941028638742866,0.25,,0,,0,,0,,0,,0,,0,1,,0,0.1,,1]], 12 | ['place', [3,,0.30,0.25,,0.08,,-0.3,,0,,0,,0,,0,,0,1,,0,,0,0.35]], 13 | ['ping', [2,,0.1,0.4,0.09,0.44,,0,,0,,0,,0,,0,,0,1,,0,0.1,,0.5]], 14 | ['dialog', [2,,0.04,0.4,0.28,0.35,,0.2,,0.15,0.25,,0,0.33,,0.62,,0,1,,0,,0,0.5]], 15 | ['win', [2,,0.14,0.4,0.5,0.56,,0,,0,,0,,0.08,,0,,0,1,,0,0.1,,0.35]], 16 | ['boom', [3,,0.39,0.73,0.38,0.06,,0.14,,0,,0,,0,,0,,0,1,,0,,0,0.3]], 17 | // ['…',[2,,0.2561,,0.4667,0.3152,,0.1037,,0.2768,0.1347,,,0.236,,,,,1,,,,,0.8]], 18 | ['error',[1,,0.15,,,0.1,,,,,,,,,,,,,1,,,0.1,,0.4]], 19 | ['thud', [3,,0.11,0.47,0.15,0.09,,-0.30,,0,,0,,0,,0,,0,1,,0,,0,0.35]], 20 | ['bloop',[2,,0.25,0.4,0.15,0.2,,0.24,,0.04,0.3,,0,,0,,0,,1,,0,,0,.5]], 21 | ].forEach(function(sound){ 22 | context.decodeAudioData(base64ToArrayBuffer(jsfxr(sound[1])), function(buffer){ 23 | audioCache[sound[0]] = buffer; 24 | }); 25 | }); 26 | 27 | function base64ToArrayBuffer(base64) { 28 | var binary_string = atob(base64.substr(base64.indexOf(',')+1)); 29 | var len = binary_string.length; 30 | var bytes = new Uint8Array( len ); 31 | for (var i = 0; i < len; i++) { 32 | bytes[i] = binary_string.charCodeAt(i); 33 | } 34 | return bytes.buffer; 35 | } 36 | 37 | return function(soundName, delay){ 38 | if(audioCache[soundName]){ 39 | var source = context.createBufferSource(); 40 | source.buffer = audioCache[soundName]; 41 | source.connect(context.destination); 42 | delay = context.currentTime + (delay||0); 43 | source.start(delay); 44 | } 45 | }; 46 | })() 47 | -------------------------------------------------------------------------------- /src/scripts/levels/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified levels 3 | * 12054 bytes without. 4 | * 12031 bytes with minification 5 | */ 6 | var spriteIndex = Object.keys(require('../sprites').sprites); 7 | // var levels = require('./levels.js'); 8 | var levels = require('./levels.json'); 9 | 10 | 11 | function numberise(a){ 12 | return Number(a); 13 | } 14 | 15 | /** 16 | * Custom base36 decoder. 17 | * @param {string} input base36/alphanumeric encoded string from the enc function 18 | * @return {string} Original numeric string as passed to the enc function. 19 | */ 20 | function dec(input){ 21 | // Split our string into chunks of 8 bytes (with whatever left over at the end) 22 | return input ? input.match(/.{8}|.+/g).map(function(chunk){ 23 | // Convert each chunk to base 10. 24 | // Lop off the first character because it will be a safety 1. 25 | return String(parseInt(chunk, 36)).substr(1); 26 | }).join('') : false; 27 | } 28 | 29 | Object.keys(levels).map(function(levelType){ 30 | levels[levelType] = levels[levelType].map(function(level){ 31 | var data = level[1].split('-').map(dec); 32 | var predef = []; 33 | if(data[2]){ 34 | predef = data[2].match(/.{4}/g).map(function(thisBit){ 35 | thisBit = thisBit.match(/(.)(.)(.+)/).map(numberise); 36 | return [ 37 | thisBit[1], 38 | thisBit[2], 39 | spriteIndex[thisBit[3]], 40 | ]; 41 | }); 42 | } 43 | 44 | var defs = data[0].match(/(.+)(.)(.)(.)(..)(.)$/); 45 | return { 46 | seed: numberise(data[1]), 47 | w: defs[2], 48 | h: defs[3], 49 | wMod: defs[4], 50 | base: spriteIndex[defs[5]], 51 | strict: !!defs[6], 52 | dist: data[1] === '0' ? 0 : data[1].split('').map(numberise), 53 | predef : predef, 54 | intro: level[2], 55 | outro: level[3], 56 | name: level[0], 57 | }; 58 | }); 59 | }); 60 | 61 | module.exports = levels; 62 | -------------------------------------------------------------------------------- /src/scripts/modal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Show/hide the tooltip over the top of the game. 3 | */ 4 | var playSound = require('./sfx'); 5 | function getElementHeight(ele){ 6 | return parseInt(w.getComputedStyle(ele, null).getPropertyValue("height")); 7 | } 8 | 9 | var tooltip = document.querySelector('#tt'); 10 | var scrim = document.querySelector('#s'); 11 | 12 | module.exports = { 13 | visible: false, 14 | show: function(message, title, tile, copyfit, cb, btn){ 15 | title = title ? '

'+title+'

' : ''; 16 | tile = tile ? '' : ''; 17 | tooltip.innerHTML = '
'+(btn||'OK')+' '+title+message+tile+'
'; 18 | tooltip.style.display = 'block'; 19 | scrim.style.display = 'block'; 20 | module.exports.visible = true; 21 | setTimeout(function(){ 22 | tooltip.className = 'active'; 23 | scrim.className = 'active'; 24 | 25 | // Copyfit the text to fit the dialog, regardless of screen size. 26 | var height = getElementHeight(tooltip); 27 | var inner = d.querySelector('#tt-inner'); 28 | var img = d.querySelector('#tt-inner img'); 29 | 30 | if(copyfit){ 31 | inner.className = ''; 32 | for(var i=35; i>10; i--){ 33 | inner.style.fontSize = i+'px'; 34 | if(getElementHeight(inner) < height - 100){ 35 | break; 36 | } 37 | } 38 | } else { 39 | inner.className = 'scroll'; 40 | } 41 | 42 | function close(e){ 43 | e.preventDefault(); 44 | playSound('select'); 45 | module.exports.hide(cb); 46 | } 47 | document.querySelector('.close').onclick = close; 48 | scrim.onclick = close; 49 | tooltip.ontouchstart = tooltip.onclick; 50 | setTimeout(function(){ 51 | playSound('dialog'); 52 | },10); 53 | }, 1); 54 | }, 55 | hide: function(cb){ 56 | tooltip.className = ''; 57 | scrim.className = ''; 58 | module.exports.visible = true; 59 | setTimeout(function(){ 60 | tooltip.style.display = 'none'; 61 | scrim.style.display = 'none'; 62 | if(cb){ 63 | cb(); 64 | } 65 | }, 150); 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /src/scripts/logo.js: -------------------------------------------------------------------------------- 1 | var logo = [ 2 | [1,1,1, , ,1,1, , , ,1, , ,1,1, , ,1,1,1, ,1, , , ,1,1, , , ,1,1, ,1, ,1, , ,1,1], 3 | [1, ,1, ,1, , ,1, ,1, ,1, ,1, ,1, ,1, ,1, ,1, , ,1, , ,1, ,1, , , ,1, ,1, ,1, , ], 4 | [1,1, , ,1, , ,1, ,1,1,1, ,1, ,1, ,1,1, , ,1, , ,1, , ,1, ,1, , , ,1,1, , , ,1, ], 5 | [1, ,1, ,1, , ,1, ,1, ,1, ,1, ,1, ,1, ,1, ,1, , ,1, , ,1, ,1, , , ,1, ,1, , , ,1], 6 | [1, ,1, , ,1,1, , ,1, ,1, ,1,1, , ,1,1,1, ,1,1,1, ,1,1, , , ,1,1, ,1, ,1, ,1,1,1] 7 | ]; 8 | var drawCube = require('./drawCube'); 9 | var getIsometricPos = require('./getisometricpos'); 10 | var shadeColor = require('./shadecolor'); 11 | var playSound = require('./sfx'); 12 | 13 | module.exports = function(canvas, ctx, cb, force){ 14 | var x = canvas.width/6.5; 15 | var y = canvas.height/8; 16 | var w = canvas.width/50; 17 | var color = '#55bbff'; 18 | var start = Date.now(); 19 | if(force){ 20 | start -= 5000; 21 | } 22 | render(); 23 | var hitYet = 0; 24 | var hits = 0; 25 | 26 | function render(){ 27 | var now = Date.now()-500*2; 28 | ctx.clearRect(0,0,canvas.width,canvas.height); 29 | logo.map(function(line, i){ 30 | line.map(function(char, j){ 31 | var pos = getIsometricPos(j, i, w); 32 | var diff = ((now + j*10)-start)/500; 33 | var yoff = Math.max(0, (screen.height) * (1-diff)); 34 | if(yoff === 0){ 35 | hits++; 36 | } 37 | if(char == 1){ 38 | drawCube( 39 | ctx, 40 | x + pos[0]*2, 41 | y + pos[1]*2 - yoff, 42 | w, 43 | w, 44 | w*.75, 45 | shadeColor('#888888', 0-j) 46 | ); 47 | drawCube( 48 | ctx, 49 | x + pos[0]*2, 50 | y + pos[1]*2 - w*0.75 - yoff, 51 | w, 52 | w, 53 | w*.25, 54 | shadeColor(color, 0-j) 55 | ); 56 | } 57 | }); 58 | }); 59 | 60 | if(hits && !hitYet){ 61 | hitYet = 1; 62 | for(var i=0; i<4; i++){ 63 | playSound('thud', i/9); 64 | } 65 | } 66 | 67 | if(now - start <= 1000){ 68 | requestAnimationFrame(render); 69 | } else { 70 | if(cb){ 71 | cb(); 72 | } 73 | } 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/images/icon-save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/images/icon-restart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 63 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /resources/mkicons.sh: -------------------------------------------------------------------------------- 1 | resize () { 2 | src=$1 3 | filename=$2; 4 | size=$3; 5 | convert $src -resize $size ../$filename; 6 | } 7 | 8 | # Make directories 9 | mkdir -p ../src/phonegap/www/res/screen/ios 10 | mkdir -p ../src/phonegap/www/res/screen/android 11 | mkdir -p ../src/phonegap/www/res/icon/ios 12 | mkdir -p ../src/phonegap/www/res/icon/android 13 | mkdir -p ../src/chrome/icons 14 | mkdir -p ../src/firefox/icons 15 | 16 | # Chrome OS 17 | resize icons/source.png 'src/chrome/icons/512.png' 512; 18 | resize icons/source.png 'src/chrome/icons/128.png' 128; 19 | resize icons/source.png 'src/chrome/icons/48.png' 48; 20 | resize icons/source.png 'src/chrome/icons/16.png' 16; 21 | 22 | # Firefox 23 | resize icons/source.png 'src/firefox/icons/512.png' 512; 24 | resize icons/source.png 'src/firefox/icons/128.png' 128; 25 | 26 | #iOS 27 | iconPath='src/phonegap/www/res/icon/ios/'; 28 | resize icons/source.png $iconPath/icon-40.png, 40; 29 | resize icons/source.png $iconPath/icon-40@2x.png, 80; 30 | resize icons/source.png $iconPath/icon-50.png, 50; 31 | resize icons/source.png $iconPath/icon-50@2x.png, 100; 32 | resize icons/source.png $iconPath/icon-60.png, 60; 33 | resize icons/source.png $iconPath/icon-60@2x.png, 120; 34 | resize icons/source.png $iconPath/icon-60@3x.png, 180; 35 | resize icons/source.png $iconPath/icon-72.png, 72; 36 | resize icons/source.png $iconPath/icon-72@2x.png, 144; 37 | resize icons/source.png $iconPath/icon-76.png, 76; 38 | resize icons/source.png $iconPath/icon-76@2x.png, 152; 39 | resize icons/source.png $iconPath/icon-small.png, 29; 40 | resize icons/source.png $iconPath/icon-small@2x.png, 58; 41 | resize icons/source.png $iconPath/icon.png, 57; 42 | resize icons/source.png $iconPath/icon@2x.png, 114; 43 | 44 | iosDestFolder='src/phonegap/www/res/screen/ios/'; 45 | promoLandscape='promos/spacekid-landscape-16x9.svg'; 46 | promoPortrait='promos/spacekid-portrait-16x9.svg'; 47 | resize $promoLandscape $iosDestFolder/screen-iphone-landscape.png 480; 48 | resize $promoLandscape $iosDestFolder/screen-iphone-landscape-2x.png 960; 49 | resize $promoPortrait $iosDestFolder/screen-iphone-portrait.png 320; 50 | resize $promoPortrait $iosDestFolder/screen-iphone-portrait-2x.png 640; 51 | 52 | promoLandscape='promos/spacekid-landscape-4x3.svg'; 53 | promoPortrait='promos/spacekid-portrait-4x3.svg'; 54 | resize $promoLandscape $iosDestFolder/screen-ipad-landscape.png 1024; 55 | resize $promoLandscape $iosDestFolder/screen-ipad-landscape-2x.png 2048; 56 | resize $promoLandscape $iosDestFolder/screen-ipad-portrait.png 768; 57 | resize $promoLandscape $iosDestFolder/screen-ipad-portrait-2x.png 1536; 58 | 59 | 60 | #Android 61 | resize icons/source.png 'src/phonegap/www/res/icon/android/icon-36-ldpi.png', 36; 62 | resize icons/source.png 'src/phonegap/www/res/icon/android/icon-48-mdpi.png', 48; 63 | resize icons/source.png 'src/phonegap/www/res/icon/android/icon-72-hdpi.png', 72; 64 | resize icons/source.png 'src/phonegap/www/res/icon/android/icon-96-xhdpi.png', 96; 65 | resize icons/source.png 'src/phonegap/www/res/icon/android/icon-96-xxhdpi.png', 144; 66 | 67 | promoLandscape='promos/spacekid-landscape-16x9.svg'; 68 | promoPortrait='promos/spacekid-portrait-16x9.svg'; 69 | androidDestFolder='src/phonegap/www/res/screen/android/'; 70 | resize $promoLandscape $androidDestFolder/screen-hdpi-landscape.png 800; 71 | resize $promoLandscape $androidDestFolder/screen-ldpi-landscape.png 320; 72 | resize $promoLandscape $androidDestFolder/screen-mdpi-landscape.png 480; 73 | resize $promoLandscape $androidDestFolder/screen-xhdpi-landscape.png 1280; 74 | resize $promoPortrait $androidDestFolder/screen-hdpi-portrait.png 480; 75 | resize $promoPortrait $androidDestFolder/screen-ldpi-portrait.png 200; 76 | resize $promoPortrait $androidDestFolder/screen-mdpi-portrait.png 320; 77 | resize $promoPortrait $androidDestFolder/screen-xhdpi-portrait.png 720; 78 | -------------------------------------------------------------------------------- /src/images/icon-screenshot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 63 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/scripts/levels/encodelevel.js: -------------------------------------------------------------------------------- 1 | 2 | // 3.61 kb - beginning 3 | // 2.75 - after index 4 | // 2.45 after combining predef array 5 | // 6 | var SpriteLib = require('../sprites'); 7 | var spriteIndex = Object.keys(SpriteLib.sprites); 8 | 9 | /** 10 | * Pad a number with the specified number zeroes. 11 | */ 12 | function pad(str, num){ 13 | str = String(str); 14 | while(str.length < num){ 15 | str = '0' + str; 16 | } 17 | return str; 18 | } 19 | 20 | /** 21 | * Encode a string using a custom base36 encoding. 22 | * * Strings are chunked into 8 byte base36 encoded pieces for integer safety. 23 | * * The final piece may not make up 8 bytes, this is piecked up by the decoder.. 24 | * @param {string} num string full of numeric digits to encode. 25 | * @return {string} base36/alphanumeric encoded string. 26 | */ 27 | function enc(num){ 28 | // Stringify our number in case it was input as an integer. 29 | num = String(num); 30 | 31 | // Keep track of our encoded chunks. 32 | var encodedChunks = []; 33 | 34 | // Continue until we've processed the entire string. 35 | while(num.length){ 36 | // Start somewhere. 37 | var splitPosition = 7; 38 | 39 | // Try incrementally larger pieces until we get one that's exectly 40 | // 8 characters long. 41 | var encodedNum = ''; 42 | do { 43 | // toString(36) converts decimal to base36. 44 | // Add a leading 1 for safety, as any leading zeroes would otherwise 45 | // be lost. 46 | encodedNum = Number('1' + num.substr(0, ++splitPosition)).toString(36); 47 | } while(encodedNum.length < 8 && splitPosition < num.length && splitPosition < 15); 48 | 49 | // Push our chunk onto the list of encoded chunks and remove these 50 | // digits from our string. 51 | encodedChunks.push(encodedNum); 52 | num = num.substr(splitPosition); 53 | } 54 | 55 | // Return a big ol' string. 56 | return encodedChunks.join(''); 57 | } 58 | 59 | 60 | /** 61 | * Custom base36 decoder. 62 | * @param {string} input base36/alphanumeric encoded string from the enc function 63 | * @return {string} Original numeric string as passed to the enc function. 64 | */ 65 | function dec(input){ 66 | // Split our string into chunks of 8 bytes (with whatever left over at the end) 67 | return input.match(/.{8}|.+/g).map(function(chunk){ 68 | // Convert each chunk to base 10. 69 | return parseInt(chunk, 36); 70 | }).join(''); 71 | } 72 | 73 | var before = 0; 74 | var after = 0; 75 | 76 | module.exports = function(level){ 77 | // Flatten 78 | level.predef = level.predef.map(function(predef){ 79 | var index = spriteIndex.indexOf(predef[2]); 80 | if(index == -1){ 81 | return predef.join(''); 82 | } 83 | predef[2] = pad(index, 2); 84 | return predef.join(''); 85 | }).join(''); 86 | 87 | // Encoded payload 1. 88 | var payload = [ 89 | level.seed, // 0 90 | level.w, // 1 91 | level.h, // 2 92 | level.wMod || level.w, // 3 93 | pad(spriteIndex.indexOf(level.base), 2),// 4 94 | level.strict ? 1 : 0, // 5 95 | ].join(''); 96 | 97 | var dist = 0; 98 | if(level.dist){ 99 | // Map them back to integers if they're defined as strings. 100 | dist = level.dist.map(function(tile){ 101 | if(typeof tile === 'string'){ 102 | tile = spriteIndex.indexOf(tile); 103 | } 104 | return tile; 105 | }).join(''); 106 | } 107 | 108 | var entry = [ 109 | level.name, 110 | [ 111 | enc(payload), 112 | '-', 113 | enc(dist), 114 | '-', 115 | enc(level.predef), 116 | ].join('') 117 | ]; 118 | 119 | before += (payload.length + (dist.length || 0) + level.predef.length); 120 | after += (enc(payload).length + (enc(dist).length || 0) + enc(level.predef).length); 121 | if(level.intro){ 122 | entry.push(level.intro); 123 | if(level.outro){ 124 | entry.push(level.outro); 125 | } 126 | } 127 | return entry; 128 | }; 129 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Road Blocks game 6 | 11 | 15 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 37 | 38 | 42 | 51 | 66 | 67 | 68 |
69 |
70 |
71 |

Road Blocks

72 |
73 |
74 |
78 |
79 |

Connect the map in this road building puzzle game.

80 |

81 | Ten levels of increasing difficulty, two map styles, and a whole 82 | bunch of road blocks. Complete the puzzles and unlock free play mode 83 | to build the little town of your dreams. 84 |

85 | 86 |
87 | Launch the game 88 |
89 |
90 |
91 | 92 |
93 |
94 |

Watch the video

95 |
96 | 101 |
102 |
103 |
104 | 105 |
106 |

Word on the street

107 |
108 |

Testimonials

109 |
110 |
“my school blocked it”
111 |
— A Google User
112 |
113 |
114 |
“I really love this!”
115 |
— Emerson S
116 |
117 |
118 |
“the best feature is the exit button”
119 |
— An unkind Google user
120 |
121 |
122 |
123 | 124 |
125 |
126 |

Try Road Blocks today

127 |
128 | Launch the game 129 |
130 |
131 |
132 | 133 |
134 |
135 |

Read the making of

136 |
137 | “Because I wanted this to be a first-class touch game, I implemented 138 | everything mobile first. This was a fantastic way to discover all 139 | the limitations of the platform at the get-go rather than having to 140 | refit a desktop game to mobile later on.” 141 |
142 |

143 |
144 | 145 | Road Blocks & js13k » 146 | 147 |

148 |
149 |
150 | 151 | 164 |
165 | 166 | 167 | -------------------------------------------------------------------------------- /src/style/style.css: -------------------------------------------------------------------------------- 1 | body, 2 | html{ 3 | margin:0; 4 | padding:0; 5 | background: #191F27; 6 | overflow:hidden; 7 | font-family: 'Trebuchet MS', 'Chalkboard', 'ChalkboardSE-Regular', sans-serif; 8 | } 9 | 10 | *{ 11 | -webkit-transition:opacity .15s, left .15s, transform .15s, top .15s; 12 | transition:opacity .15s, left .15s, transform .15s, top .15s; 13 | } 14 | 15 | #tt, 16 | #tt-inner, 17 | .close{ 18 | border:3px solid #4AADFF; 19 | border-radius: 8px; 20 | color: #2C70A8; 21 | } 22 | 23 | #tt{ 24 | display:block; 25 | position:absolute; 26 | top:30px; 27 | bottom:60px; 28 | left:30px; 29 | right:50px; 30 | padding: 20px; 31 | z-index:10; 32 | border-radius:10px; 33 | font-size:2em; 34 | -webkit-transform: scale(0); 35 | transform: scale(0); 36 | background: rgba(74, 173, 255, .6); 37 | box-shadow: 0 0 1em black; 38 | } 39 | 40 | #tt-inner{ 41 | background: rgba(255,255,255,.8); 42 | padding:10px 30px; 43 | } 44 | 45 | #tt-inner.scroll{ 46 | overflow:auto; 47 | font-size:25px !important; 48 | max-height:85%; 49 | } 50 | 51 | .attr{ 52 | position:absolute; 53 | right:10px; 54 | bottom:10px; 55 | font-size:8px; 56 | opacity:.2; 57 | color:#fff; 58 | } 59 | .attr a{ 60 | color:#fff; 61 | } 62 | .attr:hover, 63 | .attr:focus{ 64 | opacity:1; 65 | } 66 | 67 | .close{ 68 | position:absolute; 69 | bottom:20px; 70 | right:20px; 71 | background: #CCE7FE; 72 | font-weight:bold; 73 | display:block; 74 | font-size:24px; 75 | text-align:center; 76 | line-height:45px; 77 | cursor: pointer; 78 | padding: 0 2em; 79 | } 80 | .close:hover{ 81 | transform: scale(1.05); 82 | } 83 | 84 | #tt.active{ 85 | -webkit-transform: scale(1); 86 | transform: scale(1); 87 | } 88 | 89 | #s.active{ 90 | opacity:1; 91 | } 92 | 93 | #s{ 94 | opacity:0; 95 | z-index:9; 96 | position:absolute; 97 | left:0; 98 | top:0; 99 | bottom:0; 100 | right:0; 101 | background:rgba(0,0,0,.5); 102 | } 103 | 104 | #tt img{ 105 | display:block; 106 | margin:0 auto 0; 107 | padding-bottom:20px; 108 | max-width:200px; 109 | } 110 | 111 | /* buttons */ 112 | #buttons{ 113 | position:absolute; 114 | z-index: 5; 115 | bottom:10px; 116 | left:-10em; 117 | background: #55bbff; 118 | border:1px solid white; 119 | border-left-style: none; 120 | padding:.2cm .5cm; 121 | font-size:.9cm; 122 | } 123 | .ingame #buttons, 124 | .rumble{ 125 | left:0; 126 | } 127 | 128 | #buttons a img{ 129 | display:inline-block; 130 | width:1cm; 131 | height:1cm; 132 | color:white; 133 | border-radius:2px; 134 | cursor:pointer; 135 | } 136 | #buttons a:hover{ 137 | -webkit-transform: scale(1.1); 138 | transform: scale(1.1); 139 | } 140 | 141 | #buttons a img{ 142 | width:1cm; 143 | } 144 | 145 | #buttons a + a{ 146 | margin-left:.5cm; 147 | } 148 | 149 | /* points */ 150 | #p{ 151 | position:absolute; 152 | z-index:5; 153 | top:-3em; 154 | right:10px; 155 | margin-left:20px; 156 | font-weight:bold; 157 | font-size:30px; 158 | color: #55bbff; 159 | text-align:right; 160 | display:block; 161 | text-shadow: 162 | -1px -1px 0 #fff, 163 | 1px -1px 0 #fff, 164 | -1px 1px 0 #fff, 165 | 1px 1px 0 #fff; 166 | } 167 | 168 | .ingame #p{ 169 | top:10px; 170 | } 171 | 172 | @-webkit-keyframes rubberBand { 173 | from { 174 | -webkit-transform: scale3d(1, 1, 1); 175 | transform: scale3d(1, 1, 1); 176 | } 177 | 178 | 30% { 179 | -webkit-transform: scale3d(1.25, 0.75, 1); 180 | transform: scale3d(1.25, 0.75, 1); 181 | } 182 | 183 | 40% { 184 | -webkit-transform: scale3d(0.75, 1.25, 1); 185 | transform: scale3d(0.75, 1.25, 1); 186 | } 187 | 188 | 50% { 189 | -webkit-transform: scale3d(1.15, 0.85, 1); 190 | transform: scale3d(1.15, 0.85, 1); 191 | } 192 | 193 | 65% { 194 | -webkit-transform: scale3d(.95, 1.05, 1); 195 | transform: scale3d(.95, 1.05, 1); 196 | } 197 | 198 | 75% { 199 | -webkit-transform: scale3d(1.05, .95, 1); 200 | transform: scale3d(1.05, .95, 1); 201 | } 202 | 203 | 100% { 204 | -webkit-transform: scale3d(1, 1, 1); 205 | transform: scale3d(1, 1, 1); 206 | } 207 | } 208 | 209 | .rubberBand { 210 | -webkit-animation-duration: 1s; 211 | animation-duration: 1s; 212 | -webkit-animation-fill-mode: both; 213 | animation-fill-mode: both; 214 | -webkit-animation-name: rubberBand; 215 | animation-name: rubberBand; 216 | -webkit-animation-delay: .2s; 217 | animation-delay: .2s; 218 | } 219 | 220 | h1{ 221 | font-size:1.2em; 222 | font-weight:bold; 223 | } 224 | 225 | .rumble { 226 | -webkit-animation-name: rumble; 227 | -webkit-animation-duration: .25s; 228 | -webkit-animation-iteration-count: 2; 229 | 230 | animation-name: rumble; 231 | animation-duration: .25s; 232 | animation-iteration-count: 20; 233 | } 234 | 235 | @-webkit-keyframes rumble { 236 | 0% {-webkit-transform: rotate(0deg);} 237 | 25% {-webkit-transform: translate(4px, 4px);} 238 | 50% {-webkit-transform: translate(0px, -4px);} 239 | 75% {-webkit-transform: translate(-4px, 0px);} 240 | 100% {-webkit-transform: translate(0px, 2px);} 241 | } 242 | 243 | @keyframes rumble { 244 | 0% {transform: rotate(0deg);} 245 | 25% {transform: translate(4px, 4px);} 246 | 50% {transform: translate(0px, -4px);} 247 | 75% {transform: translate(-4px, 0px);} 248 | 100% {transform: translate(0px, 2px);} 249 | } 250 | 251 | #menu{ 252 | -webkit-transform:scale(0); 253 | transform:scale(0); 254 | height:0; 255 | } 256 | 257 | body.menu #menu{ 258 | display:block; 259 | position:absolute; 260 | left:0; 261 | right:0; 262 | bottom:20px; 263 | text-align: center; 264 | font-weight:bold; 265 | color:white; 266 | -webkit-transform:scale(1); 267 | transform:scale(1); 268 | height:auto; 269 | } 270 | 271 | #menu > div{ 272 | padding:10px 20px; 273 | margin:10px 15px; 274 | display:inline-block; 275 | cursor: pointer; 276 | border-radius: 2px; 277 | } 278 | 279 | #menu > div:hover, 280 | .pill.active:hover, 281 | #buttons a:hover{ 282 | background: #55bbff; 283 | transform:scale(1.1); 284 | } 285 | 286 | #menu img{ 287 | display:block; 288 | margin:0 0 10px; 289 | padding:0; 290 | } 291 | 292 | .pill{ 293 | display:block; 294 | border-radius: 5px; 295 | color:#ddd; 296 | background:#ACB2B7; 297 | padding:0.5em; 298 | margin:0.5em 0; 299 | cursor:pointer; 300 | } 301 | 302 | .pill.active{ 303 | background:#3184C7; 304 | color:white; 305 | } 306 | 307 | #tl{ 308 | position:absolute; 309 | right:0; 310 | top:0; 311 | bottom:0; 312 | width:15%; 313 | max-width:128px; 314 | background: #55bbff; 315 | border-left: 1px solid white; 316 | display:none; 317 | overflow-y:auto; 318 | /* Overlap points display we don't want */ 319 | z-index: 5; 320 | } 321 | #tl.active{ 322 | display:block; 323 | } 324 | #tl img{ 325 | width:90%; 326 | display:block; 327 | margin:0 5%; 328 | border-radius: 5px; 329 | cursor:pointer; 330 | } 331 | #tl img:hover{ 332 | transform:scale(1.1); 333 | } 334 | #tl img.active{ 335 | transform:scale(1.1); 336 | background:white; 337 | } 338 | 339 | 340 | 341 | @media(min-width: 800px){ 342 | #tt{ 343 | left:20%; 344 | right:20%; 345 | top:100px; 346 | bottom:100px; 347 | } 348 | #tt-inner{ 349 | padding:30px 50px; 350 | } 351 | 352 | #menu > div:hover, 353 | .pill.active:hover, 354 | #buttons a:hover{ 355 | background: #55bbff; 356 | transform:scale(1.01); 357 | } 358 | } 359 | 360 | @media(max-height:768px){ 361 | #tt{ 362 | top:0; 363 | left:0; 364 | bottom:0; 365 | right:0; 366 | border-radius:0; 367 | border:none; 368 | } 369 | #tt img{ 370 | max-height: 200px; 371 | } 372 | } 373 | 374 | @media(max-height:490px) and (orientation: landscape){ 375 | #tt{ 376 | padding-left: 150px; 377 | } 378 | #tt img{ 379 | position:absolute; 380 | left:20px; 381 | top:50px; 382 | } 383 | } 384 | @media(max-width:800px){ 385 | #buttons{ 386 | font-size:20px; 387 | } 388 | } 389 | 390 | textarea, 391 | input{ 392 | display:block; 393 | border:2px solid #4AADFF; 394 | border-radius:3px; 395 | padding:10px; 396 | margin:10px 0; 397 | background:transparent; 398 | color:#2C70A8; 399 | font-weight:bold; 400 | } 401 | textarea{ 402 | width:100%; 403 | } 404 | -------------------------------------------------------------------------------- /src/scripts/index.js: -------------------------------------------------------------------------------- 1 | window.d = document; 2 | window.w = window; 3 | d.body.innerHTML = require("../templates/bootstrap.js"); 4 | require("../style/style.css"); 5 | var Game = require("./game"); 6 | var canvas = d.querySelector("canvas"); 7 | canvas.width = innerWidth; 8 | canvas.height = innerHeight; 9 | var ctx = canvas.getContext("2d"); 10 | var levels = require("./levels"); 11 | var SpriteLib = require("./sprites"); 12 | var logo = require("./logo"); 13 | var playSound = require("./sfx"); 14 | var modal = require("./modal"); 15 | var Storage = require("./storage"); 16 | var tileList = d.querySelector("#tl"); 17 | var i18n = require("./i18n"); 18 | 19 | // Feature detect Chrome using chrome.storage.sync. 20 | var chromeApp = false; 21 | try { 22 | chromeApp = !!chrome.storage.sync; 23 | } catch (e) {} 24 | 25 | window.onresize = function() { 26 | d.body.width = window.innerWidth; 27 | d.body.height = window.innerHeight; 28 | }; 29 | window.onresize(); 30 | 31 | // Previous active tile. 32 | var thisActive; 33 | 34 | // Press escape to close dialogs, clear selection. 35 | d.onkeydown = function(evt) { 36 | evt = evt || window.event; 37 | if (evt.keyCode == 27) { 38 | if (thisActive) { 39 | // If we have an active tile, deselect it 40 | deselectTile(); 41 | } else if (modal.visible) { 42 | // if we have a visible modal, 43 | modal.hide(); 44 | } else if (thisGame) { 45 | // TODO: Show a dialog "are you sure you want to quit" 46 | } else { 47 | try { 48 | window.close(); 49 | } catch (e) {} 50 | } 51 | } 52 | }; 53 | 54 | d.body.onclick = function(e) { 55 | var data = e.target.dataset; 56 | if (actions[data.action]) { 57 | actions[data.action](data); 58 | return false; 59 | } 60 | if (thisGame[data.action]) { 61 | thisGame[data.action](data); 62 | return false; 63 | } 64 | }; 65 | 66 | function deselectTile() { 67 | thisGame.setTile("", 0); 68 | thisActive.className = ""; 69 | thisActive = 0; 70 | } 71 | 72 | /** 73 | * Fire up a game and render one single tile as specified. 74 | */ 75 | function drawTile(tile, size) { 76 | var drawTileCanvas = d.createElement("canvas"); 77 | drawTileCanvas.width = size; 78 | drawTileCanvas.height = size; 79 | var drawable = new Game({ 80 | tileSize: size, 81 | w: 1, 82 | h: 1, 83 | wMod: 1.3, 84 | canvas: drawTileCanvas, 85 | base: tile, 86 | predef: [], 87 | dist: [], 88 | renderOnly: true 89 | }); 90 | return drawTileCanvas.toDataURL("image/png"); 91 | } 92 | 93 | var thisGame = 0; 94 | var thisLevelId; 95 | var thisGameType; 96 | var actions = { 97 | restart: function() { 98 | thisGame.destroy(function() { 99 | loadGame(thisGameType, thisLevelId); 100 | }); 101 | }, 102 | menu: function() { 103 | if (thisGame) { 104 | thisGame.destroy(function() { 105 | showMenu(); 106 | }); 107 | thisGame = 0; 108 | } 109 | }, 110 | save: function() { 111 | modal.show( 112 | require("../templates/savedialog.js"), 113 | "Save map", 114 | null, 115 | 0, 116 | function() { 117 | var saveFile = thisGame.saveState({ 118 | name: document.querySelector("#save-name").value, 119 | intro: document.querySelector("#save-intro").value 120 | }); 121 | var games = Storage.state.customgames || []; 122 | games.push(saveFile); 123 | Storage.set("customgames", JSON.stringify(games)); 124 | }, 125 | "Save" 126 | ); 127 | }, 128 | Puzzle: function() { 129 | modal.show( 130 | levels.Puzzle.map(function(level, i) { 131 | var unlocked = !i || Storage.state["Puzzle" + i]; 132 | return ( 133 | '' + 140 | (i + 1) + 141 | ". " + 142 | i18n.t(level.name) + 143 | "" 144 | ); 145 | }).join(""), 146 | i18n.t("Puzzle Play"), 147 | null, 148 | 0, 149 | 0, 150 | i18n.t("Back") 151 | ); 152 | }, 153 | "Free map": function() { 154 | if (!Storage.state.Puzzle5) { 155 | modal.show( 156 | i18n.t("Unlock this mode by completing more puzzles."), 157 | i18n.t("Mode locked") 158 | ); 159 | } else { 160 | tileList.innerHTML = SpriteLib.placeable 161 | .map(function(sprite) { 162 | return ( 163 | '' 170 | ); 171 | }) 172 | .join(""); 173 | tileList.className = "active"; 174 | loadGame("Free", 0); 175 | } 176 | }, 177 | Exit: function() { 178 | window.close(); 179 | }, 180 | // 'Report a bug': function(){ 181 | // window.open('https://github.com/AshKyd/roadblocks/issues/new'); 182 | // }, 183 | 184 | // Load puzzle game 185 | l: function(data) { 186 | modal.hide(function() { 187 | loadGame("Puzzle", data.l); 188 | }); 189 | }, 190 | 191 | // place tile 192 | p: function(data) { 193 | var prevActive = thisActive; 194 | if (prevActive) { 195 | prevActive.className = ""; 196 | } 197 | thisActive = d.querySelector("#t" + data.s); 198 | if (prevActive === thisActive) { 199 | deselectTile(); 200 | } else { 201 | thisActive.className = "active"; 202 | thisGame.setTile(data.s, function() { 203 | deselectTile(); 204 | }); 205 | } 206 | } 207 | }; 208 | 209 | function loadGame(gameType, levelId) { 210 | d.body.className = ""; 211 | thisGameType = gameType; 212 | levelId = Number(levelId); 213 | thisLevelId = levelId; 214 | var level = levels[gameType][levelId]; 215 | if (!level) { 216 | if (thisGame) { 217 | modal.show( 218 | i18n.t( 219 | "Congratulations, you've finished all the levels. Be sure to share this game with your friends!" 220 | ), 221 | i18n.t("You won!"), 222 | 0, 223 | 0, 224 | showMenu 225 | ); 226 | } else { 227 | showMenu(); 228 | } 229 | return; 230 | } 231 | 232 | level.canvas = canvas; 233 | level.gameType = gameType; 234 | level.offsetTouch = gameType !== "Free"; 235 | 236 | level.onwin = function() { 237 | thisGame.destroy(); 238 | loadGame(gameType, levelId + 1); 239 | w.location.hash = gameType + "-" + (levelId + 1); 240 | Storage.set(gameType + (levelId + 1), 1); 241 | }; 242 | 243 | level.onlose = function() { 244 | thisGame.destroy(function() { 245 | modal.show( 246 | i18n.t("Looks like you got stuck. Tap to try again."), 247 | i18n.t("Level failed"), 248 | null, 249 | 1, 250 | function() { 251 | loadGame(gameType, levelId); 252 | } 253 | ); 254 | }); 255 | }; 256 | 257 | thisGame = new Game(level); 258 | } 259 | 260 | function showMenu() { 261 | // hide the tile list dialog from free mode 262 | tileList.className = ""; 263 | logo(canvas, ctx, 0, 1); 264 | var menuOptions = [ 265 | ["Puzzle", "roadx-base", i18n.t("Puzzle")], 266 | ["Free map", "dump", i18n.t("Free map")] 267 | ]; 268 | 269 | if (chromeApp) { 270 | menuOptions.push(["Exit", "grass", i18n.t("Exit")]); // Only useful for app modes. 271 | } 272 | d.querySelector("#menu").innerHTML = menuOptions 273 | .map(function(text) { 274 | var dac = ' data-action="' + text[0] + '"'; 275 | return ( 276 | "' + 283 | text[2] + 284 | "" 285 | ); 286 | }) 287 | .join(""); 288 | d.body.className = "menu"; 289 | playSound("dialog"); 290 | } 291 | 292 | function begin() { 293 | if (window.AudioContext || window.webkitAudioContext) { 294 | showMenu(); 295 | } else { 296 | modal.show( 297 | i18n.t("This browser is too old to run Road Blocks.") + 298 | '' + 299 | i18n.t("Find out more") + 300 | "", 301 | i18n.t("Unsupported"), 302 | null, 303 | 1 304 | ); 305 | } 306 | } 307 | 308 | // This is sad. Chrome killed languages and turned it into an async API. 309 | if (chromeApp) { 310 | chrome.i18n.getAcceptLanguages(function(languages) { 311 | i18n.setup(languages); 312 | begin(); 313 | }); 314 | } else { 315 | begin(); 316 | } 317 | -------------------------------------------------------------------------------- /src/scripts/sprites.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sprites! 3 | * 4 | * There are several formats for these. They include: 5 | * 6 | * Regular box sprite: 7 | * z (vertical position), 8 | * x (x pos/top left to bottom right), 9 | * y (y pos top left to bottom left), 10 | * wx (width on the x axis), 11 | * wy (width on the y axis), 12 | * h (height on the x axis), 13 | * color (colour to show.) 14 | * opacity (0-1, optional) 15 | * 16 | * Sprite by reference: 17 | * ref (reference to another named sprite) 18 | * x (z position) 19 | * x (x position) 20 | * y (y position) 21 | */ 22 | 23 | var exaggeration = innerWidth < 800 ? 2 : 1; 24 | 25 | var random = require('./random'); 26 | var roadColor = '#444444'; 27 | var elkColor = '#AE907A'; 28 | var grey1 = '#aaaaaa'; 29 | var red1 = '#DC6969'; 30 | var glass = '#D4ECF1'; 31 | var yellow = '#FAB41D'; 32 | var black = '#000000'; 33 | var green1 = '#66aa66'; // Grass et al 34 | var greenTree = '#9EC8A0'; // trees 35 | var yellowSand = '#D6D38C'; 36 | var brown1 = '#C8AF9E'; // tree trunk 37 | 38 | var sprites = { 39 | roady: [ 40 | [-0.25,.9, 0, 0.1, 1, 0.25, grey1], 41 | [-0.25,.1, 0, .8, 1, 0.2, roadColor], 42 | [-0.25,0, 0, 0.1, 1, 0.25, grey1], 43 | ], 44 | roadx: [ 45 | [-0.25, 0, .9, 1, 0.1, 0.25, grey1], 46 | [-0.25, 0, .1, 1, 0.8, 0.2, roadColor], 47 | [-0.25, 0, 0, 1, 0.1, 0.25, grey1], 48 | ], 49 | roadxy: [ 50 | [-0.25,0.9,0.9,0.1,.1,.25,grey1], // top 51 | ['r1', 0, 0, 0], 52 | [-0.25,0.9,0,0.1,.1,.25,grey1], //left 53 | ['r2', 0, 0, 0], 54 | ['rc', 0, 0, 0], 55 | ['r3', 0, 0, 0], 56 | [-0.25,0,0.9,0.1,.1,.25,grey1], //right 57 | ['r4', 0, 0, 0], 58 | [-0.25,0,0,0.1,.1,.25,grey1], // bottom 59 | ], 60 | roadx2yl: [ 61 | [-0.25,.1,.9, .9, .1, .25, grey1], 62 | ['r1', 0, 0, 0], 63 | [-0.25,0.9,0,0.1,.1,.25,grey1], //left 64 | [-0.25,0,0.9,0.1,.1,.25,grey1], //right 65 | ['rc', 0, 0, 0], 66 | ['r3', 0, 0, 0], 67 | [-0.25,0,0, .1, .9, .25, grey1], 68 | ], 69 | roadx2yr: [ 70 | [-0.25,0.9,0.9,0.1,.1,.25,grey1], // top 71 | ['r2', 0, 0, 0], // top right 72 | ['r1', 0, 0, 0], // top left 73 | ['rc', 0, 0, 0], // center 74 | [-.25,.1,0,.9,.1,.25,grey1], 75 | [-.25,0,.1,.1,.9,.25,grey1], 76 | [-0.25,0,0,0.1,.1,.25,grey1], // bottom 77 | ], 78 | roady2xl: [ 79 | [-0.25,0.9,0.9,0.1,.1,.25,grey1], // top 80 | [-0.25,0,.9,.9,.1,.25,grey1], // top right 81 | [-0.25,.9,0,.1,.9,.25,grey1], // top right 82 | ['rc',0,0,0], //road center 83 | ['r3', 0,0,0], //bottom left 84 | ['r4',0,0,0], //bottom right 85 | [-0.25,0,0,0.1,.1,.25,grey1], // bottom 86 | ], 87 | roady2xr: [ 88 | [-0.25,0.9,.1,0.1,.9,.25, grey1], 89 | ['r2', 0, 0, 0], // top right 90 | ['rc',0,0,0], //center 91 | [-0.25,0.9,0,0.1,.1,.25,grey1], //left 92 | [-0.25,0,0.9,0.1,.1,.25,grey1], //right 93 | ['r4',0,0,0], //bottom right 94 | [-0.25,0,0,.9,.1,.25,grey1], 95 | 96 | ], 97 | forest: [ 98 | ['ground',0,0,0], 99 | ['grassSurface',0,0,0], 100 | ['tree', 0, .3,.8], 101 | ['tree', 0, .6,.5], 102 | ['tree', 0, .4,.2], 103 | ['elk', 0, .1,.1], 104 | ], 105 | broadx: [ // bridge x 106 | ['water',0,0,0], 107 | ['roadx',0,0,0], 108 | [0,0,0,1,0,.1,grey1], 109 | [0,0,1,1,0,.1,grey1], 110 | ], 111 | broady: [ // bridge y 112 | ['water',0,0,0], 113 | ['roady',0,0,0], 114 | [0,0,0,0,1,.1,grey1], 115 | [0,1,0,0,1,.1,grey1], 116 | ], 117 | r1: [ // Top left road piece 118 | [-0.25, .9, .1, .1, .8, .2, roadColor], 119 | ], 120 | r2: [ // top right road piece 121 | [-0.25, .1, .9, .8, .1, .2, roadColor], 122 | ], 123 | r3: [ // bottom left road piece 124 | [-0.25, .1, 0, .8, .1, .2, roadColor], 125 | ], 126 | r4: [ /// bottom right road piece 127 | [-0.25, 0, .1, .1, .8, .2, roadColor], 128 | ], 129 | rc: [ // Road center piece 130 | [-0.25,.1,.1,.8,.8,.2,roadColor], 131 | ], 132 | ground: [ 133 | [-1,0, 0, 1, 1, 0.75, '#ae907a'], 134 | ], 135 | grassSurface: [ 136 | [-0.25,0, 0, 1, 1, 0.25, green1], 137 | ], 138 | sandSurface: [ 139 | [-0.25,0, 0, 1, 1, 0.25, yellowSand], 140 | ], 141 | concreteSurface: [ 142 | [-0.25,0, 0, 1, 1, 0.25, grey1], 143 | ], 144 | grass: [ 145 | ['ground',0,0,0], 146 | ['grassSurface',0,0,0], 147 | ], 148 | sand: [ 149 | ['ground',0,0,0], 150 | ['sandSurface',0,0,0], 151 | ], 152 | palm: [ 153 | ['ground',0,0,0], 154 | ['sandSurface',0,0,0], 155 | ['treePalm', 0, .8, .8], 156 | ['treePalm', 0, .4, .4], 157 | ], 158 | building: [ 159 | ['ground',0,0,0], 160 | ['grassSurface',0,0,0], 161 | 162 | // house 163 | [0,.2,.4,.8,.6,.8,brown1], 164 | 165 | // garage 166 | [0,.3,0,.7,.4,.4,brown1], 167 | 168 | //garage door 169 | [0,.3,.05,0,.3,.35,grey1], 170 | 171 | // windows 172 | [.05,.2,.75,0,.2,.28,glass], //bottom 173 | [.4,.2,.75,0,.2,.28,glass], // top 174 | [0,.2,.5,0,.2,.68,glass], // tall window 175 | ], 176 | test2: [], 177 | water: [ 178 | [-1, 0, 0, 1, 1, .1, '#ffff99'], 179 | [-0.25, 0, 0, 1, 1, 0, '#55bbff', .3], 180 | ], 181 | helipad: [ 182 | ['ground',0,0,0], 183 | ['concreteSurface',0,0,0], 184 | 185 | // rear red light 186 | [0,0.95,0.95,0.05,.05,.1,red1], 187 | 188 | [0, .05, .05, .9, .9, 0.05, grey1], 189 | [0.05, .2, .2, .6, .1, 0, red1], 190 | [0.05, .2, .7, .6, .1, 0, red1], 191 | [0.05, .45, .3, .1, .4, 0, red1], 192 | 193 | [0,0,0,0.05,.05,.1,red1], 194 | [0,0.95,0,0.05,.05,.1,green1], 195 | [0,0,0.95,0.05,.05,.1,green1], 196 | ], 197 | dump: [ 198 | ['ground',0,0,0], 199 | [-0.25,0, 0, 1, 1, 0.25, brown1], 200 | ['bulldozer',0,.5,.5], 201 | [0,.1,.2,.1,.1,.1,brown1], 202 | [0,.35,.25,.1,.1,.1,brown1], 203 | [0,.3,.4,.1,.1,.1,brown1], 204 | ], 205 | ok: [ 206 | [0,0,0,1,1,0,'#00ff00',.5] 207 | ], 208 | notok: [ 209 | [0,0,0,1,1,0,red1,.5] 210 | ], 211 | tree: function(){ 212 | var sin = Math.sin(new Date()/300); 213 | var sin2 = Math.sin(new Date()/150); 214 | var a = [ 215 | [0, -.05, -.05, .1, .1, .2, brown1] // trunk 216 | ]; 217 | 218 | for(var i=5; i>0; i--){ 219 | a.push([ 220 | .6 - i/11 + sin2*(5-i)/500, 221 | -0.15 + sin*(5-i)/300, 222 | -0.15 + 0-sin*(5-i)/300, 223 | (i)/25, 224 | (i)/25, 225 | 0.05, 226 | greenTree 227 | ]); 228 | } 229 | return a; 230 | }, 231 | treePalm: function(){ 232 | var sin = Math.sin(new Date()/300)/150; 233 | var a = []; 234 | var pos,height; 235 | for(var i=0; i<6; i++){ 236 | a.push([height=5*i/50, pos=0-sin*i*exaggeration, 0, .03, .03, .1, brown1]); 237 | } 238 | 239 | // treetop 240 | a.push([height,.05+pos,0,.02,.15,.02,greenTree]); 241 | a.push([height,0+pos,0,.2,.08,.05,greenTree]); 242 | a.push([height-0.05,-.2+pos,0,.2,.05,.05,greenTree]); 243 | a.push([height,0+pos,0,.05,.25,.02,greenTree]); 244 | a.push([height,0+pos,-.2,.05,.2,.02,greenTree]); 245 | a.push([height,-.05+pos,-.15,.02,.15,.02,greenTree]); 246 | return a; 247 | }, 248 | 249 | // Aw, car! You'll be missed. 250 | // car: function(){ 251 | // var sin = Math.sin(new Date() / 25)+1; 252 | // var startHeight = sin/200 + 0.05; 253 | // var color = '#A47BAF'; 254 | // var a = [ 255 | // [0, -.11, -0.14, .1, .1, .1, '#222222'], 256 | // [0, -.11, 0.08, .1, .1, .1, '#222222'], 257 | // [startHeight, -0.13, -0.2, 0.26, 0.4, 0.1, color], 258 | // [startHeight+0.1, -0.13, -0.13, 0.26, 0.26, 0.1, glass], 259 | // [startHeight+0.2, -0.13, -0.13, 0.26, 0.26, 0.01, color], 260 | // ]; 261 | // return a; 262 | // }, 263 | elk: function(){ 264 | var sin = Math.sin(new Date()/1000); 265 | var headHeight = sin > 0 ? 0.05 : 0.15; 266 | return [ 267 | // tail 268 | [.15, .09, 0, .02, .02, .05, elkColor], 269 | 270 | // legs 271 | [0, -.05, -.05, .02, .02, .1, elkColor], 272 | [0, -.05, .03, .02, .02, .1, elkColor], 273 | [0, .05, -.05, .02, .02, .1, elkColor], 274 | 275 | // body 276 | [.1, -.08, -.05, .16, .1, .1, elkColor], 277 | 278 | // Head 279 | [headHeight, -.08, 0.02, 0.04, 0.04, 0.04, elkColor], 280 | 281 | // Antlers 1 282 | [headHeight+0.04, -.08, 0, .01, .01, .1, elkColor], 283 | [headHeight+0.09, -.08, -0.04, .01, .02, .02, elkColor], 284 | [headHeight+0.10, -.08, -0.04, .01, .01, .05, elkColor], 285 | 286 | // Antlers 2 287 | [headHeight+0.09, -.08, 0.06, .01, .02, .02, elkColor], 288 | [headHeight+0.1, -.08, 0.08, .01, .01, .05, elkColor], 289 | [headHeight+0.04, -.08, 0.04, .01, .01, .1, elkColor], 290 | ]; 291 | }, 292 | bulldozer: function(){ 293 | var sin = Math.sin(new Date()/500); 294 | var headHeight = 0-Math.min(0,sin) * 0.1; 295 | return [ 296 | [0,0,.1,.3,.3,.1, grey1], // track 297 | [0.02,0,.12,0,.06,.06, black], 298 | [0.02,0,.22,0,.06,.06, black], 299 | [0.02,0,.32,0,.06,.06, black], 300 | [0.04,0,.1,0,.3,.02,yellow], 301 | [0.04,0,.1,.3,0,.02,black], 302 | 303 | [.12,0,.1,.3,.3,.25, '#FAB41D'], // cabin 304 | [.15,0,.12,0,.1,.2, glass], // door 305 | [.2,0,.25, 0, .13, .15, glass], // right window 306 | [.2,.03,.1, .25, 0, .15, glass], 307 | 308 | // bucket 309 | [headHeight,0,.07,.3,.01,.1, grey1], // back 310 | [headHeight,.29,0,.01,.08, .1,grey1], // far side 311 | [headHeight,0,0,.3,.08,.01,grey1], //bottom 312 | [headHeight,0,0,.01,.08, .1, yellow], // near side 313 | 314 | // Tray 315 | [headHeight/2+0.01,.05,.1,0,.1,.05,yellow], 316 | [headHeight/4+0.025,.05,.2,0,.1,.05,yellow], 317 | ] 318 | }, 319 | forest2: [ 320 | ['ground',0,0,0], 321 | ['grassSurface',0,0,0], 322 | ['tree', 0, .3,.8], 323 | ['tree', 0, .6,.5], 324 | ['tree', 0, .4,.2], 325 | ], 326 | }; 327 | Object.keys(sprites).map(function(spriteName){ 328 | if(spriteName.indexOf('road') === 0){ 329 | sprites[spriteName+'-base'] = sprites.ground.concat(sprites[spriteName]); 330 | } 331 | }); 332 | 333 | 334 | function canPlaceIfDefaultTile(existing){ 335 | return ['grass', 'sand'].indexOf(existing) !== -1; 336 | } 337 | 338 | var tileLogic = { 339 | roady: { 340 | c: [0,1,0,1], // Connections,, 341 | p: canPlaceIfDefaultTile, // Is placeable? 342 | }, 343 | roadx: { 344 | c: [1,0,1,0], 345 | p: canPlaceIfDefaultTile, 346 | }, 347 | roadxy: { 348 | c: [1,1,1,1], 349 | p: canPlaceIfDefaultTile, 350 | firstrun: 'Cars will travel straight through intersections without making turns. You can double back over road you\'ve already placed.', 351 | title: 'Intersection' 352 | }, 353 | roadx2yl: { 354 | c: [1,0,0,1], 355 | p: canPlaceIfDefaultTile, 356 | }, 357 | roadx2yr: { 358 | c: [1,1,0,0], 359 | p: canPlaceIfDefaultTile, 360 | }, 361 | roady2xl: { 362 | c: [0,0,1,1], 363 | p: canPlaceIfDefaultTile, 364 | }, 365 | roady2xr: { 366 | c: [0,1,1,0], 367 | p: canPlaceIfDefaultTile, 368 | }, 369 | forest: { 370 | p: function(tile){ 371 | if(tile !== 'water'){ 372 | return true; 373 | } 374 | }, // Forests can be placed anywhere. 375 | points: 50 376 | }, 377 | building: { 378 | p: canPlaceIfDefaultTile, 379 | title: 'Buildings', 380 | firstrun: "Place buildings alongside roads for extra points.", 381 | points: 100 382 | }, 383 | dump: 1, 384 | helipad: 1, 385 | water: 1, 386 | sand: 1, 387 | palm: 1, 388 | broadx: 1, 389 | broady: 1, 390 | forest2: 1, 391 | }; 392 | 393 | // Get a list of placeables 394 | var placeable = Object.keys(tileLogic); 395 | 396 | // Copy over the logic for bridges & road tiles with bases. 397 | placeable.map(function(spriteName){ 398 | if(spriteName.indexOf('road') === 0){ 399 | tileLogic[spriteName+'-base'] = tileLogic[spriteName]; 400 | tileLogic['b'+spriteName] = tileLogic[spriteName]; 401 | } 402 | }); 403 | 404 | tileLogic.helipad = { 405 | points: 500 406 | }; 407 | 408 | // These tiles are animated, no others. 409 | // Note: doesn't seem to affect render speed that much. 410 | var animated = { 411 | forest: 1, 412 | forest2: 1, 413 | sand: 1, 414 | helipad: 1, 415 | tree: 1, 416 | elk: 1, 417 | palm: 1, 418 | dump: 1, 419 | }; 420 | 421 | module.exports = { 422 | sprites: sprites, 423 | placeable: placeable, 424 | tileLogic: tileLogic, 425 | animated: animated, 426 | }; 427 | -------------------------------------------------------------------------------- /src/i18n.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "Unlock this mode by completing more puzzles.", 4 | "Desbloquearlo este modo, completando más puzzles.", 5 | "Déverrouiller ce mode en remplissant plus d'énigmes.", 6 | "Desbloquear este modo, completando mais quebra-cabeças.", 7 | "Разблокировать этот режим завершения больше загадок.", 8 | "ontgrendelen deze modus door het invullen van meer puzzels.", 9 | "Ontsluit hierdie modus deur die voltooiing van meer kopkrapper.", 10 | "Membuka mode ini dengan menyelesaikan teka-teki lagi.", 11 | "より多くのパズルを完了することによって、このモードのロックを解除します。" 12 | ], 13 | [ 14 | "Mode locked", 15 | "Modo bloqueado", 16 | "Mode verrouillé", 17 | "Modo bloqueado", 18 | "Режим заблокирован", 19 | "Modus afgesloten", 20 | "Modus gesluit", 21 | "Mode terkunci", 22 | "モードがロックされ" 23 | ], 24 | [ 25 | "Congratulations, you've finished all the levels. Be sure to share this game with your friends!", 26 | "Felicidades, usted ha terminado todos los niveles. Asegúrese de compartir este juego con tus amigos!", 27 | "Félicitations, vous avez terminé tous les niveaux. Soyez sûr de partager ce jeu avec vos amis!", 28 | "Parabéns, você terminou todos os níveis. Certifique-se de compartilhar esse jogo com seus amigos!", 29 | "Поздравляю, вы закончили все уровни. Будьте уверены, чтобы поделиться этой игрой со своими друзьями!", 30 | "Gefeliciteerd, u alle niveaus klaar bent. Zorg ervoor dat je dit spel delen met je vrienden!", 31 | "Veels geluk, jy al die vlakke klaar. Seker wees om hierdie spel te deel met jou vriende!", 32 | "Selamat, Anda sudah selesai semua tingkat. Pastikan untuk berbagi permainan ini dengan teman-teman Anda!", 33 | "おめでとう、あなたはすべてのレベルを終えました。あなたの友人とこのゲームを共有してください!" 34 | ], 35 | [ 36 | "You won!", 37 | "¡Ganaste!", 38 | "Tu as gagné!", 39 | "Você ganhou!", 40 | "Ты победил!", 41 | "Jij hebt gewonnen!", 42 | "Jy het gewen!", 43 | "Kamu menang!", 44 | "あなたが勝ちました!" 45 | ], 46 | [ 47 | "Looks like you got stuck. Tap to try again.", 48 | "Parece que quedó atascado. Toque para volver a intentarlo.", 49 | "On dirait que vous êtes coincé. Appuyez sur pour réessayer.", 50 | "Parece que você ficou preso. Toque para tentar novamente.", 51 | "Похоже, вы застряли. Нажмите, чтобы попробовать еще раз.", 52 | "Lijkt alsof je vast kwam te zitten. Tik op om opnieuw te proberen.", 53 | "Lyk soos jy vasgeval. Tik om weer te probeer.", 54 | "Sepertinya Anda terjebak. Ketuk untuk mencoba lagi.", 55 | "あなたが立ち往生してしまったように見えます。再試行してタップします。" 56 | ], 57 | [ 58 | "Level failed", 59 | "Fallaste", 60 | "Niveau échoué", 61 | "Nível falhado", 62 | "Уровень провален", 63 | "Level niet gehaald", 64 | "Vlak het misluk", 65 | "Tingkat gagal", 66 | "レベルに失敗しました" 67 | ], 68 | [ 69 | "Puzzle", 70 | "Rompecabezas", 71 | "Puzzle", 72 | "Quebra-cabeças", 73 | "головоломка", 74 | "Puzzel", 75 | "Legkaart", 76 | "Teka-teki", 77 | "パズル" 78 | ], 79 | [ 80 | "Puzzle Play", 81 | "Rompecabezas", 82 | "Puzzle", 83 | "Quebra-cabeças", 84 | "головоломка", 85 | "Puzzel", 86 | "Legkaart", 87 | "Teka-teki", 88 | "パズル" 89 | ], 90 | [ 91 | "Free map", 92 | "Gratis Mapa", 93 | "Carte Gratuite", 94 | "Mapa Gratuito", 95 | "Бесплатная карта", 96 | "Gratis Kaart", 97 | "Free Kaart", 98 | "Peta gratis", 99 | "無料地図" 100 | ], 101 | [ 102 | "This browser is too old to run Road Blocks.", 103 | "Este navegador es demasiado viejo para funcionar Road Blocks.", 104 | "Ce navigateur est trop vieux pour courir Road Blocks.", 105 | "Este navegador é velho demais para correr Road Blocks.", 106 | "Этот браузер слишком стар, чтобы работать Дорожные блоки.", 107 | "Deze browser is te oud om Road Blocks run.", 108 | "Hierdie leser is te oud om Road Blocks hardloop.", 109 | "Browser ini terlalu tua untuk menjalankan Jalan Blok.", 110 | "このブラウザは、「ロードブロック」を実行することが古すぎます。" 111 | ], 112 | [ 113 | "Unsupported", 114 | "No compatible", 115 | "non pris en charge", 116 | "Não suportado", 117 | "Не поддерживается", 118 | "Ondersteunde", 119 | "Nie ondersteun nie", 120 | "Tidak didukung", 121 | "サポートされていません" 122 | ], 123 | [ 124 | "Find out more", 125 | "Para saber más", 126 | "En savoir plus", 127 | "Descubra mais", 128 | "Узнать больше", 129 | "Meer te weten komen", 130 | "Vind meer uit", 131 | "Temukan lebih banyak lagi", 132 | "詳細はこちら" 133 | ], 134 | [ 135 | "Roads 101", 136 | "Roads 101", 137 | "Routes 101", 138 | "Estradas 101", 139 | "Дороги 101", 140 | "Wegen 101", 141 | "Paaie 101", 142 | "Jalan 101", 143 | "道路101" 144 | ], 145 | [ 146 | "Connect from left to right by dragging tiles from the top.", 147 | "Conecte de izquierda a derecha al arrastrar los azulejos de la parte superior.", 148 | "Connectez de gauche à droite en faisant glisser les tuiles du haut.", 149 | "Conecte-se da esquerda para a direita, arrastando telhas a partir do topo.", 150 | "Подключите слева направо, перетаскивая плитки сверху.", 151 | "Sluit van links naar rechts door het slepen van tegels uit de top.", 152 | "Koppel van links na regs deur te sleep die teëls van die top.", 153 | "Menghubungkan dari kiri ke kanan dengan menyeret ubin dari atas.", 154 | "上からタイルをドラッグして左から右に接続します。" 155 | ], 156 | [ 157 | "Left to right", 158 | "De izquierda a derecha", 159 | "De gauche à droite", 160 | "Da esquerda para direita", 161 | "Слева направо", 162 | "Van links naar rechts", 163 | "Links na regs", 164 | "Kiri ke kanan", 165 | "左から右へ" 166 | ], 167 | [ 168 | "Town planning", 169 | "Urbanismo", 170 | "Urbanisme", 171 | "Urbanismo", 172 | "Градостроительство", 173 | "Stedenbouw", 174 | "Stadsbeplanning", 175 | "Perencanaan kota", 176 | "都市計画" 177 | ], 178 | [ 179 | "Now you've got the hang of it, give it a go with all the tiles.", 180 | "Ahora tienes el truco de, darle una oportunidad con todas las baldosas.", 181 | "Maintenant, vous avez le coup de lui, lui donner un aller avec toutes les tuiles.", 182 | "Agora você tem o jeito dele, dar-lhe um ir com todas as telhas.", 183 | "Теперь у вас есть навык этого, дать ей идти со всеми плитками.", 184 | "Nu heb je de smaak te pakken gekregen, geef het een gaan met alle tegels.", 185 | "Nou het jy die hang van dit het, gee dit 'n go met al die teëls.", 186 | "Sekarang Anda punya menguasainya, mencobanya dengan semua ubin.", 187 | "今、あなたはそれのこつを持って、それをすべてのタイルでやってみます。" 188 | ], 189 | [ 190 | "Your turn", 191 | "Tu turno", 192 | "Votre tour", 193 | "Sua vez", 194 | "Твой ход", 195 | "Jouw beurt", 196 | "Jou beurt", 197 | "Giliran Anda", 198 | "あなたのターン" 199 | ], 200 | [ 201 | "Reverse the flow", 202 | "Invertir el flujo", 203 | "Inverser le flux", 204 | "Inverter o fluxo", 205 | "Обратный поток", 206 | "Omkeren van de stroom", 207 | "Draai die vloei", 208 | "Membalikkan aliran", 209 | "流れを逆に" 210 | ], 211 | [ 212 | "Use the helipad to reverse the order of your tiles.", 213 | "Utilice el helipuerto para invertir el orden de las baldosas.", 214 | "Utilisez l'héliport d'inverser l'ordre de vos tuiles.", 215 | "Use o heliporto para inverter a ordem de suas telhas.", 216 | "Используйте вертолетная площадка, чтобы изменить порядок ваших плиток.", 217 | "Gebruik de helikopterplatform om de volgorde van uw tegels te keren.", 218 | "Gebruik die helikopterlandingsplek aan die orde van jou teëls te keer.", 219 | "Gunakan helipad untuk membalik urutan ubin Anda.", 220 | "あなたのタイルの順序を逆にヘリポートを使用してください。" 221 | ], 222 | [ 223 | "Stack tiles for later", 224 | "Pila de baldosas de para más tarde", 225 | "Empilez les tuiles pour plus tard", 226 | "Empilhar azulejos para mais tarde", 227 | "Стек плитки на потом", 228 | "Stapel tegels voor later", 229 | "Stapel teëls vir later", 230 | "Stack ubin untuk nanti", 231 | "後でタイルをスタック" 232 | ], 233 | [ 234 | "The Block Forest", 235 | "El bosque de bloque", 236 | "La forêt de bloc", 237 | "A floresta bloco", 238 | "Блок леса", 239 | "Het blok bos", 240 | "Die blok bos", 241 | "Hutan blok", 242 | "ブロックの森" 243 | ], 244 | [ 245 | "Building road past special tiles like forests or the helipad gives you extra points.", 246 | "La construcción de carreteras pasado baldosas especiales como los bosques o el helipuerto le da puntos extra.", 247 | "La construction de routes passé tuiles spéciales comme les forêts ou l'héliport vous donne des points supplémentaires.", 248 | "Construção de estrada passado telhas especiais, como florestas ou o heliporto lhe dá pontos extra.", 249 | "Строительство дороги мимо специальных плиток, как леса или вертолетная площадка дает вам дополнительные очки.", 250 | "Het bouwen van weg langs bijzondere tegels zoals bossen of de helipad geeft je extra punten.", 251 | "Bou van die pad afgelope spesiale teëls soos woude of die helikopterlandingsplek gee jou ekstra punte.", 252 | "Membangun jalan melewati ubin khusus seperti hutan atau helipad memberikan poin ekstra.", 253 | "森林のように特別なタイルを過ぎて道路を構築するか、ヘリポートはあなたに余分なポイントを与えます。" 254 | ], 255 | [ 256 | "Bonus points", 257 | "Puntos extra", 258 | "Points bonus", 259 | "Os pontos de bónus", 260 | "Бонусные очки", 261 | "Bonuspunten", 262 | "Bonus punte", 263 | "Bonus poin", 264 | "ボーナスポイント" 265 | ], 266 | [ 267 | "★ Bulldozer Beach", 268 | "★ Playa Topadora", 269 | "★ Bulldozer Plage", 270 | "★ Bulldozer Praia", 271 | "★ бульдозер пляж", 272 | "★ bulldozer strand", 273 | "★ Stootskraper Strand", 274 | "★ Buldoser pantai", 275 | "★ブルドーザービーチ" 276 | ], 277 | [ 278 | "Long press to bulldoze a tile you no longer need.", 279 | "Pulsación larga para arrasar una baldosa que ya no necesita.", 280 | "Appuyez longuement pour raser une tuile, vous ne devez plus.", 281 | "Pressão longa para arrasar uma telha você não precisa mais.", 282 | "Длительно нажмите, чтобы сравнять с землей плитку вы больше не нуждаетесь.", 283 | "Lang indrukken om een tegel die u niet meer nodig bulldozer.", 284 | "Lank druk 'n teël wat jy nie meer nodig het bulldoze.", 285 | "Tekan lama untuk melibas ubin Anda tidak perlu lagi.", 286 | "長押しでは、不要になったタイルを整地します。" 287 | ], 288 | [ 289 | "Bulldoze", 290 | "Arrasar", 291 | "Démolir", 292 | "Arrasar", 293 | "сгребать бульдозером", 294 | "Bulldozer", 295 | "Bulldoze", 296 | "Meratakan", 297 | "整地します" 298 | ], 299 | [ 300 | "Congratulations. You've unlocked Free Map mode from the main menu.", 301 | "Felicidades. Has desbloqueado Gratis Mapa Modo en el menú principal.", 302 | "Félicitations à vous. Vous avez déverrouillé Carte gratuite mode à partir du menu principal.", 303 | "Parabéns. Você já desbloqueado Free Map modo a partir do menu principal.", 304 | "Поздравления. Вы разблокирован Бесплатная карта режим из главного меню.", 305 | "Gefeliciteerd. Je hebt vrijgespeeld Gratis Map modus in het hoofdmenu.", 306 | "Baie geluk. Jy het ontsluit Free Map af vanaf die hoof spyskaart.", 307 | "Selamat. Anda membuka Gratis Peta modus dari menu utama.", 308 | "おめでとうございます。あなたは、メインメニューからの無料地図のモードのロックを解除しました。" 309 | ], 310 | [ 311 | "Palm Island", 312 | "Palma de la isla", 313 | "Palm Island", 314 | "palm Island", 315 | "Palm Island", 316 | "Palmeiland", 317 | "Palm Eiland", 318 | "Pulau kelapa", 319 | "パームアイランド" 320 | ], 321 | [ 322 | "Loopy Lagoon", 323 | "Descabellado del Río", 324 | "Loopy Lagoon", 325 | "Loopy Lagoon", 326 | "Loopy Лагуна", 327 | "Loopy Lagune", 328 | "Getroubleerd strandmeer", 329 | "Laguna gila", 330 | "愚かなラグーン" 331 | ], 332 | [ 333 | "Mini Monaco", 334 | "Mini Mónaco", 335 | "Mini-Monaco", 336 | "Mini-Monaco", 337 | "мини Монако", 338 | "Mini Monaco", 339 | "Mini Monaco", 340 | "Mini Monako", 341 | "ミニモナコ" 342 | ], 343 | [ 344 | "Dual Carriageway", 345 | "Autovía", 346 | "Route à quatre voies", 347 | "Via dupla", 348 | "двойной проезжей", 349 | "Vierbaansweg", 350 | "Dubbelpad", 351 | "Kemacetan", 352 | "デュアル車道" 353 | ], 354 | [ 355 | "Back", 356 | "Volver", 357 | "Retourner", 358 | "Volte", 359 | "Возвращаться", 360 | "Ga terug", 361 | "Gaan terug", 362 | "Kembali", 363 | "戻る" 364 | ], 365 | [ 366 | "Exit", 367 | "Dejar", 368 | "Sortie", 369 | "Sair", 370 | "Уволиться", 371 | "Afslut", 372 | "Ophou", 373 | "Berhenti", 374 | "出口" 375 | ] 376 | ] 377 | -------------------------------------------------------------------------------- /src/scripts/levels/levels.js: -------------------------------------------------------------------------------- 1 | // FIXME: shim for loading spriteLib in nodejs 2 | if(typeof window === 'undefined'){ 3 | global.innerWidth = 0; 4 | } 5 | var SpriteLib = require('../sprites'); 6 | 7 | function addRow(a, x, max, type){ 8 | for(var i=0;iFree Map mode from the main menu.', 'Free Map', 'dump'], 129 | }, 130 | { 131 | seed:13, 132 | w:6, 133 | h: 6, 134 | wMod: 4, 135 | base: 'sand', 136 | strict: 1, 137 | dist: [0,1,1,5,3,4,5,6,1], 138 | predef: (function(){ 139 | var a = [ 140 | [1,5,roadBase], 141 | [3,0,roadBase], 142 | [0,0,water], 143 | [0,1,water], 144 | [1,1,water], 145 | [1,0,water], 146 | ]; 147 | addCol(a, 3, 6, water); 148 | addCol(a, 2, 3, water); 149 | a.push([5,2,'palm']); 150 | a.push([2,1,'helipad']); 151 | a.push([4,3,'broady']); 152 | return a; 153 | })(), 154 | name: 'Palm Island', 155 | }, 156 | 157 | { // 4 158 | seed: 200, 159 | w: 6, 160 | h:6, 161 | base:'grass', 162 | dist: [ 163 | 'roady2xr', 164 | 'roady2xl', 165 | 'roady', 166 | 'roadx2yl', 167 | 'roadxy', 168 | 'roadx', 169 | 'roadxy', 170 | 'roadxy', 171 | 'roadx2yl', 172 | 'roady2xr', 173 | 'roadx2yr', 174 | 'roady2xl', 175 | 'roadx2yr', 176 | 'roadxy', 177 | ], 178 | predef: (function(){ 179 | var end = [4,0,'roady-base']; 180 | var a = [ 181 | [1,5,'roady-base'], 182 | end, 183 | [1,1,'building'], 184 | ]; 185 | addRow(a, 4, 6, 'sand'); 186 | addRow(a, 5, 6, 'water'); 187 | a.push(end); // double up, addBeach overwrote this 188 | a.push([4,5,'palm']); 189 | a.push([4,4,'palm']); 190 | a.push([3,5,'helipad']); 191 | return a; 192 | })(), 193 | name: 'Loopy Lagoon', 194 | }, 195 | { // 6 196 | seed: 301, 197 | w:7, 198 | h:7, 199 | wMod: 4, 200 | base: 'sand', 201 | dist: [ 202 | 'roady', 203 | 'roadx2yl', 204 | 'roadx2yl', 205 | 'roady2xr', 206 | 'roady2xl', 207 | 'roadx', 208 | 'roadx', 209 | 'roadx', 210 | 'roadxy', 211 | 'roadx', 212 | 'roadx', 213 | 'roadx2yr', 214 | 'roady', 215 | 'roadx', 216 | 'roadx', 217 | ], 218 | predef: (function(){ 219 | var a = [ 220 | [5,6,roadBase], 221 | [0,2,'roadx-base'], 222 | [4,1,'palm'], 223 | [1,3,'water'], 224 | [1,1,'water'], 225 | [1,2,'broadx'], 226 | [2,3,'water'], 227 | [3,3,'palm'], 228 | [0,3,'palm'], 229 | [3,6,'building'], 230 | [5,2,'helipad'], 231 | [5,1,water], 232 | ]; 233 | addRow(a, 6, 7, 'water'); 234 | addCol(a, 0, 7, 'water'); 235 | return a; 236 | })(), 237 | name: 'Mini Monaco', 238 | }, 239 | { // 7 240 | seed: 400, 241 | w:7, 242 | h:7, 243 | wMod: 4, 244 | base:'grass', 245 | dist: [ 246 | 'roadx2yr', 247 | 'roady2xl', 248 | 'roadx', 249 | 'roadx2yr', 250 | 'roadx', 251 | 'roadx2yr', 252 | 'roady', 253 | 'roady2xl', 254 | 'roady', 255 | ], 256 | predef: (function(){ 257 | var a = [ 258 | [6,2,'roadx-base'], 259 | [0,4,'roadx-base'], 260 | [6,1,'palm'], 261 | [6,0,'water'], 262 | [5,1,'sand'], 263 | [5,0,'water'], 264 | [4,0,'water'], 265 | [4,1,'water'], 266 | [4,2,'water'], 267 | [3,2,'water'], 268 | [3,3,'broadx'], 269 | [3,4,'broadx'], 270 | [3,5,'water'], 271 | [3,6,'water'], 272 | [2,6,'water'], 273 | [1,6,'water'], 274 | [4,6,'water'], 275 | [4,5,'water'], 276 | [3,0,'palm'], 277 | [3,1,'sand'], 278 | [2,5,'helipad'], 279 | [1,1,'forest'], 280 | ]; 281 | return a; 282 | })(), 283 | name: 'Dual Carriageway' 284 | }, 285 | { 286 | seed: 14, 287 | w:8, 288 | h:8, 289 | wMod:4, 290 | base: 'sand', 291 | strict: 1, 292 | dist:[0,1,2,0,5,3,4,6,7,1,1,5,5,4,4,1,3], 293 | predef: (function(){ 294 | var a = [ 295 | [3,7,roadBase], 296 | [5,0,roadBase], 297 | ]; 298 | addRow(a,0,8,water); 299 | addRow(a,1,8,water); 300 | addRow(a,7,8,water); 301 | addCol(a,4,7,water); 302 | addCol(a,0,7,water); 303 | a.push([5,0,'broady']); 304 | a.push([6,4,'broady']); 305 | a.push([2,2,'building']); 306 | a.push([2,5,'palm']); 307 | a.push([2,7,'palm']); 308 | a.push([2,1,'forest']); 309 | a.push([3,1,'building']); 310 | a.push([6,6,'helipad']); 311 | 312 | return a; 313 | })(), 314 | name: 'Little condo by the sea', 315 | }, 316 | ], 317 | Free: [ 318 | { 319 | seed:0, 320 | w:7, 321 | h:7, 322 | wMod: 4, 323 | base: 'grass', 324 | dist: 0, 325 | predef: [], 326 | } 327 | ] 328 | }; 329 | 330 | // 3.61 kb - beginning 331 | // 2.75 - after index 332 | // 2.45 after combining predef array 333 | // 334 | var spriteIndex = Object.keys(SpriteLib.sprites); 335 | var emergency = 0; 336 | 337 | /** 338 | * Pad a number with the specified number zeroes. 339 | */ 340 | function pad(str, num){ 341 | str = String(str); 342 | while(str.length < num){ 343 | str = '0' + str; 344 | } 345 | return str; 346 | } 347 | 348 | /** 349 | * Encode a string using a custom base36 encoding. 350 | * * Strings are chunked into 8 byte base36 encoded pieces for integer safety. 351 | * * The final piece may not make up 8 bytes, this is piecked up by the decoder.. 352 | * @param {string} num string full of numeric digits to encode. 353 | * @return {string} base36/alphanumeric encoded string. 354 | */ 355 | function enc(num){ 356 | // Stringify our number in case it was input as an integer. 357 | num = String(num); 358 | 359 | // Keep track of our encoded chunks. 360 | var encodedChunks = []; 361 | 362 | // Continue until we've processed the entire string. 363 | while(num.length){ 364 | // Start somewhere. 365 | var splitPosition = 7; 366 | 367 | // Try incrementally larger pieces until we get one that's exectly 368 | // 8 characters long. 369 | var encodedNum = ''; 370 | do { 371 | // toString(36) converts decimal to base36. 372 | // Add a leading 1 for safety, as any leading zeroes would otherwise 373 | // be lost. 374 | encodedNum = Number('1' + num.substr(0, ++splitPosition)).toString(36); 375 | } while(encodedNum.length < 8 && splitPosition < num.length && splitPosition < 15); 376 | 377 | // Push our chunk onto the list of encoded chunks and remove these 378 | // digits from our string. 379 | encodedChunks.push(encodedNum); 380 | num = num.substr(splitPosition); 381 | } 382 | 383 | // Return a big ol' string. 384 | return encodedChunks.join(''); 385 | } 386 | 387 | 388 | /** 389 | * Custom base36 decoder. 390 | * @param {string} input base36/alphanumeric encoded string from the enc function 391 | * @return {string} Original numeric string as passed to the enc function. 392 | */ 393 | function dec(input){ 394 | // Split our string into chunks of 8 bytes (with whatever left over at the end) 395 | return input.match(/.{8}|.+/g).map(function(chunk){ 396 | // Convert each chunk to base 10. 397 | return parseInt(chunk, 36); 398 | }).join(''); 399 | } 400 | 401 | var before = 0; 402 | var after = 0; 403 | 404 | Object.keys(levels).forEach(function(key){ 405 | levels[key] = levels[key].map(require('./encodelevel')); 406 | }); 407 | console.log(JSON.stringify(levels)); 408 | module.exports = levels; 409 | -------------------------------------------------------------------------------- /src/scripts/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 | /** @constructor */ 21 | function SfxrParams() { 22 | //-------------------------------------------------------------------------- 23 | // 24 | // Settings String Methods 25 | // 26 | //-------------------------------------------------------------------------- 27 | 28 | /** 29 | * Parses a settings array into the parameters 30 | * @param array Array of the settings values, where elements 0 - 23 are 31 | * a: waveType 32 | * b: attackTime 33 | * c: sustainTime 34 | * d: sustainPunch 35 | * e: decayTime 36 | * f: startFrequency 37 | * g: minFrequency 38 | * h: slide 39 | * i: deltaSlide 40 | * j: vibratoDepth 41 | * k: vibratoSpeed 42 | * l: changeAmount 43 | * m: changeSpeed 44 | * n: squareDuty 45 | * o: dutySweep 46 | * p: repeatSpeed 47 | * q: phaserOffset 48 | * r: phaserSweep 49 | * s: lpFilterCutoff 50 | * t: lpFilterCutoffSweep 51 | * u: lpFilterResonance 52 | * v: hpFilterCutoff 53 | * w: hpFilterCutoffSweep 54 | * x: masterVolume 55 | * @return If the string successfully parsed 56 | */ 57 | this.set = function(values) 58 | { 59 | for ( var i = 0; i < 24; i++ ) 60 | { 61 | this[String.fromCharCode( 97 + i )] = values[i] || 0; 62 | } 63 | 64 | // I moved this here from the reset(true) function 65 | if (this['c'] < .01) { 66 | this['c'] = .01; 67 | } 68 | 69 | var totalTime = this['b'] + this['c'] + this['e']; 70 | if (totalTime < .18) { 71 | var multiplier = .18 / totalTime; 72 | this['b'] *= multiplier; 73 | this['c'] *= multiplier; 74 | this['e'] *= multiplier; 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * SfxrSynth 81 | * 82 | * Copyright 2010 Thomas Vian 83 | * 84 | * Licensed under the Apache License, Version 2.0 (the "License"); 85 | * you may not use this file except in compliance with the License. 86 | * You may obtain a copy of the License at 87 | * 88 | * http://www.apache.org/licenses/LICENSE-2.0 89 | * 90 | * Unless required by applicable law or agreed to in writing, software 91 | * distributed under the License is distributed on an "AS IS" BASIS, 92 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 93 | * See the License for the specific language governing permissions and 94 | * limitations under the License. 95 | * 96 | * @author Thomas Vian 97 | */ 98 | /** @constructor */ 99 | function SfxrSynth() { 100 | var _this = this; 101 | // All variables are kept alive through function closures 102 | 103 | //-------------------------------------------------------------------------- 104 | // 105 | // Sound Parameters 106 | // 107 | //-------------------------------------------------------------------------- 108 | 109 | this._params = new SfxrParams(); // Params instance 110 | 111 | //-------------------------------------------------------------------------- 112 | // 113 | // Synth Variables 114 | // 115 | //-------------------------------------------------------------------------- 116 | 117 | var _envelopeLength0, // Length of the attack stage 118 | _envelopeLength1, // Length of the sustain stage 119 | _envelopeLength2, // Length of the decay stage 120 | 121 | _period, // Period of the wave 122 | _maxPeriod, // Maximum period before sound stops (from minFrequency) 123 | 124 | _slide, // Note slide 125 | _deltaSlide, // Change in slide 126 | 127 | _changeAmount, // Amount to change the note by 128 | _changeTime, // Counter for the note change 129 | _changeLimit, // Once the time reaches this limit, the note changes 130 | 131 | _squareDuty, // Offset of center switching point in the square wave 132 | _dutySweep; // Amount to change the duty by 133 | 134 | //-------------------------------------------------------------------------- 135 | // 136 | // Synth Methods 137 | // 138 | //-------------------------------------------------------------------------- 139 | 140 | /** 141 | * Resets the runing variables from the params 142 | * Used once at the start (total reset) and for the repeat effect (partial reset) 143 | */ 144 | _this.r = function() { 145 | // Shorter reference 146 | var p = _this._params; 147 | 148 | _period = 100 / (p['f'] * p['f'] + .001); 149 | _maxPeriod = 100 / (p['g'] * p['g'] + .001); 150 | 151 | _slide = 1 - p['h'] * p['h'] * p['h'] * .01; 152 | _deltaSlide = -p['i'] * p['i'] * p['i'] * .000001; 153 | 154 | if (!p['a']) { 155 | _squareDuty = .5 - p['n'] / 2; 156 | _dutySweep = -p['o'] * .00005; 157 | } 158 | 159 | _changeAmount = 1 + p['l'] * p['l'] * (p['l'] > 0 ? -.9 : 10); 160 | _changeTime = 0; 161 | _changeLimit = p['m'] == 1 ? 0 : (1 - p['m']) * (1 - p['m']) * 20000 + 32; 162 | } 163 | 164 | // I split the reset() function into two functions for better readability 165 | _this.tr = function() { 166 | _this.r(); 167 | 168 | // Shorter reference 169 | var p = _this._params; 170 | 171 | // Calculating the length is all that remained here, everything else moved somewhere 172 | _envelopeLength0 = p['b'] * p['b'] * 100000; 173 | _envelopeLength1 = p['c'] * p['c'] * 100000; 174 | _envelopeLength2 = p['e'] * p['e'] * 100000 + 12; 175 | // Full length of the volume envelop (and therefore sound) 176 | // Make sure the length can be divided by 3 so we will not need the padding "==" after base64 encode 177 | return ((_envelopeLength0 + _envelopeLength1 + _envelopeLength2) / 3 | 0) * 3; 178 | } 179 | 180 | /** 181 | * Writes the wave to the supplied buffer ByteArray 182 | * @param buffer A ByteArray to write the wave to 183 | * @return If the wave is finished 184 | */ 185 | _this.s = function(buffer, length) { 186 | // Shorter reference 187 | var p = _this._params; 188 | 189 | // If the filters are active 190 | var _filters = p['s'] != 1 || p['v'], 191 | // Cutoff multiplier which adjusts the amount the wave position can move 192 | _hpFilterCutoff = p['v'] * p['v'] * .1, 193 | // Speed of the high-pass cutoff multiplier 194 | _hpFilterDeltaCutoff = 1 + p['w'] * .0003, 195 | // Cutoff multiplier which adjusts the amount the wave position can move 196 | _lpFilterCutoff = p['s'] * p['s'] * p['s'] * .1, 197 | // Speed of the low-pass cutoff multiplier 198 | _lpFilterDeltaCutoff = 1 + p['t'] * .0001, 199 | // If the low pass filter is active 200 | _lpFilterOn = p['s'] != 1, 201 | // masterVolume * masterVolume (for quick calculations) 202 | _masterVolume = p['x'] * p['x'], 203 | // Minimum frequency before stopping 204 | _minFreqency = p['g'], 205 | // If the phaser is active 206 | _phaser = p['q'] || p['r'], 207 | // Change in phase offset 208 | _phaserDeltaOffset = p['r'] * p['r'] * p['r'] * .2, 209 | // Phase offset for phaser effect 210 | _phaserOffset = p['q'] * p['q'] * (p['q'] < 0 ? -1020 : 1020), 211 | // Once the time reaches this limit, some of the iables are reset 212 | _repeatLimit = p['p'] ? ((1 - p['p']) * (1 - p['p']) * 20000 | 0) + 32 : 0, 213 | // The punch factor (louder at begining of sustain) 214 | _sustainPunch = p['d'], 215 | // Amount to change the period of the wave by at the peak of the vibrato wave 216 | _vibratoAmplitude = p['j'] / 2, 217 | // Speed at which the vibrato phase moves 218 | _vibratoSpeed = p['k'] * p['k'] * .01, 219 | // The type of wave to generate 220 | _waveType = p['a']; 221 | 222 | var _envelopeLength = _envelopeLength0, // Length of the current envelope stage 223 | _envelopeOverLength0 = 1 / _envelopeLength0, // (for quick calculations) 224 | _envelopeOverLength1 = 1 / _envelopeLength1, // (for quick calculations) 225 | _envelopeOverLength2 = 1 / _envelopeLength2; // (for quick calculations) 226 | 227 | // Damping muliplier which restricts how fast the wave position can move 228 | var _lpFilterDamping = 5 / (1 + p['u'] * p['u'] * 20) * (.01 + _lpFilterCutoff); 229 | if (_lpFilterDamping > .8) { 230 | _lpFilterDamping = .8; 231 | } 232 | _lpFilterDamping = 1 - _lpFilterDamping; 233 | 234 | var _finished = false, // If the sound has finished 235 | _envelopeStage = 0, // Current stage of the envelope (attack, sustain, decay, end) 236 | _envelopeTime = 0, // Current time through current enelope stage 237 | _envelopeVolume = 0, // Current volume of the envelope 238 | _hpFilterPos = 0, // Adjusted wave position after high-pass filter 239 | _lpFilterDeltaPos = 0, // Change in low-pass wave position, as allowed by the cutoff and damping 240 | _lpFilterOldPos, // Previous low-pass wave position 241 | _lpFilterPos = 0, // Adjusted wave position after low-pass filter 242 | _periodTemp, // Period modified by vibrato 243 | _phase = 0, // Phase through the wave 244 | _phaserInt, // Integer phaser offset, for bit maths 245 | _phaserPos = 0, // Position through the phaser buffer 246 | _pos, // Phase expresed as a Number from 0-1, used for fast sin approx 247 | _repeatTime = 0, // Counter for the repeats 248 | _sample, // Sub-sample calculated 8 times per actual sample, averaged out to get the super sample 249 | _superSample, // Actual sample writen to the wave 250 | _vibratoPhase = 0; // Phase through the vibrato sine wave 251 | 252 | // Buffer of wave values used to create the out of phase second wave 253 | var _phaserBuffer = new Array(1024), 254 | // Buffer of random values used to generate noise 255 | _noiseBuffer = new Array(32); 256 | for (var i = _phaserBuffer.length; i--; ) { 257 | _phaserBuffer[i] = 0; 258 | } 259 | for (var i = _noiseBuffer.length; i--; ) { 260 | _noiseBuffer[i] = Math.random() * 2 - 1; 261 | } 262 | 263 | for (var i = 0; i < length; i++) { 264 | if (_finished) { 265 | return i; 266 | } 267 | 268 | // Repeats every _repeatLimit times, partially resetting the sound parameters 269 | if (_repeatLimit) { 270 | if (++_repeatTime >= _repeatLimit) { 271 | _repeatTime = 0; 272 | _this.r(); 273 | } 274 | } 275 | 276 | // If _changeLimit is reached, shifts the pitch 277 | if (_changeLimit) { 278 | if (++_changeTime >= _changeLimit) { 279 | _changeLimit = 0; 280 | _period *= _changeAmount; 281 | } 282 | } 283 | 284 | // Acccelerate and apply slide 285 | _slide += _deltaSlide; 286 | _period *= _slide; 287 | 288 | // Checks for frequency getting too low, and stops the sound if a minFrequency was set 289 | if (_period > _maxPeriod) { 290 | _period = _maxPeriod; 291 | if (_minFreqency > 0) { 292 | _finished = true; 293 | } 294 | } 295 | 296 | _periodTemp = _period; 297 | 298 | // Applies the vibrato effect 299 | if (_vibratoAmplitude > 0) { 300 | _vibratoPhase += _vibratoSpeed; 301 | _periodTemp *= 1 + Math.sin(_vibratoPhase) * _vibratoAmplitude; 302 | } 303 | 304 | _periodTemp |= 0; 305 | if (_periodTemp < 8) { 306 | _periodTemp = 8; 307 | } 308 | 309 | // Sweeps the square duty 310 | if (!_waveType) { 311 | _squareDuty += _dutySweep; 312 | if (_squareDuty < 0) { 313 | _squareDuty = 0; 314 | } else if (_squareDuty > .5) { 315 | _squareDuty = .5; 316 | } 317 | } 318 | 319 | // Moves through the different stages of the volume envelope 320 | if (++_envelopeTime > _envelopeLength) { 321 | _envelopeTime = 0; 322 | 323 | switch (++_envelopeStage) { 324 | case 1: 325 | _envelopeLength = _envelopeLength1; 326 | break; 327 | case 2: 328 | _envelopeLength = _envelopeLength2; 329 | } 330 | } 331 | 332 | // Sets the volume based on the position in the envelope 333 | switch (_envelopeStage) { 334 | case 0: 335 | _envelopeVolume = _envelopeTime * _envelopeOverLength0; 336 | break; 337 | case 1: 338 | _envelopeVolume = 1 + (1 - _envelopeTime * _envelopeOverLength1) * 2 * _sustainPunch; 339 | break; 340 | case 2: 341 | _envelopeVolume = 1 - _envelopeTime * _envelopeOverLength2; 342 | break; 343 | case 3: 344 | _envelopeVolume = 0; 345 | _finished = true; 346 | } 347 | 348 | // Moves the phaser offset 349 | if (_phaser) { 350 | _phaserOffset += _phaserDeltaOffset; 351 | _phaserInt = _phaserOffset | 0; 352 | if (_phaserInt < 0) { 353 | _phaserInt = -_phaserInt; 354 | } else if (_phaserInt > 1023) { 355 | _phaserInt = 1023; 356 | } 357 | } 358 | 359 | // Moves the high-pass filter cutoff 360 | if (_filters && _hpFilterDeltaCutoff) { 361 | _hpFilterCutoff *= _hpFilterDeltaCutoff; 362 | if (_hpFilterCutoff < .00001) { 363 | _hpFilterCutoff = .00001; 364 | } else if (_hpFilterCutoff > .1) { 365 | _hpFilterCutoff = .1; 366 | } 367 | } 368 | 369 | _superSample = 0; 370 | for (var j = 8; j--; ) { 371 | // Cycles through the period 372 | _phase++; 373 | if (_phase >= _periodTemp) { 374 | _phase %= _periodTemp; 375 | 376 | // Generates new random noise for this period 377 | if (_waveType == 3) { 378 | for (var n = _noiseBuffer.length; n--; ) { 379 | _noiseBuffer[n] = Math.random() * 2 - 1; 380 | } 381 | } 382 | } 383 | 384 | // Gets the sample from the oscillator 385 | switch (_waveType) { 386 | case 0: // Square wave 387 | _sample = ((_phase / _periodTemp) < _squareDuty) ? .5 : -.5; 388 | break; 389 | case 1: // Saw wave 390 | _sample = 1 - _phase / _periodTemp * 2; 391 | break; 392 | case 2: // Sine wave (fast and accurate approx) 393 | _pos = _phase / _periodTemp; 394 | _pos = (_pos > .5 ? _pos - 1 : _pos) * 6.28318531; 395 | _sample = 1.27323954 * _pos + .405284735 * _pos * _pos * (_pos < 0 ? 1 : -1); 396 | _sample = .225 * ((_sample < 0 ? -1 : 1) * _sample * _sample - _sample) + _sample; 397 | break; 398 | case 3: // Noise 399 | _sample = _noiseBuffer[Math.abs(_phase * 32 / _periodTemp | 0)]; 400 | } 401 | 402 | // Applies the low and high pass filters 403 | if (_filters) { 404 | _lpFilterOldPos = _lpFilterPos; 405 | _lpFilterCutoff *= _lpFilterDeltaCutoff; 406 | if (_lpFilterCutoff < 0) { 407 | _lpFilterCutoff = 0; 408 | } else if (_lpFilterCutoff > .1) { 409 | _lpFilterCutoff = .1; 410 | } 411 | 412 | if (_lpFilterOn) { 413 | _lpFilterDeltaPos += (_sample - _lpFilterPos) * _lpFilterCutoff; 414 | _lpFilterDeltaPos *= _lpFilterDamping; 415 | } else { 416 | _lpFilterPos = _sample; 417 | _lpFilterDeltaPos = 0; 418 | } 419 | 420 | _lpFilterPos += _lpFilterDeltaPos; 421 | 422 | _hpFilterPos += _lpFilterPos - _lpFilterOldPos; 423 | _hpFilterPos *= 1 - _hpFilterCutoff; 424 | _sample = _hpFilterPos; 425 | } 426 | 427 | // Applies the phaser effect 428 | if (_phaser) { 429 | _phaserBuffer[_phaserPos % 1024] = _sample; 430 | _sample += _phaserBuffer[(_phaserPos - _phaserInt + 1024) % 1024]; 431 | _phaserPos++; 432 | } 433 | 434 | _superSample += _sample; 435 | } 436 | 437 | // Averages out the super samples and applies volumes 438 | _superSample *= .125 * _envelopeVolume * _masterVolume; 439 | 440 | // Clipping if too loud 441 | buffer[i] = _superSample >= 1 ? 32767 : _superSample <= -1 ? -32768 : _superSample * 32767 | 0; 442 | } 443 | 444 | return length; 445 | } 446 | } 447 | 448 | // Adapted from http://codebase.es/riffwave/ 449 | var synth = new SfxrSynth(); 450 | // Export for the Closure Compiler 451 | var jsfxr = function(settings) { 452 | // Initialize SfxrParams 453 | synth._params.set(settings); 454 | // Synthesize Wave 455 | var envelopeFullLength = synth.tr(); 456 | var data = new Uint8Array(((envelopeFullLength + 1) / 2 | 0) * 4 + 44); 457 | var used = synth.s(new Uint16Array(data.buffer, 44), envelopeFullLength) * 2; 458 | var dv = new Uint32Array(data.buffer, 0, 44); 459 | // Initialize header 460 | dv[0] = 0x46464952; // "RIFF" 461 | dv[1] = used + 36; // put total size here 462 | dv[2] = 0x45564157; // "WAVE" 463 | dv[3] = 0x20746D66; // "fmt " 464 | dv[4] = 0x00000010; // size of the following 465 | dv[5] = 0x00010001; // Mono: 1 channel, PCM format 466 | dv[6] = 0x0000AC44; // 44,100 samples per second 467 | dv[7] = 0x00015888; // byte rate: two bytes per sample 468 | dv[8] = 0x00100002; // 16 bits per sample, aligned on every two bytes 469 | dv[9] = 0x61746164; // "data" 470 | dv[10] = used; // put number of samples here 471 | 472 | // Base64 encoding written by me, @maettig 473 | used += 44; 474 | var i = 0, 475 | base64Characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 476 | output = 'data:audio/wav;base64,'; 477 | for (; i < used; i += 3) 478 | { 479 | var a = data[i] << 16 | data[i + 1] << 8 | data[i + 2]; 480 | output += base64Characters[a >> 18] + base64Characters[a >> 12 & 63] + base64Characters[a >> 6 & 63] + base64Characters[a & 63]; 481 | } 482 | return output; 483 | } 484 | 485 | module.exports = jsfxr; 486 | -------------------------------------------------------------------------------- /src/scripts/game.js: -------------------------------------------------------------------------------- 1 | // var Stats = require('./stats'); 2 | var drawCube = require('./drawCube'); 3 | var SpriteLib = require('./sprites.js'); 4 | var sprites = SpriteLib.sprites; 5 | var tileLogic = SpriteLib.tileLogic; 6 | var firstruns = {}; 7 | var playSound = require('./sfx'); 8 | var jsonStringify = JSON.stringify; 9 | var random = require('./random'); 10 | var touchList = require('./touchlist'); 11 | var jsonStringify = JSON.stringify; 12 | var modal = require('./modal'); 13 | var i18n = require('./i18n'); 14 | var levelEncode = require('./levels/encodelevel'); 15 | 16 | var colorInterface = '#55bbff'; 17 | var ingameclass = 'ingame'; 18 | 19 | function crawlMap(map, w, h, fn){ 20 | var returnVal, x, y; 21 | for(x=0; x touchStartSpot){ 274 | displayOffset = 0; 275 | var tile = getPixelPosFromTouch(touch); 276 | var tileType = getTileFromTouch(touch); 277 | var predefs = opts.predef.filter(function(item){ 278 | // Skipping this tile 279 | if(item[2] === opts.base){ 280 | return false; 281 | } 282 | if(tile[0] === item[0] && tile[1] === item[1]){ 283 | return true; 284 | } 285 | }); 286 | if(tileType && tileType !== opts.base && !predefs.length){ 287 | setTileFromTouch(touch, opts.base); 288 | playSound('boom'); 289 | explode(tile, '#aaaaaa'); 290 | rumble(); 291 | globalPoints -= 15; 292 | showPoints(tile, -15); 293 | calculateWinState(); 294 | } 295 | } 296 | }, 400); 297 | } 298 | 299 | } 300 | 301 | function touchmove(e){ 302 | if(!gameIsFree && !isTouching){ 303 | return; 304 | } 305 | e.preventDefault(); 306 | moves++; 307 | var touch = touchList(e); 308 | if(!selectedTile && isTouching){ 309 | // Pan the map 310 | viewport[0] += (touch.clientX - lastTouch.clientX); 311 | viewport[1] += (touch.clientY - lastTouch.clientY); 312 | } 313 | lastTouch = touch; 314 | lastHoveredTileCoords = getPixelPosFromTouch(touch); 315 | lastHoveredTileType = getTileFromTouch(touch); 316 | lastHoveredTilePos = getIsometricPos(lastHoveredTileCoords[0], lastHoveredTileCoords[1], tileSize); 317 | } 318 | 319 | function touchend(e){ 320 | isTouching=0; 321 | clearTimeout(longPress); 322 | if(selectedTile){ 323 | if( 324 | isTouchInTileQueueBounds(lastTouch) || // If we're dropping on the queue 325 | (tileSelectType===1 && lastHoveredTileType === 'helipad') // Or if we picked up & dropped on the helipad 326 | ){ 327 | // Do nothing. Assume the user wants to drop the tile later. 328 | return; 329 | } else if(lastHoveredTileType === 'helipad'){ 330 | playSound('place'); 331 | // If we pulled this tile off the helipad then put it back on, 332 | // don't shift off the main stack. 333 | if(tileSelectType != 1){ 334 | heliStack.push(selectedTile); 335 | tileStack.shift(); 336 | tileQueueReset = now; 337 | dustCloud(getPixelPosFromTouch(lastTouch)); 338 | } 339 | } else if(gameIsFree || canPlaceTileHere(selectedTile, lastHoveredTileCoords)) { 340 | if(sprites[selectedTile+'-base']){ 341 | selectedTile = selectedTile+'-base'; 342 | } 343 | if(setTileFromTouch(lastTouch, selectedTile)){ 344 | playSound('place'); 345 | dustCloud(getPixelPosFromTouch(lastTouch)); 346 | // calculatePoints(getPixelPosFromTouch(lastTouch,1)); 347 | // 0 = tileStack, 1 = heliStack. 348 | if(tileSelectType === 0){ 349 | tileQueueReset = now; 350 | tileStack.shift(); 351 | } else { 352 | heliStack.pop(); 353 | } 354 | tileSelectType = 0; 355 | } 356 | } else { 357 | playSound('error'); 358 | playSound('error',0.15); 359 | } 360 | // Calculate win state and optionally show help dialog. 361 | calculateWinState(); 362 | nextTile = tileLogic[tileStack[0]]; 363 | if(nextTile && nextTile.firstrun && !firstruns[tileStack[0]]){ 364 | showTooltip(nextTile.firstrun, nextTile.title, tileStack[[0]]); 365 | firstruns[tileStack[0]] = true; 366 | } 367 | } 368 | if(!gameIsFree){ 369 | selectedTile = false; 370 | tileSelectType = 0; 371 | calculatePointsState(); 372 | } 373 | } 374 | 375 | function isTouchInTileQueueBounds(touch){ 376 | return touch.clientY < tileHalf && 377 | touch.clientX > tileQueueBounds && 378 | touch.clientX < tileQueueBounds + tileHalf; 379 | 380 | } 381 | 382 | var events = [ 383 | ['resize', resize, window], 384 | ['touchstart', touchstart], 385 | ['touchmove', touchmove], 386 | ['touchend', touchend], 387 | ['mousedown', touchstart], 388 | ['mousemove', touchmove], 389 | ['mouseup', touchend], 390 | ]; 391 | if(!opts.renderOnly){ 392 | d.body.className = ingameclass; 393 | events.forEach(function(event){ 394 | (event[2] || canvas).addEventListener(event[0], event[1], true); 395 | }); 396 | } 397 | 398 | var particles = []; 399 | function explode(pos, color){ 400 | for(var i=0; i<8; i++){ 401 | particles.push([ 402 | now, 403 | pos[0] - 0.5, 404 | pos[1] - 0.5, 405 | Math.random()*2-1, // x 406 | Math.random()*2-1, // y 407 | 1.5, // z 408 | displayOffset ? 800 : 500, // touch devices animate for longer 409 | // because our fingers are in the way 410 | color, 411 | -1, // enable gravity 412 | 1, // Alpha = 1 413 | tileSize/30, 414 | ]); 415 | } 416 | } 417 | 418 | function dustCloud(pos){ 419 | var randomColours = ['#aaaaaa', '#cccccc', colorInterface]; 420 | for(var i=0; i<10; i++){ 421 | var xVelocity = Math.random()-0.5; 422 | var yVelocity = Math.random()-0.5; 423 | particles.push([ 424 | now, 425 | pos[0] - Math.random(), 426 | pos[1] - Math.random(), 427 | xVelocity/4, 428 | yVelocity/4, 429 | 0-Math.random()/2, 430 | 500, 431 | randomColours[Math.round(Math.random()*2)], 432 | -0.5, // reverse gravity 433 | 0.8, // Alpha = 1 434 | tileSize/50, // particle size 435 | ]); 436 | } 437 | } 438 | 439 | function drawParticles(){ 440 | if(particles.length && 1000/time > 30){ 441 | particles = particles.filter(function(p){ 442 | var diff = (now - p[0])/(p[6]); 443 | 444 | // If we're finished, don't do anything. 445 | if(diff >= 1){ 446 | return false; 447 | } 448 | 449 | // Otherwise, let's get particleing! 450 | var x = p[1] + p[3] * diff; 451 | var y = p[2] + p[4] * diff; 452 | var z = (p[5]*tileHalf*diff) * (p[8]*diff*diff); 453 | var pos = getIsometricPos(x, y, tileSize); 454 | drawCube(ctx, 455 | pos[0] + viewport[0], 456 | pos[1] + viewport[1] - z, 457 | p[10], 458 | p[10], 459 | p[10], 460 | p[7], 461 | diff > 0.5 ? Math.max(0, p[9] * (1-diff)) : p[9] 462 | ); 463 | 464 | return 1; 465 | }); 466 | } 467 | } 468 | 469 | /** 470 | * Can we place the selected tile here? 471 | * @param {String} placingThis Type of tile to place. 472 | * @param {Array} coords Coordinates of the tile we want to place on. 473 | * @return {Boolean} 474 | */ 475 | function canPlaceTileHere(placingThis, coords){ 476 | // Strict mode only lets you place tiles to continue the current road. 477 | if(opts.strict && placingThis.indexOf('road') === 0){ 478 | // This will throw when attempting to access out of bounds tiles. 479 | try{ 480 | var oldTile = map[coords[0]][coords[1]]; 481 | var oldRoute = getCalculatedTrafficPath(); 482 | map[coords[0]][coords[1]] = placingThis; 483 | var newRoute = getCalculatedTrafficPath(); 484 | map[coords[0]][coords[1]] = oldTile; 485 | if(newRoute.win){ 486 | return true; 487 | } 488 | if(oldRoute.lose.length === newRoute.lose.length){ 489 | return false; 490 | } 491 | }catch(e){ 492 | return false; 493 | } 494 | } 495 | for(var i=0;i<2; i++){ 496 | if(jsonStringify(coords) === jsonStringify(opts.predef[i].slice(0,2))){ 497 | return false; 498 | } 499 | } 500 | if(map[coords[0]]){ 501 | return tileLogic[placingThis].p(map[coords[0]][coords[1]]); 502 | } 503 | } 504 | function getPixelPosFromTouch(touch, includeOffset){ 505 | var pp = getPixelPos(touch.clientX - viewport[0], touch.clientY - viewport[1] - (includeOffset!==0 ? displayOffset : 0), tileSize); 506 | return [Math.ceil(pp[0]), Math.ceil(pp[1])]; 507 | } 508 | function getTileFromTouch(touch, includeOffset){ 509 | var pos = getPixelPosFromTouch(touch, includeOffset); 510 | try{ 511 | return map[pos[0]][pos[1]]; 512 | } catch(ex){ 513 | } 514 | } 515 | function setTileFromTouch(touch, val){ 516 | var pos = getPixelPosFromTouch(touch); 517 | 518 | try{ 519 | map[pos[0]][pos[1]] = val; 520 | } catch(ex){ 521 | return 0; 522 | } 523 | return 1; 524 | } 525 | 526 | function rumble(){ 527 | d.body.className = ingameclass+' rumble'; 528 | setTimeout(function(){ 529 | d.body.className = ingameclass; 530 | }, 500); 531 | } 532 | 533 | function showTooltip(message, title, tile, cb){ 534 | var referenceCanvas = spriteCache[tile].c; 535 | var newCanvas = document.createElement('canvas'); 536 | newCanvas.width = referenceCanvas.width; 537 | newCanvas.height = referenceCanvas.height/1.5; 538 | var newContext = newCanvas.getContext('2d'); 539 | newContext.drawImage(referenceCanvas, 0, 0-referenceCanvas.height/3); 540 | modal.show( 541 | i18n.t(message), 542 | i18n.t(title), 543 | tile ? newCanvas.toDataURL() : 0, 544 | 1, 545 | cb 546 | ); 547 | } 548 | 549 | _this.setTile = function(tile, cb){ 550 | selectedTile = tile; 551 | tileChangeCallback = cb; 552 | }; 553 | 554 | /** 555 | * Fall away animation for use in mapOverrides/this.destroy 556 | */ 557 | function fallAway(currentDrawPos){ 558 | var offset = Math.pow(1.02, Math.max(0, (now - this.start)/3)) / 200 * canvas.height; 559 | currentDrawPos[1] += offset; 560 | } 561 | 562 | _this.destroy = function(cb){ 563 | 564 | // If we have a callback, perform the animation. 565 | if(cb){ 566 | mapOverrides = crawlMap([], opts.w, opts.h, function(){ 567 | var start = now + Math.random()*100; 568 | return { 569 | fn: fallAway, 570 | start: start, 571 | end: start + 500, 572 | }; 573 | }); 574 | setTimeout(teardown,1000); 575 | setTimeout(rumble,50); 576 | playSound('boom'); 577 | } else { 578 | // otherwise just tear down immediately. 579 | teardown(); 580 | } 581 | function teardown(){ 582 | // Stop the boats 583 | running = false; 584 | 585 | // tear down the infrastructure. 586 | events.forEach(function(event){ 587 | (event[2] || canvas).removeEventListener(event[0], event[1], true); 588 | }); 589 | 590 | d.body.className = ''; 591 | 592 | if(cb){ 593 | cb(); 594 | } 595 | } 596 | }; 597 | 598 | _this.saveState = function(stateOpts){ 599 | var level = { 600 | name: stateOpts.name, 601 | intro: stateOpts.intro, 602 | outro: stateOpts.outro, 603 | w: opts.w, 604 | h: opts.w, 605 | wMod: opts.wMod, 606 | base: opts.base, 607 | predef: [] 608 | }; 609 | crawlMap(map, opts.w, opts.h, function(x, y, tile){ 610 | if(tile !== opts.base){ 611 | level.predef.push([x,y,tile]); 612 | } 613 | }); 614 | return levelEncode(level); 615 | }; 616 | 617 | /** 618 | * Take screenshot 619 | */ 620 | this.ss = function(){ 621 | renderChrome = 0; 622 | drawMap(); 623 | var a = d.createElement('a'); 624 | a.setAttribute('target', '_blank'); 625 | a.setAttribute('download', 'screenshot.png'); 626 | a.href = canvas.toDataURL('image/png'); 627 | a.click(); 628 | renderChrome = 1; 629 | drawMap(); 630 | }; 631 | 632 | // A handy dandy array that maps tiles to one another. See also sprites.js 633 | var trafficDirections = [ 634 | [0,[-1,0]], // go south 635 | [1,[0,-1]], // go north 636 | [2,[1,0]], // go east 637 | [3,[0,1]], // go south 638 | ]; 639 | 640 | function calculatePoints(here){ 641 | // Look on each side of the tile. 642 | trafficDirections.forEach(function(dir){ 643 | var pos = [ 644 | here[0]+dir[1][0], 645 | here[1]+dir[1][1] 646 | ]; 647 | try{ 648 | // Work out which tile we're looking at. 649 | var thisTile = map[pos[0]][pos[1]]; 650 | // If this tile gives us points 651 | if(tileLogic[thisTile]){ 652 | // Work out how many points 653 | var thesePoints = tileLogic[thisTile].points || 0; 654 | 655 | // Then set the points for this tile. 656 | points[pos[0]][pos[1]] = [ 657 | thesePoints, 658 | points[pos[0]][pos[1]][0] !== thesePoints, // Flags that this tile has changed. 659 | 1 660 | ]; 661 | } else { 662 | 663 | // Set no points for this tile, flagging if it's changed. 664 | points[pos[0]][pos[1]] = [ 665 | 0, 666 | points[pos[0]][pos[1]][0] !== 0, 667 | 1 668 | ]; 669 | } 670 | }catch(e){} 671 | }); 672 | } 673 | 674 | function calculatePointsState(path){ 675 | if(!currentConnectedPath){ 676 | return; 677 | } 678 | currentConnectedPath.forEach(function(tile){ 679 | calculatePoints(tile); 680 | }); 681 | totalPoints = 0; 682 | crawlMap(points, opts.w, opts.h, function(x, y, points){ 683 | // 0: points 684 | // 1: Have these changed from last time 685 | // 2: Do we need to invalidate this 686 | 687 | // If we need to invalidate this, do so. 688 | if(!points[2] && points[0]){ 689 | showPoints([x,y], 0 - points[0]); 690 | points[0] = 0; 691 | } else if(points[1]){ 692 | showPoints([x,y], points[0]); 693 | } 694 | 695 | // reset our changed flag. 696 | points[1] = 0; 697 | points[2] = 0; 698 | totalPoints += points[0]; 699 | updatePointsDisplay(); 700 | }); 701 | } 702 | 703 | var visiblePoints = []; 704 | function showPoints(here, howMany){ 705 | if(howMany){ 706 | visiblePoints.push([ 707 | now, 708 | howMany, 709 | getIsometricPos(here[0], here[1], tileSize), 710 | howMany > 1 // Color flag 711 | ]); 712 | } 713 | updatePointsDisplay(); 714 | return howMany; 715 | } 716 | function updatePointsDisplay(){ 717 | document.querySelector('#p').innerText = totalPoints + globalPoints; 718 | } 719 | function drawPoints(){ 720 | ctx.font = "bold 25px serif"; 721 | ctx.strokeStyle = '#fff'; 722 | ctx.lineWidth = 5; 723 | if(visiblePoints.length){ 724 | visiblePoints = visiblePoints.filter(function(spec,i){ 725 | var diff = now - spec[0]; 726 | if(diff < 1000){ 727 | var args = [ 728 | spec[1], 729 | spec[2][0] + viewport[0], 730 | spec[2][1] + viewport[1] - diff/1000*tileHalf - tileHalf 731 | ]; 732 | ctx.globalAlpha = 1-(diff/1000); 733 | ctx.fillStyle = spec[3] ? colorInterface : '#FF5566'; 734 | ctx.strokeText.apply(ctx,args); 735 | ctx.fillText.apply(ctx,args); 736 | } else if(diff > 1000){ 737 | return false; 738 | } 739 | return true; 740 | }); 741 | ctx.globalAlpha = 1; 742 | } 743 | } 744 | 745 | function calculateTrafficPath(here, there, lastMove, path){ 746 | if(gameIsFree){ 747 | return []; 748 | } 749 | // Init our route. 750 | if(!path){ 751 | path = [here]; 752 | } 753 | 754 | // If we've reached our destination, do nothing more. 755 | if( 756 | here[0] === there[0] && 757 | here[1] === there[1] 758 | ){ 759 | return { 760 | win: path 761 | }; 762 | } 763 | 764 | // Get our current tile details to compare against. 765 | var thisTile = map[here[0]][here[1]]; 766 | var thisTileSpec = tileLogic[thisTile]; 767 | var oppositeThisMovement = lastMove[0] > 1 ? 0 - 2 + lastMove[0] : lastMove[0] + 2; 768 | var nextMovement; 769 | 770 | // If we can go straight ahead, go for it. 771 | if(thisTileSpec.c[lastMove[0]]){ 772 | nextMovement = lastMove; 773 | } else { 774 | // Otherwise pick the next direction we can. 775 | var possibleMovements = trafficDirections.filter(function(option){ 776 | if(thisTileSpec.c[option[0]] && option[0] !== oppositeThisMovement){ 777 | return true; 778 | } 779 | }); 780 | 781 | // If there are other possibilities, use them. 782 | if(possibleMovements.length){ 783 | nextMovement = possibleMovements[0]; 784 | } else { 785 | return {lose:path}; 786 | } 787 | } 788 | 789 | 790 | // If it does connect, pick the next tile it connects to. 791 | // Try is slow, but this runs infrequently. 792 | var nextTile, nextTileSpec; 793 | var nextCoords = [ 794 | here[0] + nextMovement[1][0], 795 | here[1] + nextMovement[1][1] 796 | ]; 797 | try{ 798 | nextTile = map[nextCoords[0]][nextCoords[1]]; 799 | nextTileSpec = tileLogic[nextTile]; 800 | } catch(ex) { 801 | return {lose:path}; 802 | } 803 | 804 | 805 | // and see if it connects back to this tile. 806 | var oppositeNextMovement = nextMovement[0] > 1 ? 0 - 2 + nextMovement[0] : nextMovement[0] + 2; 807 | if(nextTileSpec && nextTileSpec.c && nextTileSpec.c[oppositeNextMovement]){ 808 | path.push(nextCoords); 809 | return calculateTrafficPath(nextCoords, there, nextMovement, path); 810 | } else { 811 | return {lose:path}; 812 | } 813 | } 814 | 815 | // Convenience method 816 | function getCalculatedTrafficPath(){ 817 | return calculateTrafficPath(opts.predef[0], opts.predef[1], trafficDirections[1]); 818 | } 819 | 820 | /** 821 | * Calculate the win state. 822 | */ 823 | function calculateWinState(){ 824 | if(gameIsFree){ 825 | return; 826 | } 827 | var result = getCalculatedTrafficPath(); 828 | currentConnectedPath = result.win || result.lose; 829 | if(result && result.win){ 830 | var sinPath = function(layer, coords){ 831 | if(now > this.start && now < this.end){ 832 | layer[1] += 0-(Math.sin((now-this.start)/125))*20; 833 | if(!this.s){ 834 | this.s = 1; 835 | calculatePoints(coords); 836 | } 837 | } 838 | }; 839 | result.win.forEach(function(coords, i){ 840 | setTimeout(function(){ 841 | playSound('ping'); 842 | showPoints(coords, 10); 843 | globalPoints += 10; 844 | }, i*100); 845 | mapOverrides[coords[0]][coords[1]] = { 846 | fn: sinPath, 847 | start: i*100 + now, 848 | end: i*100 + now + 350, 849 | }; 850 | }); 851 | playSound('win', result.win.length/10+0.1); 852 | playSound('win', result.win.length/10+0.2); 853 | setTimeout(function(){ 854 | // If we have an outro, show it. 855 | if(opts.outro){ 856 | showTooltip( 857 | opts.outro[0], // message 858 | opts.outro[1], // title 859 | opts.outro[2], // tile 860 | function(){ 861 | // then call back. 862 | if(opts.onwin){ 863 | opts.onwin.call(_this); 864 | } 865 | } 866 | ); 867 | } else { 868 | // Just call back straight away 869 | if(opts.onwin){ 870 | opts.onwin.call(_this); 871 | } 872 | } 873 | },result.win.length*100+2000); 874 | } else { 875 | var full = true; 876 | 877 | // If tileStack is empty, we lose. 878 | // Otherwise check if the map is full; if it is, we lose. 879 | if(tileStack.length !== 0){ 880 | crawlMap(map, opts.w, opts.h, function(x,y,tile){ 881 | if(tile === opts.base){ 882 | full = false; 883 | } 884 | }); 885 | } 886 | if(full){ 887 | if(opts.onlose){ 888 | opts.onlose.call(_this); 889 | } 890 | } 891 | } 892 | } 893 | 894 | /** 895 | * Draw a single item at currentDrawPos 896 | * @param {Array} layer Array layer containing params to draw 897 | */ 898 | function drawItem(layer, pos){ 899 | if(typeof layer === 'function'){ 900 | layer = layer(); 901 | } 902 | var layerPos = getIsometricPos(layer[1], layer[2], tileSize); 903 | drawCube( 904 | currentCtx, 905 | 0-layerPos[0], 906 | 0-layerPos[1] - layer[0]*tileHalf, 907 | layer[3]*tileHalf, 908 | layer[4]*tileHalf, 909 | layer[5]*tileHalf, 910 | layer[6], 911 | layer[7] 912 | ); 913 | } 914 | 915 | /** 916 | * Draw a layer at currentDrawPos 917 | * @param {Array} layer Array layer containing params to draw 918 | */ 919 | function drawLayer(layer, ctxOverride){ 920 | if(layer.length === 4){ 921 | var layerPos = getIsometricPos(layer[2], layer[3], tileSize); 922 | currentCtx.translate(0 - layerPos[0] - layer[1], 0 - layerPos[1]); 923 | var layers = sprites[layer[0]]; 924 | if(typeof layers === 'function'){ 925 | layers = layers(layerPos); 926 | } 927 | layers.forEach(drawItem); 928 | currentCtx.translate(layerPos[0] - layer[1], layerPos[1]); 929 | 930 | } else { 931 | drawItem(layer); 932 | } 933 | } 934 | 935 | function renderCanvas(tiles, seed){ 936 | currentCtx.translate(tileSize/2, tileSize*1.5); 937 | if(typeof tiles === 'function'){ 938 | tiles = tiles(seed); 939 | } 940 | tiles.forEach(drawLayer); 941 | currentCtx.translate(0-tileSize/2, 0-tileSize*1.5); 942 | 943 | } 944 | 945 | /** 946 | * Draw a single tile 947 | */ 948 | function cacheSprite(tile, spriteCanvas, seed){ 949 | if(!spriteCanvas){ 950 | spriteCanvas = { 951 | c: d.createElement('canvas'), 952 | seed: seed 953 | }; 954 | spriteCanvas.c.width = tileSize+1; 955 | spriteCanvas.c.height = tileSize*2; 956 | currentCtx = spriteCanvas.c.getContext('2d'); 957 | spriteCanvas.x = currentCtx; 958 | } else { 959 | if(spriteCanvas.lastRender === now){ 960 | return spriteCanvas; 961 | } 962 | currentCtx = spriteCanvas.x; 963 | currentCtx.clearRect(0,0,spriteCanvas.c.width, spriteCanvas.c.height); 964 | } 965 | spriteCanvas.lastRender = now; 966 | renderCanvas(sprites[tile], spriteCanvas.seed); 967 | spriteCache[tile] = spriteCanvas; 968 | return spriteCanvas; 969 | } 970 | 971 | function drawSprite(x, y, tile){ 972 | var spriteCanvas = spriteCache[tile]; 973 | var currentDrawPos = getIsometricPos(x, y, tileSize); 974 | 975 | // Don't render if we're not on-screen 976 | if( 977 | currentDrawPos[1] < 0 - viewport[1] - tileSize || 978 | currentDrawPos[0] < 0 - viewport[0] - tileSize || 979 | currentDrawPos[1] > 0 - viewport[1] + canvas.height + tileHalf || 980 | currentDrawPos[0] > 0 - viewport[0] + canvas.width + tileHalf 981 | 982 | ){ 983 | return; 984 | } 985 | 986 | // Cache/recache sprites before drawing 987 | if(!spriteCanvas){ 988 | spriteCanvas = cacheSprite(tile); 989 | } else if(SpriteLib.animated[tile]){ 990 | cacheSprite(tile, spriteCanvas); 991 | } 992 | 993 | if(mapOverrides[x][y]){ 994 | mapOverrides[x][y].fn.call(mapOverrides[x][y], currentDrawPos, [x,y]); 995 | } 996 | 997 | // Treat water tiles differently so we can get the sweet sine wave ripple. 998 | // Only do this if we're over 20 fps as it's kinda slow. 999 | if(tile === 'water' && 1000/time > 30){ 1000 | ctx.drawImage(spriteCanvas.c, currentDrawPos[0] - tileSize/2, currentDrawPos[1] - tileSize*1.5 - Math.sin(x+y+now/200)*2 ); 1001 | } else { 1002 | ctx.drawImage(spriteCanvas.c, currentDrawPos[0] - tileSize/2, currentDrawPos[1] - tileSize*1.5); 1003 | } 1004 | 1005 | if(tile === 'helipad'){ 1006 | drawHeliStack(currentDrawPos); 1007 | } 1008 | } 1009 | 1010 | function getTileQueuePos(i){ 1011 | return 0 - tileHalf/2 + (queueSize - i - 1)*(tileHalf+spacing); 1012 | } 1013 | 1014 | var drawTileQueueOffset = 0; 1015 | function drawTileQueue(){ 1016 | if(!renderChrome || !showTileQueue){ 1017 | return; 1018 | } 1019 | if(tileQueueReset){ 1020 | drawTileQueueOffset = (1-(now - tileQueueReset)/200) * (getTileQueuePos(1) - getTileQueuePos(0)); 1021 | if(now > tileQueueReset+200){ 1022 | tileQueueReset = 0; 1023 | drawTileQueueOffset = 0; 1024 | } 1025 | } 1026 | 1027 | // Draw the background/outline. 1028 | for(var i=1; i>-1; i--){ 1029 | tileQueueContext.fillStyle = i === 0 ? colorInterface : '#fff'; 1030 | tileQueueContext.fillRect( 1031 | 0+i, 1032 | 1 - i, 1033 | Math.round(getTileQueuePos(0) + tileSize /2 + 10), 1034 | Math.round(tileHalf + i*2) 1035 | ); 1036 | } 1037 | 1038 | // Draw the tiles themselves. 1039 | for(i=0; i