├── .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 |
--------------------------------------------------------------------------------