├── 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 | Name your creation
3 |
4 |
5 |
6 | Add some optional intro text, tips & tricks.
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 |
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 = '';
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 |
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 |
89 |
90 |
91 |
92 |
93 |
94 |
Watch the video
95 |
96 | VIDEO
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 |
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 b> Modo en el menú principal.",
302 | "Félicitations à vous. Vous avez déverrouillé Carte gratuite b> mode à partir du menu principal.",
303 | "Parabéns. Você já desbloqueado Free Map b> modo a partir do menu principal.",
304 | "Поздравления. Вы разблокирован Бесплатная карта режим из главного меню.",
305 | "Gefeliciteerd. Je hebt vrijgespeeld Gratis Map b> modus in het hoofdmenu.",
306 | "Baie geluk. Jy het ontsluit Free Map b> af vanaf die hoof spyskaart.",
307 | "Selamat. Anda membuka Gratis Peta b> modus dari menu utama.",
308 | "おめでとうございます。あなたは、メインメニューからの無料地図 b>のモードのロックを解除しました。"
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