├── .babelrc ├── .gitignore ├── src ├── js │ ├── index.js │ ├── Terminal.js │ ├── Intro.js │ ├── Camera.js │ ├── Asset.js │ ├── Menu.js │ ├── Input.js │ ├── Player.js │ ├── Door.js │ ├── Audio.js │ ├── Util.js │ ├── Enemy.js │ └── Game.js ├── assets │ ├── _door.png │ ├── _enter.png │ ├── _exit.png │ ├── _floor.png │ ├── _wall.png │ ├── asset1.pxm │ ├── sprites.png │ └── sprites.pxm ├── index.html ├── css │ └── app.css └── levels │ ├── raven.tsx │ └── level01.json ├── zip ├── screenshot1.jpg ├── screenshot2.jpg └── js13k-2018-raven.zip ├── .eslintrc.js ├── package.json ├── level-metadata.js ├── gulpfile.js ├── README.md └── level-packer.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | node_modules 4 | raven 5 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | window.game = new Game(); 2 | game.init().start(); 3 | -------------------------------------------------------------------------------- /src/assets/_door.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliot-nelson/js13k-2018-raven/HEAD/src/assets/_door.png -------------------------------------------------------------------------------- /src/assets/_enter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliot-nelson/js13k-2018-raven/HEAD/src/assets/_enter.png -------------------------------------------------------------------------------- /src/assets/_exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliot-nelson/js13k-2018-raven/HEAD/src/assets/_exit.png -------------------------------------------------------------------------------- /src/assets/_floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliot-nelson/js13k-2018-raven/HEAD/src/assets/_floor.png -------------------------------------------------------------------------------- /src/assets/_wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliot-nelson/js13k-2018-raven/HEAD/src/assets/_wall.png -------------------------------------------------------------------------------- /src/assets/asset1.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliot-nelson/js13k-2018-raven/HEAD/src/assets/asset1.pxm -------------------------------------------------------------------------------- /zip/screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliot-nelson/js13k-2018-raven/HEAD/zip/screenshot1.jpg -------------------------------------------------------------------------------- /zip/screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliot-nelson/js13k-2018-raven/HEAD/zip/screenshot2.jpg -------------------------------------------------------------------------------- /src/assets/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliot-nelson/js13k-2018-raven/HEAD/src/assets/sprites.png -------------------------------------------------------------------------------- /src/assets/sprites.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliot-nelson/js13k-2018-raven/HEAD/src/assets/sprites.pxm -------------------------------------------------------------------------------- /zip/js13k-2018-raven.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliot-nelson/js13k-2018-raven/HEAD/zip/js13k-2018-raven.zip -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0px; 3 | padding: 0px; 4 | background-color: black; 5 | } 6 | 7 | div { 8 | position: absolute; 9 | top: 32px; 10 | bottom: 32px; 11 | left: 32px; 12 | right: 32px; 13 | } 14 | 15 | #canvas { 16 | width: 100%; 17 | height: 100%; 18 | border: solid 1px #333; 19 | } 20 | 21 | #los { 22 | display: none; 23 | } 24 | 25 | #tile { 26 | display: none; 27 | } 28 | -------------------------------------------------------------------------------- /src/levels/raven.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb-base", 3 | "rules": { 4 | // This stuff is turned off because in this context, I don't care to fix it. 5 | // (This is "post-competition" stuff.) 6 | "indent": "off", 7 | "no-underscore-dangle": "off", 8 | "max-len": "off", 9 | "prefer-const": "off", 10 | "no-multi-spaces": "off", 11 | 12 | // Personal preferences (love ++, hate dangling commas, love single quotes) 13 | "quotes": ["error", "single"], 14 | "no-plusplus": "off", 15 | "comma-dangle": ["error", "never"] 16 | }, 17 | "globals": { 18 | "game": true, 19 | "window": true 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "offline", 3 | "version": "1.0.0", 4 | "description": "My 2018 entry for the js13kgames compo, 'Raven'.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Elliot Nelson", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "babel-core": "^6.26.3", 13 | "babel-preset-env": "^1.7.0", 14 | "del": "^3.0.0", 15 | "eslint": "^5.5.0", 16 | "eslint-config-airbnb-base": "^13.1.0", 17 | "eslint-plugin-import": "^2.14.0", 18 | "glob": "^7.1.2", 19 | "gulp": "^3.9.1", 20 | "gulp-add": "^0.1.0", 21 | "gulp-babel": "^7.0.1", 22 | "gulp-clean-css": "^3.10.0", 23 | "gulp-concat": "^2.6.1", 24 | "gulp-eslint": "^5.0.0", 25 | "gulp-htmlmin": "^4.0.0", 26 | "gulp-imagemin": "^4.1.0", 27 | "gulp-shell": "^0.6.5", 28 | "gulp-size": "^3.0.0", 29 | "gulp-sourcemaps": "^2.6.4", 30 | "gulp-terser": "^1.0.1", 31 | "gulp-uglify": "^3.0.1", 32 | "gulp-zip": "^4.2.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/js/Terminal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Terminals are togglable by the player, and typically activate one or more 3 | * security cameras. All terminals must be toggled on by the player in order to 4 | * exit a level. 5 | */ 6 | class Terminal { 7 | constructor(terminalData) { 8 | Object.assign(this, terminalData); 9 | this.x = this.u * 32 + 16; 10 | this.y = this.v * 32 + 16; 11 | 12 | // 26x19 13 | 14 | this._determinePlacement(); 15 | 16 | this.toggleRadius = 19; 17 | 18 | this.cameras = []; 19 | 20 | this.enabled = false; 21 | this._toggled = undefined; 22 | } 23 | 24 | update() { 25 | if (this._toggled) { 26 | this.enabled = !this.enabled; 27 | this.cameras.forEach(camera => camera.toggle()); 28 | game.audio.playBloop(); 29 | } 30 | 31 | this._toggled = undefined; 32 | } 33 | 34 | render() { 35 | game.ctx.save(); 36 | game.ctx.translate(game.offset.x + this.x, game.offset.y + this.y); 37 | game.ctx.rotate(Util.d2r(this.facing)); 38 | Asset.drawSprite('terminal', game.ctx, -16, -16); 39 | game.ctx.fillStyle = this.enabled ? 'rgba(36, 204, 36, 0.8)' : 'rgba(204, 36, 36, 0.8)'; 40 | game.ctx.fillRect(18 - 13, 10 - 16, 3, 3); 41 | game.ctx.restore(); 42 | } 43 | 44 | toggle() { 45 | this._toggled = true; 46 | } 47 | 48 | _determinePlacement() { 49 | if (Util.wallAtUV(this.u, this.v - 1)) { 50 | this.facing = 0; 51 | } else if (Util.wallAtUV(this.u - 1, this.v)) { 52 | this.facing = 270; 53 | } else if (Util.wallAtUV(this.u, this.v + 1)) { 54 | this.facing = 180; 55 | } else { 56 | this.facing = 90; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/js/Intro.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Intro handles overlay "level text" - like intro and outro text played in scrolling 3 | * style. The user can skip through the scrolling by tapping spacebar/enter. 4 | */ 5 | class Intro { 6 | constructor(text) { 7 | this._text = text; 8 | this._duration = 0; 9 | this._charsPerSecond = 56; 10 | 11 | this.state = 'alive'; 12 | } 13 | 14 | update(delta) { 15 | this._duration += delta; 16 | this._chars = Math.min(this._text.length, this._duration * this._charsPerSecond); 17 | 18 | if (this._chars !== this._text.length) { 19 | // Text "scroll" audio effect 20 | game.audio.playClick(); 21 | } 22 | } 23 | 24 | render() { 25 | game.ctx.font = Asset.getFontString(18); 26 | game.ctx.fillStyle = 'rgba(204,255,204,0.9)'; 27 | 28 | let text = this._text.substring(0, this._chars); 29 | let lines = text.split('\n'); 30 | 31 | for (let i = 0; i < lines.length; i++) { 32 | let line = lines[i]; 33 | 34 | while (game.ctx.measureText(line).width > game.canvas.width - 30) { 35 | line = line.split(' ').slice(0, -1).join(' '); 36 | } 37 | 38 | // Trim off leading and trailing space while rendering (don't include in math) 39 | game.ctx.fillText(line.trim(), 15, 30 + i * 28); 40 | 41 | let leftover = lines[i].slice(line.length); 42 | if (leftover.length > 0) { 43 | lines = lines.slice(0, i + 1).concat([leftover]).concat(lines.slice(i + 1)); 44 | } 45 | } 46 | 47 | // Interactivity indicator 48 | if (this._chars === this._text.length) { 49 | Util.renderTogglePrompt(game.canvas.width - 20, game.canvas.height - 20); 50 | } 51 | } 52 | 53 | toggle() { 54 | if (this._chars === this._text.length) { 55 | this.state = 'dead'; 56 | game.audio.playBloop(); 57 | } else { 58 | this._duration = 10000; 59 | game.audio.playBloop(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/js/Camera.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Camera handles security cameras, which are simple update/render entities. 3 | * 4 | * Security cameras provide LOS, which locks enemies down if they are in the cone, 5 | * and provides the player with safe passage. Usually turned on by terminals. 6 | */ 7 | class Camera { 8 | constructor(cameraData) { 9 | Object.assign(this, cameraData); 10 | this.fov = 60; 11 | 12 | // camera head = 21x12 13 | 14 | this._determineArmPlacement(); 15 | 16 | this._toggled = undefined; 17 | } 18 | 19 | update(delta) { 20 | if (this._toggled) { 21 | this.enabled = !this.enabled; 22 | } 23 | 24 | this._toggled = undefined; 25 | } 26 | 27 | render() { 28 | // Arm 29 | game.ctx.save(); 30 | game.ctx.translate(game.offset.x + this.u * 32 + 16, game.offset.y + this.v * 32 + 16); 31 | game.ctx.rotate(Util.d2r(this.armFacing)); 32 | Asset.drawSprite('camera_arm', game.ctx, -16, -16); 33 | game.ctx.restore(); 34 | 35 | // Head 36 | game.ctx.save(); 37 | game.ctx.translate(game.offset.x + this.x, game.offset.y + this.y); 38 | game.ctx.rotate(Util.d2r(this.facing)); 39 | Asset.drawSprite('camera_head', game.ctx, -6, -5); 40 | game.ctx.fillStyle = this.enabled ? 'rgba(36,204,36,0.8)' : 'rgba(204,36,36,0.8)'; 41 | game.ctx.fillRect(0, -1, 3, 3); 42 | game.ctx.restore(); 43 | } 44 | 45 | toggle() { 46 | this._toggled = true; 47 | } 48 | 49 | _determineArmPlacement() { 50 | if (Util.wallAtUV(this.u, this.v - 1)) { 51 | this.armFacing = 0; 52 | this.x = this.u * 32 + 15; 53 | this.y = this.v * 32 + 11; 54 | } else if (Util.wallAtUV(this.u - 1, this.v)) { 55 | this.armFacing = 270; 56 | this.x = this.u * 32 + 11; 57 | this.y = this.v * 32 + 16; 58 | } else if (Util.wallAtUV(this.u, this.v + 1)) { 59 | this.armFacing = 180; 60 | this.x = this.u * 32 + 15; 61 | this.y = this.v * 32 + 20; 62 | } else { 63 | this.armFacing = 90; 64 | this.x = this.u * 32 + 20; 65 | this.y = this.v * 32 + 16; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/js/Asset.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Asset handles images (drawn from a sprite sheet) and font strings. 3 | */ 4 | const Asset = { 5 | _sprites: { 6 | camera_arm: { 7 | x: 32, 8 | y: 32, 9 | w: 32, 10 | h: 32 11 | }, 12 | camera_head: { 13 | x: 0, 14 | y: 32, 15 | w: 21, 16 | h: 12 17 | }, 18 | door: { 19 | x: 64, 20 | y: 0, 21 | w: 32, 22 | h: 32 23 | }, 24 | floor: { 25 | x: 32, 26 | y: 0, 27 | w: 32, 28 | h: 32 29 | }, 30 | player: { 31 | x: 64, 32 | y: 32, 33 | w: 21, 34 | h: 15 35 | }, 36 | raven: { 37 | x: 96, 38 | y: 32, 39 | w: 18, 40 | h: 30 41 | }, 42 | terminal: { 43 | x: 96, 44 | y: 0, 45 | w: 32, 46 | h: 32 47 | }, 48 | wall: { 49 | x: 0, 50 | y: 0, 51 | w: 32, 52 | h: 32 53 | } 54 | }, 55 | _img: { 56 | }, 57 | 58 | // Obviously, the ideal would be to bundle in an OTT or pixel art font and give every user 59 | // the same experience. 60 | // 61 | // However, I've tested all of these and they all "work", so this should cover us. 62 | _fontFamily: "Monaco,'Lucida Sans Typewriter','Andale Mono','Lucida Console','Courier New',Courier,monospace", 63 | 64 | _loadImage(src) { 65 | const img = new Image(); 66 | img.src = src; 67 | return img; 68 | }, 69 | 70 | drawSprite(name, ctx, x, y) { 71 | let sprite = this._sprites[name]; 72 | ctx.drawImage(this._img._sprites, sprite.x, sprite.y, sprite.w, sprite.h, x, y, sprite.w, sprite.h); 73 | }, 74 | 75 | drawSprite2(name, ctx, sx, sy, sw, sh, dx, dy, dw, dh) { 76 | let sprite = this._sprites[name]; 77 | ctx.drawImage(this._img._sprites, sprite.x + sx, sprite.y + sy, sw, sh, dx, dy, dw, dh); 78 | }, 79 | 80 | loadAllAssets() { 81 | // Originally, I loaded a bunch of PNGs here; no matter how much you compress them, 82 | // though, you cannot beat a sprite sheet for space. (I think if space were not an 83 | // issue, it's debatable whether sprite sheets are still the best route for a game 84 | // overall, but if space is your only concern...) 85 | Asset._img._sprites = Asset._loadImage('assets/sprites.png'); 86 | }, 87 | 88 | getFontString(pixels) { 89 | return '' + pixels + 'px ' + Asset._fontFamily; 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /src/js/Menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Menu represents a generic "menu" interface, customized by providing a number of text 3 | * options and handlers that contorl the behavior when clicked. Menus can be standalone 4 | * (like the start menu) or overlay an active level. 5 | */ 6 | class Menu { 7 | constructor(options, escapeHandler) { 8 | this._options = options.slice(0); 9 | this._escapeHandler = escapeHandler; 10 | this._selected = 0; 11 | } 12 | 13 | open() { 14 | this.scale = 5; 15 | } 16 | 17 | update() { 18 | if (this.scale > 0) this.scale -= 1; 19 | } 20 | 21 | render() { 22 | let entryHeight = 36; 23 | 24 | game.ctx.fillStyle = 'rgba(0,0,0,0.7)'; 25 | game.ctx.fillRect(0, 0, game.canvas.width, game.canvas.height); 26 | 27 | let menuTop = game.canvas.height / 2 - this._options.length * (entryHeight * 0.75); 28 | 29 | game.ctx.save(); 30 | game.ctx.translate(game.canvas.width / 2, game.canvas.height / 2); 31 | game.ctx.scale(1 + this.scale, 1 + this.scale); 32 | game.ctx.translate(-game.canvas.width / 2, -game.canvas.height / 2); 33 | 34 | this._options.forEach((entry, idx) => { 35 | game.ctx.font = Asset.getFontString(18); 36 | 37 | entry.w = game.ctx.measureText(entry.text).width; 38 | entry.x = game.canvas.width / 2 - entry.w / 2; 39 | entry.y = menuTop + idx * entryHeight; 40 | 41 | if (idx === this._selected && this.scale === 0) { 42 | game.ctx.fillStyle = 'rgba(255,255,255,1)'; 43 | } else { 44 | game.ctx.fillStyle = 'rgba(204,204,204,1)'; 45 | } 46 | game.ctx.fillText(entry.text, entry.x, entry.y); 47 | }); 48 | 49 | game.ctx.restore(); 50 | } 51 | 52 | onUp() { 53 | this._selected = (this._selected - 1 + this._options.length) % this._options.length; 54 | game.audio.playClick(); 55 | } 56 | 57 | onDown() { 58 | this._selected = (this._selected + 1) % this._options.length; 59 | game.audio.playClick(); 60 | } 61 | 62 | onEscape() { 63 | this._escapeHandler(); 64 | } 65 | 66 | onMouseMove(x, y) { 67 | let oldSelected = this._selected; 68 | this._options.forEach((entry, idx) => { 69 | if (x >= entry.x && x <= entry.x + entry.w && 70 | y >= entry.y - 30 && y <= entry.y) { 71 | this._selected = idx; 72 | } 73 | }); 74 | 75 | if (oldSelected !== this._selected) { 76 | game.audio.playClick(); 77 | } 78 | } 79 | 80 | select() { 81 | this._options[this._selected].handler(); 82 | game.audio.playBloop(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /level-metadata.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Level metadata. The level packer will merge this into the JSON output from Tiled. 3 | * 4 | * Anything in this file could probably be saved within Tiled levels as properties, from 5 | * the looks of it, but I'd rather edit code when I have the choice (and for the longer 6 | * text, like the intro text, the Tiled input forms aren't ideal). 7 | */ 8 | const LevelMetadata = { 9 | level01: { 10 | name: '01 Rear Security Annex', 11 | hint: '! Look around with the mouse, move with W/A/S/D.', 12 | intro: 13 | '[SEC REF 672.A]\n\n' + 14 | 'Thank you for arriving quickly. As you can see, the facility has been breached, and our ' + 15 | 'security monitoring is offline. Containment and establishment of vision are our top priorities.\n\n' + 16 | 'All occupants, codenamed \'Raven\', will manifest as stationary statues when visible. Do ' + 17 | 'not let your guard down, as they are active and extremely dangerous.\n\n' + 18 | 'Your mission is simple: enter the facility, bring our security cameras back online, and ' + 19 | 'contain all active Raven. Ni pukha, ni pyera, comrade.' 20 | }, 21 | level02: { 22 | name: '02 Rear Annex Corridor', 23 | hint: '! Turn on all cameras using nearby terminals to proceed.', 24 | }, 25 | level03: { 26 | name: '03 Rear Hallway SW', 27 | hint: '! Raven are immobilized if spotted by security cameras.', 28 | chx: 0, 29 | chy: 32 30 | }, 31 | level04: { 32 | name: '04 Lobby SW', 33 | hint: '! Plan a route that exposes you as little as possible.' 34 | }, 35 | level05: { 36 | name: '05 Mezzanine', 37 | hint: '! Some Raven are more active than others.' 38 | }, 39 | level06: { 40 | name: '06 Lab Storage', 41 | hint: '! Although dangerous, looking away may be a useful ruse.', 42 | chx: 0, 43 | chy: 32 44 | }, 45 | level07: { 46 | name: '07 Chemical Lab', 47 | hint: '! Most Raven will not attack you until they see you.' 48 | }, 49 | level08: { 50 | name: '08 Maintenance Corridor', 51 | hint: '' 52 | }, 53 | level09: { 54 | name: '09 Front Annex', 55 | hint: '', 56 | chx: 0, 57 | chy: 32 58 | }, 59 | level10: { 60 | name: '10 Facility Parking', 61 | hint: '! So many choices...' 62 | }, 63 | outro: 64 | '[SEC REF 672.C]\n\n' + 65 | 'The facility is now in code yellow, thanks to your efforts. With all cameras ' + 66 | 'back online, secondary cleanup crews will finish the job with minimal danger. We ' + 67 | 'are pleased to report that no Raven escaped during the operation.\n\n' + 68 | 'You are cleared to leave. Do svidaniya, comrade...\n\n' + 69 | 'YOU WIN' 70 | }; 71 | 72 | module.exports = LevelMetadata; 73 | -------------------------------------------------------------------------------- /src/js/Input.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Input handles game input (keyboard presses and mouse movement), allowing the routing 3 | * of these events to the appropriate place via event handlers. 4 | */ 5 | class Input { 6 | constructor(handlers) { 7 | // Input queue (used only for cheat codes) 8 | this.queue = []; 9 | 10 | // Map keys to in-game inputs 11 | this.map = []; 12 | this.map[87] = 'up'; // W 13 | this.map[83] = 'down'; // A 14 | this.map[65] = 'left'; // S 15 | this.map[68] = 'right'; // D 16 | this.map[38] = 'up'; // UpArrow 17 | this.map[40] = 'down'; // DownArrow 18 | this.map[37] = 'left'; // LeftArrow 19 | this.map[39] = 'right'; // RightArrow 20 | this.map[32] = 'toggle'; // Space 21 | this.map[13] = 'toggle'; // Enter 22 | this.map[27] = 'escape'; // Escape 23 | 24 | // TODO: A nice extension would be to have key remapping, which isn't that 25 | // hard - make the above settings configurable, add in a Keys menu, etc. etc. 26 | // Not going to bother for submission, though. 27 | 28 | // Key press handlers 29 | this.handlers = handlers; 30 | 31 | // Mouse location 32 | this.virtualMove = []; 33 | this.virtualX = 0; 34 | this.virtualY = 0; 35 | this.mouseAngle = 0; 36 | } 37 | 38 | init() { 39 | document.addEventListener('keydown', event => { 40 | let k = this.map[event.keyCode]; 41 | 42 | // Uncomment to see key codes in console (an easy way to gather potential keys) 43 | // console.log(event.keyCode); 44 | 45 | if (k) { 46 | // Some keys are "stateful" (we evaluate each frame whether they are still 47 | // held down), other keys are more like events (we want to do something 48 | // specific one time on key press). Provide an API for both. 49 | // 50 | // Note: this is not only useful, it's also required in some cases -- for 51 | // example, the requestPointerLock() API call is ignored unless it is 52 | // triggered by a user input event. 53 | this[k] = true; 54 | 55 | if (this.handlers[k] && typeof this.handlers[k] === 'function') { 56 | this.handlers[k](); 57 | } else if (this.handlers[k] && typeof this.handlers[k].down === 'function') { 58 | this.handlers[k].down(); 59 | } 60 | } 61 | }); 62 | 63 | document.addEventListener('keyup', event => { 64 | let k = this.map[event.keyCode]; 65 | 66 | this.queue.unshift(event.key); 67 | this.queue.splice(10); 68 | 69 | if (k) { 70 | this[k] = undefined; 71 | 72 | if (this.handlers[k] && typeof this.handlers[k].up === 'function') { 73 | this.handlers[k].up(); 74 | } 75 | } 76 | }); 77 | 78 | document.addEventListener('mousemove', event => { 79 | this.handlers['mousemove'](event.movementX, event.movementY, event.clientX, event.clientY); 80 | }); 81 | 82 | document.addEventListener('click', event => { 83 | // TODO: Today, we just treat a click as a click (like pressing spacebar). 84 | // This can be a little weird if the user clicks without ever moving the mouse. 85 | this.handlers['mouseclick'](); 86 | }); 87 | 88 | return this; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require("gulp"); 2 | const babel = require("gulp-babel"); 3 | const zip = require("gulp-zip"); 4 | const size = require("gulp-size"); 5 | const sourcemaps = require("gulp-sourcemaps"); 6 | const concat = require("gulp-concat"); 7 | const add = require("gulp-add"); 8 | const uglify = require('gulp-uglify'); 9 | const terser = require('gulp-terser'); 10 | const imagemin = require('gulp-imagemin'); 11 | const cleancss = require('gulp-clean-css'); 12 | const htmlmin = require('gulp-htmlmin'); 13 | const shell = require('gulp-shell'); 14 | const eslint = require('gulp-eslint'); 15 | 16 | const levelPacker = require("./level-packer"); 17 | 18 | gulp.task('build:html', () => { 19 | gulp.src('src/*.html') 20 | .pipe(htmlmin()) 21 | .pipe(gulp.dest('raven')); 22 | }); 23 | 24 | gulp.task('build:css', () => { 25 | gulp.src('src/css/*.css') 26 | .pipe(cleancss()) 27 | .pipe(gulp.dest('raven')); 28 | }); 29 | 30 | gulp.task('build:assets', () => { 31 | // Assets starting with _ (underscore) don't need to be included in the build. 32 | // (These are PNG files I'm using in my Tiled tileset, just for ease of editing.) 33 | gulp.src(['src/assets/*.png', '!src/assets/_*.png']) 34 | .pipe(imagemin()) 35 | .pipe(gulp.dest('raven/assets')); 36 | }); 37 | 38 | gulp.task('build:js', () => { 39 | let compatMode = false; 40 | let debugMode = false; 41 | let build = gulp.src('src/js/*.js') 42 | .pipe(add("LevelCache.js", levelPacker.packAll("src/levels/level*.json"), true)) 43 | .pipe(sourcemaps.init()) 44 | .pipe(concat('app.js')) 45 | .pipe(size()); 46 | 47 | if (debugMode) { 48 | // do nothing; best stack traces for tricky bugs 49 | // (yes, we have source maps, but my overly-aggressive mangling strategy 50 | // fubars the sourcemap) 51 | 52 | // I only turn these on for special occasions - for this game, I really didn't bother 53 | // with linting, and I ignore all the feedback that wouldn't save me space. (Basically, 54 | // the only things I'm interested in are unused var, prefer destructuring, and prefer 55 | // simple property notation, anything else is going to be smashed up by terser anyway.) 56 | // 57 | build = build 58 | .pipe(eslint()) 59 | .pipe(eslint.format()); 60 | } else if (compatMode) { 61 | // To generate ES5 code, for more compatibility, use babel+uglify 62 | build = build 63 | .pipe(babel()) 64 | .pipe(uglify({ toplevel: true })); 65 | } else { 66 | // For smaller ES6 code, use terser 67 | build = build 68 | .pipe(terser({ 69 | toplevel: true, 70 | mangle: { 71 | properties: { 72 | // Properties beginning with "_" are private methods, which on each class 73 | // I've specified to indicate they aren't called by other objects, and can 74 | // be safely squished. 75 | // 76 | // Some key methods on Util are used a lot, so calling them out explicitly 77 | // lets us get some extra squishiness. (Actually, ideally we'd just squish 78 | // everything on Util, but I don't see a good way to do that.) 79 | regex: /^_|^wallAt|^tileAt|^doorAt|^pointIn|^getVisCone|^getVisBounds|^enforceEntityMovement|^renderTogglePrompt|^entitySpotted|^pointSpotted|^drawSprite|^distance|^getFontString/ 80 | } 81 | } 82 | })); 83 | } 84 | 85 | build = build 86 | .pipe(size()) 87 | .pipe(sourcemaps.write('.')) 88 | .pipe(gulp.dest('raven')); 89 | }); 90 | 91 | gulp.task('build', ['build:html', 'build:css', 'build:assets', 'build:js']); 92 | 93 | // These two tasks require the advpng and advzip tools which you can download and 94 | // build from http://www.advancemame.it/download (from js13k resources page). 95 | gulp.task('zip:pre', shell.task('../advpng -z -4 raven/assets/*.png')); 96 | gulp.task('zip:post', shell.task('../advzip -z -4 zip/js13k-2018-raven.zip')); 97 | 98 | gulp.task('zip', () => { 99 | gulp.src(['raven/**', '!raven/app.js.map'], { base: '.' }) 100 | .pipe(zip('js13k-2018-raven.zip')) 101 | .pipe(size()) 102 | .pipe(gulp.dest('zip')); 103 | }); 104 | 105 | gulp.task('watch', () => { 106 | gulp.watch('src/*.html', ['build:html']); 107 | gulp.watch('src/css/*.css', ['build:css']); 108 | gulp.watch('src/assets/*', ['build:assets']); 109 | 110 | gulp.watch('src/js/*.js', ['build:js']); 111 | gulp.watch('src/levels/*', ['build:js']); 112 | gulp.watch('*.js', ['build:js']); 113 | }); 114 | 115 | gulp.task('default', ['build', 'watch']); 116 | -------------------------------------------------------------------------------- /src/js/Player.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The player class encapsulates the player's current state. 3 | * 4 | * ... sort of. A lot of things you'd think are player state, like where we 5 | * are facing and where our crosshair is, belong to Game today. This is 6 | * something I'd factor back out, maybe in the future... 7 | */ 8 | class Player { 9 | constructor() { 10 | this.x = 105; 11 | this.y = 40; 12 | this.vx = 0; 13 | this.vy = 0; 14 | 15 | this._accel = 400; // per second 16 | this._decel = 400; // per second 17 | this._maxSpeed = 110; // per second 18 | 19 | this.width = 8; 20 | this.height = 6; 21 | 22 | this.dead = false; 23 | } 24 | 25 | update(delta) { 26 | // TODO: yes, the player suffers from the classic "fast diagonal" problem. 27 | // This time around, I don't care enough to fix it :) 28 | 29 | if (this.dead) { 30 | return; 31 | } 32 | 33 | if (game.levelComplete) { 34 | let target = { 35 | x: (game.level.exit.p1.x + game.level.exit.p2.x) / 2, 36 | y: (game.level.exit.p1.y + game.level.exit.p2.y) / 2 37 | }; 38 | let angle = Util.atanPoints(this, target); 39 | this.vx = Util.cos(angle) * this._maxSpeed / 2; 40 | this.vy = Util.sin(angle) * this._maxSpeed / 2; 41 | this.x += this.vx * delta; 42 | this.y += this.vy * delta; 43 | return; 44 | } 45 | 46 | if (game.input.up) { 47 | this.vy -= this._accel * delta; 48 | if (this.vy < -this._maxSpeed) { 49 | this.vy = -this._maxSpeed; 50 | } 51 | } else if (game.input.down) { 52 | this.vy += this._accel * delta; 53 | if (this.vy > this._maxSpeed) { 54 | this.vy = this._maxSpeed; 55 | } 56 | } else { 57 | let dir = this.vy > 0 ? -1 : 1; 58 | this.vy += this._decel * dir * delta; 59 | if (this.vy < 0 && dir === -1 || this.vy > 0 && dir === 1) { 60 | this.vy = 0; 61 | } 62 | } 63 | if (game.input.left) { 64 | this.vx -= this._accel * delta; 65 | if (this.vx < -this._maxSpeed) { 66 | this.vx = -this._maxSpeed; 67 | } 68 | } else if (game.input.right) { 69 | this.vx += this._accel * delta; 70 | if (this.vx > this._maxSpeed) { 71 | this.vx = this._maxSpeed; 72 | } 73 | } else { 74 | let dir = this.vx > 0 ? -1 : 1; 75 | this.vx += this._decel * dir * delta; 76 | if (this.vx < 0 && dir === -1 || this.vx > 0 && dir === 1) { 77 | this.vx = 0; 78 | } 79 | } 80 | 81 | let oldX = this.x, oldY = this.y; 82 | 83 | this.x += this.vx * delta; 84 | this.y += this.vy * delta; 85 | Util.enforceEntityMovement(this); 86 | 87 | // Move the crosshair by the same amount we moved the player. Note we do this 88 | // after enforcing entity movement, so the crosshair doesn't slide when player 89 | // hits walls. 90 | if (!game.lockCrosshairToMap) { 91 | game.crosshair.x += (this.x - oldX); 92 | game.crosshair.y += (this.y - oldY); 93 | } 94 | } 95 | 96 | render() { 97 | if (this.dead) { 98 | // TODO: A really awesome, Hotline Miami blood splatter would be cool here. 99 | // I think that will need to wait til a different game. 100 | // (Really, the thing to do here is just draw a nice "blood splatter" sprite, 101 | // but I don't have the space!) 102 | 103 | let pal = [ 104 | // 10% shining white bone 105 | [204,204,204], 106 | // 10% gristle 107 | [54,10,10], 108 | // 50% pure power of will 109 | // oops, i meant, shades of blood 110 | [239,17,35], 111 | [211,15,31], 112 | [171,12,15], 113 | [120,6,6] 114 | ]; 115 | 116 | if (!this._bloodSplatter) { 117 | this._bloodSplatter = []; 118 | 119 | for (let i = 0; i < 45; i++) { 120 | let x = Math.floor(Math.random() * 30); 121 | let y = Math.floor(Math.random() * 30); 122 | let color = Math.floor(Math.random() * pal.length); 123 | 124 | this._bloodSplatter[y * 30 + x] = color; 125 | } 126 | } 127 | 128 | game.ctx.save(); 129 | game.ctx.translate(game.offset.x + this.x, game.offset.y + this.y); 130 | for (let i = 0; i < 30; i++) { 131 | for(let j = 0; j < 30; j++) { 132 | let color = this._bloodSplatter[i * 30 + j]; 133 | if (color) { 134 | color = pal[color]; 135 | game.ctx.fillStyle = 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ')'; 136 | game.ctx.fillRect(i - 15, j - 15, 2, 2); 137 | } 138 | } 139 | } 140 | game.ctx.restore(); 141 | return; 142 | } 143 | 144 | // Walking animation frames would be great, but let's hack it. 145 | // Actual walking images would be nice, but a filled dark grey 146 | // rectangle will need to take the place of a shoe, this time. 147 | let walk = [-6,,3,][Math.floor((game.framems % 800) / 200)]; 148 | if (this.vx === 0 && this.vy === 0) walk = false; 149 | 150 | game.ctx.save(); 151 | game.ctx.translate(game.offset.x + this.x, game.offset.y + this.y); 152 | game.ctx.rotate(Util.d2r(game.facing + 90)); 153 | if (walk) { 154 | game.ctx.fillStyle = 'rgba(32, 32, 48, 1)'; 155 | game.ctx.fillRect(walk, -6, 3, 3); 156 | } 157 | Asset.drawSprite('player', game.ctx, -10, -7); 158 | game.ctx.restore(); 159 | } 160 | 161 | renderCrosshair() { 162 | let x = game.offset.x + game.crosshair.x; 163 | let y = game.offset.y + game.crosshair.y; 164 | 165 | game.ctx.strokeStyle = 'rgba(255, 24, 24, 0.9)'; 166 | game.ctx.beginPath(); 167 | [ 168 | [-2, -2], 169 | [-2, 2], 170 | [2, -2], 171 | [2, 2] 172 | ].forEach(c => { 173 | game.ctx.moveTo(x + c[0] * 3, y + c[1]); 174 | game.ctx.lineTo(x + c[0], y + c[1]); 175 | game.ctx.lineTo(x + c[0], y + c[1] * 3); 176 | }); 177 | game.ctx.stroke(); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/js/Door.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Door handles the security doors, drawn on top of a floor tile, and can slide open 3 | * and closed. 4 | * 5 | * A locked door doesn't open when the player is near. Today there are only two 6 | * types of doors, "entrance" and "exit" - entrance doors lock after the player leaves 7 | * the elevator, exit doors are locked until there are no security terminals remaining 8 | * on the level. 9 | * 10 | * On the map, a door takes up two spaces; the left/uppermost space is the u/v coordinates 11 | * of the door. TODO: Horizontal (left-right) doors are the only doors implemented right now, 12 | * to save space for js13k. 13 | */ 14 | class Door { 15 | constructor(doorData) { 16 | this.u = doorData.u; 17 | this.v = doorData.v; 18 | this.type = doorData.type; 19 | this.exitDoor = doorData.exitDoor; 20 | this.entranceDoor = doorData.entranceDoor; 21 | 22 | if (this.exitDoor) this.locked = true; 23 | 24 | //if (this.type === 'h') { 25 | this.x = this.u * 32 + 32; 26 | this.y = this.v * 32 + 16; 27 | //} else { 28 | // this.x = this.u * 32 + 16; 29 | // this.y = this.v * 32 + 32; 30 | //} 31 | 32 | // Select a radius just big enough to include our traditional 33 | // "elevator start position" (we want the effect of the doors 34 | // sliding open as each level starts). 35 | this.toggleRadius = 58; 36 | 37 | this.control = doorData.control; 38 | 39 | this.slide = 0; 40 | 41 | this.toggled = undefined; 42 | } 43 | 44 | update(delta) { 45 | let playerNear = Util.pointNearPoint(this, game.player, this.toggleRadius); 46 | 47 | if (playerNear && !game.levelComplete && !this.locked) { 48 | if (this.slide < 30) { 49 | this.slide = Math.min(30, this.slide + 32 * delta); 50 | } 51 | } else { 52 | if (this.slide > 0) { 53 | this.slide = Math.max(0, this.slide - 32 * delta); 54 | } 55 | } 56 | 57 | if (this.entranceDoor && this.slide === 0 && !Util.pointInBounds(game.player, game.level.enter)) { 58 | this.locked = true; 59 | } 60 | 61 | let terminalsLeft = 0; 62 | game.terminals.forEach(terminal => { 63 | if (!terminal.enabled) terminalsLeft++; 64 | }); 65 | 66 | if (this.exitDoor && terminalsLeft === 0) { 67 | this.locked = false; 68 | } 69 | 70 | this.toggled = undefined; 71 | } 72 | 73 | render() { 74 | let slide = Math.floor(this.slide); 75 | 76 | // TODO: For now, "vertical" doors are not implemented, because I needed to cut 77 | // a couple hundred extra bytes :(. 78 | 79 | //if (this.type === 'h') { 80 | game.ctx.save(); 81 | game.ctx.translate(game.offset.x + this.x, game.offset.y + this.y); 82 | Asset.drawSprite2('door', game.ctx, slide, 0, 32 - slide, 32, -32, -16, 32 - slide, 32); 83 | game.ctx.rotate(Util.d2r(180)); 84 | Asset.drawSprite2('door', game.ctx, slide, 0, 32 - slide, 32, -32, -16, 32 - slide, 32); 85 | 86 | if (this.locked) { 87 | game.ctx.fillStyle = 'rgba(204, 36, 36, 0.8)'; 88 | game.ctx.fillRect(10, 8, 14, 2); 89 | game.ctx.fillRect(-24, 8, 14, 2); 90 | game.ctx.fillRect(10, -10, 14, 2); 91 | game.ctx.fillRect(-24, -10, 14, 2); 92 | } 93 | 94 | game.ctx.restore(); 95 | //} else { 96 | // game.ctx.save(); 97 | // game.ctx.translate(game.offset.x + this.x, game.offset.y + this.y); 98 | // game.ctx.rotate(Util.d2r(90)); 99 | // game.ctx.drawImage(Asset.tile.door, slide, 0, 32 - slide, 32, -32, -16, 32 - slide, 32); 100 | // game.ctx.rotate(Util.d2r(270)); 101 | // game.ctx.drawImage(Asset.tile.door, slide, 0, 32 - slide, 32, -32, -16, 32 - slide, 32); 102 | // game.ctx.restore(); 103 | //} 104 | } 105 | 106 | toggle() { 107 | this.toggled = true; 108 | } 109 | 110 | getLosEdges() { 111 | let cut = game.tileVisibilityInset; 112 | 113 | // TODO: Edges implemented for horizontal doors only; to add vertical doors, 114 | // need to add an extra check and rotate these coordinates around. 115 | 116 | if (this.slide < 3) { 117 | return [ 118 | // top edge 119 | { 120 | p1: { 121 | x: this.x - 32 - cut, 122 | y: this.y - 12 + cut 123 | }, 124 | p2: { 125 | x: this.x + 32 + cut, 126 | y: this.y - 12 + cut 127 | } 128 | }, 129 | // bottom edge 130 | { 131 | p1: { 132 | x: this.x - 32 - cut, 133 | y: this.y + 12 - cut, 134 | }, 135 | p2: { 136 | x: this.x + 32 + cut, 137 | y: this.y + 12 - cut 138 | } 139 | } 140 | ]; 141 | } else { 142 | return [ 143 | // left top edge 144 | { 145 | p1: { 146 | x: this.x - 32 - cut, 147 | y: this.y - 12 + cut 148 | }, 149 | p2: { 150 | x: this.x - this.slide - cut, 151 | y: this.y - 12 + cut 152 | } 153 | }, 154 | // left frame 155 | { 156 | p1: { 157 | x: this.x - this.slide - cut, 158 | y: this.y - 12 + cut 159 | }, 160 | p2: { 161 | x: this.x - this.slide - cut, 162 | y: this.y + 12 - cut 163 | } 164 | }, 165 | // left bottom edge 166 | { 167 | p1: { 168 | x: this.x - 32 - cut, 169 | y: this.y + 12 - cut 170 | }, 171 | p2: { 172 | x: this.x - this.slide - cut, 173 | y: this.y + 12 - cut 174 | } 175 | }, 176 | // right top edge 177 | { 178 | p1: { 179 | x: this.x + this.slide + cut, 180 | y: this.y - 12 + cut 181 | }, 182 | p2: { 183 | x: this.x + 32 + cut, 184 | y: this.y - 12 + cut 185 | } 186 | }, 187 | // right frame 188 | { 189 | p1: { 190 | x: this.x + this.slide + cut, 191 | y: this.y - 12 + cut 192 | }, 193 | p2: { 194 | x: this.x + this.slide + cut, 195 | y: this.y + 12 - cut 196 | } 197 | }, 198 | // right bottom edge 199 | { 200 | p1: { 201 | x: this.x + this.slide + cut, 202 | y: this.y + 12 - cut 203 | }, 204 | p2: { 205 | x: this.x + 32 + cut, 206 | y: this.y + 12 - cut 207 | } 208 | } 209 | ]; 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js13k-2018-raven 2 | 3 | My 2018 entry for the js13kgames competition, "Raven". 4 | 5 | Play the game online at [js13kgames.com](http://js13kgames.com/entries/raven). 6 | 7 | ## Description 8 | 9 | In this 2D top-down action-puzzle game, the security cameras for a secret facility have been taken offline, and it is YOUR job to fix the problem. Take on a series of floors overrun by mysterious enemies known only as "Raven" -- although harmless as long as you can see them, they are deadly if you turn your back. 10 | 11 | Playable on the desktop in Chrome, Firefox, and Safari. Use your mouse or touchpad to look around, and W/A/S/D or the arrow keys to move. 12 | 13 | Good luck! 14 | 15 | ## Building the game 16 | 17 | - `/src` contains the game source files and assets 18 | - `/raven` would contain the built game (not checked in) 19 | - `/zip` contains the built game bundled into a zip file 20 | 21 | To rebuild, `npm install && gulp build` from the project folder. 22 | 23 | Build the zip file with `gulp zip`, or to get the smallest possible size, `gulp zip:pre && gulp zip && gulp zip:post`. Using the `pre` and `post` steps requires additional tools (`advpng` and `advzip`, from http://www.advancemame.it/download). 24 | 25 | ## Postmortem 26 | 27 | ### Inspiration 28 | 29 | This year was the first year I had heard about the competition, and I knew almost immediately that I wanted to build something based heavily on line-of-sight, as I had been doing a lot of reading on visibility algorithms (see the References section at the bottom for links). From there it was pretty easy to shoehorn in the theme (by adding security cameras that had gone offline). 30 | 31 | As implemented in the game, the Raven are very similar to the infamous [Weeping Angels](http://tardis.wikia.com/wiki/Weeping_Angel), although my original idea was actually based on the similar [SCP-173](http://www.scp-wiki.net/scp-173). Either way, I knew that I wanted my game to be heavily based on what you were looking at, with your line of sight being your only "weapon" against creatures that were very dangerous when you weren't looking at them. 32 | 33 | Overall I'm pretty pleased with how it turned out! The enemy behavior in certain corner cases could use some love, and you can imagine some extra stuff that would spice the levels up (security cameras that move, terminals that open doors instead of turning on cameras, enemies that patrol even after spotting you, better "pack attacks" - intentionally splitting up to cover more attack angles, etc.). Of course, if the enemy evolved, the player would need to evolve too -- maybe by having a mobile "partner" they could toss onto the ground that can look around a corner for a few seconds, or adding a sprint button... 34 | 35 | ### Build process 36 | 37 | Early on, to save time, I decided that I would opt out of any of the existing "module" systems (closure, webpack, bundler, etc). The code for my game is organized into separate javascript files (one per class), and they all _assume_ that you will build by concatenating them all first, producing one large javascript file. This, to me, is actually the simplest and most straightforward way to approach building; the downside is that you do some lose some options - for example, I can't write unit tests for any of my math functions, I can't use linting tools unless I turn off a lot of global var checking, etc. 38 | 39 | Before next year, I'll do some further research on this topic, and see if I can find a module system I like that would give me the flexibility back (valid javascript files when required by node.js) without adding extra cruft to the output file. 40 | 41 | ### Cramming it into 13k 42 | 43 | By far the biggest limit on this game was the size limit. I spent a _lot_ of my code budget on math and algorithms (for line-of-sight and pathfinding). Here's a rough list of the stuff I ended up doing, over time, to continue to squash the game as small as possible: 44 | 45 | - Sprite sheet (this is a big savings, getting all assets into one PNG). 46 | - Even with the sprite sheet, my floor/wall tiles were too much. I ended up simplifying them down to just a couple colors, and removing all "noise". I add that noise back in at the start of the game by rendering it on top of the wall/floor tiles, so that everything isn't one solid color. 47 | - I used Tiled to create my levels, but I ended up creating a post-processing setup in my build to squash those levels into a tiny game-specific format. There is a balancing act here; at a certain point, the extra bytes saved in your level specification are lost in the code to unpack it into something useable. I think in this game, I have just about the smallest possible level + unpacking code combination that I could get. 48 | - I do a bunch of property mangling with `terser` (generic private properties using a `_` prefix, plus a bunch of specifically named utility functions that I know are safe to mangle), to eliminate more bytes in the output file. 49 | - A big space sink is ES5 shims. Although I started the project using babel+uglify, I ended up _not_ going to ES5 and using terser by the end of the project, in order to eliminate those shims. This gives you room for significantly more of your own code (but you do need to make sure not to use any javascript constructs not supported by your target browsers). 50 | - Last, you end up just making feature cuts. An example in this game is that although I started with both horizontal and vertical doors, I ended up cutting the code for updating and rendering vertical doors (and not using them in any levels). This last cut was necessary to squeeze in a tenth level, which was the nice round number I was shooting for. 51 | 52 | ### Lessons learned / moving forward 53 | 54 | As I said, I really like this first foray into the competition. Currently, I have no idea what I would do next year, but I know there are some things that I would like to get much, much better at: 55 | 56 | 1. Pathfinding/AI. If given more time and another 2K of code to work with, my enemies could be even more scary, just because I could brute-force in more checks and logic. I think there's probably an art here that I'm just not that good at yet, and that with some more research, I could figure out ways to get smarter, deadlier enemies in _less_ code. 57 | 58 | 2. Sound effects! I managed to whip up some very simple, oscillator-based sound effects in this game, and they work in a retro kind of way. I'd like to really focus on sound and music design for the 2019 competition, so deciding whether to bite the bullet and eat the 2K cost of jsfxr, or dig into custom `createPeriodicWave` functions, figure out how to make even better sounding Web Audio music, it's all up for grabs at this point. 59 | 60 | 3. Particle effects. I had some rudimentary partiles in this game and I totally cut them out for space. I'd like to work on some basic particle effects so things like footsteps/movement, things opening/closing, maybe attacks (depending on the type of game I do next), they all feel more real. 61 | 62 | ## References 63 | 64 | I couldn't have made this game without the following stellar resources. They may be of help to you on your own games: 65 | 66 | * [How to make a simple HTML5 Canvas game](http://www.lostdecadegames.com/how-to-make-a-simple-html5-canvas-game/) 67 | 68 | I knew I needed to knock some rust off when I started, and this tutorial was an excellent way to do so. My first couple hours was spent working off these notes. 69 | 70 | * [Pointer Lock and First Person Shooter Controls](https://www.html5rocks.com/en/tutorials/pointerlock/intro/) 71 | 72 | Useful and very thorough introduction to the Pointer Lock API. (Some of the notes on compatibility are now outdated, but otherwise still aces.) 73 | 74 | * [2d Visibility](https://www.redblobgames.com/articles/visibility/) 75 | 76 | Like every other tutorial on Red Blob, this one is super cool, and in some ways his little demos are the inspiration for this game. Actually, almost everything in this game can be traced back one of Amit's articles (ray casting, path finding, etc.). 77 | 78 | * [Grid pathfinding optimizations](https://www.redblobgames.com/pathfinding/grids/algorithms.html) 79 | 80 | Speaking of Amit's articles... To be honest, very little of the the advice in this article is implemented in this game, as time and space (ie lines of code) were not on my side. But I did reference this article frequently while working on the enemy AI, and if I ever work on the game post-competition, the enemies could probably get much smarter. 81 | 82 | * [Line intersection and its applications](https://www.topcoder.com/community/data-science/data-science-tutorials/geometry-concepts-line-intersection-and-its-applications/) 83 | 84 | Math resource (does a line intersect with another line?). 85 | 86 | * [Accurate point in triangle test](http://totologic.blogspot.com/2014/01/accurate-point-in-triangle-test.html) 87 | 88 | Math resource (is a point within a known triangle?). 89 | 90 | * [Even-odd rule](https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule) 91 | * [How to check if a given point lies inside a polygon](https://www.geeksforgeeks.org/how-to-check-if-a-given-point-lies-inside-a-polygon/) 92 | 93 | More math resources; two different explanations of the same algorithm for determining if a point lies within a polygon. (I find that this comes up a lot, and not just in games either, so it's a nice tool to have at your fingertips). 94 | 95 | * [Tiled Map Editor](https://www.mapeditor.org/) 96 | 97 | I used Tiled to create all of the levels for this game, and overall I was pretty pleased. I have to say it wasn't a perfect match, some of the things I wanted to do regarding enemies and cameras and terminals felt kind of difficult to do, but I think that was my experience level more than the tool itself. 98 | 99 | I ended up implementing a relatively serious post-processing step for the Tiled levels, to get them compact enough to include in the final app bundle, and this is where I merge in the rest of my level metadata as well. 100 | 101 | * [miniMusic](https://xem.github.io/miniMusic/) 102 | 103 | A small and simple music generator for the Web Audio API. The music for this game was composed on the "advanced" miniMusic composer. I ended up making a lot of changes to the generated javascript, but the original audio snippet came right from Maxime's generator. 104 | 105 | * [Web Audio, the ugly click and the human ear](http://alemangui.github.io/blog//2015/12/26/ramp-to-value.html) 106 | 107 | Excellent article that gives a couple ways to prevent oscillator "clicks". Your ears will thank you. 108 | 109 | * [AdvanceCOMP](http://www.advancemame.it/download) 110 | 111 | Additional compression tools that are quite nice for a competition like this (it's linked to on the js13kgames resources page as well). My experience is that the imagemin tool is already quite good, so `advpng` will likely only save you a handful of bytes, if any. However, `advzip` is great at squeezing those last 100 bytes out of your zip file, when you've already squeezed everything else you can. 112 | 113 | -------------------------------------------------------------------------------- /src/js/Audio.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Audio handles background music and sound effects. 3 | */ 4 | class Audio { 5 | constructor() { 6 | let ctxClass = (window.AudioContext || window.webkitAudioContext); 7 | if (ctxClass) { 8 | this.ctx = new ctxClass(); 9 | } else { 10 | this.disabled = true; 11 | return; 12 | } 13 | 14 | // Create a gain node for each sound type we'll be playing. 15 | this._sounds = {}; 16 | [ 17 | ['click', 0.1], 18 | ['bloop', 0.5], 19 | ['siren', 0.1], 20 | ['tri', 0.5], 21 | ['music', 0.4], 22 | ['m1', 1, 'music'], 23 | ['m2', 1, 'music'], 24 | ['m3', 1, 'music'] 25 | ].forEach(([name, volume, parent]) => { 26 | this._sounds[name] = this.ctx.createGain(); 27 | this._sounds[name].gain.value = volume; 28 | this._sounds[name].connect(parent ? this._sounds[parent] : this.ctx.destination); 29 | }); 30 | 31 | // We rotate musical notes between 3 separate gain nodes, so we can control gain on 32 | // each note without breaking other notes. Note that m1, m2, and m3 are 33 | // our "0-1" individual note nodes, which are hooked up to the overall "music" node 34 | // at 0.4 gain, which is then hooked up to the destination node. 35 | this._musicNodes = [this._sounds.m1, this._sounds.m2, this._sounds.m3]; 36 | this._musicIndex = 0; 37 | 38 | // Used to track the most recent play of a sound. 39 | this._last = { 40 | click: 0 41 | }; 42 | 43 | // I am not a music expert, so please assume that any statements I make about 44 | // keys, chords, and other music theory in these comments may be 100% wrong. 45 | 46 | // I experimented with more complicated 3- and 4-note chords, which 47 | // sound good on the piano, but my experiments seemed to show that less-is-more 48 | // when dealing with simple oscillators (you'd need a more complicated instrument, 49 | // I think, for big min7 type chords to sound good). 50 | 51 | // Overall, this is just a C minor chord, alternating fingers 1+3 and 1+5, with 52 | // scattered dissonant (but not too dissonant) notes in between. Where I could, I 53 | // tried to give it this odd start/stop feeling (like carnival music?) by alternating 54 | // where the interstitial notes ended up (beat 3 vs beat 2/4). 55 | this._tracks = { 56 | game: [ 57 | // Measure 1 58 | [22, 19], // C Eb 59 | [], 60 | [], 61 | [], 62 | [22, 15], // C G 63 | [21], // C# 64 | [], 65 | [], 66 | [22, 19], // C Eb 67 | [], 68 | [16], // F# 69 | [], 70 | [22, 15], // C G 71 | [], 72 | [14], // Ab 73 | [], 74 | 75 | // Measure 2 76 | [22, 19], // C Eb 77 | [], 78 | [], 79 | [16], // F# 80 | [22, 15], // C G 81 | [], 82 | [20], // D 83 | [], 84 | [22, 19], // C Eb 85 | [14], // Ab 86 | [], 87 | [16], // F# 88 | [22, 15], // C G 89 | [], 90 | [21], // C# 91 | [20] // D 92 | ], 93 | death: [ 94 | [22, 16], 95 | [21, 15], 96 | [22, 16], 97 | [21, 15], 98 | 99 | [22, 16], 100 | [19], 101 | [23, 17], 102 | [20], 103 | 104 | [24, 18], 105 | [21], 106 | [25, 19], 107 | [22], 108 | 109 | [26, 20], 110 | [23], 111 | [27, 21], 112 | [24], 113 | 114 | [28, 22], 115 | [25], 116 | [29, 23], 117 | [26], 118 | 119 | [30, 24], 120 | [27], 121 | [31, 25], 122 | [28], 123 | 124 | [], 125 | [], 126 | [], 127 | [], 128 | 129 | [], 130 | [], 131 | [], 132 | [], 133 | 134 | // Gain fade out finished here 135 | 136 | [], 137 | [], 138 | [], 139 | [] 140 | ] 141 | }; 142 | 143 | // Track 2 is Track 1 plus major 3rd (+4 notes), this transition sounds "normal", but 144 | // makes the next transition a little more jarring. 145 | let track2 = this._tracks.game.map(x => { 146 | return x.map(y => y - 4); 147 | }); 148 | // tweak: maj3rd -> min3rd sounds muddled unless we move the last couple notes a few higher 149 | track2[30] = [16]; 150 | track2[31] = [15]; 151 | 152 | // Track 3 is Track 1 plus minor 3rd (+3 notes), a simple sinister transition. 153 | let track3 = this._tracks.game.map((x, i) => { 154 | return x.map(y => y - 3); 155 | }); 156 | 157 | // Full 4 = our sequence in key of [C, C, E, Eb], repeat. 158 | this._tracks.game = this._tracks.game.concat(this._tracks.game).concat(track2).concat(track3); 159 | 160 | this._tracks.game.repeat = true; 161 | this._tracks.death.repeat = false; 162 | 163 | this._tickLength = 1/5; 164 | this._nextTick = this._tickLength; 165 | } 166 | 167 | update(delta) { 168 | if (this.disabled) return; 169 | 170 | if (game.player && game.player.dead) { 171 | this._sounds.music.gain.value = Math.max(0, 0.4 - game.deathFrame / 1000); 172 | if (this._track !== this._tracks.death) { 173 | this._track = this._tracks.death; 174 | this._tick = 0; 175 | } 176 | } else { 177 | this._sounds.music.gain.value = 0.4; 178 | if (this._track !== this._tracks.game) { 179 | this._track = this._tracks.game; 180 | this._tick = 0; 181 | } 182 | } 183 | 184 | // Each frame, schedule some notes if we're close to where the note should be played. 185 | // The longer the time comparison here, the more "buffer" you have, but the longer 186 | // it takes to react e.g. to play death music. 187 | if (this._nextTick - this.ctx.currentTime < 0.2) { 188 | this._musicIndex = (this._musicIndex + 1) % this._musicNodes.length; 189 | let node = this._musicNodes[this._musicIndex]; 190 | this._scheduleForTick(this._track, this._tick, this._nextTick, this._tickLength, node); 191 | this._tick++; 192 | 193 | // If we go into the background, the Audio Context's currentTime will keep increasing, 194 | // but Audio.update will stop running (because we only run when the next animation 195 | // frame is received). This "automatically" stops our bg music when switching tabs, 196 | // which I think is a feature-not-a-bug. However, when we come back, we need to 197 | // make sure we don't spend a bunch of frames slowly catching back up to the 198 | // current time. 199 | if (this._nextTick < this.ctx.currentTime) this._nextTick = this.ctx.currentTime; 200 | 201 | this._nextTick += this._tickLength; 202 | } 203 | } 204 | 205 | _scheduleForTick(track, tick, nextTick, tickLength, dest) { 206 | let notes = track.repeat ? track[tick % track.length] : track[tick]; 207 | let noteLength = tickLength * 1.39; 208 | 209 | if (!notes) return; 210 | 211 | // Oscillator creation taken from the generator at https://xem.github.io/miniMusic/advanced.html, 212 | // although I ended up moving the note creation out of the loop below, just so it is easier for 213 | // me to reason about. 214 | for (let i = 0; i < notes.length; i++) { 215 | let o = this.ctx.createOscillator(); 216 | let freq = 988/1.06**notes[i]; 217 | o.frequency.value = freq; 218 | o.type = 'triangle'; 219 | o.connect(dest); 220 | o.start(nextTick); 221 | 222 | // Safari as a browser is exceptionally "clicky", so it's a good truth test. Here we create 223 | // an envelope where 5% of the note length is linear 0-1 ramp, then we play 90% of the note 224 | // at volume 1, and then the last 5% of the note length is linear 1-0 ramp. You could also 225 | // use exponential ramp and 0.01 or 0.001 as volume, but I couldn't get Safari to stop clicking, 226 | // so a linear ramp it is. 227 | dest.gain.setValueAtTime(0, nextTick); 228 | dest.gain.linearRampToValueAtTime(1, nextTick + noteLength * 0.05); 229 | dest.gain.setValueAtTime(1, nextTick + noteLength * 0.95); 230 | dest.gain.linearRampToValueAtTime(0, nextTick + noteLength); 231 | 232 | o.stop(nextTick + noteLength); 233 | } 234 | } 235 | 236 | _playOscillatorSound(channel, type, freq, rampFreq, rampTime, length, timeSinceLast, timeOffset) { 237 | if (this.disabled) return; 238 | 239 | let time = timeOffset || this.ctx.currentTime; 240 | if (timeSinceLast && time - this._last[channel] < timeSinceLast) return; 241 | this._last[channel] = time; 242 | 243 | // Important: on any browser but Chrome, setting ".value" and using ramps are NOT 244 | // COMPATIBLE! For any AudioParam, if you want to use exponential/linear ramp 245 | // functions, make sure you set your value using setValueAtTime, and not explicitly 246 | // setting value. 247 | 248 | let o = this.ctx.createOscillator(); 249 | o.frequency.setValueAtTime(freq, time); 250 | if (rampFreq) { 251 | o.frequency.exponentialRampToValueAtTime(rampFreq, time + rampTime); 252 | } 253 | o.type = type; 254 | o.connect(this._sounds[channel]); 255 | o.start(time); 256 | o.stop(time + length); 257 | } 258 | 259 | // "Clicks" are a very short, smooth sound, and used for menu item movement and 260 | // the sound of keys being pressed during intro/outro/level music. 261 | playClick() { 262 | let freq = 988/1.06**1; 263 | this._playOscillatorSound('click', 'sine', freq, freq * 0.6, 0.1, 0.01, 0.05); 264 | } 265 | 266 | // A "bloop" is an arcade-sounding slide whistley sound, used for player interactions 267 | // like toggling a terminal, selecting a menu item, skipping level text, etc. 268 | playBloop() { 269 | let freq = 988/1.06**28; 270 | this._playOscillatorSound('bloop', 'square', freq, freq * 1.6, 0.2, 0.09); 271 | } 272 | 273 | // The "siren" is a short, harsh, slide sound like the beginning of a siren/airhorn. 274 | // It's not perfect, but it is unmistakably "not good" (used for enemy targeting). 275 | playSiren() { 276 | let freq = 988/1.06**11; 277 | this._playOscillatorSound('siren', 'sawtooth', freq, freq * 2, 1.1, 0.65, 0.5); 278 | } 279 | 280 | // A "tri" is a short series of TRIUMPHANT NOTES played when you get in the chopper. 281 | // Er... elevator. 282 | playTri() { 283 | let time = this.ctx.currentTime; 284 | let freq1 = 988/1.06**10; 285 | let freq2 = 988/1.06**8; 286 | let freq3 = 988/1.06**2; 287 | this._playOscillatorSound('tri', 'square', freq1, 0, 0, 0.1, false, time); 288 | this._playOscillatorSound('tri', 'square', freq2, 0, 0, 0.1, false, time + 0.08); 289 | this._playOscillatorSound('tri', 'square', freq3, 0, 0, 0.2, false, time + 0.16); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/js/Util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Util contains a whole bunch of generic stuff used by other modules. Where possible, 3 | * I've stuck most of the math and algorithm stuff (visibility, flood fill, etc.) into 4 | * this module, along with some often-used tile checks, point checks, etc. 5 | */ 6 | const Util = { 7 | // 8 | // Various math helpers 9 | // 10 | 11 | atan(y, x) { 12 | return Util.r2d(Math.atan2(y, x)); 13 | }, 14 | 15 | atanPoints(p1, p2) { 16 | return Util.atan(p2.y - p1.y, p2.x - p1.x); 17 | }, 18 | 19 | // cos (degrees) 20 | cos(d) { 21 | return Math.cos(Util.d2r(d)); 22 | }, 23 | 24 | // sin (degrees) 25 | sin(d) { 26 | return Math.sin(Util.d2r(d)); 27 | }, 28 | 29 | // radians to degrees 30 | r2d(r) { 31 | return Math.floor(r * 3600 / Math.PI / 2) / 10; 32 | }, 33 | 34 | // degrees 2 radians 35 | d2r(d) { 36 | return d * Math.PI * 2 / 360; 37 | }, 38 | 39 | // degree wrap 40 | dw(d) { 41 | return (d + 720) % 360; 42 | }, 43 | 44 | // rand floor 45 | rf(x) { 46 | return Math.floor(Math.random() * x); 47 | }, 48 | 49 | // 50 | // Points 51 | // 52 | 53 | distance(p1, p2) { 54 | let dx = p2.x - p1.x; 55 | let dy = p2.y - p1.y; 56 | return Math.sqrt(dx * dx + dy * dy); 57 | }, 58 | 59 | pointNearPoint(p1, p2, range) { 60 | return (Util.distance(p1, p2) <= range); 61 | }, 62 | 63 | // Return true if point is inside given triangle. This particular version 64 | // is an implementation of the barycentric coordinate check. 65 | pointInTriangle(p, t1, t2, t3) { 66 | let d = (t2.y - t3.y) * (t1.x - t3.x) + (t3.x - t2.x) * (t1.y - t3.y); 67 | let a = ((t2.y - t3.y) * (p.x - t3.x) + (t3.x - t2.x) * (p.y - t3.y)) / d; 68 | let b = ((t3.y - t1.y) * (p.x - t3.x) + (t1.x - t3.x) * (p.y - t3.y)) / d; 69 | let c = 1 - a - b; 70 | 71 | return 0 <= a && a <= 1 && 0 <= b && b <= 1 && 0 <= c && c <= 1; 72 | }, 73 | 74 | pointSpottedXY(x, y) { 75 | for (let i = 0; i < game.vision.length; i++) { 76 | if (Util.pointInPolygon({ x, y }, game.vision[i])) return true; 77 | } 78 | return false; 79 | }, 80 | 81 | entitySpotted(entity) { 82 | let dx = entity.width / 2; 83 | let dy = entity.height / 2; 84 | 85 | // 5 point check (center, each corner) 86 | return Util.pointSpottedXY(entity.x, entity.y) || 87 | Util.pointSpottedXY(entity.x - dx, entity.y - dy) || 88 | Util.pointSpottedXY(entity.x + dx, entity.y + dy) || 89 | Util.pointSpottedXY(entity.x - dx, entity.y + dy) || 90 | Util.pointSpottedXY(entity.x + dx, entity.y - dy); 91 | }, 92 | 93 | // Return true if the given point is within the specified polygon. This algorithm 94 | // is a simple even-odd check. 95 | // 96 | // See: 97 | // https://en.wikipedia.org/wiki/Even%E2%80%93odd_rule 98 | // https://www.geeksforgeeks.org/how-to-check-if-a-given-point-lies-inside-a-polygon/ 99 | // 100 | pointInPolygon(p, polygon) { 101 | let inside = false; 102 | 103 | for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i, i++) { 104 | if ((polygon[i].y > p.y) !== (polygon[j].y > p.y) && 105 | p.x < polygon[i].x + (polygon[j].x - polygon[i].x) * (p.y - polygon[i].y) / (polygon[j].y - polygon[i].y)) 106 | inside = !inside; 107 | } 108 | 109 | return inside; 110 | }, 111 | 112 | pointInBounds(p, bounds, fudge) { 113 | fudge = fudge || 0; 114 | let a = bounds.p1.x, b = bounds.p2.x, c = bounds.p1.y, d = bounds.p2.y; 115 | if (a > b) [a, b] = [b, a]; 116 | if (c > d) [c, d] = [d, c]; 117 | return p.x >= a - fudge && p.x <= b + fudge && p.y >= c - fudge && p.y <= d + fudge; 118 | }, 119 | 120 | // Calculating visibility 121 | 122 | // Math wizards everywhere, avert your eyes... 123 | // https://www.topcoder.com/community/data-science/data-science-tutorials/geometry-concepts-line-intersection-and-its-applications/ 124 | // Intersecting lines... 125 | // First, given (x1,y1)->(x2,y2), Ax+By=C. 126 | // A = y2-y1 127 | // B = x1-x2 128 | // C = Ax1+By1 129 | intersection(line1, line2) { 130 | let A1 = line1.p2.y - line1.p1.y; 131 | let B1 = line1.p1.x - line1.p2.x; 132 | let C1 = A1 * line1.p1.x + B1 * line1.p1.y; 133 | 134 | let A2 = line2.p2.y - line2.p1.y; 135 | let B2 = line2.p1.x - line2.p2.x; 136 | let C2 = A2 * line2.p1.x + B2 * line2.p1.y; 137 | 138 | let det = A1*B2 - A2*B1; 139 | 140 | if (det !== 0) { 141 | let p = { 142 | x: (B2*C1 - B1*C2)/det, 143 | y: (A1*C2 - A2*C1)/det 144 | }; 145 | 146 | if (Util.pointInBounds(p, line1, 1) && Util.pointInBounds(p, line2, 1)) { 147 | return p; 148 | } 149 | } 150 | }, 151 | 152 | getVisCone(origin, facing, coneAngle, offset, backwalk, opacity) { 153 | // Get pre-calculated visibility edges 154 | let edges = game.losEdges; 155 | 156 | // Add in dynamic visibility edges 157 | game.doors.forEach(door => edges = edges.concat(door.getLosEdges())); 158 | 159 | let startAngle = Util.dw(facing - coneAngle / 2); 160 | let endAngle = Util.dw(facing + coneAngle / 2); 161 | 162 | if (endAngle < startAngle) endAngle += 360; 163 | 164 | // How much space between the "origin point" and the arc of vision? Imagine 165 | // for example, a security camera (the arc of vision starts at the lens, 166 | // not the base of the camera). 167 | offset = offset || 0; 168 | 169 | // Backwalk - how many pixels to walk "backwards" before casting rays. Sometimes 170 | // you need some pixels of backwalk to prevent the arc of vision from being 171 | // too far in front of the subject (mostly it just doesn't look good). 172 | backwalk = backwalk || 0; 173 | 174 | // Calculate a new temporary origin point, with backwalk taken into account. 175 | origin = { 176 | x: origin.x - Math.cos(Util.d2r(facing)) * backwalk, 177 | y: origin.y - Math.sin(Util.d2r(facing)) * backwalk 178 | }; 179 | 180 | // Gap between rays cast. More of an art than a science... a higher gap is faster, 181 | // but potentially introduces artifacts at corners. 182 | let sweep = 0.8; 183 | 184 | // Shadows actually seem a little unnatural if they are super crisp. Introduce 185 | // just enough jitter that the user won't see a sharp unmoving line for more 186 | // than ~1sec. 187 | // 188 | // TODO: jitter is disabled, it doesn't look quite right. 189 | //let jitter = (game.framems % 1000) / 1000; 190 | 191 | let polygon = []; 192 | 193 | let angle = startAngle; //+ jitter; 194 | while (angle < endAngle) { 195 | // Calculate a source, taking the offset into account 196 | let source = { 197 | x: origin.x + Math.cos(Util.d2r(angle)) * offset, 198 | y: origin.y + Math.sin(Util.d2r(angle)) * offset 199 | }; 200 | 201 | // Calculate the ray endpoint 202 | let ray = { 203 | x: origin.x + Math.cos(Util.d2r(angle)) * 1000, 204 | y: origin.y + Math.sin(Util.d2r(angle)) * 1000 205 | }; 206 | 207 | // Loop through all known LOS edges, and when we intersect one, shorten 208 | // the current ray. TODO: This is a potential area of improvement (edge 209 | // culling, early exits, etc.). 210 | for (let j = 0; j < edges.length; j++) { 211 | let inter = this.intersection({ p1: source, p2: ray }, edges[j]); 212 | if (inter) { 213 | ray = inter; 214 | } 215 | } 216 | 217 | // In theory, this is where we would keep an array of vision polygons, 218 | // each one being: 219 | // 220 | // [lastSource, source, ray, lastRay] 221 | // 222 | // (If offset=0, then we could further optimize and just save the vision 223 | // polygons as triangles, but using triangles when source changes for each 224 | // ray results in ugly lines near the player.) 225 | // 226 | // Rather than keep polygons at all, though, we can just "sweep" forwards 227 | // for each point far from the player (the ray) and "sweep" backwards for 228 | // each point near the player (the source). Concatenating all these points 229 | // together then produces a single polygon representing the entire field of 230 | // vision, which we can draw in a single fill call. 231 | // 232 | // Note order is important: we need the final polygons to be stored with 233 | // edges "clockwise" (in this case, we are optimizing for enemy pathing, which 234 | // means we want NON-VISIBLE on the left and VISIBLE on the right). 235 | polygon.unshift(ray); 236 | polygon.push(source); 237 | 238 | angle += sweep; 239 | } 240 | 241 | polygon.opacity = opacity; 242 | return [polygon]; 243 | }, 244 | 245 | getVisBounds(bounds, opacity) { 246 | let polygon = [ 247 | { x: bounds.p1.x, y: bounds.p1.y }, 248 | { x: bounds.p2.x, y: bounds.p1.y }, 249 | { x: bounds.p2.x, y: bounds.p2.y }, 250 | { x: bounds.p1.x, y: bounds.p2.y } 251 | ]; 252 | polygon.opacity = opacity; 253 | return polygon; 254 | }, 255 | 256 | // 257 | // Map-related 258 | // 259 | 260 | tileAtUV(u, v) { 261 | return game.level.data[v * game.level.width + u]; 262 | }, 263 | 264 | tileAtXY(x, y) { 265 | return Util.tileAtUV(Math.floor(x / 32), Math.floor(y / 32)); 266 | }, 267 | 268 | wallAtUV(u, v) { 269 | return Util.tileAtUV(u, v) !== 2; 270 | }, 271 | 272 | wallAtXY(x, y) { 273 | return Util.wallAtUV(Math.floor(x / 32), Math.floor(y / 32)); 274 | }, 275 | 276 | doorAtXY(x, y) { 277 | let door, u = Math.floor(x / 32), v = Math.floor(y / 32); 278 | for (let i = 0; i < game.doors.length; i++) { 279 | if ((game.doors[i].u === u && game.doors[i].v === v) || 280 | (game.doors[i].u === u - 1 && game.doors[i].v === v)) { 281 | door = game.doors[i]; 282 | break; 283 | } 284 | } 285 | 286 | if (door && door.slide < 10) { 287 | if (y % 32 > 3 && y % 32 < 28) return true; 288 | } 289 | 290 | return false; 291 | }, 292 | 293 | enforceEntityMovement(entity) { 294 | // Todo: should terminals have a small hit box so you can't just walk through them? 295 | // It'd be more realistic, but I don't think it's a must-have... 296 | 297 | function check(x, y, dirX, dirY, offset) { 298 | if (Util.wallAtXY(x, y)) { 299 | entity.x += dirX * offset; 300 | entity.y += dirY * offset; 301 | } else if (Util.doorAtXY(x, y)) { 302 | entity.x += dirX * (offset - 4); 303 | entity.y += dirY * (offset - 4); 304 | } 305 | } 306 | 307 | check(entity.x - entity.width / 2, entity.y, 1, 0, 32 - ((entity.x - entity.width / 2) % 32)); 308 | check(entity.x + entity.width / 2, entity.y, -1, 0, ((entity.x + entity.width / 2) % 32)); 309 | check(entity.x, entity.y - entity.height / 2, 0, 1, 32 - ((entity.y - entity.height / 2) % 32)); 310 | check(entity.x, entity.y + entity.height / 2, 0, -1, ((entity.y + entity.height / 2) % 32)); 311 | }, 312 | 313 | renderTogglePrompt(x, y) { 314 | let radius = (game.framems % 1000 < 500 ? 4 : 6); 315 | game.ctx.fillStyle = 'rgba(204, 204, 204, 168)'; 316 | game.ctx.strokeStyle = 'rgba(204, 204, 204, 168)'; 317 | game.ctx.beginPath(); 318 | game.ctx.arc(x, y, radius, 0, 2 * Math.PI); 319 | game.ctx.fill(); 320 | game.ctx.beginPath(); 321 | game.ctx.arc(x, y, radius + 2, 0, 2 * Math.PI); 322 | game.ctx.stroke(); 323 | } 324 | }; 325 | -------------------------------------------------------------------------------- /level-packer.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob'); 2 | const util = require('util'); 3 | const LevelMetadata = require('./level-metadata'); 4 | 5 | /** 6 | * The level packer takes JSON levels, exported by Tiled, and converts them into 7 | * our game-specific level format. I suppose this insulates the game code a little 8 | * bit from our reliance on a third-party tool, but mostly it's just to get our 9 | * level data as small as possible. 10 | */ 11 | const levelPacker = { 12 | packAll: function (levelGlob) { 13 | const levels = glob.sync(levelGlob).map(filename => levelPacker.pack(filename)); 14 | //const string = JSON.stringify(levels, undefined, 2); 15 | const string = util.inspect(levels, { depth: null }); 16 | 17 | return "const LevelCache = " + string + ";\n" + 18 | "LevelCache.outro = " + JSON.stringify(LevelMetadata.outro) + ";\n"; 19 | }, 20 | 21 | pack: function (filename) { 22 | const raw = require("./" + filename); 23 | let terrainLayer, metaLayer, objectsLayer; 24 | 25 | // We expect each level to have layers with these exact names 26 | for (let i = 0; i < raw.layers.length; i++) { 27 | if (raw.layers[i].name === 'Terrain') { 28 | terrainLayer = raw.layers[i]; 29 | } else if (raw.layers[i].name === 'Meta') { 30 | metaLayer = raw.layers[i]; 31 | } else if (raw.layers[i].name === 'Objects') { 32 | objectsLayer = raw.layers[i]; 33 | } else { 34 | throw new Error('Invalid layer name ' + raw.layers[i].name); 35 | } 36 | } 37 | if (!terrainLayer || !metaLayer || !objectsLayer) { 38 | throw new Error('Missing required layer'); 39 | } 40 | 41 | let width = terrainLayer.width; 42 | let height = terrainLayer.height; 43 | 44 | // The first thing we want to do is calculate the minimum width and height of the level 45 | // data in the Tiled map (for ease of editing, I create all the levels in the middle of a 46 | // 100x100 map, but we don't need all that cruft in the game!) 47 | 48 | const tileBounds = { 49 | top: terrainLayer.height, 50 | bottom: 0, 51 | left: terrainLayer.width, 52 | right: 0 53 | }; 54 | 55 | for (let i = 0; i < height; i++) { 56 | for (let j = 0; j < width; j++) { 57 | if (terrainLayer.data[i * width + j] > 0) { 58 | tileBounds.top = Math.min(tileBounds.top, i); 59 | tileBounds.bottom = Math.max(tileBounds.bottom, i); 60 | tileBounds.left = Math.min(tileBounds.left, j); 61 | tileBounds.right = Math.max(tileBounds.right, j); 62 | } 63 | } 64 | } 65 | 66 | width = tileBounds.right - tileBounds.left + 1; 67 | height = tileBounds.bottom - tileBounds.top + 1; 68 | 69 | // After determining the new width and height, we need to translate all layers to new coords 70 | terrainLayer = levelPacker.cropLayer(terrainLayer, tileBounds.left, tileBounds.top, width, height); 71 | metaLayer = levelPacker.cropLayer(metaLayer, tileBounds.left, tileBounds.top, width, height); 72 | objectsLayer = levelPacker.cropLayer(objectsLayer, tileBounds.left, tileBounds.top, width, height); 73 | 74 | const level = { 75 | enemies: [], 76 | cameras: [], 77 | terminals: [], 78 | doors: [], 79 | width: width, 80 | height: height, 81 | data: levelPacker.packData(terrainLayer.data) 82 | }; 83 | let short = filename.match(/\/([^/]*)\.json/)[1]; 84 | Object.assign(level, LevelMetadata[short]); 85 | 86 | const enterBounds = { 87 | top: terrainLayer.height * 32, 88 | bottom: 0, 89 | left: terrainLayer.width * 32, 90 | right: 0 91 | }; 92 | 93 | const exitBounds = { 94 | top: terrainLayer.height * 32, 95 | bottom: 0, 96 | left: terrainLayer.width * 32, 97 | right: 0 98 | }; 99 | 100 | // Process the objects in the objects layer, inserting them into the appropriate 101 | // collections as we go. 102 | for (let i = 0; i < objectsLayer.objects.length; i++) { 103 | let object = objectsLayer.objects[i]; 104 | if (object.type === "enemy") { 105 | let enemy = { 106 | x: object.x, 107 | y: object.y 108 | }; 109 | if (object.properties) { 110 | if (object.properties.Wake) enemy.wake = object.properties.Wake; 111 | if (object.properties.WakeRadius) enemy.wakeRadius = parseInt(object.properties.WakeRadius, 10); 112 | if (object.properties.PatrolDX) enemy.patrolDX = parseInt(object.properties.PatrolDX, 10); 113 | if (object.properties.PatrolDY) enemy.patrolDY = parseInt(object.properties.PatrolDY, 10); 114 | if (object.properties.PatrolStart) enemy.patrolStart = parseInt(object.properties.PatrolStart, 10); 115 | } 116 | level.enemies.push(enemy); 117 | } 118 | if (object.type === "camera") { 119 | level.cameras.push({ 120 | u: Math.floor(object.x / 32), 121 | v: Math.floor(object.y / 32), 122 | control: object.properties.Control, 123 | facing: parseFloat(object.properties.Facing), 124 | enabled: object.properties.Enabled === 'true' 125 | }); 126 | } 127 | if (object.type === "terminal") { 128 | level.terminals.push({ 129 | u: Math.floor(object.x / 32), 130 | v: Math.floor(object.y / 32), 131 | control: object.properties.Control 132 | }); 133 | } 134 | } 135 | 136 | // Process the tiles in the "meta" layer (currently, meta tiles are doors, enter, or 137 | // exit markers). Technically, I designed the enter and exit areas to allow any size (like 2x3 138 | // or 4x2) and the game will respect it, but the doors are hard-coded to be 2x1 tiles, so 139 | // there's not much use for that flexibility atm. 140 | for (let i = 0; i < height; i++) { 141 | for (let j = 0; j < width; j++) { 142 | if (metaLayer.data[i * width + j] === 3) { 143 | enterBounds.top = Math.min(enterBounds.top, i * 32); 144 | enterBounds.bottom = Math.max(enterBounds.bottom, i * 32 + 32); 145 | enterBounds.left = Math.min(enterBounds.left, j * 32); 146 | enterBounds.right = Math.max(enterBounds.right, j * 32 + 32); 147 | } else if (metaLayer.data[i * width + j] === 4) { 148 | exitBounds.top = Math.min(exitBounds.top, i * 32); 149 | exitBounds.bottom = Math.max(exitBounds.bottom, i * 32 + 32); 150 | exitBounds.left = Math.min(exitBounds.left, j * 32); 151 | exitBounds.right = Math.max(exitBounds.right, j * 32 + 32); 152 | } else if (metaLayer.data[i * width + j] === 7) { 153 | let door; 154 | if (metaLayer.data[i * width + j + 1] === 7) { 155 | door = { 156 | u: j, 157 | v: i, 158 | type: 'h' 159 | }; 160 | } else if (metaLayer.data[(i + 1) * width + j] === 7) { 161 | door = { 162 | u: j, 163 | v: i, 164 | type: 'v' 165 | }; 166 | } 167 | if (door) { 168 | // Kind of backed myself into this one... I ended up wanting to know 169 | // what "type" of door I'm interacting with. Figure it out based on 170 | // what tiles are near. 171 | if (metaLayer.data[i * width + j + 1] === 4 || 172 | metaLayer.data[i * width + j - 1] === 4 || 173 | metaLayer.data[(i + 1) * width + j] === 4 || 174 | metaLayer.data[(i - 1) * width + j] === 4) { 175 | door.exitDoor = true; 176 | } else { 177 | door.entranceDoor = true; 178 | } 179 | level.doors.push(door); 180 | } 181 | } 182 | } 183 | } 184 | 185 | level.enter = { 186 | p1: { x: enterBounds.left, y: enterBounds.top }, 187 | p2: { x: enterBounds.right, y: enterBounds.bottom } 188 | }; 189 | level.exit = { 190 | p1: { x: exitBounds.left, y: exitBounds.top }, 191 | p2: { x: exitBounds.right, y: exitBounds.bottom } 192 | }; 193 | 194 | // The "clean up step". Level data is repeated quite a few times, so 195 | // maybe we'll save ourselves a few measly bytes by shortening the 196 | // names of our level cache properties... 197 | 198 | level.d = level.doors; 199 | delete level.doors; 200 | // Hack: door.type isn't implemented right now (it's always "h"), so 201 | // let's just cut it. 202 | level.d.forEach(door => { delete door.type; }); 203 | 204 | level.e = level.enemies; 205 | delete level.enemies; 206 | 207 | level.t = level.terminals; 208 | delete level.terminals; 209 | 210 | level.c = level.cameras; 211 | delete level.cameras; 212 | 213 | return level; 214 | }, 215 | 216 | /** 217 | * Return a new copy of the provided layer, where all tiles and objects 218 | * are repositioned to reflect a "cropped" level based on the provided 219 | * tile offset, width, and height. 220 | */ 221 | cropLayer: function (layer, u, v, width, height) { 222 | let data; 223 | let objects; 224 | 225 | if (layer.data) { 226 | data = []; 227 | for (let i = 0; i < height; i++) { 228 | for (let j = 0; j < width; j++) { 229 | data[i * width + j] = layer.data[(i + v) * layer.width + j + u]; 230 | } 231 | } 232 | } 233 | 234 | if (layer.objects) { 235 | objects = layer.objects.map(object => { 236 | return Object.assign({}, object, { 237 | x: object.x - u * 32, 238 | y: object.y - v * 32 239 | }); 240 | }); 241 | } 242 | 243 | return Object.assign({}, layer, { 244 | data: data, 245 | objects: objects 246 | }); 247 | }, 248 | 249 | /** 250 | * Smash up data as small as possible. Assumes that we have a max of EIGHT possible 251 | * tiles, freeing up remaining bits for length vars. 252 | * 253 | * Basically I'm being super weird in this function, as it's kinda-sorta just base64, 254 | * but I'm optimizing for the level domain. I want to print only characters with zero 255 | * side effects, which means: 256 | * 35-91 (avoiding 34=" and 92=\) 257 | * 93-123 (avoiding 92=\, 123 is just the highest multiple) 258 | * 259 | * I give myself 0-7 for the tile value, and add (8 * number of tiles), subtracting 260 | * 1 because I don't need a 0 length run. That lets me pack up to 11 tiles into 1 byte, 261 | * with no "double-byte" processing nonsense, and no binary-to-ascii back and forth. 262 | * 263 | * Having it be totally custom kind of sucks, but this seems to be the best bang for my 264 | * buck with the smallest possible unpacking code (every LOC you need to unpack on the 265 | * other side eats into your packing savings...). 266 | * 267 | * NOTE: Obviously, any edits to this function must also be reflected in Game._unpackData()! 268 | */ 269 | packData(data) { 270 | let result = []; 271 | 272 | let a = data[0], l = 1, byte; 273 | for (let i = 1; i < data.length; i++) { 274 | b = data[i]; 275 | 276 | if (a > 7) throw new Error("cannot pack level: tile value>7"); 277 | 278 | if (b === a && l < 11) { 279 | l++; 280 | continue; 281 | } 282 | 283 | // Yes, this is kind of unorthodox, it would make the most sense to pack into 284 | // bits, like `((l<<3)+a)`, and then perhaps convert the binary string into 285 | // a Base64 string and read it. In this case I'm optimizing for simplicity/least 286 | // code in the level reading logic. 287 | byte = 35 + (l - 1) * 8 + a; 288 | if (byte >= 92) byte++; 289 | result.push(String.fromCharCode(byte)); 290 | a = b; 291 | l = 1; 292 | } 293 | byte = 35 + (l - 1) * 8 + a; 294 | if (byte >= 92) byte++; 295 | result.push(String.fromCharCode(byte)); 296 | 297 | return result.join(''); 298 | } 299 | }; 300 | 301 | module.exports = levelPacker; 302 | -------------------------------------------------------------------------------- /src/js/Enemy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enemy handles the Raven, which are update/render entities with several states. 3 | * 4 | * In this game enemies cannot be killed, but they can be permanently contained 5 | * with security cameras. 6 | */ 7 | class Enemy { 8 | constructor(enemyData) { 9 | this.x = enemyData.x; 10 | this.y = enemyData.y; 11 | this.vx = 0; 12 | this.vy = 0; 13 | this.ax = 10; 14 | this.ay = 10; 15 | this.maxSpeed = 0; 16 | 17 | this._idleWidth = 18; 18 | this._idleHeight = 30; 19 | this._attackWidth = 18; 20 | this._attackHeight = 6; 21 | 22 | this.width = this._idleWidth; 23 | this.height = this._idleHeight; 24 | 25 | this.wake = enemyData.wake || 'radius'; 26 | this.wakeRadius = enemyData.wakeRadius || 384; 27 | this.killRadius = 14; 28 | this._eyeQueue = [,,,,,,,,,,,,]; 29 | 30 | if (enemyData.patrolDX !== undefined && enemyData.patrolDY !== undefined) { 31 | this._patrol = [ 32 | { x: this.x, y: this.y }, 33 | { x: this.x + enemyData.patrolDX, y: this.y + enemyData.patrolDY } 34 | ]; 35 | this._patrol.next = 1; 36 | this._patrol.after = game.framems + 1000 + (enemyData.patrolStart || 0); 37 | } 38 | 39 | this.state = 'asleep'; 40 | } 41 | 42 | update(delta) { 43 | let spotted = Util.entitySpotted(this); 44 | let attackAngle; 45 | 46 | // TODO: For now, I've made the intentional decision to let statues ignore physics 47 | // (if they are running at the player and you spot them, they full-stop, no sliding 48 | // or deceleration involved). Might be interesting visually to change that. 49 | 50 | switch (this.state) { 51 | case 'asleep': 52 | if (spotted) { 53 | // Being seen by anything immediately freezes the statue, and it 54 | // begin idling after it is unfrozen. 55 | this.state = 'frozen'; 56 | } else if (this.wake === 'radius' && Util.distance(this, game.player) < this.wakeRadius) { 57 | // Default - wake when player reaches a certain radius 58 | this.state = 'idle'; 59 | } else if (this.wake === 'los' && this._sprintToTargetAngle(0, true) !== undefined) { 60 | // Wake when player is visible to the enemy 61 | this.state = 'idle'; 62 | } else { 63 | // "Asleep" is turning out not to be a good name for this state! 64 | if (this._patrol && game.framems > this._patrol.after) { 65 | let target = this._patrol[this._patrol.next]; 66 | if (this.x === target.x && this.y === target.y) { 67 | this._patrol.after = game.framems + 1000; 68 | this._patrol.next = (this._patrol.next + 1) % this._patrol.length; 69 | } else { 70 | let angle = Util.atanPoints(this, target); 71 | let dist = Util.distance(this, target); 72 | if (50 * delta > dist) { 73 | this.vx = 0; 74 | this.vy = 0; 75 | this.x = target.x; 76 | this.y = target.y; 77 | } else { 78 | this.vx = Util.cos(angle) * 50; 79 | this.vy = Util.sin(angle) * 50; 80 | this.x += this.vx * delta; 81 | this.y += this.vy * delta; 82 | } 83 | } 84 | } 85 | } 86 | 87 | break; 88 | case 'idle': 89 | // TODO: It would be cool if enemies would strategically move around 90 | // slowly, to reposition themselves for a better attack, even if they 91 | // knew they couldn't reach the player. 92 | // 93 | // For now, an idle statue stays perfectly still UNLESS it thinks it could 94 | // reach the player, then it enters attack mode. 95 | if (spotted) { 96 | this.state = 'frozen'; 97 | break; 98 | } 99 | 100 | attackAngle = this._bestAttackAngle(); 101 | 102 | if (attackAngle !== undefined) { 103 | this.state = 'attack'; 104 | this.attackAngle = attackAngle; 105 | this.attackVel = 265; 106 | this.attackAccel = 670; // currently ignored, no accel physics 107 | game.audio.playSiren(); 108 | } 109 | break; 110 | case 'attack': 111 | if (spotted) { 112 | this.state = 'idle'; 113 | break; 114 | } 115 | 116 | // The player is moving too, so we still need to recalculate our 117 | // angles each frame. 118 | attackAngle = this._bestAttackAngle(); 119 | 120 | if (attackAngle === undefined) { 121 | this.state = 'idle'; 122 | } else { 123 | this.vx = Util.cos(attackAngle) * this.attackVel; 124 | this.vy = Util.sin(attackAngle) * this.attackVel; 125 | 126 | this.x += this.vx * delta; 127 | this.y += this.vy * delta; 128 | } 129 | break; 130 | case 'frozen': 131 | if (!spotted) { 132 | this.state = 'idle'; 133 | } 134 | break; 135 | } 136 | 137 | if (this.state === 'attack') { 138 | this.width = this._attackWidth; 139 | this.height = this._attackHeight; 140 | } else { 141 | this.width = this._idleWidth; 142 | this.height = this._idleHeight; 143 | } 144 | } 145 | 146 | render() { 147 | if (this.state !== 'attack' && (this.state !== 'asleep' || (this.vx === 0 && this.vy === 0))) { 148 | let jitter = game.framems - this.frozenms; 149 | let r1, r2, r3, r4, a1, a2, a3; 150 | 151 | // When you first spot a statue, it is immediately agitated, but 152 | // calms down (that is, becomes less conspicuous) over a couple seconds. 153 | // However, even after a long period, there should be a slight shakiness 154 | // to the statue. 155 | if (jitter < 300) { 156 | [r1, r2, r3, r4, a1, a2, a3] = [ 157 | Util.rf(11) - 5, Util.rf(11) - 5, 158 | Util.rf(7) - 3, Util.rf(7) - 3, 159 | 0.4, 160 | 0.7, 161 | 1 162 | ]; 163 | } else if (jitter < 600) { 164 | [r1, r2, r3, r4, a1, a2, a3] = [ 165 | Util.rf(7) - 3, Util.rf(7) - 3, 166 | Util.rf(5) - 2, Util.rf(5) - 2, 167 | 0.3, 168 | 0.6, 169 | 1 170 | ]; 171 | } else { 172 | [r1, r2, r3, r4, a1, a2, a3] = [ 173 | Util.rf(5) - 2, Util.rf(5) - 2, 174 | Util.rf(3) - 1, Util.rf(3) - 1, 175 | 0.1, 176 | 0.2, 177 | 1 178 | ]; 179 | } 180 | 181 | game.ctx.globalAlpha = a1; 182 | Asset.drawSprite('raven', game.ctx, game.offset.x + this.x + r1 - this.width / 2, game.offset.y + this.y + r2 - this.height / 2); 183 | game.ctx.globalAlpha = a2; 184 | Asset.drawSprite('raven', game.ctx, game.offset.x + this.x + r2 - this.width / 2, game.offset.y + this.y + r4 - this.height / 2); 185 | game.ctx.globalAlpha = a3; 186 | Asset.drawSprite('raven', game.ctx, game.offset.x + this.x - this.width / 2, game.offset.y + this.y - this.height / 2); 187 | game.ctx.globalAlpha = 1; 188 | } else if (this.state === 'asleep' && this._patrol) { 189 | // TODO: This is lazy. Ideally, instead of "showing" the statue 10% of the time while 190 | // moving on patrol, we would instead sync up a Math.sin on framems, so that the statue 191 | // fades in and out smoothly based on the movement of the eyes. 192 | if (Math.random() > 0.9) { 193 | game.ctx.globalAlpha = 0.4; 194 | Asset.drawSprite('raven', game.ctx, game.offset.x + this.x - this.width/2, game.offset.y + this.y - this.height / 2); 195 | game.ctx.globalAlpha = 1; 196 | } 197 | } 198 | } 199 | 200 | renderPost() { 201 | // Eyes are rendered in post, so they glow on top of the LOS blanket. 202 | 203 | // By adding in some sin/cos action, if you were to watch a statue's eyes while standing 204 | // still, they would "swing" like a pendulum from one side to another. Although it's subtle, 205 | // this gives the impression of a hulking, breathing creature in the room; when the eyes 206 | // are in motion, this gives just enough herky-jerk stutter to make the creature feel like 207 | // it is in some kind of running/walking animation. 208 | if (this.state === 'attack' || (this.state === 'asleep' && this._patrol && (this.vx !== 0 || this.vy !== 0))) { 209 | this._eyeQueue.push({ 210 | x: this.x + Math.cos(game.framems / 300) * 8, 211 | y: this.y - 6 + Math.abs(Math.sin(game.framems / 300) * 5) 212 | }); 213 | } else { 214 | this._eyeQueue.push(false); 215 | } 216 | this._eyeQueue.shift(); 217 | 218 | for (let i = 0; i < this._eyeQueue.length; i++) { 219 | let ec = this._eyeQueue[i]; 220 | if (ec) { 221 | //game.ctx.globalAlpha = 0.05 * i; 222 | game.ctx.fillStyle = 'rgba(165, 10, 16, ' + (0.05 * i) + ')'; 223 | //game.ctx.fillStyle = 'rgba(165, 10, 16, ' + (0.05 * i1)'; 224 | game.ctx.fillRect(game.offset.x + ec.x - 5 - 1, game.offset.y + ec.y - 1, 3, 3); 225 | game.ctx.fillRect(game.offset.x + ec.x + 5 - 1, game.offset.y + ec.y - 1, 3, 3); 226 | game.ctx.fillStyle = 'rgba(254, 20, 32, ' + (0.05*i) + ')'; 227 | //game.ctx.fillStyle = 'rgba(254, 20, 32, 1)'; 228 | game.ctx.fillRect(game.offset.x + ec.x - 5, game.offset.y + ec.y, 2, 2); 229 | game.ctx.fillRect(game.offset.x + ec.x + 5 - 1, game.offset.y + ec.y, 2, 2); 230 | } 231 | } 232 | } 233 | 234 | _bestAttackAngle() { 235 | // What is happening here? 236 | // 237 | // First, we try to draw a direct line from us to the player, without hitting an obstacle 238 | // or an LOS cone. If that doesn't work, we do the same thing again, but 24 pixels BEHIND 239 | // the player (based on current facing). If that doesn't work, once more, but 48 pixels 240 | // BEHIND the player. 241 | // 242 | // If none of that direct attack stuff worked, use the attack grid generated on each frame 243 | // (using standard flood fill, in Game.js) to pick the closest tile that would bring us 244 | // closest the player, and move to it. Note that the attack grid will only propogate 245 | // tiles it believes can reach the player without being seen, so lowestAttackCost will 246 | // return undefined if the player can see us or any tile in the way. 247 | // 248 | // This actually works pretty well and makes the enemy dangerous! There are some tweaks I'd 249 | // like to make, though. 250 | // 251 | // Biggest: "Behind" the player is too strict, it makes backing up to a wall too powerful of an 252 | // option. A lot of the time, if the enemy is (say) 90 degrees to my right, and I'm 40 253 | // pixels in front of a wall, the enemy could easily aim for a tile 48 degrees behind me 254 | // at a 30 degree angle, and then swoop in to get me; but it won't, because 24p 255 | // hits my LOS, and 48p is in a wall. 256 | // 257 | // Secondary: the attack grid isn't perfect, it basically checks the center of each tile, 258 | // which is why sometimes the enemy "attacks" and slams into a security camera. In an ideal 259 | // world, the enemy would never voluntarily place itself into a situation it can't get 260 | // back out of. 261 | // 262 | // If I could calculate the best angle between nearest player obstacle and LOS cone, the 263 | // enemy could make even more "surprise" attacks on the player when they make a mistake. 264 | 265 | let angle = this._sprintToTargetAngle(0); 266 | if (angle === undefined) { 267 | angle = this._sprintToTargetAngle(24); 268 | } 269 | if (angle === undefined) { 270 | angle = this._sprintToTargetAngle(48); 271 | } 272 | if (angle === undefined) { 273 | let attackChoice = this._lowestAttackCostUV(Math.floor(this.x / 32), Math.floor(this.y / 32)); 274 | if (attackChoice) { 275 | angle = Util.atanPoints(this, { 276 | x: attackChoice[0] * 32 + 16, 277 | y: attackChoice[1] * 32 + 16 278 | }); 279 | } 280 | } 281 | 282 | return angle; 283 | } 284 | 285 | _lowestAttackCostUV(u, v, iterations) { 286 | let options = [ 287 | [u, v], 288 | [u - 1, v], 289 | [u + 1, v], 290 | [u, v - 1], 291 | [u, v + 1] 292 | ]; 293 | 294 | // If the game isn't ready to generate an attack grid, then don't attempt to use it. 295 | if (!game.attackGrid) return; 296 | 297 | for (let i = 0; i < options.length; i++) { 298 | options[i][2] = game.attackGrid[options[i][1] * game.level.width + options[i][0]]; 299 | } 300 | 301 | // Allow diagonal movement through grid if both sides of that corner are clear (hopefully, 302 | // prevents "stuck in corner" shenanigans). 303 | if (options[0] < 10000 && options[1] < 10000 && options[3] < 10000) { 304 | options.push([u - 1, v - 1, game.attackGrid[(v - 1) * game.level.width + u - 1]]); 305 | } 306 | if (options[0] < 10000 && options[2] < 10000 && options[4] < 10000) { 307 | options.push([u + 1, v + 1, game.attackGrid[(v + 1) * game.level.width + u + 1]]); 308 | } 309 | if (options[0] < 10000 && options[1] < 10000 && options[4] < 10000) { 310 | options.push([u - 1, v + 1, game.attackGrid[(v + 1) * game.level.width + u - 1]]); 311 | } 312 | if (options[0] < 10000 && options[2] < 10000 && options[3] < 10000) { 313 | options.push([u + 1, v - 1, game.attackGrid[(v - 1) * game.level.width + u + 1]]); 314 | } 315 | 316 | let choice = options.sort((a, b) => a[2] - b[2])[0]; 317 | if (choice[2] < 10000) { 318 | return choice; 319 | } 320 | } 321 | 322 | _sprintToTargetAngle(offset, ignoreVision) { 323 | let shadow = { x: this.x, y: this.y, width: this.width, height: this.height }; 324 | let target = { 325 | x: game.player.x - Util.cos(game.facing) * offset, 326 | y: game.player.y - Util.sin(game.facing) * offset 327 | }; 328 | let attackAngle = Util.atanPoints(this, target); 329 | let dx = Util.cos(attackAngle) * 4; 330 | let dy = Util.sin(attackAngle) * 4; 331 | 332 | while ((ignoreVision || !Util.entitySpotted(shadow)) && !Util.wallAtXY(shadow.x, shadow.y)) { 333 | if (Util.distance(shadow, game.player) <= this.killRadius) { 334 | return attackAngle; 335 | } 336 | 337 | shadow.x += dx; 338 | shadow.y += dy; 339 | } 340 | } 341 | 342 | static renderAttackWarning() { 343 | if (game.player.state === 'dead') return; 344 | 345 | let attacked = false; 346 | for (let i = 0; i < game.enemies.length; i++) { 347 | if (game.enemies[i].state === 'attack') { 348 | attacked = true; 349 | break; 350 | } 351 | } 352 | 353 | if (attacked) { 354 | if (!Enemy.attackWarningMs) { 355 | Enemy.attackWarningMs = game.framems; 356 | } 357 | 358 | let alpha = ((game.framems - Enemy.attackWarningMs) % 500) / 1800 + 0.1; 359 | 360 | let w = game.canvas.width; 361 | let h = game.canvas.height; 362 | 363 | game.ctx.save(); 364 | game.ctx.fillStyle = 'rgba(255, 0, 0, ' + alpha + ')'; 365 | game.ctx.beginPath(); 366 | game.ctx.moveTo(0, h * 0.25); 367 | game.ctx.lineTo(w * 0.1, h * 0.5); 368 | game.ctx.lineTo(0, h * 0.75); 369 | game.ctx.closePath(); 370 | game.ctx.fill(); 371 | game.ctx.beginPath(); 372 | game.ctx.moveTo(w, h * 0.25); 373 | game.ctx.lineTo(w * 0.9, h * 0.5); 374 | game.ctx.lineTo(w, h * 0.75); 375 | game.ctx.closePath(); 376 | game.ctx.fill(); 377 | game.ctx.restore(); 378 | } else { 379 | Enemy.attackWarningMs = undefined; 380 | } 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/js/Game.js: -------------------------------------------------------------------------------- 1 | class Game { 2 | init() { 3 | // Prep canvas 4 | this.canvas = document.getElementById('canvas'); 5 | this.canvas.width = this.canvas.clientWidth; 6 | this.canvas.height = this.canvas.clientHeight; 7 | this.ctx = this.canvas.getContext('2d'); 8 | this.canvasBounds = this.canvas.getBoundingClientRect(); 9 | 10 | this._losCanvas = document.getElementById('los'); 11 | this._losCanvas.width = this.canvas.width; 12 | this._losCanvas.height = this.canvas.height; 13 | this._losCtx = this._losCanvas.getContext('2d'); 14 | 15 | this._tileCanvas = document.getElementById('tile'); 16 | this._tileCtx = this._tileCanvas.getContext('2d'); 17 | 18 | Asset.loadAllAssets(); 19 | 20 | this.input = new Input({ 21 | up: this.onUp.bind(this), 22 | down: this.onDown.bind(this), 23 | left: this.onLeft.bind(this), 24 | right: this.onRight.bind(this), 25 | toggle: this.onToggle.bind(this), 26 | escape: { 27 | // For the ESC key, wait until the user releases the key. This is simplistic 28 | // and slightly delays input, but is the easy to make sure that if we request 29 | // pointer lock, it won't be immediately released again by the browser. 30 | up: this.onEscape.bind(this) 31 | }, 32 | mousemove: this.onMouseMove.bind(this), 33 | mouseclick: this.onMouseClick.bind(this) 34 | }).init(); 35 | 36 | this.audio = new Audio(); 37 | 38 | // Check whether local storage is writable. 39 | // Why not typeof()? See https://stackoverflow.com/questions/11214404/how-to-detect-if-browser-supports-html5-local-storage 40 | try { 41 | localStorage.setItem('lc', 7); 42 | localStorage.removeItem('lc'); 43 | } catch (e) { 44 | this._storageDisabled = true; 45 | } 46 | 47 | if (this._storageDisabled) { 48 | this._startLevel = 0; 49 | } else { 50 | this._startLevel = parseInt(localStorage.getItem('level') || '0', 10); 51 | } 52 | 53 | this.level = undefined; 54 | this.intro = undefined; 55 | this.player = undefined; 56 | this.levelComplete = undefined; 57 | this.levelCompleteMs = undefined; 58 | 59 | this.framems = 0; 60 | this.enemies = []; 61 | 62 | this.crosshair = { x: 0, y: 0 }; 63 | this.mouse = { x: 0, y: 0 }; 64 | 65 | // How "deep" a player's vision cone cuts into a wall tile. Very important 66 | // that this be a global respected value, without it, the corners we are cutting 67 | // will result in e.g. light shining through the corners of moving doors. 68 | this.tileVisibilityInset = 4; 69 | 70 | // When "lock crosshair to map" is true, leaving mouse at rest and moving 71 | // with WASD will "strafe" (for example, moving around a raven in a circle 72 | // stay looking at the raven). The default is false, which means leaving the 73 | // mouse at rest will keep the player's orientation steady as you move. 74 | this.lockCrosshairToMap = false; 75 | 76 | // Yes, technically, facing and fov are properties of the player. But because 77 | // we treat the crosshair as a separate entity, it's easier to just make it 78 | // part of game state. 79 | this.facing = 0; 80 | this.fov = 120; 81 | 82 | this.mouselocked = false; 83 | this.paused = true; 84 | this._renderPrep = true; 85 | document.addEventListener('pointerlockchange', this.onMouseLock.bind(this)); 86 | document.addEventListener('mozpointerlockchange', this.onMouseLock.bind(this)); 87 | document.addEventListener('webkitpointerlockchange', this.onMouseLock.bind(this)); 88 | 89 | this._startMenu = new Menu( 90 | [ 91 | { 92 | text: 'START NEW GAME', 93 | handler: () => { 94 | this._pendingLevelIndex = 0; 95 | this.unpause(); 96 | } 97 | } 98 | ], 99 | () => false 100 | ); 101 | 102 | this._continueMenu = new Menu( 103 | [ 104 | { 105 | text: 'CONTINUE GAME', 106 | handler: () => { 107 | this._pendingLevelIndex = this._startLevel; 108 | this.unpause(); 109 | } 110 | }, 111 | { 112 | text: 'START NEW GAME', 113 | handler: () => { 114 | this._pendingLevelIndex = 0; 115 | this.unpause(); 116 | } 117 | } 118 | ], 119 | () => false 120 | ); 121 | 122 | this._pauseMenu = new Menu( 123 | [ 124 | { 125 | text: 'RESUME', 126 | handler: () => { 127 | this.unpause(); 128 | } 129 | }, 130 | { 131 | text: 'RESTART LEVEL', 132 | handler: () => { 133 | this._pendingLevelIndex = this.levelIndex; 134 | this.unpause(); 135 | } 136 | } 137 | ], 138 | () => this.unpause() 139 | ); 140 | 141 | return this; 142 | } 143 | 144 | update(delta) { 145 | if (typeof this._pendingLevelIndex !== 'undefined') { 146 | this._load(this._pendingLevelIndex); 147 | this._pendingLevelIndex = undefined; 148 | } 149 | 150 | this.audio.update(delta); 151 | 152 | if (this.menu) { 153 | this.menu.update(delta); 154 | } else if (this.intro) { 155 | this.intro.update(delta); 156 | if (this.intro.state === 'dead') { 157 | this.intro = undefined; 158 | if (this.level) { 159 | // A "level" intro naturally transitions into the next level. 160 | // No action needed. 161 | } else { 162 | // If there's no level, then this is actually the outro, and 163 | // the next step is the main menu. Note we always open the Start Menu, 164 | // because there will be no continuing. 165 | this.openMenu(this._startMenu); 166 | } 167 | } 168 | this.levelms = performance.now(); 169 | } else if (this.level) { 170 | if (this.player.dead) { 171 | this.deathFrame++; 172 | } 173 | 174 | if (this.levelComplete && (this.framems - this.levelCompleteMs) > 2200) { 175 | this._pendingLevelIndex = this.levelIndex + 1; 176 | if (this._pendingLevelIndex >= LevelCache.length) { 177 | this._pendingLevelIndex = undefined; 178 | this.level = undefined; 179 | this.intro = new Intro(LevelCache.outro); 180 | this.intro.update(delta); 181 | this._renderPrep = false; 182 | this._startLevel = 0; 183 | if (!this._storageDisabled) localStorage.setItem('level', 0); 184 | return; 185 | } else { 186 | if (!this._storageDisabled) localStorage.setItem('level', this._pendingLevelIndex); 187 | } 188 | } 189 | 190 | this.player.update(delta); 191 | this.terminals.forEach(terminal => terminal.update(delta)); 192 | this.cameras.forEach(camera => camera.update(delta)); 193 | this.doors.forEach(door => door.update(delta)); 194 | 195 | this.offset = { 196 | x: this.canvas.width / 2 - this.player.x, 197 | y: this.canvas.height / 2 - this.player.y, 198 | crosshairX: this.player.x - this.canvas.width / 2, 199 | crosshairY: this.player.y - this.canvas.height / 2 200 | }; 201 | 202 | let cd = 4; 203 | let bound = { 204 | left: this.offset.crosshairX + cd, 205 | right: this.offset.crosshairX + this.canvas.width - cd, 206 | top: this.offset.crosshairY + cd, 207 | bottom: this.offset.crosshairY + this.canvas.height - cd 208 | }; 209 | 210 | if (this.crosshair.x < bound.left) { 211 | this.crosshair.x = bound.left; 212 | } else if (this.crosshair.x > bound.right) { 213 | this.crosshair.x = bound.right; 214 | } 215 | if (this.crosshair.y < bound.top) { 216 | this.crosshair.y = bound.top; 217 | } else if (this.crosshair.y > bound.bottom) { 218 | this.crosshair.y = bound.bottom; 219 | } 220 | 221 | this.facing = Util.atanPoints(this.player, this.crosshair); 222 | 223 | this.vision = []; 224 | let safe = false; 225 | if (Util.pointInBounds(this.player, this.level.enter)) { 226 | this.vision.push(Util.getVisBounds(this.level.enter, 0.69)); 227 | safe = true; 228 | } 229 | if (Util.pointInBounds(this.player, this.level.exit)) { 230 | this.vision.push(Util.getVisBounds(this.level.exit, 0.69)); 231 | safe = true; 232 | } 233 | this.cameras.forEach(camera => { 234 | if (camera.enabled) { 235 | this.vision = this.vision.concat(Util.getVisCone(camera, camera.facing, camera.fov, 12, 0, 0.69)); 236 | } 237 | }); 238 | if (!this.player.dead) { 239 | this.vision = this.vision.concat(Util.getVisCone(this.player, this.facing, this.fov, 4, 0, 1)); 240 | } 241 | 242 | this._buildAttackGrid(); 243 | 244 | this.enemies.forEach(enemy => enemy.update(delta)); 245 | this.enemies.forEach(enemy => Util.enforceEntityMovement(enemy)); 246 | 247 | if (!this.player.dead) { 248 | this.enemies.forEach(enemy => { 249 | if (Util.pointNearPoint(enemy, this.player, enemy.killRadius)) { 250 | this.playerDied(); 251 | } 252 | }); 253 | 254 | let activeTerminal = undefined; 255 | this.terminals.forEach(terminal => { 256 | if (Util.pointNearPoint(terminal, this.player, terminal.toggleRadius)) { 257 | activeTerminal = terminal; 258 | } 259 | }); 260 | this._activeTerminal = activeTerminal; 261 | 262 | if (!this.levelComplete && Util.pointInBounds(this.player, this.level.exit)) { 263 | this.levelComplete = true; 264 | this.levelCompleteMs = performance.now(); 265 | this.audio.playTri(); 266 | } 267 | } 268 | 269 | this._renderPrep = true; 270 | } 271 | 272 | this._handleCheatCodes(); 273 | } 274 | 275 | render() { 276 | this.ctx.fillStyle = 'black'; 277 | this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); 278 | 279 | if (this.level && this._renderPrep && !this.intro) { 280 | if (this.player.dead) { 281 | let scale = Math.min(3, 1 + this.deathFrame / 50); 282 | this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2); 283 | this.ctx.rotate(Util.d2r(this.deathFrame / 5)); 284 | this.ctx.scale(scale, scale); 285 | this.ctx.translate(-this.canvas.width / 2, -this.canvas.height / 2); 286 | } 287 | 288 | // "Draw" the pre-rendered level onto the canvas. Normally here we'd loop through 289 | // level width and height, drawing each tile, but that many drawImage calls is 290 | // way too slow. 291 | this.ctx.drawImage(this._tileCanvas, this.offset.x, this.offset.y); 292 | 293 | this.terminals.forEach(terminal => terminal.render()); 294 | this.enemies.forEach(enemy => enemy.render()); 295 | this.player.render(); 296 | this.cameras.forEach(camera => camera.render()); 297 | this.doors.forEach(door => door.render()); 298 | 299 | // Uncomment this block to draw dashed yellow lines along the various 300 | // visibility edges. Pretty much just for debugging. 301 | /*let losEdges = this.losEdges; 302 | this.doors.forEach(door => losEdges = losEdges.concat(door.getLosEdges())); 303 | losEdges.forEach(edge => { 304 | this.ctx.save(); 305 | this.ctx.globalAlpha = 0.9; 306 | this.ctx.strokeStyle = 'yellow'; 307 | this.ctx.setLineDash([4, 2]); 308 | this.ctx.beginPath(); 309 | this.ctx.moveTo(this.offset.x + edge.p1.x, this.offset.y + edge.p1.y); 310 | this.ctx.lineTo(this.offset.x + edge.p2.x, this.offset.y + edge.p2.y); 311 | this.ctx.stroke(); 312 | this.ctx.restore(); 313 | });*/ 314 | 315 | if (this.player.dead) { 316 | this._losCtx.clearRect(0, 0, this.canvas.width, this.canvas.height); 317 | let opacity = Math.max(0, 0.8 - this.deathFrame / 40); 318 | this._losCtx.fillStyle = 'rgba(0,0,0,' + opacity + ')'; 319 | this._losCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); 320 | } 321 | 322 | // Next, we "render" the LOS canvas 323 | this._losCtx.clearRect(0, 0, this.canvas.width, this.canvas.height); 324 | this._losCtx.fillStyle = 'rgba(0,0,0,0.8)'; 325 | this._losCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); 326 | this.vision.forEach(polygon => { 327 | this._losCtx.fillStyle = 'rgba(255,255,255,' + polygon.opacity + ')'; 328 | this._losCtx.beginPath(); 329 | this._losCtx.moveTo(this.offset.x + polygon[0].x, this.offset.y + polygon[0].y); 330 | for (let i = 1; i < polygon.length; i++) { 331 | this._losCtx.lineTo(this.offset.x + polygon[i].x, this.offset.y + polygon[i].y); 332 | } 333 | this._losCtx.closePath(); 334 | this._losCtx.fill(); 335 | }); 336 | 337 | // Prepare to put LOS visibility on top of the canvas 338 | this.ctx.save(); 339 | 340 | // LOS Blur actually looks REALLY nice in Chrome, gives your LOS beam a flashlight effect, 341 | // but it totally screws up frame rate. I'd have to really optimize the rest of my code 342 | // before I could turn on blur. (Uncomment if you want to check it out.) 343 | //this.ctx.filter = 'blur(6px)'; 344 | 345 | // attempted: lighten, multiply, darken, source-in (darken looks best for shadows so far) 346 | this.ctx.globalCompositeOperation = 'darken'; 347 | this.ctx.drawImage(this._losCanvas, 0, 0); 348 | this.ctx.restore(); 349 | 350 | if (!this.player.dead) { 351 | this.player.renderCrosshair(); 352 | 353 | // Interactivity indicator 354 | if (this._activeTerminal) { 355 | Util.renderTogglePrompt(this.offset.x + this.player.x - 18, this.offset.y + this.player.y + 18); 356 | } 357 | } 358 | 359 | // Post-visibility rendering 360 | this.player.render(0.8); 361 | this.enemies.forEach(enemy => enemy.renderPost()); 362 | 363 | // Reset all global transforms. Note: do not render anything except "HUD UI" 364 | // after this point, as it won't line up with the rest of the map in case of, 365 | // e.g., the death spin animation. 366 | this.ctx.setTransform(1, 0, 0, 1, 0, 0); 367 | 368 | Enemy.renderAttackWarning(); 369 | 370 | if (this.player.dead) { 371 | let mult = Math.min(1, this.deathFrame / 100); 372 | let b = -75 + mult * (this.canvas.height + 100); 373 | 374 | // A poor man's "blood splatter" 375 | this.ctx.fillStyle = 'rgba(204,0,0,0.8)'; 376 | for (let i = 0; i < this.canvas.width; i++) { 377 | this.ctx.fillRect(i, 0, 1, 378 | b + Math.abs(Math.cos(i / 29) * 30 + 379 | Math.sin(0.5 + i / 22) * 40 * mult + 380 | Math.cos(i / 19) * 50 * mult + 381 | Math.sin(i / 13) * 60 * mult + 382 | Math.cos(i / 7) * 30 * mult) 383 | ); 384 | } 385 | 386 | /*let opacity = Math.min(0.8, this.deathFrame / 40); 387 | this.ctx.fillStyle = 'rgba(204, 0, 0, ' + opacity + ')'; 388 | this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);*/ 389 | 390 | let size = Math.min(80, 20 + this.deathFrame / 5); 391 | let opacity = Math.min(0.5, this.deathFrame / 50); 392 | this.ctx.font = Asset.getFontString(size); 393 | let x = this.canvas.width / 2 - this.ctx.measureText('YOU ARE DEAD').width / 2; 394 | this.ctx.fillStyle = 'rgba(255, 255, 255, ' + opacity + ')'; 395 | this.ctx.fillText('YOU ARE DEAD', x, this.canvas.height / 2); 396 | } 397 | 398 | if (this.levelComplete) { 399 | let opacity = Math.min(1, (game.framems - game.levelCompleteMs) / 2000); 400 | this.ctx.fillStyle = 'rgba(0, 0, 0, ' + opacity + ')'; 401 | this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); 402 | 403 | this.ctx.font = Asset.getFontString(40); 404 | let x = this.canvas.width / 2 - this.ctx.measureText('CLEAR').width / 2; 405 | this.ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; 406 | this.ctx.fillText('CLEAR', x, this.canvas.height / 2); 407 | } 408 | 409 | if (!this.menu) { 410 | this.renderLevelText(); 411 | } 412 | } 413 | 414 | if (this.intro && !this.menu) { 415 | this.intro.render(); 416 | } 417 | 418 | if (this.menu) { 419 | this.menu.render(); 420 | } 421 | } 422 | 423 | renderLevelText() { 424 | let chars = Math.floor((this.framems - this.levelms) / 19); 425 | let nameChars = Math.min(this.level.name.length, chars); 426 | let hintChars = Math.max(0, chars - nameChars - 3); 427 | 428 | let delayStart = (this.level.hint.length + 3 + this.level.name.length) * 19; 429 | 430 | if (this.framems - this.levelms < delayStart) { 431 | // Text "scroll" audio effect 432 | this.audio.playClick(); 433 | } 434 | 435 | if (this.framems - this.levelms - delayStart < 3000) { 436 | this.ctx.font = Asset.getFontString(22); 437 | this.ctx.fillStyle = 'rgba(204, 255, 204, 0.9)'; 438 | this.ctx.fillText(this.level.name.substring(0, nameChars), 18, 36); 439 | 440 | if (this.level.hint) { 441 | this.ctx.font = Asset.getFontString(18); 442 | this.ctx.fillStyle = 'rgba(204, 204, 204, 0.8)'; 443 | this.ctx.fillText(this.level.hint.substring(0, hintChars), 18, this.canvas.height - 30); 444 | } 445 | } 446 | } 447 | 448 | frame(nextms) { 449 | let delta = nextms - this.framems; 450 | this.framems = nextms; 451 | 452 | // Gut check - absorb random lag spike / frame jumps 453 | // (The expected delta is 1000/60 = ~16.67ms.) 454 | if (delta > 500) { 455 | delta = 500; 456 | } 457 | 458 | this.update(delta / 1000); 459 | this.render(); 460 | 461 | window.requestAnimationFrame(this.frame.bind(this)); 462 | } 463 | 464 | start() { 465 | if (this._startLevel > 0) { 466 | this.openMenu(this._continueMenu); 467 | } else { 468 | this.openMenu(this._startMenu); 469 | } 470 | window.requestAnimationFrame(this.frame.bind(this)); 471 | } 472 | 473 | openMenu(menu) { 474 | this.menu = menu; 475 | this.menu.open(); 476 | } 477 | 478 | playerDied() { 479 | this.player.dead = true; 480 | this.deathFrame = 0; 481 | } 482 | 483 | // 484 | // Event Handlers 485 | // 486 | 487 | unpause() { 488 | this.canvas.requestPointerLock(); 489 | } 490 | 491 | onUp() { 492 | if (this.menu) this.menu.onUp(); 493 | } 494 | 495 | onDown() { 496 | if (this.menu) this.menu.onDown(); 497 | } 498 | 499 | onLeft() { 500 | } 501 | 502 | onRight() { 503 | } 504 | 505 | onToggle() { 506 | if (this.menu) { 507 | this.menu.select(); 508 | } else if (this.intro) { 509 | this.intro.toggle(); 510 | } else if (this.player.dead) { 511 | this._pendingLevelIndex = this.levelIndex; 512 | } else { 513 | let activeTerminal = this._activeTerminal; 514 | if (activeTerminal) { 515 | activeTerminal.toggle(); 516 | } 517 | } 518 | } 519 | 520 | onEscape() { 521 | if (this.menu) { 522 | this.menu.onEscape(); 523 | } else { 524 | // NOTE: My reading of the spec is that we should never reach this point, because 525 | // pressing ESC while we have no menu should be captured by the browser, and used 526 | // to release pointer lock. 527 | // 528 | // On Safari, it seems like (even though the hover bar says it will), the pointer lock 529 | // is never released. So we'll explicitly ask to release pointer lock, which will then 530 | // trigger the change handler below and open the pause menu. 531 | document.exitPointerLock(); 532 | } 533 | } 534 | 535 | onMouseLock() { 536 | if (document.pointerLockElement === this.canvas) { 537 | this.mouselocked = true; 538 | this.paused = false; 539 | this.menu = undefined; 540 | this.framems = performance.now(); 541 | } else { 542 | this.mouselocked = false; 543 | this.paused = true; 544 | this.openMenu(this._pauseMenu); 545 | } 546 | } 547 | 548 | onMouseMove(deltaX, deltaY, clientX, clientY) { 549 | if (!this.paused) { 550 | this.crosshair.x += deltaX; 551 | this.crosshair.y += deltaY; 552 | } 553 | 554 | this.mouse.x = clientX - this.canvasBounds.left; 555 | this.mouse.y = clientY - this.canvasBounds.top; 556 | 557 | if (this.menu) this.menu.onMouseMove(this.mouse.x, this.mouse.y); 558 | } 559 | 560 | onMouseClick() { 561 | if (this.menu) { 562 | this.menu.select(); 563 | } else if (this.player.dead) { 564 | this._pendingLevelIndex = this.levelIndex; 565 | } 566 | } 567 | 568 | _load(levelIndex) { 569 | this.levelIndex = levelIndex; 570 | this.level = Object.assign({}, LevelCache[levelIndex]); 571 | this.level.data = this._unpackData(this.level.data); 572 | this.levelComplete = false; 573 | 574 | let eb = this.level.enter; 575 | 576 | this.player = new Player(); 577 | this.player.x = (eb.p1.x + eb.p2.x) / 2; 578 | this.player.y = (eb.p1.y + eb.p2.y) / 2; 579 | this.crosshair.x = this.player.x + (this.level.chx || 0); 580 | this.crosshair.y = this.player.y + (this.level.chy || -32); 581 | 582 | this._polygonizeLevel(this.level); 583 | 584 | this.enemies = []; 585 | this.level.e.forEach(enemyData => { 586 | let enemy = new Enemy(enemyData); 587 | this.enemies.push(enemy); 588 | }); 589 | 590 | this.cameras = []; 591 | this.level.c.forEach(cameraData => { 592 | let camera = new Camera(cameraData); 593 | this.cameras.push(camera); 594 | }); 595 | 596 | this.terminals = []; 597 | this.level.t.forEach(terminalData => { 598 | let terminal = new Terminal(terminalData); 599 | terminal.cameras = this.cameras.filter(camera => camera.control === terminal.control); 600 | this.terminals.push(terminal); 601 | }); 602 | 603 | this.doors = []; 604 | this.level.d.forEach(doorData => { 605 | let door = new Door(doorData); 606 | this.doors.push(door); 607 | }); 608 | 609 | // Pre-render static level. Rendering the entire tiled map ahead of time 610 | // saves us hundreds-thousands of drawImage calls per frame, which according 611 | // to Chrome perf is the biggest CPU hit in this game. 612 | this._tileCanvas.width = this.level.width * 32; 613 | this._tileCanvas.height = this.level.height * 32; 614 | this._tileCtx.fillStyle = 'black'; 615 | this._tileCtx.fillRect(0, 0, this.level.width * 32, this.level.height * 32); 616 | 617 | for (let i = 0; i < this.level.height; i++) { 618 | for(let j = 0; j < this.level.width; j++) { 619 | let tile = Util.tileAtUV(j, i); 620 | if (tile === 1) { 621 | Asset.drawSprite('wall', this._tileCtx, j * 32, i * 32); 622 | this._renderTileNoise(1, j * 32, i * 32); 623 | } else if (tile === 2) { 624 | // Rotate floor pieces in a predictable pattern. 625 | let rot = ((i * 3 + j * 7) % 4) * 90; 626 | 627 | this._tileCtx.save(); 628 | // Totally cheating... mute the floor a little bit. 629 | this._tileCtx.globalAlpha = 0.81; 630 | this._tileCtx.translate(j * 32 + 16, i * 32 + 16); 631 | this._tileCtx.rotate(Util.d2r(rot)); 632 | Asset.drawSprite('floor', this._tileCtx, -16, -16); 633 | this._tileCtx.restore(); 634 | this._renderTileNoise(2, j * 32, i * 32); 635 | } 636 | } 637 | } 638 | 639 | if (this.level.intro) { 640 | this.intro = new Intro(this.level.intro); 641 | } else { 642 | this.levelms = performance.now(); 643 | } 644 | 645 | this._renderPrep = false; 646 | } 647 | 648 | _unpackData(data) { 649 | let result = [], v, c, l; 650 | for (let i = 0; i < data.length; i++) { 651 | v = data.charCodeAt(i) - 35; 652 | if (v >= 58) v--; 653 | c = v % 8; 654 | l = (v - c) / 8 + 1; 655 | for (let j = 0; j < l; j++) result.push(c); 656 | } 657 | return result; 658 | } 659 | 660 | _renderTileNoise(seed, x, y) { 661 | // Adding some noise makes most tiles look much more natural (easier on 662 | // the eyes), but it also explodes PNG size by an order of magnitude. Cheat 663 | // by saving the PNGs as mostly-solid-color and add noise in when we render 664 | // the level. 665 | //let seeded = Util.Alea(seed); 666 | //let rand = () => Math.floor(seeded() * 256); 667 | let r,g,b,a,w; 668 | for (let i = 1; i < 31; i++) { 669 | for(let j = 1; j < 31; j++) { 670 | if (Util.rf(100) > 40) { 671 | r = g = b = Util.rf(256); 672 | a = Util.rf(0.2 * 100) / 100; 673 | w = 1; 674 | this._tileCtx.fillStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; 675 | this._tileCtx.fillRect(x + j, y + i, w, 1); 676 | } 677 | } 678 | } 679 | } 680 | 681 | // Warning: brute force incoming... 682 | // 683 | // Given a tiled level, precalculate a set of wall-floor "edges". More generally, 684 | // an "edge" is a straight line that divides a non-vision-blocking area from a 685 | // vision-blocking area. 686 | // 687 | // (Doors are dynamic and are not included in this phase.) 688 | _polygonizeLevel(level) { 689 | let edges = {}; 690 | let addedge = (x1,y1,x2,y2,type) => { 691 | let key1 = `${x1},${y1}${type}`; 692 | let key2 = `${x2},${y2}${type}`; 693 | let existingEdge = edges[key1]; 694 | if (existingEdge) { 695 | delete edges[key1]; 696 | edges[key2] = [existingEdge[0], existingEdge[1], x2, y2]; 697 | } else { 698 | edges[key2] = [x1, y1, x2, y2]; 699 | } 700 | }; 701 | 702 | // Loop through all floor tiles, checking for adjacent wall tiles, and 703 | // create or extend an LOS edge whenever we find one. 704 | for (let i = 0; i < level.height; i++) { 705 | for(let j = 0; j < level.width; j++) { 706 | let value = level.data[i * level.width + j]; 707 | // value=2 is floor "non light obstructing" 708 | if (value !== 2) { 709 | continue; 710 | } 711 | if (level.data[i * level.width + j - 1] !== 2) { 712 | // left edge 713 | addedge(j * 32, i * 32, j * 32, i * 32 + 32, 'left'); 714 | } 715 | if (level.data[i * level.width + j + 1] !== 2) { 716 | // right edge 717 | addedge(j * 32 + 32, i * 32, j * 32 + 32, i * 32 + 32, 'right'); 718 | } 719 | if (level.data[(i - 1) * level.width + j] !== 2) { 720 | // top edge 721 | addedge(j * 32, i * 32, j * 32 + 32, i * 32, 'top'); 722 | } 723 | if (level.data[(i + 1) * level.width + j] !== 2) { 724 | // bottom edge 725 | addedge(j * 32, i * 32 + 32, j * 32 + 32, i * 32 + 32, 'bottom'); 726 | } 727 | } 728 | } 729 | 730 | // More brute force (there should be something more elegant, surely?). We don't 731 | // always want our visibility to end _right_ at the edge of a tile, perhaps we'd 732 | // like to cut into the tile; but our simplistic algorithm above doesn't distinguish 733 | // between concave and convex corners. So we make up a bit of the legwork here. 734 | this.losEdges = Object.keys(edges).map(k => { 735 | let ax = 0, bx = 0, ay = 0, by = 0, flip = false; 736 | let cut = this.tileVisibilityInset; 737 | 738 | if (k.endsWith('left')) { 739 | ax = bx = -cut; 740 | ay = -cut; 741 | by = cut; 742 | flip = true; 743 | if (!Util.wallAtXY(edges[k][0] + ax, edges[k][1] + ay)) ay = -ay; 744 | if (!Util.wallAtXY(edges[k][2] + bx, edges[k][3] + by)) by = -by; 745 | } else if (k.endsWith('right')) { 746 | ax = bx = cut; 747 | ay = -cut; 748 | by = cut; 749 | if (!Util.wallAtXY(edges[k][0] + ax, edges[k][1] + ay)) ay = -ay; 750 | if (!Util.wallAtXY(edges[k][2] + bx, edges[k][3] + by)) by = -by; 751 | } else if (k.endsWith('top')) { 752 | ay = by = -cut; 753 | ax = -cut; 754 | bx = cut; 755 | if (!Util.wallAtXY(edges[k][0] + ax, edges[k][1] + ay)) ax = -ax; 756 | if (!Util.wallAtXY(edges[k][2] + bx, edges[k][3] + by)) bx = -bx; 757 | } else if (k.endsWith('bottom')) { 758 | ay = by = cut; 759 | ax = -cut; 760 | bx = cut; 761 | flip = true; 762 | if (!Util.wallAtXY(edges[k][0] + ax, edges[k][1] + ay)) ax = -ax; 763 | if (!Util.wallAtXY(edges[k][2] + bx, edges[k][3] + by)) bx = -bx; 764 | } 765 | 766 | let result = { 767 | p1: { 768 | x: edges[k][0] + ax, 769 | y: edges[k][1] + ay 770 | }, 771 | p2: { 772 | x: edges[k][2] + bx, 773 | y: edges[k][3] + by 774 | } 775 | }; 776 | 777 | // Definitely room for improvement here (and in this whole function). I've managed 778 | // to scrape together something that works, but making it work in the general case 779 | // (and correctly) is beyond me in 30 days :). 780 | // 781 | // This "flips" the appropriate edges so that ALL edges produced by this function 782 | // are clockwise (that is: following the edge from p1->p2 should always have floor 783 | // on the LEFT side and wall on the RIGHT side). This allows us to make a lot of 784 | // time-saving assumptions in the pathing phase. 785 | if (flip) [result.p1, result.p2] = [result.p2, result.p1]; 786 | 787 | return result; 788 | }); 789 | } 790 | 791 | // Cheat codes are disabled for submission. They are more of a development tool than 792 | // actually useful for playing. (Actually, I think cheat codes are kind of a fun 793 | // easter egg, but I needed those extra bytes!) 794 | _handleCheatCodes() { 795 | /* 796 | // GOTOnn (nn = 01-99, number of a valid level) 797 | if (this.input.queue[0] >= '0' && this.input.queue[0] <= '9' && 798 | this.input.queue[1] >= '0' && this.input.queue[1] <= '9' && 799 | this.input.queue[2] === 'o' && 800 | this.input.queue[3] === 't' && 801 | this.input.queue[4] === 'o' && 802 | this.input.queue[5] === 'g') { 803 | this._pendingLevelIndex = parseInt(this.input.queue[1] + this.input.queue[0], 10) - 1; 804 | if (this._pendingLevelIndex >= LevelCache.length || this._pendingLevelIndex < 0) { 805 | this._pendingLevelIndex = undefined; 806 | } 807 | this.input.queue = []; 808 | // DEAD 809 | } else if (this.input.queue[0] === 'd' && 810 | this.input.queue[1] === 'a' && 811 | this.input.queue[2] === 'e' && 812 | this.input.queue[3] === 'd') { 813 | this.playerDied(); 814 | this.input.queue = []; 815 | } 816 | */ 817 | } 818 | 819 | // This is what I ended up with instead, which is a basic map "flood fill". Because 820 | // none of my levels are very large, I didn't really have to implement an A* or 821 | // anything, I just do basic breadth-first search of the map. 822 | _buildAttackGrid() { 823 | let target = { 824 | x: game.player.x - Util.cos(game.facing) * 34, 825 | y: game.player.y - Util.sin(game.facing) * 34, 826 | }; 827 | 828 | let pu = Math.floor(target.x / 32); 829 | let pv = Math.floor(target.y / 32); 830 | let open = [[pu, pv, 2]]; 831 | let grid = []; 832 | 833 | const examine = (u, v, c) => { 834 | if (Util.wallAtUV(u, v)) { 835 | grid[v * this.level.width + u] = 50000; 836 | return; 837 | } 838 | 839 | let priorCost = grid[v * this.level.width + u]; 840 | 841 | if (!(u === pu && v === pv) && Util.pointSpottedXY(u * 32 + 16, v * 32 + 16)) { 842 | c += 10000; 843 | } 844 | 845 | if (!priorCost || c < priorCost) { 846 | grid[v * this.level.width + u] = c; 847 | open.push([u - 1, v, c + 32]); 848 | open.push([u + 1, v, c + 32]); 849 | open.push([u, v - 1, c + 32]); 850 | open.push([u, v + 1, c + 32]); 851 | } 852 | } 853 | 854 | while(open.length > 0) { 855 | let tile = open.shift(); 856 | examine(tile[0], tile[1], tile[2]); 857 | } 858 | 859 | this.attackGrid = grid; 860 | } 861 | } 862 | -------------------------------------------------------------------------------- /src/levels/level01.json: -------------------------------------------------------------------------------- 1 | { "height":100, 2 | "infinite":false, 3 | "layers":[ 4 | { 5 | "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 1, 2, 2, 1, 2, 2, 1, 2, 2, 1, 2, 2, 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 1, 2, 2, 1, 2, 2, 1, 2, 2, 1, 2, 2, 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 1, 2, 2, 1, 2, 1, 0, 1, 2, 1, 2, 2, 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 1, 0, 1, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 1, 0, 1, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 1, 2, 2, 1, 2, 1, 0, 1, 2, 1, 2, 2, 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 1, 0, 1, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 6 | "height":100, 7 | "name":"Terrain", 8 | "opacity":1, 9 | "type":"tilelayer", 10 | "visible":true, 11 | "width":100, 12 | "x":0, 13 | "y":0 14 | }, 15 | { 16 | "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 17 | "height":100, 18 | "name":"Meta", 19 | "opacity":1, 20 | "type":"tilelayer", 21 | "visible":true, 22 | "width":100, 23 | "x":0, 24 | "y":0 25 | }, 26 | { 27 | "draworder":"topdown", 28 | "name":"Objects", 29 | "objects":[ 30 | { 31 | "height":0, 32 | "id":4, 33 | "name":"", 34 | "point":true, 35 | "properties": 36 | { 37 | "Wake":"los" 38 | }, 39 | "propertytypes": 40 | { 41 | "Wake":"string" 42 | }, 43 | "rotation":0, 44 | "type":"enemy", 45 | "visible":true, 46 | "width":0, 47 | "x":1871, 48 | "y":1288 49 | }], 50 | "opacity":1, 51 | "type":"objectgroup", 52 | "visible":true, 53 | "x":0, 54 | "y":0 55 | }], 56 | "nextobjectid":6, 57 | "orientation":"orthogonal", 58 | "renderorder":"right-down", 59 | "tiledversion":"1.1.6", 60 | "tileheight":32, 61 | "tilesets":[ 62 | { 63 | "firstgid":1, 64 | "source":"raven.tsx" 65 | }], 66 | "tilewidth":32, 67 | "type":"map", 68 | "version":1, 69 | "width":100 70 | } 71 | --------------------------------------------------------------------------------