From f6cb01cc4d91a9deac8e8bb8d5bd4bbe6020304f Mon Sep 17 00:00:00 2001
From: Gooby <34070642+jmdiamond3@users.noreply.github.com>
Date: Wed, 8 Jan 2020 17:21:59 -0500
Subject: [PATCH 001/577] Set theme jekyll-theme-merlot
---
_config.yml | 1 +
1 file changed, 1 insertion(+)
create mode 100644 _config.yml
diff --git a/_config.yml b/_config.yml
new file mode 100644
index 00000000..c50ff38d
--- /dev/null
+++ b/_config.yml
@@ -0,0 +1 @@
+theme: jekyll-theme-merlot
\ No newline at end of file
From b16ca060a743f4cc4808816a6357031bc2b303cd Mon Sep 17 00:00:00 2001
From: Gooby <34070642+jmdiamond3@users.noreply.github.com>
Date: Wed, 8 Jan 2020 17:23:36 -0500
Subject: [PATCH 002/577] Create CNAME
---
CNAME | 1 +
1 file changed, 1 insertion(+)
create mode 100644 CNAME
diff --git a/CNAME b/CNAME
new file mode 100644
index 00000000..0c36ad75
--- /dev/null
+++ b/CNAME
@@ -0,0 +1 @@
+poop.com
\ No newline at end of file
From 9a0d3726df16f35e23bf25f8b66af3e3cd359510 Mon Sep 17 00:00:00 2001
From: Gooby <34070642+jmdiamond3@users.noreply.github.com>
Date: Wed, 8 Jan 2020 17:23:48 -0500
Subject: [PATCH 003/577] Delete CNAME
---
CNAME | 1 -
1 file changed, 1 deletion(-)
delete mode 100644 CNAME
diff --git a/CNAME b/CNAME
deleted file mode 100644
index 0c36ad75..00000000
--- a/CNAME
+++ /dev/null
@@ -1 +0,0 @@
-poop.com
\ No newline at end of file
From b7939f697291f006fa34660d3260b93f20b5e194 Mon Sep 17 00:00:00 2001
From: Gooby <34070642+jmdiamond3@users.noreply.github.com>
Date: Sun, 12 Jan 2020 11:30:54 -0500
Subject: [PATCH 004/577] Update a.js
---
a.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/a.js b/a.js
index bdcf7822..4cb24fad 100644
--- a/a.js
+++ b/a.js
@@ -2221,7 +2221,7 @@ function render() {
if (countSnakes() === 0) {
context.fillStyle = "#ff0";
context.font = "100px Arial";
- context.fillText("You Win!", 0, canvas.height / 2);
+ context.fillText("test!", 0, canvas.height / 2);
}
if (isDead()) {
context.fillStyle = "#f00";
From 5fa63e57fcca14c632f173f0bf78cffaa861f64b Mon Sep 17 00:00:00 2001
From: Gooby <34070642+jmdiamond3@users.noreply.github.com>
Date: Wed, 22 Jan 2020 15:55:49 -0500
Subject: [PATCH 005/577] Create index
---
index | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 82 insertions(+)
create mode 100644 index
diff --git a/index b/index
new file mode 100644
index 00000000..7ea92665
--- /dev/null
+++ b/index
@@ -0,0 +1,82 @@
+
+
+ Snakefall
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+
+ Edits: 0+0
+ |
+ |
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+ |
+
+
+
+
+ Controls (hover for hotkeys):
+
+ Arrows
/WASD to move
+
+
+
+
+ Moves:
0+0
+
+
+
+
+
+
+
+
+
+
+ Share link:
+
+
+
+
+
+
+ This game is a clone of
Snakebird by Noumenon Games.
+
+
+
+
+
From 13adf1526e0eb1211541d927a510583983c369a7 Mon Sep 17 00:00:00 2001
From: Gooby <34070642+jmdiamond3@users.noreply.github.com>
Date: Wed, 22 Jan 2020 15:57:00 -0500
Subject: [PATCH 006/577] Delete index.html
---
index.html | 77 ------------------------------------------------------
1 file changed, 77 deletions(-)
delete mode 100644 index.html
diff --git a/index.html b/index.html
deleted file mode 100644
index 358a21d9..00000000
--- a/index.html
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
- Snakefall
-
-
-
-
- |
-
-
- |
-
-
-
- Edits: 0+0
- |
- |
-
-
-
- |
- |
- |
- |
- |
- |
- |
- |
- |
- |
- |
-
- |
-
-
-
-
- Controls (hover for hotkeys):
-
- Arrows
/WASD to move
-
-
-
-
- Moves:
0+0
-
-
-
-
-
-
-
-
-
-
- Share link:
-
-
-
-
-
-
- This game is a clone of
Snakebird by Noumenon Games.
-
-
-
-
From da65c8a3295368f3d86ffcb906dc11f5e96036d0 Mon Sep 17 00:00:00 2001
From: Gooby <34070642+jmdiamond3@users.noreply.github.com>
Date: Wed, 22 Jan 2020 15:57:14 -0500
Subject: [PATCH 007/577] Rename index to index.html
---
index => index.html | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename index => index.html (100%)
diff --git a/index b/index.html
similarity index 100%
rename from index
rename to index.html
From 871ca1397594dd7b6b9e9ccfbcf88b8b4d54c41d Mon Sep 17 00:00:00 2001
From: Gooby <34070642+jmdiamond3@users.noreply.github.com>
Date: Wed, 22 Jan 2020 15:57:35 -0500
Subject: [PATCH 008/577] Delete a.js
---
a.js | 2758 ----------------------------------------------------------
1 file changed, 2758 deletions(-)
delete mode 100644 a.js
diff --git a/a.js b/a.js
deleted file mode 100644
index 4cb24fad..00000000
--- a/a.js
+++ /dev/null
@@ -1,2758 +0,0 @@
-function unreachable() { return new Error("unreachable"); }
-if (typeof VERSION !== "undefined") {
- document.getElementById("versionSpan").innerHTML =
- '' + VERSION.tag + '';
-}
-var canvas = document.getElementById("canvas");
-
-// tile codes
-var SPACE = 0;
-var WALL = 1;
-var SPIKE = 2;
-var FRUIT_v0 = 3; // legacy
-var EXIT = 4;
-var PORTAL = 5;
-var validTileCodes = [SPACE, WALL, SPIKE, EXIT, PORTAL];
-
-// object types
-var SNAKE = "s";
-var BLOCK = "b";
-var FRUIT = "f";
-
-var tileSize = 30;
-var level;
-var unmoveStuff = {undoStack:[], redoStack:[], spanId:"movesSpan", undoButtonId:"unmoveButton", redoButtonId:"removeButton"};
-var uneditStuff = {undoStack:[], redoStack:[], spanId:"editsSpan", undoButtonId:"uneditButton", redoButtonId:"reeditButton"};
-var paradoxes = [];
-function loadLevel(newLevel) {
- level = newLevel;
- currentSerializedLevel = compressSerialization(stringifyLevel(newLevel));
-
- activateAnySnakePlease();
- unmoveStuff.undoStack = [];
- unmoveStuff.redoStack = [];
- undoStuffChanged(unmoveStuff);
- uneditStuff.undoStack = [];
- uneditStuff.redoStack = [];
- undoStuffChanged(uneditStuff);
- blockSupportRenderCache = {};
- render();
-}
-
-
-var magicNumber_v0 = "3tFRIoTU";
-var magicNumber = "HyRr4JK1";
-var exampleLevel = magicNumber_v0 + "&" +
- "17&31" +
- "?" +
- "0000000000000000000000000000000" +
- "0000000000000000000000000000000" +
- "0000000000000000000000000000000" +
- "0000000000000000000000000000000" +
- "0000000000000000000000000000000" +
- "0000000000000000000000000000000" +
- "0000000000000000000040000000000" +
- "0000000000000110000000000000000" +
- "0000000000000111100000000000000" +
- "0000000000000011000000000000000" +
- "0000000000000010000010000000000" +
- "0000000000000010100011000000000" +
- "0000001111111000110000000110000" +
- "0000011111111111111111111110000" +
- "0000011111111101111111111100000" +
- "0000001111111100111111111100000" +
- "0000001111111000111111111100000" +
- "/" +
- "s0 ?351&350&349/" +
- "f0 ?328/" +
- "f1 ?366/";
-
-var testLevel_v0 = "3tFRIoTU&5&5?0005*00300024005*001000/b0?7&6&15&23/s3?18/s0?1&0&5/s1?2/s4?10/s2?17/b2?9/b3?14/b4?19/b1?4&20/b5?24/";
-var testLevel_v0_converted = "HyRr4JK1&5&5?0005*4024005*001000/b0?7&6&15&23/s3?18/s0?1&0&5/s1?2/s4?10/s2?17/b2?9/b3?14/b4?19/b1?4&20/b5?24/f0?8/";
-
-function parseLevel(string) {
- // magic number
- var cursor = 0;
- skipWhitespace();
- var versionTag = string.substr(cursor, magicNumber.length);
- switch (versionTag) {
- case magicNumber_v0:
- case magicNumber: break;
- default: throw new Error("not a snakefall level");
- }
- cursor += magicNumber.length;
- consumeKeyword("&");
-
- var level = {
- height: -1,
- width: -1,
- map: [],
- objects: [],
- };
-
- // height, width
- level.height = readInt();
- consumeKeyword("&");
- level.width = readInt();
-
- // map
- var mapData = readRun();
- mapData = decompressSerialization(mapData);
- if (level.height * level.width !== mapData.length) throw parserError("height, width, and map.length do not jive");
- var upconvertedObjects = [];
- var fruitCount = 0;
- for (var i = 0; i < mapData.length; i++) {
- var tileCode = mapData[i].charCodeAt(0) - "0".charCodeAt(0);
- if (tileCode === FRUIT_v0 && versionTag === magicNumber_v0) {
- // fruit used to be a tile code. now it's an object.
- upconvertedObjects.push({
- type: FRUIT,
- id: fruitCount++,
- dead: false, // unused
- locations: [i],
- });
- tileCode = SPACE;
- }
- if (validTileCodes.indexOf(tileCode) === -1) throw parserError("invalid tilecode: " + JSON.stringify(mapData[i]));
- level.map.push(tileCode);
- }
-
- // objects
- skipWhitespace();
- while (cursor < string.length) {
- var object = {
- type: "?",
- id: -1,
- dead: false,
- locations: [],
- };
-
- // type
- object.type = string[cursor];
- var locationsLimit;
- if (object.type === SNAKE) locationsLimit = -1;
- else if (object.type === BLOCK) locationsLimit = -1;
- else if (object.type === FRUIT) locationsLimit = 1;
- else throw parserError("expected object type code");
- cursor += 1;
-
- // id
- object.id = readInt();
-
- // locations
- var locationsData = readRun();
- var locationStrings = locationsData.split("&");
- if (locationStrings.length === 0) throw parserError("locations must be non-empty");
- if (locationsLimit !== -1 && locationStrings.length > locationsLimit) throw parserError("too many locations");
-
- locationStrings.forEach(function(locationString) {
- var location = parseInt(locationString);
- if (!(0 <= location && location < level.map.length)) throw parserError("location out of bounds: " + JSON.stringify(locationString));
- object.locations.push(location);
- });
-
- level.objects.push(object);
- skipWhitespace();
- }
- for (var i = 0; i < upconvertedObjects.length; i++) {
- level.objects.push(upconvertedObjects[i]);
- }
-
- return level;
-
- function skipWhitespace() {
- while (" \n\t\r".indexOf(string[cursor]) !== -1) {
- cursor += 1;
- }
- }
- function consumeKeyword(keyword) {
- skipWhitespace();
- if (string.indexOf(keyword, cursor) !== cursor) throw parserError("expected " + JSON.stringify(keyword));
- cursor += 1;
- }
- function readInt() {
- skipWhitespace();
- for (var i = cursor; i < string.length; i++) {
- if ("0123456789".indexOf(string[i]) === -1) break;
- }
- var substring = string.substring(cursor, i);
- if (substring.length === 0) throw parserError("expected int");
- cursor = i;
- return parseInt(substring, 10);
- }
- function readRun() {
- consumeKeyword("?");
- var endIndex = string.indexOf("/", cursor);
- var substring = string.substring(cursor, endIndex);
- cursor = endIndex + 1;
- return substring;
- }
- function parserError(message) {
- return new Error("parse error at position " + cursor + ": " + message);
- }
-}
-
-function stringifyLevel(level) {
- var output = magicNumber + "&";
- output += level.height + "&" + level.width + "\n";
-
- output += "?\n";
- for (var r = 0; r < level.height; r++) {
- output += " " + level.map.slice(r * level.width, (r + 1) * level.width).join("") + "\n";
- }
- output += "/\n";
-
- output += serializeObjects(level.objects);
-
- // sanity check
- var shouldBeTheSame = parseLevel(output);
- if (!deepEquals(level, shouldBeTheSame)) throw asdf; // serialization/deserialization is broken
-
- return output;
-}
-function serializeObjects(objects) {
- var output = "";
- for (var i = 0; i < objects.length; i++) {
- var object = objects[i];
- output += object.type + object.id + " ";
- output += "?" + object.locations.join("&") + "/\n";
- }
- return output;
-}
-function serializeObjectState(object) {
- if (object == null) return [0,[]];
- return [object.dead, copyArray(object.locations)];
-}
-
-var base66 = "----0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
-function compressSerialization(string) {
- string = string.replace(/\s+/g, "");
- // run-length encode several 0's in a row, etc.
- // 2000000000000003 -> 2*A03 ("A" is 14 in base66 defined above)
- var result = "";
- var runStart = 0;
- for (var i = 1; i < string.length + 1; i++) {
- var runLength = i - runStart;
- if (string[i] === string[runStart] && runLength < base66.length - 1) continue;
- // end of run
- if (runLength >= 4) {
- // compress
- result += "*" + base66[runLength] + string[runStart];
- } else {
- // literal
- result += string.substring(runStart, i);
- }
- runStart = i;
- }
- return result;
-}
-function decompressSerialization(string) {
- string = string.replace(/\s+/g, "");
- var result = "";
- for (var i = 0; i < string.length; i++) {
- if (string[i] === "*") {
- i += 1;
- var runLength = base66.indexOf(string[i]);
- i += 1;
- var char = string[i];
- for (var j = 0; j < runLength; j++) {
- result += char;
- }
- } else {
- result += string[i];
- }
- }
- return result;
-}
-
-var replayMagicNumber = "nmGTi8PB";
-function stringifyReplay() {
- var output = replayMagicNumber + "&";
- // only specify the snake id in an input if it's different from the previous.
- // the first snake index is 0 to optimize for the single-snake case.
- var currentSnakeId = 0;
- for (var i = 0; i < unmoveStuff.undoStack.length; i++) {
- var firstChange = unmoveStuff.undoStack[i][0];
- if (firstChange[0] !== "i") throw unreachable();
- var snakeId = firstChange[1];
- var dr = firstChange[2];
- var dc = firstChange[3];
- var directionCode;
- if (dr ===-1 && dc === 0) directionCode = "u";
- else if (dr === 0 && dc ===-1) directionCode = "l";
- else if (dr === 1 && dc === 0) directionCode = "d";
- else if (dr === 0 && dc === 1) directionCode = "r";
- else throw unreachable();
- if (snakeId !== currentSnakeId) {
- output += snakeId; // int to string
- currentSnakeId = snakeId;
- }
- output += directionCode;
- }
- return output;
-}
-function parseAndLoadReplay(string) {
- string = decompressSerialization(string);
- var expectedPrefix = replayMagicNumber + "&";
- if (string.substring(0, expectedPrefix.length) !== expectedPrefix) throw new Error("unrecognized replay string");
- var cursor = expectedPrefix.length;
-
- // the starting snakeid is 0, which may not exist, but we only validate it when doing a move.
- activeSnakeId = 0;
- while (cursor < string.length) {
- var snakeIdStr = "";
- var c = string.charAt(cursor);
- cursor += 1;
- while ('0' <= c && c <= '9') {
- snakeIdStr += c;
- if (cursor >= string.length) throw new Error("replay string has unexpected end of input");
- c = string.charAt(cursor);
- cursor += 1;
- }
- if (snakeIdStr.length > 0) {
- activeSnakeId = parseInt(snakeIdStr);
- // don't just validate when switching snakes, but on every move.
- }
-
- // doing a move.
- if (!getSnakes().some(function(snake) {
- return snake.id === activeSnakeId;
- })) {
- throw new Error("invalid snake id: " + activeSnakeId);
- }
- switch (c) {
- case 'l': move( 0, -1); break;
- case 'u': move(-1, 0); break;
- case 'r': move( 0, 1); break;
- case 'd': move( 1, 0); break;
- default: throw new Error("replay string has invalid direction: " + c);
- }
- }
-
- // now that the replay was executed successfully, undo it all so that it's available in the redo buffer.
- reset(unmoveStuff);
- document.getElementById("removeButton").classList.add("click-me");
-}
-
-var currentSerializedLevel;
-function saveLevel() {
- if (isDead()) return alert("Can't save while you're dead!");
- var serializedLevel = compressSerialization(stringifyLevel(level));
- currentSerializedLevel = serializedLevel;
- var hash = "#level=" + serializedLevel;
- expectHash = hash;
- location.hash = hash;
-
- // This marks a starting point for solving the level.
- unmoveStuff.undoStack = [];
- unmoveStuff.redoStack = [];
- editorHasBeenTouched = false;
- undoStuffChanged(unmoveStuff);
-}
-
-function saveReplay() {
- if (dirtyState === EDITOR_DIRTY) return alert("Can't save a replay with unsaved editor changes.");
- // preserve the level in the url bar.
- var hash = "#level=" + currentSerializedLevel;
- if (dirtyState === REPLAY_DIRTY) {
- // there is a replay to save
- hash += "#replay=" + compressSerialization(stringifyReplay());
- }
- expectHash = hash;
- location.hash = hash;
-}
-
-function deepEquals(a, b) {
- if (a == null) return b == null;
- if (typeof a === "string" || typeof a === "number" || typeof a === "boolean") return a === b;
- if (Array.isArray(a)) {
- if (!Array.isArray(b)) return false;
- if (a.length !== b.length) return false;
- for (var i = 0; i < a.length; i++) {
- if (!deepEquals(a[i], b[i])) return false;
- }
- return true;
- }
- // must be objects
- var aKeys = Object.keys(a);
- var bKeys = Object.keys(b);
- if (aKeys.length !== bKeys.length) return false;
- aKeys.sort();
- bKeys.sort();
- if (!deepEquals(aKeys, bKeys)) return false;
- for (var i = 0; i < aKeys.length; i++) {
- if (!deepEquals(a[aKeys[i]], b[bKeys[i]])) return false;
- }
- return true;
-}
-
-function getLocation(level, r, c) {
- if (!isInBounds(level, r, c)) throw unreachable();
- return r * level.width + c;
-}
-function getRowcol(level, location) {
- if (location < 0 || location >= level.width * level.height) throw unreachable();
- var r = Math.floor(location / level.width);
- var c = location % level.width;
- return {r:r, c:c};
-}
-function isInBounds(level, r, c) {
- if (c < 0 || c >= level.width) return false;;
- if (r < 0 || r >= level.height) return false;;
- return true;
-}
-function offsetLocation(location, dr, dc) {
- var rowcol = getRowcol(level, location);
- return getLocation(level, rowcol.r + dr, rowcol.c + dc);
-}
-
-var SHIFT = 1;
-var CTRL = 2;
-var ALT = 4;
-document.addEventListener("keydown", function(event) {
- var modifierMask = (
- (event.shiftKey ? SHIFT : 0) |
- (event.ctrlKey ? CTRL : 0) |
- (event.altKey ? ALT : 0)
- );
- switch (event.keyCode) {
- case 37: // left
- if (modifierMask === 0) { move(0, -1); break; }
- return;
- case 38: // up
- if (modifierMask === 0) { move(-1, 0); break; }
- return;
- case 39: // right
- if (modifierMask === 0) { move(0, 1); break; }
- return;
- case 40: // down
- if (modifierMask === 0) { move(1, 0); break; }
- return;
- case 8: // backspace
- if (modifierMask === 0) { undo(unmoveStuff); break; }
- if (modifierMask === SHIFT) { redo(unmoveStuff); break; }
- return;
- case "Q".charCodeAt(0):
- if (modifierMask === 0) { undo(unmoveStuff); break; }
- if (modifierMask === SHIFT) { redo(unmoveStuff); break; }
- return;
- case "Z".charCodeAt(0):
- if (modifierMask === 0) { undo(unmoveStuff); break; }
- if (modifierMask === SHIFT) { redo(unmoveStuff); break; }
- if (persistentState.showEditor && modifierMask === CTRL) { undo(uneditStuff); break; }
- if (persistentState.showEditor && modifierMask === CTRL|SHIFT) { redo(uneditStuff); break; }
- return;
- case "Y".charCodeAt(0):
- if (modifierMask === 0) { redo(unmoveStuff); break; }
- if (persistentState.showEditor && modifierMask === CTRL) { redo(uneditStuff); break; }
- return;
- case "R".charCodeAt(0):
- if (persistentState.showEditor && modifierMask === SHIFT) { setPaintBrushTileCode("select"); break; }
- if (modifierMask === 0) { reset(unmoveStuff); break; }
- if (modifierMask === SHIFT) { unreset(unmoveStuff); break; }
- return;
-
- case 220: // backslash
- if (modifierMask === 0) { toggleShowEditor(); break; }
- return;
- case "A".charCodeAt(0):
- if (!persistentState.showEditor && modifierMask === 0) { move(0, -1); break; }
- if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(PORTAL); break; }
- if ( persistentState.showEditor && modifierMask === CTRL) { selectAll(); break; }
- return;
- case "E".charCodeAt(0):
- if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(SPACE); break; }
- return;
- case 46: // delete
- if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(SPACE); break; }
- return;
- case "W".charCodeAt(0):
- if (!persistentState.showEditor && modifierMask === 0) { move(-1, 0); break; }
- if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(WALL); break; }
- return;
- case "S".charCodeAt(0):
- if (!persistentState.showEditor && modifierMask === 0) { move(1, 0); break; }
- if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(SPIKE); break; }
- if ( persistentState.showEditor && modifierMask === SHIFT) { setPaintBrushTileCode("resize"); break; }
- if ( persistentState.showEditor && modifierMask === CTRL) { saveLevel(); break; }
- if (!persistentState.showEditor && modifierMask === CTRL) { saveReplay(); break; }
- if (modifierMask === (CTRL|SHIFT)) { saveReplay(); break; }
- return;
- case "X".charCodeAt(0):
- if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(EXIT); break; }
- if ( persistentState.showEditor && modifierMask === CTRL) { cutSelection(); break; }
- return;
- case "F".charCodeAt(0):
- if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(FRUIT); break; }
- return;
- case "D".charCodeAt(0):
- if (!persistentState.showEditor && modifierMask === 0) { move(0, 1); break; }
- if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(SNAKE); break; }
- return;
- case "B".charCodeAt(0):
- if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(BLOCK); break; }
- return;
- case "G".charCodeAt(0):
- if (modifierMask === 0) { toggleGrid(); break; }
- if ( persistentState.showEditor && modifierMask === SHIFT) { toggleGravity(); break; }
- return;
- case "C".charCodeAt(0):
- if ( persistentState.showEditor && modifierMask === SHIFT) { toggleCollision(); break; }
- if ( persistentState.showEditor && modifierMask === CTRL) { copySelection(); break; }
- return;
- case "V".charCodeAt(0):
- if ( persistentState.showEditor && modifierMask === CTRL) { setPaintBrushTileCode("paste"); break; }
- return;
- case 32: // spacebar
- case 9: // tab
- if (modifierMask === 0) { switchSnakes( 1); break; }
- if (modifierMask === SHIFT) { switchSnakes(-1); break; }
- return;
- case "1".charCodeAt(0):
- case "2".charCodeAt(0):
- case "3".charCodeAt(0):
- case "4".charCodeAt(0):
- var index = event.keyCode - "1".charCodeAt(0);
- var delta;
- if (modifierMask === 0) {
- delta = 1;
- } else if (modifierMask === SHIFT) {
- delta = -1;
- } else return;
- if (isAlive()) {
- (function() {
- var snakes = findSnakesOfColor(index);
- if (snakes.length === 0) return;
- for (var i = 0; i < snakes.length; i++) {
- if (snakes[i].id === activeSnakeId) {
- activeSnakeId = snakes[(i + delta + snakes.length) % snakes.length].id;
- return;
- }
- }
- activeSnakeId = snakes[0].id;
- })();
- }
- break;
- case 27: // escape
- if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(null); break; }
- return;
- default: return;
- }
- event.preventDefault();
- render();
-});
-
-document.getElementById("switchSnakesButton").addEventListener("click", function() {
- switchSnakes(1);
- render();
-});
-function switchSnakes(delta) {
- if (!isAlive()) return;
- var snakes = getSnakes();
- snakes.sort(compareId);
- for (var i = 0; i < snakes.length; i++) {
- if (snakes[i].id === activeSnakeId) {
- activeSnakeId = snakes[(i + delta + snakes.length) % snakes.length].id;
- return;
- }
- }
- activeSnakeId = snakes[0].id;
-}
-document.getElementById("showGridButton").addEventListener("click", function() {
- toggleGrid();
-});
-document.getElementById("saveProgressButton").addEventListener("click", function() {
- saveReplay();
-});
-document.getElementById("restartButton").addEventListener("click", function() {
- reset(unmoveStuff);
- render();
-});
-document.getElementById("unmoveButton").addEventListener("click", function() {
- undo(unmoveStuff);
- render();
-});
-document.getElementById("removeButton").addEventListener("click", function() {
- redo(unmoveStuff);
- render();
-});
-
-document.getElementById("showHideEditor").addEventListener("click", function() {
- toggleShowEditor();
-});
-function toggleShowEditor() {
- persistentState.showEditor = !persistentState.showEditor;
- savePersistentState();
- showEditorChanged();
-}
-function toggleGrid() {
- persistentState.showGrid = !persistentState.showGrid;
- savePersistentState();
- render();
-}
-["serializationTextarea", "shareLinkTextbox"].forEach(function(id) {
- document.getElementById(id).addEventListener("keydown", function(event) {
- // let things work normally
- event.stopPropagation();
- });
-});
-document.getElementById("submitSerializationButton").addEventListener("click", function() {
- var string = document.getElementById("serializationTextarea").value;
- try {
- var newLevel = parseLevel(string);
- } catch (e) {
- alert(e);
- return;
- }
- loadLevel(newLevel);
-});
-document.getElementById("shareLinkTextbox").addEventListener("focus", function() {
- setTimeout(function() {
- document.getElementById("shareLinkTextbox").select();
- }, 0);
-});
-
-var paintBrushTileCode = null;
-var paintBrushSnakeColorIndex = 0;
-var paintBrushBlockId = 0;
-var paintBrushObject = null;
-var selectionStart = null;
-var selectionEnd = null;
-var resizeDragAnchorRowcol = null;
-var clipboardData = null;
-var clipboardOffsetRowcol = null;
-var paintButtonIdAndTileCodes = [
- ["resizeButton", "resize"],
- ["selectButton", "select"],
- ["pasteButton", "paste"],
- ["paintSpaceButton", SPACE],
- ["paintWallButton", WALL],
- ["paintSpikeButton", SPIKE],
- ["paintExitButton", EXIT],
- ["paintFruitButton", FRUIT],
- ["paintPortalButton", PORTAL],
- ["paintSnakeButton", SNAKE],
- ["paintBlockButton", BLOCK],
-];
-paintButtonIdAndTileCodes.forEach(function(pair) {
- var id = pair[0];
- var tileCode = pair[1];
- document.getElementById(id).addEventListener("click", function() {
- setPaintBrushTileCode(tileCode);
- });
-});
-document.getElementById("uneditButton").addEventListener("click", function() {
- undo(uneditStuff);
- render();
-});
-document.getElementById("reeditButton").addEventListener("click", function() {
- redo(uneditStuff);
- render();
-});
-document.getElementById("saveLevelButton").addEventListener("click", function() {
- saveLevel();
-});
-document.getElementById("copyButton").addEventListener("click", function() {
- copySelection();
-});
-document.getElementById("cutButton").addEventListener("click", function() {
- cutSelection();
-});
-document.getElementById("cheatGravityButton").addEventListener("click", function() {
- toggleGravity();
-});
-document.getElementById("cheatCollisionButton").addEventListener("click", function() {
- toggleCollision();
-});
-function toggleGravity() {
- isGravityEnabled = !isGravityEnabled;
- isCollisionEnabled = true;
- refreshCheatButtonText();
-}
-function toggleCollision() {
- isCollisionEnabled = !isCollisionEnabled;
- isGravityEnabled = false;
- refreshCheatButtonText();
-}
-function refreshCheatButtonText() {
- document.getElementById("cheatGravityButton").textContent = isGravityEnabled ? "Gravity: ON" : "Gravity: OFF";
- document.getElementById("cheatGravityButton").style.background = isGravityEnabled ? "" : "#f88";
-
- document.getElementById("cheatCollisionButton").textContent = isCollisionEnabled ? "Collision: ON" : "Collision: OFF";
- document.getElementById("cheatCollisionButton").style.background = isCollisionEnabled ? "" : "#f88";
-}
-
-// be careful with location vs rowcol, because this variable is used when resizing
-var lastDraggingRowcol = null;
-var hoverLocation = null;
-var draggingChangeLog = null;
-canvas.addEventListener("mousedown", function(event) {
- if (event.altKey) return;
- if (event.button !== 0) return;
- event.preventDefault();
- var location = getLocationFromEvent(event);
- if (persistentState.showEditor && paintBrushTileCode != null) {
- // editor tool
- lastDraggingRowcol = getRowcol(level, location);
- if (paintBrushTileCode === "select") selectionStart = location;
- if (paintBrushTileCode === "resize") resizeDragAnchorRowcol = lastDraggingRowcol;
- draggingChangeLog = [];
- paintAtLocation(location, draggingChangeLog);
- } else {
- // playtime
- var object = findObjectAtLocation(location);
- if (object == null) return;
- if (object.type !== SNAKE) return;
- // active snake
- activeSnakeId = object.id;
- render();
- }
-});
-canvas.addEventListener("dblclick", function(event) {
- if (event.altKey) return;
- if (event.button !== 0) return;
- event.preventDefault();
- if (persistentState.showEditor && paintBrushTileCode === "select") {
- // double click with select tool
- var location = getLocationFromEvent(event);
- var object = findObjectAtLocation(location);
- if (object == null) return;
- stopDragging();
- if (object.type === SNAKE) {
- // edit snakes of this color
- paintBrushTileCode = SNAKE;
- paintBrushSnakeColorIndex = object.id % snakeColors.length;
- } else if (object.type === BLOCK) {
- // edit this particular block
- paintBrushTileCode = BLOCK;
- paintBrushBlockId = object.id;
- } else if (object.type === FRUIT) {
- // edit fruits, i guess
- paintBrushTileCode = FRUIT;
- } else throw unreachable();
- paintBrushTileCodeChanged();
- }
-});
-document.addEventListener("mouseup", function(event) {
- stopDragging();
-});
-function stopDragging() {
- if (lastDraggingRowcol != null) {
- // release the draggin'
- lastDraggingRowcol = null;
- paintBrushObject = null;
- resizeDragAnchorRowcol = null;
- pushUndo(uneditStuff, draggingChangeLog);
- draggingChangeLog = null;
- }
-}
-canvas.addEventListener("mousemove", function(event) {
- if (!persistentState.showEditor) return;
- var location = getLocationFromEvent(event);
- var mouseRowcol = getRowcol(level, location);
- if (lastDraggingRowcol != null) {
- // Dragging Force - Through the Fruit and Flames
- var lastDraggingLocation = getLocation(level, lastDraggingRowcol.r, lastDraggingRowcol.c);
- // we need to get rowcols for everything before we start dragging, because dragging might resize the world.
- var path = getNaiveOrthogonalPath(lastDraggingLocation, location).map(function(location) {
- return getRowcol(level, location);
- });
- path.forEach(function(rowcol) {
- // convert to location at the last minute in case each of these steps is changing the coordinate system.
- paintAtLocation(getLocation(level, rowcol.r, rowcol.c), draggingChangeLog);
- });
- lastDraggingRowcol = mouseRowcol;
- hoverLocation = null;
- } else {
- // hovering
- if (hoverLocation !== location) {
- hoverLocation = location;
- render();
- }
- }
-});
-canvas.addEventListener("mouseout", function() {
- if (hoverLocation !== location) {
- // turn off the hover when the mouse leaves
- hoverLocation = null;
- render();
- }
-});
-function getLocationFromEvent(event) {
- var r = Math.floor(eventToMouseY(event, canvas) / tileSize);
- var c = Math.floor(eventToMouseX(event, canvas) / tileSize);
- // since the canvas is centered, the bounding client rect can be half-pixel aligned,
- // resulting in slightly out-of-bounds mouse events.
- r = clamp(r, 0, level.height);
- c = clamp(c, 0, level.width);
- return getLocation(level, r, c);
-}
-function eventToMouseX(event, canvas) { return event.clientX - canvas.getBoundingClientRect().left; }
-function eventToMouseY(event, canvas) { return event.clientY - canvas.getBoundingClientRect().top; }
-
-function selectAll() {
- selectionStart = 0;
- selectionEnd = level.map.length - 1;
- setPaintBrushTileCode("select");
-}
-
-function setPaintBrushTileCode(tileCode) {
- if (tileCode === "paste") {
- // make sure we have something to paste
- if (clipboardData == null) return;
- }
- if (paintBrushTileCode === "select" && tileCode !== "select" && selectionStart != null && selectionEnd != null) {
- // usually this means to fill in the selection
- if (tileCode == null) {
- // cancel selection
- selectionStart = null;
- selectionEnd = null;
- return;
- }
- if (typeof tileCode === "number" && tileCode !== PORTAL) {
- // fill in the selection
- fillSelection(tileCode);
- selectionStart = null;
- selectionEnd = null;
- return;
- }
- // ok, just select something else then.
- selectionStart = null;
- selectionEnd = null;
- }
- if (tileCode === SNAKE) {
- if (paintBrushTileCode === SNAKE) {
- // next snake color
- paintBrushSnakeColorIndex = (paintBrushSnakeColorIndex + 1) % snakeColors.length;
- }
- } else if (tileCode === BLOCK) {
- var blocks = getBlocks();
- if (paintBrushTileCode === BLOCK && blocks.length > 0) {
- // cycle through block ids
- blocks.sort(compareId);
- if (paintBrushBlockId != null) {
- (function() {
- for (var i = 0; i < blocks.length; i++) {
- if (blocks[i].id === paintBrushBlockId) {
- i += 1;
- if (i < blocks.length) {
- // next block id
- paintBrushBlockId = blocks[i].id;
- } else {
- // new block id
- paintBrushBlockId = null;
- }
- return;
- }
- }
- throw unreachable()
- })();
- } else {
- // first one
- paintBrushBlockId = blocks[0].id;
- }
- } else {
- // new block id
- paintBrushBlockId = null;
- }
- } else if (tileCode == null) {
- // escape
- if (paintBrushTileCode === BLOCK && paintBrushBlockId != null) {
- // stop editing this block, but keep the block brush selected
- tileCode = BLOCK;
- paintBrushBlockId = null;
- }
- }
- paintBrushTileCode = tileCode;
- paintBrushTileCodeChanged();
-}
-function paintBrushTileCodeChanged() {
- paintButtonIdAndTileCodes.forEach(function(pair) {
- var id = pair[0];
- var tileCode = pair[1];
- var backgroundStyle = "";
- if (tileCode === paintBrushTileCode) {
- if (tileCode === SNAKE) {
- // show the color of the active snake in the color of the button
- backgroundStyle = snakeColors[paintBrushSnakeColorIndex];
- } else {
- backgroundStyle = "#ff0";
- }
- }
- document.getElementById(id).style.background = backgroundStyle;
- });
-
- var isSelectionMode = paintBrushTileCode === "select";
- ["cutButton", "copyButton"].forEach(function (id) {
- document.getElementById(id).disabled = !isSelectionMode;
- });
- document.getElementById("pasteButton").disabled = clipboardData == null;
-
- render();
-}
-
-function cutSelection() {
- copySelection();
- fillSelection(SPACE);
- render();
-}
-function copySelection() {
- var selectedLocations = getSelectedLocations();
- if (selectedLocations.length === 0) return;
- var selectedObjects = [];
- selectedLocations.forEach(function(location) {
- var object = findObjectAtLocation(location);
- if (object != null) addIfNotPresent(selectedObjects, object);
- });
- setClipboardData({
- level: JSON.parse(JSON.stringify(level)),
- selectedLocations: selectedLocations,
- selectedObjects: JSON.parse(JSON.stringify(selectedObjects)),
- });
-}
-function setClipboardData(data) {
- // find the center
- var minR = Infinity;
- var maxR = -Infinity;
- var minC = Infinity;
- var maxC = -Infinity;
- data.selectedLocations.forEach(function(location) {
- var rowcol = getRowcol(data.level, location);
- if (rowcol.r < minR) minR = rowcol.r;
- if (rowcol.r > maxR) maxR = rowcol.r;
- if (rowcol.c < minC) minC = rowcol.c;
- if (rowcol.c > maxC) maxC = rowcol.c;
- });
- var offsetR = Math.floor((minR + maxR) / 2);
- var offsetC = Math.floor((minC + maxC) / 2);
-
- clipboardData = data;
- clipboardOffsetRowcol = {r:offsetR, c:offsetC};
- paintBrushTileCodeChanged();
-}
-function fillSelection(tileCode) {
- var changeLog = [];
- var locations = getSelectedLocations();
- locations.forEach(function(location) {
- if (level.map[location] !== tileCode) {
- changeLog.push(["m", location, level.map[location], tileCode]);
- level.map[location] = tileCode;
- }
- removeAnyObjectAtLocation(location, changeLog);
- });
- pushUndo(uneditStuff, changeLog);
-}
-function getSelectedLocations() {
- if (selectionStart == null || selectionEnd == null) return [];
- var rowcol1 = getRowcol(level, selectionStart);
- var rowcol2 = getRowcol(level, selectionEnd);
- var r1 = rowcol1.r;
- var c1 = rowcol1.c;
- var r2 = rowcol2.r;
- var c2 = rowcol2.c;
- if (r2 < r1) {
- var tmp = r1;
- r1 = r2;
- r2 = tmp;
- }
- if (c2 < c1) {
- var tmp = c1;
- c1 = c2;
- c2 = tmp;
- }
- var objects = [];
- var locations = [];
- for (var r = r1; r <= r2; r++) {
- for (var c = c1; c <= c2; c++) {
- var location = getLocation(level, r, c);
- locations.push(location);
- var object = findObjectAtLocation(location);
- if (object != null) addIfNotPresent(objects, object);
- }
- }
- // select the rest of any partially-selected objects
- objects.forEach(function(object) {
- object.locations.forEach(function(location) {
- addIfNotPresent(locations, location);
- });
- });
- return locations;
-}
-
-function setHeight(newHeight, changeLog) {
- if (newHeight < level.height) {
- // crop
- for (var r = newHeight; r < level.height; r++) {
- for (var c = 0; c < level.width; c++) {
- var location = getLocation(level, r, c);
- removeAnyObjectAtLocation(location, changeLog);
- // also delete non-space tiles
- paintTileAtLocation(location, SPACE, changeLog);
- }
- }
- level.map.splice(newHeight * level.width);
- } else {
- // expand
- for (var r = level.height; r < newHeight; r++) {
- for (var c = 0; c < level.width; c++) {
- level.map.push(SPACE);
- }
- }
- }
- changeLog.push(["h", level.height, newHeight]);
- level.height = newHeight;
-}
-function setWidth(newWidth, changeLog) {
- if (newWidth < level.width) {
- // crop
- for (var r = level.height - 1; r >= 0; r--) {
- for (var c = level.width - 1; c >= newWidth; c--) {
- var location = getLocation(level, r, c);
- removeAnyObjectAtLocation(location, changeLog);
- paintTileAtLocation(location, SPACE, changeLog);
- level.map.splice(location, 1);
- }
- }
- } else {
- // expand
- for (var r = level.height - 1; r >= 0; r--) {
- var insertionPoint = level.width * (r + 1);
- for (var c = level.width; c < newWidth; c++) {
- // boy is this inefficient. ... YOLO!
- level.map.splice(insertionPoint, 0, SPACE);
- }
- }
- }
-
- var transformLocation = makeScaleCoordinatesFunction(level.width, newWidth);
- level.objects.forEach(function(object) {
- object.locations = object.locations.map(transformLocation);
- });
-
- changeLog.push(["w", level.width, newWidth]);
- level.width = newWidth;
-}
-
-function newSnake(color, location) {
- var snakes = findSnakesOfColor(color);
- snakes.sort(compareId);
- for (var i = 0; i < snakes.length; i++) {
- if (snakes[i].id !== i * snakeColors.length + color) break;
- }
- return {
- type: SNAKE,
- id: i * snakeColors.length + color,
- dead: false,
- locations: [location],
- };
-}
-function newBlock(location) {
- var blocks = getBlocks();
- blocks.sort(compareId);
- for (var i = 0; i < blocks.length; i++) {
- if (blocks[i].id !== i) break;
- }
- return {
- type: BLOCK,
- id: i,
- dead: false, // unused
- locations: [location],
- };
-}
-function newFruit(location) {
- var fruits = getObjectsOfType(FRUIT);
- fruits.sort(compareId);
- for (var i = 0; i < fruits.length; i++) {
- if (fruits[i].id !== i) break;
- }
- return {
- type: FRUIT,
- id: i,
- dead: false, // unused
- locations: [location],
- };
-}
-function paintAtLocation(location, changeLog) {
- if (typeof paintBrushTileCode === "number") {
- removeAnyObjectAtLocation(location, changeLog);
- paintTileAtLocation(location, paintBrushTileCode, changeLog);
- } else if (paintBrushTileCode === "resize") {
- var toRowcol = getRowcol(level, location);
- var dr = toRowcol.r - resizeDragAnchorRowcol.r;
- var dc = toRowcol.c - resizeDragAnchorRowcol.c;
- resizeDragAnchorRowcol = toRowcol;
- if (dr !== 0) setHeight(level.height + dr, changeLog);
- if (dc !== 0) setWidth(level.width + dc, changeLog);
- } else if (paintBrushTileCode === "select") {
- selectionEnd = location;
- } else if (paintBrushTileCode === "paste") {
- var hoverRowcol = getRowcol(level, location);
- var pastedData = previewPaste(hoverRowcol.r, hoverRowcol.c);
- pastedData.selectedLocations.forEach(function(location) {
- var tileCode = pastedData.level.map[location];
- removeAnyObjectAtLocation(location, changeLog);
- paintTileAtLocation(location, tileCode, changeLog);
- });
- pastedData.selectedObjects.forEach(function(object) {
- // refresh the ids so there are no collisions.
- if (object.type === SNAKE) {
- object.id = newSnake(object.id % snakeColors.length).id;
- } else if (object.type === BLOCK) {
- object.id = newBlock().id;
- } else if (object.type === FRUIT) {
- object.id = newFruit().id;
- } else throw unreachable();
- level.objects.push(object);
- changeLog.push([object.type, object.id, [0,[]], serializeObjectState(object)]);
- });
- } else if (paintBrushTileCode === SNAKE) {
- var oldSnakeSerialization = serializeObjectState(paintBrushObject);
- if (paintBrushObject != null) {
- // keep dragging
- if (paintBrushObject.locations[0] === location) return; // we just did that
- // watch out for self-intersection
- var selfIntersectionIndex = paintBrushObject.locations.indexOf(location);
- if (selfIntersectionIndex !== -1) {
- // truncate from here back
- paintBrushObject.locations.splice(selfIntersectionIndex);
- }
- }
-
- // make sure there's space behind us
- paintTileAtLocation(location, SPACE, changeLog);
- removeAnyObjectAtLocation(location, changeLog);
- if (paintBrushObject == null) {
- var thereWereNoSnakes = countSnakes() === 0;
- paintBrushObject = newSnake(paintBrushSnakeColorIndex, location);
- level.objects.push(paintBrushObject);
- if (thereWereNoSnakes) activateAnySnakePlease();
- } else {
- // extend le snake
- paintBrushObject.locations.unshift(location);
- }
- changeLog.push([paintBrushObject.type, paintBrushObject.id, oldSnakeSerialization, serializeObjectState(paintBrushObject)]);
- } else if (paintBrushTileCode === BLOCK) {
- var objectHere = findObjectAtLocation(location);
- if (paintBrushBlockId == null && objectHere != null && objectHere.type === BLOCK) {
- // just start editing this block
- paintBrushBlockId = objectHere.id;
- } else {
- // make a change
- // make sure there's space behind us
- paintTileAtLocation(location, SPACE, changeLog);
- var thisBlock = null;
- if (paintBrushBlockId != null) {
- thisBlock = findBlockById(paintBrushBlockId);
- }
- var oldBlockSerialization = serializeObjectState(thisBlock);
- if (thisBlock == null) {
- // create new block
- removeAnyObjectAtLocation(location, changeLog);
- thisBlock = newBlock(location);
- level.objects.push(thisBlock);
- paintBrushBlockId = thisBlock.id;
- } else {
- var existingIndex = thisBlock.locations.indexOf(location);
- if (existingIndex !== -1) {
- // reclicking part of this object means to delete just part of it.
- if (thisBlock.locations.length === 1) {
- // goodbye
- removeObject(thisBlock, changeLog);
- paintBrushBlockId = null;
- } else {
- thisBlock.locations.splice(existingIndex, 1);
- }
- } else {
- // add a tile to the block
- removeAnyObjectAtLocation(location, changeLog);
- thisBlock.locations.push(location);
- }
- }
- changeLog.push([thisBlock.type, thisBlock.id, oldBlockSerialization, serializeObjectState(thisBlock)]);
- delete blockSupportRenderCache[thisBlock.id];
- }
- } else if (paintBrushTileCode === FRUIT) {
- paintTileAtLocation(location, SPACE, changeLog);
- removeAnyObjectAtLocation(location, changeLog);
- var object = newFruit(location)
- level.objects.push(object);
- changeLog.push([object.type, object.id, serializeObjectState(null), serializeObjectState(object)]);
- } else throw unreachable();
- render();
-}
-
-function paintTileAtLocation(location, tileCode, changeLog) {
- if (level.map[location] === tileCode) return;
- changeLog.push(["m", location, level.map[location], tileCode]);
- level.map[location] = tileCode;
-}
-
-function pushUndo(undoStuff, changeLog) {
- // changeLog = [
- // ["i", 0, -1, 0, animationQueue, freshlyRemovedAnimatedObjects],
- // // player input for snake 0, dr:-1, dc:0. has no effect on state.
- // // "i" is always the first change in normal player movement.
- // // if a changeLog does not start with "i", then it is an editor action.
- // // animationQueue and freshlyRemovedAnimatedObjects
- // // are used for animating re-move.
- // ["m", 21, 0, 1], // map at location 23 changed from 0 to 1
- // ["s", 0, [false, [1,2]], [false, [2,3]]], // snake id 0 moved from alive at [1, 2] to alive at [2, 3]
- // ["s", 1, [false, [11,12]], [true, [12,13]]], // snake id 1 moved from alive at [11, 12] to dead at [12, 13]
- // ["b", 1, [false, [20,30]], [false, []]], // block id 1 was deleted from location [20, 30]
- // ["f", 0, [false, [40]], [false, []]], // fruit id 0 was deleted from location [40]
- // ["h", 25, 10], // height changed from 25 to 10. all cropped tiles are guaranteed to be SPACE.
- // ["w", 8, 10], // width changed from 8 to 10. a change in the coordinate system.
- // ["m", 23, 2, 0], // map at location 23 changed from 2 to 0 in the new coordinate system.
- // 10, // the last change is always a declaration of the final width of the map.
- // ];
- reduceChangeLog(changeLog);
- if (changeLog.length === 0) return;
- changeLog.push(level.width);
- undoStuff.undoStack.push(changeLog);
- undoStuff.redoStack = [];
- paradoxes = [];
-
- if (undoStuff === uneditStuff) editorHasBeenTouched = true;
-
- undoStuffChanged(undoStuff);
-}
-function reduceChangeLog(changeLog) {
- for (var i = 0; i < changeLog.length - 1; i++) {
- var change = changeLog[i];
- if (change[0] === "i") {
- continue; // don't reduce player input
- } else if (change[0] === "h") {
- for (var j = i + 1; j < changeLog.length; j++) {
- var otherChange = changeLog[j];
- if (otherChange[0] === "h") {
- // combine
- change[2] = otherChange[2];
- changeLog.splice(j, 1);
- j--;
- continue;
- } else if (otherChange[0] === "w") {
- continue; // no interaction between height and width
- } else break; // no more reduction possible
- }
- if (change[1] === change[2]) {
- // no change
- changeLog.splice(i, 1);
- i--;
- }
- } else if (change[0] === "w") {
- for (var j = i + 1; j < changeLog.length; j++) {
- var otherChange = changeLog[j];
- if (otherChange[0] === "w") {
- // combine
- change[2] = otherChange[2];
- changeLog.splice(j, 1);
- j--;
- continue;
- } else if (otherChange[0] === "h") {
- continue; // no interaction between height and width
- } else break; // no more reduction possible
- }
- if (change[1] === change[2]) {
- // no change
- changeLog.splice(i, 1);
- i--;
- }
- } else if (change[0] === "m") {
- for (var j = i + 1; j < changeLog.length; j++) {
- var otherChange = changeLog[j];
- if (otherChange[0] === "m" && otherChange[1] === change[1]) {
- // combine
- change[3] = otherChange[3];
- changeLog.splice(j, 1);
- j--;
- } else if (otherChange[0] === "w" || otherChange[0] === "h") {
- break; // can't reduce accros resizes
- }
- }
- if (change[2] === change[3]) {
- // no change
- changeLog.splice(i, 1);
- i--;
- }
- } else if (change[0] === SNAKE || change[0] === BLOCK || change[0] === FRUIT) {
- for (var j = i + 1; j < changeLog.length; j++) {
- var otherChange = changeLog[j];
- if (otherChange[0] === change[0] && otherChange[1] === change[1]) {
- // combine
- change[3] = otherChange[3];
- changeLog.splice(j, 1);
- j--;
- } else if (otherChange[0] === "w" || otherChange[0] === "h") {
- break; // can't reduce accros resizes
- }
- }
- if (deepEquals(change[2], change[3])) {
- // no change
- changeLog.splice(i, 1);
- i--;
- }
- } else throw unreachable();
- }
-}
-function undo(undoStuff) {
- if (undoStuff.undoStack.length === 0) return; // already at the beginning
- animationQueue = [];
- animationQueueCursor = 0;
- paradoxes = [];
- undoOneFrame(undoStuff);
- undoStuffChanged(undoStuff);
-}
-function reset(undoStuff) {
- animationQueue = [];
- animationQueueCursor = 0;
- paradoxes = [];
- while (undoStuff.undoStack.length > 0) {
- undoOneFrame(undoStuff);
- }
- undoStuffChanged(undoStuff);
-}
-function undoOneFrame(undoStuff) {
- var doThis = undoStuff.undoStack.pop();
- var redoChangeLog = [];
- undoChanges(doThis, redoChangeLog);
- if (redoChangeLog.length > 0) {
- redoChangeLog.push(level.width);
- undoStuff.redoStack.push(redoChangeLog);
- }
-
- if (undoStuff === uneditStuff) editorHasBeenTouched = true;
-}
-function redo(undoStuff) {
- if (undoStuff.redoStack.length === 0) return; // already at the beginning
- animationQueue = [];
- animationQueueCursor = 0;
- paradoxes = [];
- redoOneFrame(undoStuff);
- undoStuffChanged(undoStuff);
-}
-function unreset(undoStuff) {
- animationQueue = [];
- animationQueueCursor = 0;
- paradoxes = [];
- while (undoStuff.redoStack.length > 0) {
- redoOneFrame(undoStuff);
- }
- undoStuffChanged(undoStuff);
-
- // don't animate the last frame
- animationQueue = [];
- animationQueueCursor = 0;
- freshlyRemovedAnimatedObjects = [];
-}
-function redoOneFrame(undoStuff) {
- var doThis = undoStuff.redoStack.pop();
- var undoChangeLog = [];
- undoChanges(doThis, undoChangeLog);
- if (undoChangeLog.length > 0) {
- undoChangeLog.push(level.width);
- undoStuff.undoStack.push(undoChangeLog);
- }
-
- if (undoStuff === uneditStuff) editorHasBeenTouched = true;
-}
-function undoChanges(changes, changeLog) {
- var widthContext = changes.pop();
- var transformLocation = widthContext === level.width ? identityFunction : makeScaleCoordinatesFunction(widthContext, level.width);
- for (var i = changes.length - 1; i >= 0; i--) {
- var paradoxDescription = undoChange(changes[i]);
- if (paradoxDescription != null) paradoxes.push(paradoxDescription);
- }
-
- var lastChange = changes[changes.length - 1];
- if (lastChange[0] === "i") {
- // replay animation
- animationQueue = lastChange[4];
- animationQueueCursor = 0;
- freshlyRemovedAnimatedObjects = lastChange[5];
- animationStart = new Date().getTime();
- }
-
- function undoChange(change) {
- // note: everything here is going backwards: to -> from
- if (change[0] === "i") {
- // no state change, but preserve the intention.
- changeLog.push(change);
- return null;
- } else if (change[0] === "h") {
- // change height
- var fromHeight = change[1];
- var toHeight = change[2];
- if (level.height !== toHeight) return "Impossible";
- setHeight(fromHeight, changeLog);
- } else if (change[0] === "w") {
- // change width
- var fromWidth = change[1];
- var toWidth = change[2];
- if (level.width !== toWidth) return "Impossible";
- setWidth(fromWidth, changeLog);
- } else if (change[0] === "m") {
- // change map tile
- var location = transformLocation(change[1]);
- var fromTileCode = change[2];
- var toTileCode = change[3];
- if (location >= level.map.length) return "Can't turn " + describe(toTileCode) + " into " + describe(fromTileCode) + " out of bounds";
- if (level.map[location] !== toTileCode) return "Can't turn " + describe(toTileCode) + " into " + describe(fromTileCode) + " because there's " + describe(level.map[location]) + " there now";
- paintTileAtLocation(location, fromTileCode, changeLog);
- } else if (change[0] === SNAKE || change[0] === BLOCK || change[0] === FRUIT) {
- // change object
- var type = change[0];
- var id = change[1];
- var fromDead = change[2][0];
- var toDead = change[3][0];
- var fromLocations = change[2][1].map(transformLocation);
- var toLocations = change[3][1].map(transformLocation);
- if (fromLocations.filter(function(location) { return location >= level.map.length; }).length > 0) {
- return "Can't move " + describe(type, id) + " out of bounds";
- }
- var object = findObjectOfTypeAndId(type, id);
- if (toLocations.length !== 0) {
- // should exist at this location
- if (object == null) return "Can't move " + describe(type, id) + " because it doesn't exit";
- if (!deepEquals(object.locations, toLocations)) return "Can't move " + describe(object) + " because it's in the wrong place";
- if (object.dead !== toDead) return "Can't move " + describe(object) + " because it's alive/dead state doesn't match";
- // doit
- if (fromLocations.length !== 0) {
- var oldState = serializeObjectState(object);
- object.locations = fromLocations;
- object.dead = fromDead;
- changeLog.push([object.type, object.id, oldState, serializeObjectState(object)]);
- } else {
- removeObject(object, changeLog);
- }
- } else {
- // shouldn't exist
- if (object != null) return "Can't create " + describe(type, id) + " because it already exists";
- // doit
- object = {
- type: type,
- id: id,
- dead: fromDead,
- locations: fromLocations,
- };
- level.objects.push(object);
- changeLog.push([object.type, object.id, [0,[]], serializeObjectState(object)]);
- }
- } else throw unreachable();
- }
-}
-function describe(arg1, arg2) {
- // describe(0) -> "Space"
- // describe(SNAKE, 0) -> "Snake 0 (Red)"
- // describe(object) -> "Snake 0 (Red)"
- // describe(BLOCK, 1) -> "Block 1"
- // describe(FRUIT) -> "Fruit"
- if (typeof arg1 === "number") {
- switch (arg1) {
- case SPACE: return "Space";
- case WALL: return "a Wall";
- case SPIKE: return "Spikes";
- case EXIT: return "an Exit";
- case PORTAL: return "a Portal";
- default: throw unreachable();
- }
- }
- if (arg1 === SNAKE) {
- var color = (function() {
- switch (snakeColors[arg2 % snakeColors.length]) {
- case "#f00": return " (Red)";
- case "#0f0": return " (Green)";
- case "#00f": return " (Blue)";
- case "#ff0": return " (Yellow)";
- default: throw unreachable();
- }
- })();
- return "Snake " + arg2 + color;
- }
- if (arg1 === BLOCK) {
- return "Block " + arg2;
- }
- if (arg1 === FRUIT) {
- return "Fruit";
- }
- if (typeof arg1 === "object") return describe(arg1.type, arg1.id);
- throw unreachable();
-}
-
-function undoStuffChanged(undoStuff) {
- var movesText = undoStuff.undoStack.length + "+" + undoStuff.redoStack.length;
- document.getElementById(undoStuff.spanId).textContent = movesText;
- document.getElementById(undoStuff.undoButtonId).disabled = undoStuff.undoStack.length === 0;
- document.getElementById(undoStuff.redoButtonId).disabled = undoStuff.redoStack.length === 0;
-
- // render paradox display
- var uniqueParadoxes = [];
- var paradoxCounts = [];
- paradoxes.forEach(function(paradoxDescription) {
- var index = uniqueParadoxes.indexOf(paradoxDescription);
- if (index !== -1) {
- paradoxCounts[index] += 1;
- } else {
- uniqueParadoxes.push(paradoxDescription);
- paradoxCounts.push(1);
- }
- });
- var paradoxDivContent = "";
- uniqueParadoxes.forEach(function(paradox, i) {
- if (i > 0) paradoxDivContent += "
\n";
- if (paradoxCounts[i] > 1) paradoxDivContent += "(" + paradoxCounts[i] + "x) ";
- paradoxDivContent += "Time Travel Paradox! " + uniqueParadoxes[i];
- });
- document.getElementById("paradoxDiv").innerHTML = paradoxDivContent;
-
- updateDirtyState();
-
- if (unmoveStuff.redoStack.length === 0) {
- document.getElementById("removeButton").classList.remove("click-me");
- }
-}
-
-var CLEAN_NO_TIMELINES = 0;
-var CLEAN_WITH_REDO = 1;
-var REPLAY_DIRTY = 2;
-var EDITOR_DIRTY = 3;
-var dirtyState = CLEAN_NO_TIMELINES;
-var editorHasBeenTouched = false;
-function updateDirtyState() {
- if (haveCheatcodesBeenUsed() || editorHasBeenTouched) {
- dirtyState = EDITOR_DIRTY;
- } else if (unmoveStuff.undoStack.length > 0) {
- dirtyState = REPLAY_DIRTY;
- } else if (unmoveStuff.redoStack.length > 0) {
- dirtyState = CLEAN_WITH_REDO;
- } else {
- dirtyState = CLEAN_NO_TIMELINES;
- }
-
- var saveLevelButton = document.getElementById("saveLevelButton");
- // the save button clears your timelines
- saveLevelButton.disabled = dirtyState === CLEAN_NO_TIMELINES;
- if (dirtyState >= EDITOR_DIRTY) {
- // you should save
- saveLevelButton.classList.add("click-me");
- saveLevelButton.textContent = "*" + "Save Level";
- } else {
- saveLevelButton.classList.remove("click-me");
- saveLevelButton.textContent = "Save Level";
- }
-
- var saveProgressButton = document.getElementById("saveProgressButton");
- // you can't save a replay if your level is dirty
- if (dirtyState === CLEAN_WITH_REDO) {
- saveProgressButton.textContent = "Forget Progress";
- } else {
- saveProgressButton.textContent = "Save Progress";
- }
- saveProgressButton.disabled = dirtyState >= EDITOR_DIRTY || dirtyState === CLEAN_NO_TIMELINES;
-}
-function haveCheatcodesBeenUsed() {
- return !unmoveStuff.undoStack.every(function(changeLog) {
- // normal movement always starts with "i".
- return changeLog[0][0] === "i";
- });
-}
-
-var persistentState = {
- showEditor: false,
- showGrid: false,
-};
-function savePersistentState() {
- localStorage.snakefall = JSON.stringify(persistentState);
-}
-function loadPersistentState() {
- try {
- persistentState = JSON.parse(localStorage.snakefall);
- } catch (e) {
- }
- persistentState.showEditor = !!persistentState.showEditor;
- persistentState.showGrid = !!persistentState.showGrid;
- showEditorChanged();
-}
-var isGravityEnabled = true;
-function isGravity() {
- return isGravityEnabled || !persistentState.showEditor;
-}
-var isCollisionEnabled = true;
-function isCollision() {
- return isCollisionEnabled || !persistentState.showEditor;
-}
-function isAnyCheatcodeEnabled() {
- return persistentState.showEditor && (
- !isGravityEnabled || !isCollisionEnabled
- );
-}
-
-
-function showEditorChanged() {
- document.getElementById("showHideEditor").textContent = (persistentState.showEditor ? "Hide" : "Show") + " Editor Stuff";
- ["editorDiv", "editorPane"].forEach(function(id) {
- document.getElementById(id).style.display = persistentState.showEditor ? "block" : "none";
- });
- document.getElementById("wasdSpan").textContent = persistentState.showEditor ? "" : "/WASD";
-
- render();
-}
-
-function move(dr, dc) {
- if (!isAlive()) return;
- animationQueue = [];
- animationQueueCursor = 0;
- freshlyRemovedAnimatedObjects = [];
- animationStart = new Date().getTime();
- var activeSnake = findActiveSnake();
- var headRowcol = getRowcol(level, activeSnake.locations[0]);
- var newRowcol = {r:headRowcol.r + dr, c:headRowcol.c + dc};
- if (!isInBounds(level, newRowcol.r, newRowcol.c)) return;
- var newLocation = getLocation(level, newRowcol.r, newRowcol.c);
- var changeLog = [];
-
- // The changeLog for a player movement starts with the input
- // when playing normally.
- if (!isAnyCheatcodeEnabled()) {
- changeLog.push(["i", activeSnake.id, dr, dc, animationQueue, freshlyRemovedAnimatedObjects]);
- }
-
- var ate = false;
- var pushedObjects = [];
-
- if (isCollision()) {
- var newTile = level.map[newLocation];
- if (!isTileCodeAir(newTile)) return; // can't go through that tile
- var otherObject = findObjectAtLocation(newLocation);
- if (otherObject != null) {
- if (otherObject === activeSnake) return; // can't push yourself
- if (otherObject.type === FRUIT) {
- // eat
- removeObject(otherObject, changeLog);
- ate = true;
- } else {
- // push objects
- if (!checkMovement(activeSnake, otherObject, dr, dc, pushedObjects)) return false;
- }
- }
- }
-
- // slither forward
- var activeSnakeOldState = serializeObjectState(activeSnake);
- var size1 = activeSnake.locations.length === 1;
- var slitherAnimations = [
- 70,
- [
- // size-1 snakes really do more of a move than a slither
- size1 ? MOVE_SNAKE : SLITHER_HEAD,
- activeSnake.id,
- dr,
- dc,
- ]
- ];
- activeSnake.locations.unshift(newLocation);
- if (!ate) {
- // drag your tail forward
- var oldRowcol = getRowcol(level, activeSnake.locations[activeSnake.locations.length - 1]);
- var newRowcol = getRowcol(level, activeSnake.locations[activeSnake.locations.length - 2]);
- if (!size1) {
- slitherAnimations.push([
- SLITHER_TAIL,
- activeSnake.id,
- newRowcol.r - oldRowcol.r,
- newRowcol.c - oldRowcol.c,
- ]);
- }
- activeSnake.locations.pop();
- }
- changeLog.push([activeSnake.type, activeSnake.id, activeSnakeOldState, serializeObjectState(activeSnake)]);
-
- // did you just push your face into a portal?
- var portalLocations = getActivePortalLocations();
- var portalActivationLocations = [];
- if (portalLocations.indexOf(newLocation) !== -1) {
- portalActivationLocations.push(newLocation);
- }
- // push everything, too
- moveObjects(pushedObjects, dr, dc, portalLocations, portalActivationLocations, changeLog, slitherAnimations);
- animationQueue.push(slitherAnimations);
-
- // gravity loop
- var stateToAnimationIndex = {};
- if (isGravity()) for (var fallHeight = 1;; fallHeight++) {
- var serializedState = serializeObjects(level.objects);
- var infiniteLoopStartIndex = stateToAnimationIndex[serializedState];
- if (infiniteLoopStartIndex != null) {
- // infinite loop
- animationQueue.push([0, [INFINITE_LOOP, animationQueue.length - infiniteLoopStartIndex]]);
- break;
- } else {
- stateToAnimationIndex[serializedState] = animationQueue.length;
- }
- // do portals separate from falling logic
- if (portalActivationLocations.length === 1) {
- var portalAnimations = [500];
- if (activatePortal(portalLocations, portalActivationLocations[0], portalAnimations, changeLog)) {
- animationQueue.push(portalAnimations);
- }
- portalActivationLocations = [];
- }
- // now do falling logic
- var didAnything = false;
- var fallingAnimations = [
- 70 / Math.sqrt(fallHeight),
- ];
- var exitAnimationQueue = [];
-
- // check for exit
- if (!isUneatenFruit()) {
- var snakes = getSnakes();
- for (var i = 0; i < snakes.length; i++) {
- var snake = snakes[i];
- if (level.map[snake.locations[0]] === EXIT) {
- // (one of) you made it!
- removeAnimatedObject(snake, changeLog);
- exitAnimationQueue.push([
- 200,
- [EXIT_SNAKE, snake.id, 0, 0],
- ]);
- didAnything = true;
- }
- }
- }
-
- // fall
- var dyingObjects = [];
- var fallingObjects = level.objects.filter(function(object) {
- if (object.type === FRUIT) return; // can't fall
- var theseDyingObjects = [];
- if (!checkMovement(null, object, 1, 0, [], theseDyingObjects)) return false;
- // this object can fall. maybe more will fall with it too. we'll check those separately.
- theseDyingObjects.forEach(function(object) {
- addIfNotPresent(dyingObjects, object);
- });
- return true;
- });
- if (dyingObjects.length > 0) {
- var anySnakesDied = false;
- dyingObjects.forEach(function(object) {
- if (object.type === SNAKE) {
- // look what you've done
- var oldState = serializeObjectState(object);
- object.dead = true;
- changeLog.push([object.type, object.id, oldState, serializeObjectState(object)]);
- anySnakesDied = true;
- } else if (object.type === BLOCK) {
- // a box fell off the world
- removeAnimatedObject(object, changeLog);
- removeFromArray(fallingObjects, object);
- exitAnimationQueue.push([
- 200,
- [
- DIE_BLOCK,
- object.id,
- 0, 0
- ],
- ]);
- didAnything = true;
- } else throw unreachable();
- });
- if (anySnakesDied) break;
- }
- if (fallingObjects.length > 0) {
- moveObjects(fallingObjects, 1, 0, portalLocations, portalActivationLocations, changeLog, fallingAnimations);
- didAnything = true;
- }
-
- if (!didAnything) break;
- Array.prototype.push.apply(animationQueue, exitAnimationQueue);
- if (fallingAnimations.length > 1) animationQueue.push(fallingAnimations);
- }
-
- pushUndo(unmoveStuff, changeLog);
- render();
-}
-
-function checkMovement(pusher, pushedObject, dr, dc, pushedObjects, dyingObjects) {
- // pusher can be null (for gravity)
- pushedObjects.push(pushedObject);
- // find forward locations
- var forwardLocations = [];
- for (var i = 0; i < pushedObjects.length; i++) {
- pushedObject = pushedObjects[i];
- for (var j = 0; j < pushedObject.locations.length; j++) {
- var rowcol = getRowcol(level, pushedObject.locations[j]);
- var forwardRowcol = {r:rowcol.r + dr, c:rowcol.c + dc};
- if (!isInBounds(level, forwardRowcol.r, forwardRowcol.c)) {
- if (dyingObjects == null) {
- // can't push things out of bounds
- return false;
- } else {
- // this thing is going to fall out of bounds
- addIfNotPresent(dyingObjects, pushedObject);
- addIfNotPresent(pushedObjects, pushedObject);
- continue;
- }
- }
- var forwardLocation = getLocation(level, forwardRowcol.r, forwardRowcol.c);
- var yetAnotherObject = findObjectAtLocation(forwardLocation);
- if (yetAnotherObject != null) {
- if (yetAnotherObject.type === FRUIT) {
- // not pushable
- return false;
- }
- if (yetAnotherObject === pusher) {
- // indirect pushing ourselves.
- // special check for when we're indirectly pushing the tip of our own tail.
- if (forwardLocation === pusher.locations[pusher.locations.length -1]) {
- // for some reason this is ok.
- continue;
- }
- return false;
- }
- addIfNotPresent(pushedObjects, yetAnotherObject);
- } else {
- addIfNotPresent(forwardLocations, forwardLocation);
- }
- }
- }
- // check forward locations
- for (var i = 0; i < forwardLocations.length; i++) {
- var forwardLocation = forwardLocations[i];
- // many of these locations can be inside objects,
- // but that means the tile must be air,
- // and we already know pushing that object.
- var tileCode = level.map[forwardLocation];
- if (!isTileCodeAir(tileCode)) {
- if (dyingObjects != null) {
- if (tileCode === SPIKE) {
- // uh... which object was this again?
- var deadObject = findObjectAtLocation(offsetLocation(forwardLocation, -dr, -dc));
- if (deadObject.type === SNAKE) {
- // ouch!
- addIfNotPresent(dyingObjects, deadObject);
- continue;
- }
- }
- }
- // can't push into something solid
- return false;
- }
- }
- // the push is go
- return true;
-}
-
-function activateAnySnakePlease() {
- var snakes = getSnakes();
- if (snakes.length === 0) return; // nope.avi
- activeSnakeId = snakes[0].id;
-}
-
-function moveObjects(objects, dr, dc, portalLocations, portalActivationLocations, changeLog, animations) {
- objects.forEach(function(object) {
- var oldState = serializeObjectState(object);
- var oldPortals = getSetIntersection(portalLocations, object.locations);
- for (var i = 0; i < object.locations.length; i++) {
- object.locations[i] = offsetLocation(object.locations[i], dr, dc);
- }
- changeLog.push([object.type, object.id, oldState, serializeObjectState(object)]);
- animations.push([
- "m" + object.type, // MOVE_SNAKE | MOVE_BLOCK
- object.id,
- dr,
- dc,
- ]);
-
- var newPortals = getSetIntersection(portalLocations, object.locations);
- var activatingPortals = newPortals.filter(function(portalLocation) {
- return oldPortals.indexOf(portalLocation) === -1;
- });
- if (activatingPortals.length === 1) {
- // exactly one new portal we're touching. activate it
- portalActivationLocations.push(activatingPortals[0]);
- }
- });
-}
-
-function activatePortal(portalLocations, portalLocation, animations, changeLog) {
- var otherPortalLocation = portalLocations[1 - portalLocations.indexOf(portalLocation)];
- var portalRowcol = getRowcol(level, portalLocation);
- var otherPortalRowcol = getRowcol(level, otherPortalLocation);
- var delta = {r:otherPortalRowcol.r - portalRowcol.r, c:otherPortalRowcol.c - portalRowcol.c};
-
- var object = findObjectAtLocation(portalLocation);
- var newLocations = [];
- for (var i = 0; i < object.locations.length; i++) {
- var rowcol = getRowcol(level, object.locations[i]);
- var r = rowcol.r + delta.r;
- var c = rowcol.c + delta.c;
- if (!isInBounds(level, r, c)) return false; // out of bounds
- newLocations.push(getLocation(level, r, c));
- }
-
- for (var i = 0; i < newLocations.length; i++) {
- var location = newLocations[i];
- if (!isTileCodeAir(level.map[location])) return false; // blocked by tile
- var otherObject = findObjectAtLocation(location);
- if (otherObject != null && otherObject !== object) return false; // blocked by object
- }
-
- // zappo presto!
- var oldState = serializeObjectState(object);
- object.locations = newLocations;
- changeLog.push([object.type, object.id, oldState, serializeObjectState(object)]);
- animations.push([
- "t" + object.type, // TELEPORT_SNAKE | TELEPORT_BLOCK
- object.id,
- delta.r,
- delta.c,
- ]);
- return true;
-}
-
-function isTileCodeAir(tileCode) {
- return tileCode === SPACE || tileCode === EXIT || tileCode === PORTAL;
-}
-
-function addIfNotPresent(array, element) {
- if (array.indexOf(element) !== -1) return;
- array.push(element);
-}
-function removeAnyObjectAtLocation(location, changeLog) {
- var object = findObjectAtLocation(location);
- if (object != null) removeObject(object, changeLog);
-}
-function removeAnimatedObject(object, changeLog) {
- removeObject(object, changeLog);
- freshlyRemovedAnimatedObjects.push(object);
-}
-function removeObject(object, changeLog) {
- removeFromArray(level.objects, object);
- changeLog.push([object.type, object.id, [object.dead, copyArray(object.locations)], [0,[]]]);
- if (object.type === SNAKE && object.id === activeSnakeId) {
- activateAnySnakePlease();
- }
- if (object.type === BLOCK && paintBrushTileCode === BLOCK && paintBrushBlockId === object.id) {
- // no longer editing an object that doesn't exit
- paintBrushBlockId = null;
- }
- if (object.type === BLOCK) {
- delete blockSupportRenderCache[object.id];
- }
-}
-function removeFromArray(array, element) {
- var index = array.indexOf(element);
- if (index === -1) throw unreachable();
- array.splice(index, 1);
-}
-function findActiveSnake() {
- var snakes = getSnakes();
- for (var i = 0; i < snakes.length; i++) {
- if (snakes[i].id === activeSnakeId) return snakes[i];
- }
- throw unreachable();
-}
-function findBlockById(id) {
- return findObjectOfTypeAndId(BLOCK, id);
-}
-function findSnakesOfColor(color) {
- return level.objects.filter(function(object) {
- if (object.type !== SNAKE) return false;
- return object.id % snakeColors.length === color;
- });
-}
-function findObjectOfTypeAndId(type, id) {
- for (var i = 0; i < level.objects.length; i++) {
- var object = level.objects[i];
- if (object.type === type && object.id === id) return object;
- }
- return null;
-}
-function findObjectAtLocation(location) {
- for (var i = 0; i < level.objects.length; i++) {
- var object = level.objects[i];
- if (object.locations.indexOf(location) !== -1)
- return object;
- }
- return null;
-}
-function isUneatenFruit() {
- return getObjectsOfType(FRUIT).length > 0;
-}
-function getActivePortalLocations() {
- var portalLocations = getPortalLocations();
- if (portalLocations.length !== 2) return []; // nice try
- return portalLocations;
-}
-function getPortalLocations() {
- var result = [];
- for (var i = 0; i < level.map.length; i++) {
- if (level.map[i] === PORTAL) result.push(i);
- }
- return result;
-}
-function countSnakes() {
- return getSnakes().length;
-}
-function getSnakes() {
- return getObjectsOfType(SNAKE);
-}
-function getBlocks() {
- return getObjectsOfType(BLOCK);
-}
-function getObjectsOfType(type) {
- return level.objects.filter(function(object) {
- return object.type == type;
- });
-}
-function isDead() {
- if (animationQueue.length > 0 && animationQueue[animationQueue.length - 1][1][0] === INFINITE_LOOP) return true;
- return getSnakes().filter(function(snake) {
- return !!snake.dead;
- }).length > 0;
-}
-function isAlive() {
- return countSnakes() > 0 && !isDead();
-}
-
-var snakeColors = [
- "#f00",
- "#0f0",
- "#00f",
- "#ff0",
-];
-var blockForeground = ["#de5a6d","#fa65dd","#c367e3","#9c62fa","#625ff0"];
-var blockBackground = ["#853641","#963c84","#753d88","#5d3a96","#3a3990"];
-
-var activeSnakeId = null;
-
-var SLITHER_HEAD = "sh";
-var SLITHER_TAIL = "st";
-var MOVE_SNAKE = "ms";
-var MOVE_BLOCK = "mb";
-var TELEPORT_SNAKE = "ts";
-var TELEPORT_BLOCK = "tb";
-var EXIT_SNAKE = "es";
-var DIE_SNAKE = "ds";
-var DIE_BLOCK = "db";
-var INFINITE_LOOP = "il";
-var animationQueue = [
- // // sequence of disjoint animation groups.
- // // each group completes before the next begins.
- // [
- // 70, // duration of this animation group
- // // multiple things to animate simultaneously
- // [
- // SLITHER_HEAD | SLITHER_TAIL | MOVE_SNAKE | MOVE_BLOCK | TELEPORT_SNAKE | TELEPORT_BLOCK,
- // objectId,
- // dr,
- // dc,
- // ],
- // [
- // INFINITE_LOOP,
- // loopSizeNotIncludingThis,
- // ],
- // ],
-];
-var animationQueueCursor = 0;
-var animationStart = null; // new Date().getTime()
-var animationProgress; // 0.0 <= x < 1.0
-var freshlyRemovedAnimatedObjects = [];
-
-// render the support beams for blocks into a temporary buffer, and remember it.
-// this is due to stencil buffers causing slowdown on some platforms. see #25.
-var blockSupportRenderCache = {
- // id: canvas,
- // "0": document.createElement("canvas"),
-};
-
-function render() {
- if (level == null) return;
- if (animationQueueCursor < animationQueue.length) {
- var animationDuration = animationQueue[animationQueueCursor][0];
- animationProgress = (new Date().getTime() - animationStart) / animationDuration;
- if (animationProgress >= 1.0) {
- // animation group complete
- animationProgress -= 1.0;
- animationQueueCursor++;
- if (animationQueueCursor < animationQueue.length && animationQueue[animationQueueCursor][1][0] === INFINITE_LOOP) {
- var infiniteLoopSize = animationQueue[animationQueueCursor][1][1];
- animationQueueCursor -= infiniteLoopSize;
- }
- animationStart = new Date().getTime();
- }
- }
- if (animationQueueCursor === animationQueue.length) animationProgress = 1.0;
- canvas.width = tileSize * level.width;
- canvas.height = tileSize * level.height;
- var context = canvas.getContext("2d");
- context.fillStyle = "#88f"; // sky
- context.fillRect(0, 0, canvas.width, canvas.height);
-
- if (persistentState.showGrid && !persistentState.showEditor) {
- drawGrid();
- }
-
- var activePortalLocations = getActivePortalLocations();
-
- // normal render
- renderLevel();
-
- if (persistentState.showGrid && persistentState.showEditor) {
- drawGrid();
- }
- // active snake halo
- if (countSnakes() !== 0 && isAlive()) {
- var activeSnake = findActiveSnake();
- var activeSnakeRowcol = getRowcol(level, activeSnake.locations[0]);
- drawCircle(activeSnakeRowcol.r, activeSnakeRowcol.c, 2, "rgba(256,256,256,0.3)");
- }
-
- if (persistentState.showEditor) {
- if (paintBrushTileCode === BLOCK) {
- if (paintBrushBlockId != null) {
- // fade everything else away
- context.fillStyle = "rgba(0, 0, 0, 0.8)";
- context.fillRect(0, 0, canvas.width, canvas.height);
- // and render just this object in focus
- var activeBlock = findBlockById(paintBrushBlockId);
- renderLevel([activeBlock]);
- }
- } else if (paintBrushTileCode === "select") {
- getSelectedLocations().forEach(function(location) {
- var rowcol = getRowcol(level, location);
- drawRect(rowcol.r, rowcol.c, "rgba(128, 128, 128, 0.3)");
- });
- }
- }
-
- // serialize
- if (!isDead()) {
- var serialization = stringifyLevel(level);
- document.getElementById("serializationTextarea").value = serialization;
- var link = location.href.substring(0, location.href.length - location.hash.length);
- link += "#level=" + compressSerialization(serialization);
- document.getElementById("shareLinkTextbox").value = link;
- }
-
- // throw this in there somewhere
- document.getElementById("showGridButton").textContent = (persistentState.showGrid ? "Hide" : "Show") + " Grid";
-
- if (animationProgress < 1.0) requestAnimationFrame(render);
- return; // this is the end of the function proper
-
- function renderLevel(onlyTheseObjects) {
- var objects = level.objects;
- if (onlyTheseObjects != null) {
- objects = onlyTheseObjects;
- } else {
- objects = level.objects.concat(freshlyRemovedAnimatedObjects.filter(function(object) {
- // the object needs to have a future removal animation, or else, it's gone already.
- return hasFutureRemoveAnimation(object);
- }));
- }
- // begin by rendering the background connections for blocks
- objects.forEach(function(object) {
- if (object.type !== BLOCK) return;
- var animationDisplacementRowcol = findAnimationDisplacementRowcol(object.type, object.id);
- var minR = Infinity;
- var maxR = -Infinity;
- var minC = Infinity;
- var maxC = -Infinity;
- object.locations.forEach(function(location) {
- var rowcol = getRowcol(level, location);
- if (rowcol.r < minR) minR = rowcol.r;
- if (rowcol.r > maxR) maxR = rowcol.r;
- if (rowcol.c < minC) minC = rowcol.c;
- if (rowcol.c > maxC) maxC = rowcol.c;
- });
- var image = blockSupportRenderCache[object.id];
- if (image == null) {
- // render the support beams to a buffer
- blockSupportRenderCache[object.id] = image = document.createElement("canvas");
- image.width = (maxC - minC + 1) * tileSize;
- image.height = (maxR - minR + 1) * tileSize;
- var bufferContext = image.getContext("2d");
- // Make a stencil that excludes the insides of blocks.
- // Then when we render the support beams, we won't see the supports inside the block itself.
- bufferContext.beginPath();
- // Draw a path around the whole screen in the opposite direction as the rectangle paths below.
- // This means that the below rectangles will be removing area from the greater rectangle.
- bufferContext.rect(image.width, 0, -image.width, image.height);
- for (var i = 0; i < object.locations.length; i++) {
- var rowcol = getRowcol(level, object.locations[i]);
- var r = rowcol.r - minR;
- var c = rowcol.c - minC;
- bufferContext.rect(c * tileSize, r * tileSize, tileSize, tileSize);
- }
- bufferContext.clip();
- for (var i = 0; i < object.locations.length - 1; i++) {
- var rowcol1 = getRowcol(level, object.locations[i]);
- rowcol1.r -= minR;
- rowcol1.c -= minC;
- var rowcol2 = getRowcol(level, object.locations[i + 1]);
- rowcol2.r -= minR;
- rowcol2.c -= minC;
- var cornerRowcol = {r:rowcol1.r, c:rowcol2.c};
- drawConnector(bufferContext, rowcol1.r, rowcol1.c, cornerRowcol.r, cornerRowcol.c, blockBackground[object.id % blockBackground.length]);
- drawConnector(bufferContext, rowcol2.r, rowcol2.c, cornerRowcol.r, cornerRowcol.c, blockBackground[object.id % blockBackground.length]);
- }
- }
- var r = minR + animationDisplacementRowcol.r;
- var c = minC + animationDisplacementRowcol.c;
- context.drawImage(image, c * tileSize, r * tileSize);
- });
-
- // terrain
- if (onlyTheseObjects == null) {
- for (var r = 0; r < level.height; r++) {
- for (var c = 0; c < level.width; c++) {
- var location = getLocation(level, r, c);
- var tileCode = level.map[location];
- drawTile(tileCode, r, c, level, location);
- }
- }
- }
-
- // objects
- objects.forEach(drawObject);
-
- // banners
- if (countSnakes() === 0) {
- context.fillStyle = "#ff0";
- context.font = "100px Arial";
- context.fillText("test!", 0, canvas.height / 2);
- }
- if (isDead()) {
- context.fillStyle = "#f00";
- context.font = "100px Arial";
- context.fillText("You Dead!", 0, canvas.height / 2);
- }
-
- // editor hover
- if (persistentState.showEditor && paintBrushTileCode != null && hoverLocation != null && hoverLocation < level.map.length) {
-
- var savedContext = context;
- var buffer = document.createElement("canvas");
- buffer.width = canvas.width;
- buffer.height = canvas.height;
- context = buffer.getContext("2d");
-
- var hoverRowcol = getRowcol(level, hoverLocation);
- var objectHere = findObjectAtLocation(hoverLocation);
- if (typeof paintBrushTileCode === "number") {
- if (level.map[hoverLocation] !== paintBrushTileCode) {
- drawTile(paintBrushTileCode, hoverRowcol.r, hoverRowcol.c, level, hoverLocation);
- }
- } else if (paintBrushTileCode === SNAKE) {
- if (!(objectHere != null && objectHere.type === SNAKE && objectHere.id === paintBrushSnakeColorIndex)) {
- drawObject(newSnake(paintBrushSnakeColorIndex, hoverLocation));
- }
- } else if (paintBrushTileCode === BLOCK) {
- if (!(objectHere != null && objectHere.type === BLOCK && objectHere.id === paintBrushBlockId)) {
- drawObject(newBlock(hoverLocation));
- }
- } else if (paintBrushTileCode === FRUIT) {
- if (!(objectHere != null && objectHere.type === FRUIT)) {
- drawObject(newFruit(hoverLocation));
- }
- } else if (paintBrushTileCode === "resize") {
- void 0; // do nothing
- } else if (paintBrushTileCode === "select") {
- void 0; // do nothing
- } else if (paintBrushTileCode === "paste") {
- // show what will be pasted if you click
- var pastedData = previewPaste(hoverRowcol.r, hoverRowcol.c);
- pastedData.selectedLocations.forEach(function(location) {
- var tileCode = pastedData.level.map[location];
- var rowcol = getRowcol(level, location);
- drawTile(tileCode, rowcol.r, rowcol.c, pastedData.level, location);
- });
- pastedData.selectedObjects.forEach(drawObject);
- } else throw unreachable();
-
- context = savedContext;
- context.save();
- context.globalAlpha = 0.2;
- context.drawImage(buffer, 0, 0);
- context.restore();
- }
- }
- function drawTile(tileCode, r, c, level, location) {
- switch (tileCode) {
- case SPACE:
- break;
- case WALL:
- drawWall(r, c, getAdjacentTiles());
- break;
- case SPIKE:
- drawSpikes(r, c, level);
- break;
- case EXIT:
- var radiusFactor = isUneatenFruit() ? 0.7 : 1.2;
- drawQuarterPie(r, c, radiusFactor, "#f00", 0);
- drawQuarterPie(r, c, radiusFactor, "#0f0", 1);
- drawQuarterPie(r, c, radiusFactor, "#00f", 2);
- drawQuarterPie(r, c, radiusFactor, "#ff0", 3);
- break;
- case PORTAL:
- drawCircle(r, c, 0.8, "#888");
- drawCircle(r, c, 0.6, "#111");
- if (activePortalLocations.indexOf(location) !== -1) drawCircle(r, c, 0.3, "#666");
- break;
- default: throw unreachable();
- }
- function getAdjacentTiles() {
- return [
- [getTile(r - 1, c - 1),
- getTile(r - 1, c + 0),
- getTile(r - 1, c + 1)],
- [getTile(r + 0, c - 1),
- null,
- getTile(r + 0, c + 1)],
- [getTile(r + 1, c - 1),
- getTile(r + 1, c + 0),
- getTile(r + 1, c + 1)],
- ];
- }
- function getTile(r, c) {
- if (!isInBounds(level, r, c)) return null;
- return level.map[getLocation(level, r, c)];
- }
- }
-
- function drawObject(object) {
- switch (object.type) {
- case SNAKE:
- var animationDisplacementRowcol = findAnimationDisplacementRowcol(object.type, object.id);
- var lastRowcol = null
- var color = snakeColors[object.id % snakeColors.length];
- var headRowcol;
- for (var i = 0; i <= object.locations.length; i++) {
- var animation;
- var rowcol;
- if (i === 0 && (animation = findAnimation([SLITHER_HEAD], object.id)) != null) {
- // animate head slithering forward
- rowcol = getRowcol(level, object.locations[i]);
- rowcol.r += animation[2] * (animationProgress - 1);
- rowcol.c += animation[3] * (animationProgress - 1);
- } else if (i === object.locations.length) {
- // animated tail?
- if ((animation = findAnimation([SLITHER_TAIL], object.id)) != null) {
- // animate tail slithering to catch up
- rowcol = getRowcol(level, object.locations[i - 1]);
- rowcol.r += animation[2] * (animationProgress - 1);
- rowcol.c += animation[3] * (animationProgress - 1);
- } else {
- // no animated tail needed
- break;
- }
- } else {
- rowcol = getRowcol(level, object.locations[i]);
- }
- if (object.dead) rowcol.r += 0.5;
- rowcol.r += animationDisplacementRowcol.r;
- rowcol.c += animationDisplacementRowcol.c;
- if (i === 0) {
- // head
- headRowcol = rowcol;
- drawDiamond(rowcol.r, rowcol.c, color);
- } else {
- // middle
- var cx = (rowcol.c + 0.5) * tileSize;
- var cy = (rowcol.r + 0.5) * tileSize;
- context.fillStyle = color;
- var orientation;
- if (lastRowcol.r < rowcol.r) {
- orientation = 0;
- context.beginPath();
- context.moveTo((lastRowcol.c + 0) * tileSize, (lastRowcol.r + 0.5) * tileSize);
- context.lineTo((lastRowcol.c + 1) * tileSize, (lastRowcol.r + 0.5) * tileSize);
- context.arc(cx, cy, tileSize/2, 0, Math.PI);
- context.fill();
- } else if (lastRowcol.r > rowcol.r) {
- orientation = 2;
- context.beginPath();
- context.moveTo((lastRowcol.c + 1) * tileSize, (lastRowcol.r + 0.5) * tileSize);
- context.lineTo((lastRowcol.c + 0) * tileSize, (lastRowcol.r + 0.5) * tileSize);
- context.arc(cx, cy, tileSize/2, Math.PI, 0);
- context.fill();
- } else if (lastRowcol.c < rowcol.c) {
- orientation = 3;
- context.beginPath();
- context.moveTo((lastRowcol.c + 0.5) * tileSize, (lastRowcol.r + 1) * tileSize);
- context.lineTo((lastRowcol.c + 0.5) * tileSize, (lastRowcol.r + 0) * tileSize);
- context.arc(cx, cy, tileSize/2, 1.5 * Math.PI, 2.5 * Math.PI);
- context.fill();
- } else if (lastRowcol.c > rowcol.c) {
- orientation = 1;
- context.beginPath();
- context.moveTo((lastRowcol.c + 0.5) * tileSize, (lastRowcol.r + 0) * tileSize);
- context.lineTo((lastRowcol.c + 0.5) * tileSize, (lastRowcol.r + 1) * tileSize);
- context.arc(cx, cy, tileSize/2, 2.5 * Math.PI, 1.5 * Math.PI);
- context.fill();
- }
- }
- lastRowcol = rowcol;
- }
- // eye
- if (object.id === activeSnakeId) {
- drawCircle(headRowcol.r, headRowcol.c, 0.5, "#fff");
- drawCircle(headRowcol.r, headRowcol.c, 0.2, "#000");
- }
- break;
- case BLOCK:
- drawBlock(object);
- break;
- case FRUIT:
- var rowcol = getRowcol(level, object.locations[0]);
- drawCircle(rowcol.r, rowcol.c, 1, "#f0f");
- break;
- default: throw unreachable();
- }
- }
-
- function drawWall(r, c, adjacentTiles) {
- drawRect(r, c, "#844204"); // dirt
- context.fillStyle = "#282"; // grass
- drawTileOutlines(r, c, isWall, 0.2);
-
- function isWall(dc, dr) {
- var tileCode = adjacentTiles[1 + dr][1 + dc];
- return tileCode == null || tileCode === WALL;
- }
- }
- function drawTileOutlines(r, c, isOccupied, outlineThickness) {
- var complement = 1 - outlineThickness;
- var outlinePixels = outlineThickness * tileSize;
- var complementPixels = (1 - 2 * outlineThickness) * tileSize;
- if (!isOccupied(-1, -1)) context.fillRect((c) * tileSize, (r) * tileSize, outlinePixels, outlinePixels);
- if (!isOccupied( 1, -1)) context.fillRect((c+complement) * tileSize, (r) * tileSize, outlinePixels, outlinePixels);
- if (!isOccupied(-1, 1)) context.fillRect((c) * tileSize, (r+complement) * tileSize, outlinePixels, outlinePixels);
- if (!isOccupied( 1, 1)) context.fillRect((c+complement) * tileSize, (r+complement) * tileSize, outlinePixels, outlinePixels);
- if (!isOccupied( 0, -1)) context.fillRect((c) * tileSize, (r) * tileSize, tileSize, outlinePixels);
- if (!isOccupied( 0, 1)) context.fillRect((c) * tileSize, (r+complement) * tileSize, tileSize, outlinePixels);
- if (!isOccupied(-1, 0)) context.fillRect((c) * tileSize, (r) * tileSize, outlinePixels, tileSize);
- if (!isOccupied( 1, 0)) context.fillRect((c+complement) * tileSize, (r) * tileSize, outlinePixels, tileSize);
- }
- function drawSpikes(r, c) {
- var x = c * tileSize;
- var y = r * tileSize;
- context.fillStyle = "#333";
- context.beginPath();
- context.moveTo(x + tileSize * 0.3, y + tileSize * 0.3);
- context.lineTo(x + tileSize * 0.4, y + tileSize * 0.0);
- context.lineTo(x + tileSize * 0.5, y + tileSize * 0.3);
- context.lineTo(x + tileSize * 0.6, y + tileSize * 0.0);
- context.lineTo(x + tileSize * 0.7, y + tileSize * 0.3);
- context.lineTo(x + tileSize * 1.0, y + tileSize * 0.4);
- context.lineTo(x + tileSize * 0.7, y + tileSize * 0.5);
- context.lineTo(x + tileSize * 1.0, y + tileSize * 0.6);
- context.lineTo(x + tileSize * 0.7, y + tileSize * 0.7);
- context.lineTo(x + tileSize * 0.6, y + tileSize * 1.0);
- context.lineTo(x + tileSize * 0.5, y + tileSize * 0.7);
- context.lineTo(x + tileSize * 0.4, y + tileSize * 1.0);
- context.lineTo(x + tileSize * 0.3, y + tileSize * 0.7);
- context.lineTo(x + tileSize * 0.0, y + tileSize * 0.6);
- context.lineTo(x + tileSize * 0.3, y + tileSize * 0.5);
- context.lineTo(x + tileSize * 0.0, y + tileSize * 0.4);
- context.lineTo(x + tileSize * 0.3, y + tileSize * 0.3);
- context.fill();
- }
- function drawConnector(context, r1, c1, r2, c2, color) {
- // either r1 and r2 or c1 and c2 must be equal
- if (r1 > r2 || c1 > c2) {
- var rTmp = r1;
- var cTmp = c1;
- r1 = r2;
- c1 = c2;
- r2 = rTmp;
- c2 = cTmp;
- }
- var xLo = (c1 + 0.4) * tileSize;
- var yLo = (r1 + 0.4) * tileSize;
- var xHi = (c2 + 0.6) * tileSize;
- var yHi = (r2 + 0.6) * tileSize;
- context.fillStyle = color;
- context.fillRect(xLo, yLo, xHi - xLo, yHi - yLo);
- }
- function drawBlock(block) {
- var animationDisplacementRowcol = findAnimationDisplacementRowcol(block.type, block.id);
- var rowcols = block.locations.map(function(location) {
- return getRowcol(level, location);
- });
- rowcols.forEach(function(rowcol) {
- var r = rowcol.r + animationDisplacementRowcol.r;
- var c = rowcol.c + animationDisplacementRowcol.c;
- context.fillStyle = blockForeground[block.id % blockForeground.length];
- drawTileOutlines(r, c, isAlsoThisBlock, 0.3);
- function isAlsoThisBlock(dc, dr) {
- for (var i = 0; i < rowcols.length; i++) {
- var otherRowcol = rowcols[i];
- if (rowcol.r + dr === otherRowcol.r && rowcol.c + dc === otherRowcol.c) return true;
- }
- return false;
- }
- });
- }
- function drawQuarterPie(r, c, radiusFactor, fillStyle, quadrant) {
- var cx = (c + 0.5) * tileSize;
- var cy = (r + 0.5) * tileSize;
- context.fillStyle = fillStyle;
- context.beginPath();
- context.moveTo(cx, cy);
- context.arc(cx, cy, radiusFactor * tileSize/2, quadrant * Math.PI/2, (quadrant + 1) * Math.PI/2);
- context.fill();
- }
- function drawDiamond(r, c, fillStyle) {
- var x = c * tileSize;
- var y = r * tileSize;
- context.fillStyle = fillStyle;
- context.beginPath();
- context.moveTo(x + tileSize/2, y);
- context.lineTo(x + tileSize, y + tileSize/2);
- context.lineTo(x + tileSize/2, y + tileSize);
- context.lineTo(x, y + tileSize/2);
- context.lineTo(x + tileSize/2, y);
- context.fill();
- }
- function drawCircle(r, c, radiusFactor, fillStyle) {
- context.fillStyle = fillStyle;
- context.beginPath();
- context.arc((c + 0.5) * tileSize, (r + 0.5) * tileSize, tileSize/2 * radiusFactor, 0, 2*Math.PI);
- context.fill();
- }
- function drawRect(r, c, fillStyle) {
- context.fillStyle = fillStyle;
- context.fillRect(c * tileSize, r * tileSize, tileSize, tileSize);
- }
-
- function drawGrid() {
- var buffer = document.createElement("canvas");
- buffer.width = canvas.width;
- buffer.height = canvas.height;
- var localContext = buffer.getContext("2d");
-
- localContext.strokeStyle = "#fff";
- localContext.beginPath();
- for (var r = 0; r < level.height; r++) {
- localContext.moveTo(0, tileSize*r);
- localContext.lineTo(tileSize*level.width, tileSize*r);
- }
- for (var c = 0; c < level.width; c++) {
- localContext.moveTo(tileSize*c, 0);
- localContext.lineTo(tileSize*c, tileSize*level.height);
- }
- localContext.stroke();
-
- context.save();
- context.globalAlpha = 0.4;
- context.drawImage(buffer, 0, 0);
- context.restore();
- }
-}
-
-function findAnimation(animationTypes, objectId) {
- if (animationQueueCursor === animationQueue.length) return null;
- var currentAnimation = animationQueue[animationQueueCursor];
- for (var i = 1; i < currentAnimation.length; i++) {
- var animation = currentAnimation[i];
- if (animationTypes.indexOf(animation[0]) !== -1 &&
- animation[1] === objectId) {
- return animation;
- }
- }
-}
-function findAnimationDisplacementRowcol(objectType, objectId) {
- var dr = 0;
- var dc = 0;
- var animationTypes = [
- "m" + objectType, // MOVE_SNAKE | MOVE_BLOCK
- "t" + objectType, // TELEPORT_SNAKE | TELEPORT_BLOCK
- ];
- // skip the current one
- for (var i = animationQueueCursor + 1; i < animationQueue.length; i++) {
- var animations = animationQueue[i];
- for (var j = 1; j < animations.length; j++) {
- var animation = animations[j];
- if (animationTypes.indexOf(animation[0]) !== -1 &&
- animation[1] === objectId) {
- dr += animation[2];
- dc += animation[3];
- }
- }
- }
- var movementAnimation = findAnimation(animationTypes, objectId);
- if (movementAnimation != null) {
- dr += movementAnimation[2] * (1 - animationProgress);
- dc += movementAnimation[3] * (1 - animationProgress);
- }
- return {r: -dr, c: -dc};
-}
-function hasFutureRemoveAnimation(object) {
- var animationTypes = [
- EXIT_SNAKE,
- DIE_BLOCK,
- ];
- for (var i = animationQueueCursor; i < animationQueue.length; i++) {
- var animations = animationQueue[i];
- for (var j = 1; j < animations.length; j++) {
- var animation = animations[j];
- if (animationTypes.indexOf(animation[0]) !== -1 &&
- animation[1] === object.id) {
- return true;
- }
- }
- }
-}
-
-function previewPaste(hoverR, hoverC) {
- var offsetR = hoverR - clipboardOffsetRowcol.r;
- var offsetC = hoverC - clipboardOffsetRowcol.c;
-
- var newLevel = JSON.parse(JSON.stringify(level));
- var selectedLocations = [];
- var selectedObjects = [];
- clipboardData.selectedLocations.forEach(function(location) {
- var tileCode = clipboardData.level.map[location];
- var rowcol = getRowcol(clipboardData.level, location);
- var r = rowcol.r + offsetR;
- var c = rowcol.c + offsetC;
- if (!isInBounds(newLevel, r, c)) return;
- var newLocation = getLocation(newLevel, r, c);
- newLevel.map[newLocation] = tileCode;
- selectedLocations.push(newLocation);
- });
- clipboardData.selectedObjects.forEach(function(object) {
- var newLocations = [];
- for (var i = 0; i < object.locations.length; i++) {
- var rowcol = getRowcol(clipboardData.level, object.locations[i]);
- rowcol.r += offsetR;
- rowcol.c += offsetC;
- if (!isInBounds(newLevel, rowcol.r, rowcol.c)) {
- // this location is oob
- if (object.type === SNAKE) {
- // snakes must be completely in bounds
- return;
- }
- // just skip it
- continue;
- }
- var newLocation = getLocation(newLevel, rowcol.r, rowcol.c);
- newLocations.push(newLocation);
- }
- if (newLocations.length === 0) return; // can't have a non-present object
- var newObject = JSON.parse(JSON.stringify(object));
- newObject.locations = newLocations;
- selectedObjects.push(newObject);
- });
- return {
- level: newLevel,
- selectedLocations: selectedLocations,
- selectedObjects: selectedObjects,
- };
-}
-
-function getNaiveOrthogonalPath(a, b) {
- // does not include a, but does include b.
- var rowcolA = getRowcol(level, a);
- var rowcolB = getRowcol(level, b);
- var path = [];
- if (rowcolA.r < rowcolB.r) {
- for (var r = rowcolA.r; r < rowcolB.r; r++) {
- path.push(getLocation(level, r + 1, rowcolA.c));
- }
- } else {
- for (var r = rowcolA.r; r > rowcolB.r; r--) {
- path.push(getLocation(level, r - 1, rowcolA.c));
- }
- }
- if (rowcolA.c < rowcolB.c) {
- for (var c = rowcolA.c; c < rowcolB.c; c++) {
- path.push(getLocation(level, rowcolB.r, c + 1));
- }
- } else {
- for (var c = rowcolA.c; c > rowcolB.c; c--) {
- path.push(getLocation(level, rowcolB.r, c - 1));
- }
- }
- return path;
-}
-function identityFunction(x) {
- return x;
-}
-function compareId(a, b) {
- return operatorCompare(a.id, b.id);
-}
-function operatorCompare(a, b) {
- return a < b ? -1 : a > b ? 1 : 0;
-}
-function clamp(value, min, max) {
- if (value < min) return min;
- if (value > max) return max;
- return value;
-}
-function copyArray(array) {
- return array.map(identityFunction);
-}
-function getSetIntersection(array1, array2) {
- if (array1.length * array2.length === 0) return [];
- return array1.filter(function(x) { return array2.indexOf(x) !== -1; });
-}
-function makeScaleCoordinatesFunction(width1, width2) {
- return function(location) {
- return location + (width2 - width1) * Math.floor(location / width1);
- };
-}
-
-var expectHash;
-window.addEventListener("hashchange", function() {
- if (location.hash === expectHash) {
- // We're in the middle of saveLevel() or saveReplay().
- // Don't react to that event.
- expectHash = null;
- return;
- }
- // The user typed into the url bar or used Back/Forward browser buttons, etc.
- loadFromLocationHash();
-});
-function loadFromLocationHash() {
- var hashSegments = location.hash.split("#");
- hashSegments.shift(); // first element is always ""
- if (!(1 <= hashSegments.length && hashSegments.length <= 2)) return false;
- var hashPairs = hashSegments.map(function(segment) {
- var equalsIndex = segment.indexOf("=");
- if (equalsIndex === -1) return ["", segment]; // bad
- return [segment.substring(0, equalsIndex), segment.substring(equalsIndex + 1)];
- });
-
- if (hashPairs[0][0] !== "level") return false;
- try {
- var level = parseLevel(hashPairs[0][1]);
- } catch (e) {
- alert(e);
- return false;
- }
- loadLevel(level);
- if (hashPairs.length > 1) {
- try {
- if (hashPairs[1][0] !== "replay") throw new Error("unexpected hash pair: " + hashPairs[1][0]);
- parseAndLoadReplay(hashPairs[1][1]);
- } catch (e) {
- alert(e);
- return false;
- }
- }
- return true;
-}
-
-// run test suite
-var testTime = new Date().getTime();
-if (compressSerialization(stringifyLevel(parseLevel(testLevel_v0))) !== testLevel_v0_converted) throw new Error("v0 level conversion is broken");
-// ask the debug console for this variable if you're concerned with how much time this wastes.
-testTime = new Date().getTime() - testTime;
-
-loadPersistentState();
-if (!loadFromLocationHash()) {
- loadLevel(parseLevel(exampleLevel));
-}
From ed80174d0843541e074e565be82b7e746f078755 Mon Sep 17 00:00:00 2001
From: Gooby <34070642+jmdiamond3@users.noreply.github.com>
Date: Wed, 22 Jan 2020 15:57:44 -0500
Subject: [PATCH 009/577] Create a.js
---
a.js | 3059 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 3059 insertions(+)
create mode 100644 a.js
diff --git a/a.js b/a.js
new file mode 100644
index 00000000..3088eecc
--- /dev/null
+++ b/a.js
@@ -0,0 +1,3059 @@
+function unreachable() { return new Error("unreachable"); }
+if (typeof VERSION !== "undefined") {
+ document.getElementById("versionSpan").innerHTML =
+ '' + VERSION.tag + '';
+}
+
+var img3 = document.createElement('img'); //Gooby
+img3.src = '/Snakefall/Snakebird Images/Cherry2.png';
+
+var canvas = document.getElementById("canvas");
+
+// tile codes
+var SPACE = 0;
+var WALL = 1;
+var SPIKE = 2;
+var FRUIT_v0 = 3; // legacy
+var EXIT = 4;
+var PORTAL = 5;
+var validTileCodes = [SPACE, WALL, SPIKE, EXIT, PORTAL]; //Gooby
+
+// object types
+var SNAKE = "s";
+var BLOCK = "b";
+var FRUIT = "f";
+
+var tileSize = 30;
+var level;
+var unmoveStuff = {undoStack:[], redoStack:[], spanId:"movesSpan", undoButtonId:"unmoveButton", redoButtonId:"removeButton"};
+var uneditStuff = {undoStack:[], redoStack:[], spanId:"editsSpan", undoButtonId:"uneditButton", redoButtonId:"reeditButton"};
+var paradoxes = [];
+function loadLevel(newLevel) {
+ level = newLevel;
+ currentSerializedLevel = compressSerialization(stringifyLevel(newLevel));
+
+ activateAnySnakePlease();
+ unmoveStuff.undoStack = [];
+ unmoveStuff.redoStack = [];
+ undoStuffChanged(unmoveStuff);
+ uneditStuff.undoStack = [];
+ uneditStuff.redoStack = [];
+ undoStuffChanged(uneditStuff);
+ blockSupportRenderCache = {};
+ render();
+}
+
+
+var magicNumber_v0 = "3tFRIoTU";
+var magicNumber = "HyRr4JK1";
+var exampleLevel = magicNumber_v0 + "&" +
+ "17&31" +
+ "?" +
+ "0000000000000000000000000000000" +
+ "0000000000000000000000000000000" +
+ "0000000000000000000000000000000" +
+ "0000000000000000000000000000000" +
+ "0000000000000000000000000000000" +
+ "0000000000000000000000000000000" +
+ "0000000000000000000040000000000" +
+ "0000000000000110000000000000000" +
+ "0000000000000111100000000000000" +
+ "0000000000000011000000000000000" +
+ "0000000000000010000010000000000" +
+ "0000000000000010100011000000000" +
+ "0000001111111000110000000110000" +
+ "0000011111111111111111111110000" +
+ "0000011111111101111111111100000" +
+ "0000001111111100111111111100000" +
+ "0000001111111000111111111100000" +
+ "/" +
+ "s0 ?351&350&349/" +
+ "f0 ?328/" +
+ "f1 ?366/";
+
+var testLevel_v0 = "3tFRIoTU&5&5?0005*00300024005*001000/b0?7&6&15&23/s3?18/s0?1&0&5/s1?2/s4?10/s2?17/b2?9/b3?14/b4?19/b1?4&20/b5?24/";
+var testLevel_v0_converted = "HyRr4JK1&5&5?0005*4024005*001000/b0?7&6&15&23/s3?18/s0?1&0&5/s1?2/s4?10/s2?17/b2?9/b3?14/b4?19/b1?4&20/b5?24/f0?8/";
+
+function parseLevel(string) {
+ // magic number
+ var cursor = 0;
+ skipWhitespace();
+ var versionTag = string.substr(cursor, magicNumber.length);
+ switch (versionTag) {
+ case magicNumber_v0:
+ case magicNumber: break;
+ default: throw new Error("not a snakefall level");
+ }
+ cursor += magicNumber.length;
+ consumeKeyword("&");
+
+ var level = {
+ height: -1,
+ width: -1,
+ map: [],
+ objects: [],
+ };
+
+ // height, width
+ level.height = readInt();
+ consumeKeyword("&");
+ level.width = readInt();
+
+ // map
+ var mapData = readRun();
+ mapData = decompressSerialization(mapData);
+ if (level.height * level.width !== mapData.length) throw parserError("height, width, and map.length do not jive");
+ var upconvertedObjects = [];
+ var fruitCount = 0;
+ for (var i = 0; i < mapData.length; i++) {
+ var tileCode = mapData[i].charCodeAt(0) - "0".charCodeAt(0);
+ if (tileCode === FRUIT_v0 && versionTag === magicNumber_v0) {
+ // fruit used to be a tile code. now it's an object.
+ upconvertedObjects.push({
+ type: FRUIT,
+ id: fruitCount++,
+ dead: false, // unused
+ locations: [i],
+ });
+ tileCode = SPACE;
+ }
+ if (validTileCodes.indexOf(tileCode) === -1) throw parserError("invalid tilecode: " + JSON.stringify(mapData[i]));
+ level.map.push(tileCode);
+ }
+
+ // objects
+ skipWhitespace();
+ while (cursor < string.length) {
+ var object = {
+ type: "?",
+ id: -1,
+ dead: false,
+ locations: [],
+ };
+
+ // type
+ object.type = string[cursor];
+ var locationsLimit;
+ if (object.type === SNAKE) locationsLimit = -1;
+ else if (object.type === BLOCK) locationsLimit = -1;
+ else if (object.type === FRUIT) locationsLimit = 1;
+ else throw parserError("expected object type code");
+ cursor += 1;
+
+ // id
+ object.id = readInt();
+
+ // locations
+ var locationsData = readRun();
+ var locationStrings = locationsData.split("&");
+ if (locationStrings.length === 0) throw parserError("locations must be non-empty");
+ if (locationsLimit !== -1 && locationStrings.length > locationsLimit) throw parserError("too many locations");
+
+ locationStrings.forEach(function(locationString) {
+ var location = parseInt(locationString);
+ if (!(0 <= location && location < level.map.length)) throw parserError("location out of bounds: " + JSON.stringify(locationString));
+ object.locations.push(location);
+ });
+
+ level.objects.push(object);
+ skipWhitespace();
+ }
+ for (var i = 0; i < upconvertedObjects.length; i++) {
+ level.objects.push(upconvertedObjects[i]);
+ }
+
+ return level;
+
+ function skipWhitespace() {
+ while (" \n\t\r".indexOf(string[cursor]) !== -1) {
+ cursor += 1;
+ }
+ }
+ function consumeKeyword(keyword) {
+ skipWhitespace();
+ if (string.indexOf(keyword, cursor) !== cursor) throw parserError("expected " + JSON.stringify(keyword));
+ cursor += 1;
+ }
+ function readInt() {
+ skipWhitespace();
+ for (var i = cursor; i < string.length; i++) {
+ if ("0123456789".indexOf(string[i]) === -1) break;
+ }
+ var substring = string.substring(cursor, i);
+ if (substring.length === 0) throw parserError("expected int");
+ cursor = i;
+ return parseInt(substring, 10);
+ }
+ function readRun() {
+ consumeKeyword("?");
+ var endIndex = string.indexOf("/", cursor);
+ var substring = string.substring(cursor, endIndex);
+ cursor = endIndex + 1;
+ return substring;
+ }
+ function parserError(message) {
+ return new Error("parse error at position " + cursor + ": " + message);
+ }
+}
+
+function stringifyLevel(level) {
+ var output = magicNumber + "&";
+ output += level.height + "&" + level.width + "\n";
+
+ output += "?\n";
+ for (var r = 0; r < level.height; r++) {
+ output += " " + level.map.slice(r * level.width, (r + 1) * level.width).join("") + "\n";
+ }
+ output += "/\n";
+
+ output += serializeObjects(level.objects);
+
+ // sanity check
+ var shouldBeTheSame = parseLevel(output);
+ if (!deepEquals(level, shouldBeTheSame)) throw asdf; // serialization/deserialization is broken
+
+ return output;
+}
+function serializeObjects(objects) {
+ var output = "";
+ for (var i = 0; i < objects.length; i++) {
+ var object = objects[i];
+ output += object.type + object.id + " ";
+ output += "?" + object.locations.join("&") + "/\n";
+ }
+ return output;
+}
+function serializeObjectState(object) {
+ if (object == null) return [0,[]];
+ return [object.dead, copyArray(object.locations)];
+}
+
+var base66 = "----0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+function compressSerialization(string) {
+ string = string.replace(/\s+/g, "");
+ // run-length encode several 0's in a row, etc.
+ // 2000000000000003 -> 2*A03 ("A" is 14 in base66 defined above)
+ var result = "";
+ var runStart = 0;
+ for (var i = 1; i < string.length + 1; i++) {
+ var runLength = i - runStart;
+ if (string[i] === string[runStart] && runLength < base66.length - 1) continue;
+ // end of run
+ if (runLength >= 4) {
+ // compress
+ result += "*" + base66[runLength] + string[runStart];
+ } else {
+ // literal
+ result += string.substring(runStart, i);
+ }
+ runStart = i;
+ }
+ return result;
+}
+function decompressSerialization(string) {
+ string = string.replace(/\s+/g, "");
+ var result = "";
+ for (var i = 0; i < string.length; i++) {
+ if (string[i] === "*") {
+ i += 1;
+ var runLength = base66.indexOf(string[i]);
+ i += 1;
+ var char = string[i];
+ for (var j = 0; j < runLength; j++) {
+ result += char;
+ }
+ } else {
+ result += string[i];
+ }
+ }
+ return result;
+}
+
+var replayMagicNumber = "nmGTi8PB";
+function stringifyReplay() {
+ var output = replayMagicNumber + "&";
+ // only specify the snake id in an input if it's different from the previous.
+ // the first snake index is 0 to optimize for the single-snake case.
+ var currentSnakeId = 0;
+ for (var i = 0; i < unmoveStuff.undoStack.length; i++) {
+ var firstChange = unmoveStuff.undoStack[i][0];
+ if (firstChange[0] !== "i") throw unreachable();
+ var snakeId = firstChange[1];
+ var dr = firstChange[2];
+ var dc = firstChange[3];
+ var directionCode;
+ if (dr ===-1 && dc === 0) directionCode = "u";
+ else if (dr === 0 && dc ===-1) directionCode = "l";
+ else if (dr === 1 && dc === 0) directionCode = "d";
+ else if (dr === 0 && dc === 1) directionCode = "r";
+ else throw unreachable();
+ if (snakeId !== currentSnakeId) {
+ output += snakeId; // int to string
+ currentSnakeId = snakeId;
+ }
+ output += directionCode;
+ }
+ return output;
+}
+function parseAndLoadReplay(string) {
+ string = decompressSerialization(string);
+ var expectedPrefix = replayMagicNumber + "&";
+ if (string.substring(0, expectedPrefix.length) !== expectedPrefix) throw new Error("unrecognized replay string");
+ var cursor = expectedPrefix.length;
+
+ // the starting snakeid is 0, which may not exist, but we only validate it when doing a move.
+ activeSnakeId = 0;
+ while (cursor < string.length) {
+ var snakeIdStr = "";
+ var c = string.charAt(cursor);
+ cursor += 1;
+ while ('0' <= c && c <= '9') {
+ snakeIdStr += c;
+ if (cursor >= string.length) throw new Error("replay string has unexpected end of input");
+ c = string.charAt(cursor);
+ cursor += 1;
+ }
+ if (snakeIdStr.length > 0) {
+ activeSnakeId = parseInt(snakeIdStr);
+ // don't just validate when switching snakes, but on every move.
+ }
+
+ // doing a move.
+ if (!getSnakes().some(function(snake) {
+ return snake.id === activeSnakeId;
+ })) {
+ throw new Error("invalid snake id: " + activeSnakeId);
+ }
+ switch (c) {
+ case 'l': move( 0, -1); break;
+ case 'u': move(-1, 0); break;
+ case 'r': move( 0, 1); break;
+ case 'd': move( 1, 0); break;
+ default: throw new Error("replay string has invalid direction: " + c);
+ }
+ }
+
+ // now that the replay was executed successfully, undo it all so that it's available in the redo buffer.
+ reset(unmoveStuff);
+ document.getElementById("removeButton").classList.add("click-me");
+}
+
+var currentSerializedLevel;
+function saveLevel() {
+ if (isDead()) return alert("Can't save while you're dead!");
+ var serializedLevel = compressSerialization(stringifyLevel(level));
+ currentSerializedLevel = serializedLevel;
+ var hash = "#level=" + serializedLevel;
+ expectHash = hash;
+ location.hash = hash;
+
+ // This marks a starting point for solving the level.
+ unmoveStuff.undoStack = [];
+ unmoveStuff.redoStack = [];
+ editorHasBeenTouched = false;
+ undoStuffChanged(unmoveStuff);
+}
+
+function saveReplay() {
+ if (dirtyState === EDITOR_DIRTY) return alert("Can't save a replay with unsaved editor changes.");
+ // preserve the level in the url bar.
+ var hash = "#level=" + currentSerializedLevel;
+ if (dirtyState === REPLAY_DIRTY) {
+ // there is a replay to save
+ hash += "#replay=" + compressSerialization(stringifyReplay());
+ }
+ expectHash = hash;
+ location.hash = hash;
+}
+
+function deepEquals(a, b) {
+ if (a == null) return b == null;
+ if (typeof a === "string" || typeof a === "number" || typeof a === "boolean") return a === b;
+ if (Array.isArray(a)) {
+ if (!Array.isArray(b)) return false;
+ if (a.length !== b.length) return false;
+ for (var i = 0; i < a.length; i++) {
+ if (!deepEquals(a[i], b[i])) return false;
+ }
+ return true;
+ }
+ // must be objects
+ var aKeys = Object.keys(a);
+ var bKeys = Object.keys(b);
+ if (aKeys.length !== bKeys.length) return false;
+ aKeys.sort();
+ bKeys.sort();
+ if (!deepEquals(aKeys, bKeys)) return false;
+ for (var i = 0; i < aKeys.length; i++) {
+ if (!deepEquals(a[aKeys[i]], b[bKeys[i]])) return false;
+ }
+ return true;
+}
+
+function getLocation(level, r, c) {
+ if (!isInBounds(level, r, c)) throw unreachable();
+ return r * level.width + c;
+}
+function getRowcol(level, location) {
+ if (location < 0 || location >= level.width * level.height) throw unreachable();
+ var r = Math.floor(location / level.width);
+ var c = location % level.width;
+ return {r:r, c:c};
+}
+function isInBounds(level, r, c) {
+ if (c < 0 || c >= level.width) return false;;
+ if (r < 0 || r >= level.height) return false;;
+ return true;
+}
+function offsetLocation(location, dr, dc) {
+ var rowcol = getRowcol(level, location);
+ return getLocation(level, rowcol.r + dr, rowcol.c + dc);
+}
+
+var SHIFT = 1;
+var CTRL = 2;
+var ALT = 4;
+document.addEventListener("keydown", function(event) {
+ var modifierMask = (
+ (event.shiftKey ? SHIFT : 0) |
+ (event.ctrlKey ? CTRL : 0) |
+ (event.altKey ? ALT : 0)
+ );
+ switch (event.keyCode) {
+ case 37: // left
+ if (modifierMask === 0) { move(0, -1); break; }
+ return;
+ case 38: // up
+ if (modifierMask === 0) { move(-1, 0); break; }
+ return;
+ case 39: // right
+ if (modifierMask === 0) { move(0, 1); break; }
+ return;
+ case 40: // down
+ if (modifierMask === 0) { move(1, 0); break; }
+ return;
+ case 8: // backspace
+ if (modifierMask === 0) { undo(unmoveStuff); break; }
+ if (modifierMask === SHIFT) { redo(unmoveStuff); break; }
+ return;
+ case "Q".charCodeAt(0):
+ if (modifierMask === 0) { undo(unmoveStuff); break; }
+ if (modifierMask === SHIFT) { redo(unmoveStuff); break; }
+ return;
+ case "Z".charCodeAt(0):
+ if (modifierMask === 0) { undo(unmoveStuff); break; }
+ if (modifierMask === SHIFT) { redo(unmoveStuff); break; }
+ if (persistentState.showEditor && modifierMask === CTRL) { undo(uneditStuff); break; }
+ if (persistentState.showEditor && modifierMask === CTRL|SHIFT) { redo(uneditStuff); break; }
+ return;
+ case "Y".charCodeAt(0):
+ if (modifierMask === 0) { redo(unmoveStuff); break; }
+ if (persistentState.showEditor && modifierMask === CTRL) { redo(uneditStuff); break; }
+ return;
+ case "R".charCodeAt(0):
+ if (persistentState.showEditor && modifierMask === SHIFT) { setPaintBrushTileCode("select"); break; }
+ if (modifierMask === 0) { reset(unmoveStuff); break; }
+ if (modifierMask === SHIFT) { unreset(unmoveStuff); break; }
+ return;
+
+ case 220: // backslash
+ if (modifierMask === 0) { toggleShowEditor(); break; }
+ return;
+ case "A".charCodeAt(0):
+ if (!persistentState.showEditor && modifierMask === 0) { move(0, -1); break; }
+ if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(PORTAL); break; }
+ if ( persistentState.showEditor && modifierMask === CTRL) { selectAll(); break; }
+ return;
+ case "E".charCodeAt(0):
+ if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(SPACE); break; }
+ return;
+ case 46: // delete
+ if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(SPACE); break; }
+ return;
+ case "W".charCodeAt(0):
+ if (!persistentState.showEditor && modifierMask === 0) { move(-1, 0); break; }
+ if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(WALL); break; }
+ return;
+ case "S".charCodeAt(0):
+ if (!persistentState.showEditor && modifierMask === 0) { move(1, 0); break; }
+ if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(SPIKE); break; }
+ if ( persistentState.showEditor && modifierMask === SHIFT) { setPaintBrushTileCode("resize"); break; }
+ if ( persistentState.showEditor && modifierMask === CTRL) { saveLevel(); break; }
+ if (!persistentState.showEditor && modifierMask === CTRL) { saveReplay(); break; }
+ if (modifierMask === (CTRL|SHIFT)) { saveReplay(); break; }
+ return;
+ case "X".charCodeAt(0):
+ if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(EXIT); break; }
+ if ( persistentState.showEditor && modifierMask === CTRL) { cutSelection(); break; }
+ return;
+ case "F".charCodeAt(0):
+ if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(FRUIT); break; }
+ return;
+ case "D".charCodeAt(0):
+ if (!persistentState.showEditor && modifierMask === 0) { move(0, 1); break; }
+ if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(SNAKE); break; }
+ return;
+ case "B".charCodeAt(0):
+ if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(BLOCK); break; }
+ return;
+ case "G".charCodeAt(0):
+ if (modifierMask === 0) { toggleGrid(); break; }
+ if ( persistentState.showEditor && modifierMask === SHIFT) { toggleGravity(); break; }
+ return;
+ case "C".charCodeAt(0):
+ if ( persistentState.showEditor && modifierMask === SHIFT) { toggleCollision(); break; }
+ if ( persistentState.showEditor && modifierMask === CTRL) { copySelection(); break; }
+ return;
+ case "V".charCodeAt(0):
+ if ( persistentState.showEditor && modifierMask === CTRL) { setPaintBrushTileCode("paste"); break; }
+ return;
+ case 32: // spacebar
+ case 9: // tab
+ if (modifierMask === 0) { switchSnakes( 1); break; }
+ if (modifierMask === SHIFT) { switchSnakes(-1); break; }
+ return;
+ case "1".charCodeAt(0):
+ case "2".charCodeAt(0):
+ case "3".charCodeAt(0):
+ case "4".charCodeAt(0):
+ var index = event.keyCode - "1".charCodeAt(0);
+ var delta;
+ if (modifierMask === 0) {
+ delta = 1;
+ } else if (modifierMask === SHIFT) {
+ delta = -1;
+ } else return;
+ if (isAlive()) {
+ (function() {
+ var snakes = findSnakesOfColor(index);
+ if (snakes.length === 0) return;
+ for (var i = 0; i < snakes.length; i++) {
+ if (snakes[i].id === activeSnakeId) {
+ activeSnakeId = snakes[(i + delta + snakes.length) % snakes.length].id;
+ return;
+ }
+ }
+ activeSnakeId = snakes[0].id;
+ })();
+ }
+ break;
+ case 27: // escape
+ if ( persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(null); break; }
+ return;
+ default: return;
+ }
+ event.preventDefault();
+ render();
+});
+
+document.getElementById("switchSnakesButton").addEventListener("click", function() {
+ switchSnakes(1);
+ render();
+});
+function switchSnakes(delta) {
+ if (!isAlive()) return;
+ var snakes = getSnakes();
+ snakes.sort(compareId);
+ for (var i = 0; i < snakes.length; i++) {
+ if (snakes[i].id === activeSnakeId) {
+ activeSnakeId = snakes[(i + delta + snakes.length) % snakes.length].id;
+ return;
+ }
+ }
+ activeSnakeId = snakes[0].id;
+}
+document.getElementById("showGridButton").addEventListener("click", function() {
+ toggleGrid();
+});
+document.getElementById("saveProgressButton").addEventListener("click", function() {
+ saveReplay();
+});
+document.getElementById("restartButton").addEventListener("click", function() {
+ reset(unmoveStuff);
+ render();
+});
+document.getElementById("unmoveButton").addEventListener("click", function() {
+ undo(unmoveStuff);
+ render();
+});
+document.getElementById("removeButton").addEventListener("click", function() {
+ redo(unmoveStuff);
+ render();
+});
+
+document.getElementById("showHideEditor").addEventListener("click", function() {
+ toggleShowEditor();
+});
+function toggleShowEditor() {
+ persistentState.showEditor = !persistentState.showEditor;
+ savePersistentState();
+ showEditorChanged();
+}
+function toggleGrid() {
+ persistentState.showGrid = !persistentState.showGrid;
+ savePersistentState();
+ render();
+}
+["serializationTextarea", "shareLinkTextbox"].forEach(function(id) {
+ document.getElementById(id).addEventListener("keydown", function(event) {
+ // let things work normally
+ event.stopPropagation();
+ });
+});
+document.getElementById("submitSerializationButton").addEventListener("click", function() {
+ var string = document.getElementById("serializationTextarea").value;
+ try {
+ var newLevel = parseLevel(string);
+ } catch (e) {
+ alert(e);
+ return;
+ }
+ loadLevel(newLevel);
+});
+document.getElementById("shareLinkTextbox").addEventListener("focus", function() {
+ setTimeout(function() {
+ document.getElementById("shareLinkTextbox").select();
+ }, 0);
+});
+
+var paintBrushTileCode = null;
+var paintBrushSnakeColorIndex = 0;
+var paintBrushBlockId = 0;
+var paintBrushObject = null;
+var selectionStart = null;
+var selectionEnd = null;
+var resizeDragAnchorRowcol = null;
+var clipboardData = null;
+var clipboardOffsetRowcol = null;
+var paintButtonIdAndTileCodes = [
+ ["resizeButton", "resize"],
+ ["selectButton", "select"],
+ ["pasteButton", "paste"],
+ ["paintSpaceButton", SPACE],
+ ["paintWallButton", WALL],
+ ["paintSpikeButton", SPIKE],
+ ["paintExitButton", EXIT],
+ ["paintFruitButton", FRUIT],
+ ["paintPortalButton", PORTAL],
+ ["paintSnakeButton", SNAKE],
+ ["paintBlockButton", BLOCK],
+];
+paintButtonIdAndTileCodes.forEach(function(pair) {
+ var id = pair[0];
+ var tileCode = pair[1];
+ document.getElementById(id).addEventListener("click", function() {
+ setPaintBrushTileCode(tileCode);
+ });
+});
+document.getElementById("uneditButton").addEventListener("click", function() {
+ undo(uneditStuff);
+ render();
+});
+document.getElementById("reeditButton").addEventListener("click", function() {
+ redo(uneditStuff);
+ render();
+});
+document.getElementById("saveLevelButton").addEventListener("click", function() {
+ saveLevel();
+});
+document.getElementById("copyButton").addEventListener("click", function() {
+ copySelection();
+});
+document.getElementById("cutButton").addEventListener("click", function() {
+ cutSelection();
+});
+document.getElementById("cheatGravityButton").addEventListener("click", function() {
+ toggleGravity();
+});
+document.getElementById("cheatCollisionButton").addEventListener("click", function() {
+ toggleCollision();
+});
+document.getElementById("backgroundButton").addEventListener("click", function() {
+ toggleBackground();
+});
+function toggleBackground() {
+ if(background == "sky") background = "gradient";
+ else background = "sky";
+}
+function toggleGravity() {
+ isGravityEnabled = !isGravityEnabled;
+ isCollisionEnabled = true;
+ refreshCheatButtonText();
+}
+function toggleCollision() {
+ isCollisionEnabled = !isCollisionEnabled;
+ isGravityEnabled = false;
+ refreshCheatButtonText();
+}
+function refreshCheatButtonText() {
+ document.getElementById("cheatGravityButton").textContent = isGravityEnabled ? "Gravity: ON" : "Gravity: OFF";
+ document.getElementById("cheatGravityButton").style.background = isGravityEnabled ? "" : "#f88";
+
+ document.getElementById("cheatCollisionButton").textContent = isCollisionEnabled ? "Collision: ON" : "Collision: OFF";
+ document.getElementById("cheatCollisionButton").style.background = isCollisionEnabled ? "" : "#f88";
+}
+
+// be careful with location vs rowcol, because this variable is used when resizing
+var lastDraggingRowcol = null;
+var hoverLocation = null;
+var draggingChangeLog = null;
+canvas.addEventListener("mousedown", function(event) {
+ if (event.altKey) return;
+ if (event.button !== 0) return;
+ event.preventDefault();
+ var location = getLocationFromEvent(event);
+ if (persistentState.showEditor && paintBrushTileCode != null) {
+ // editor tool
+ lastDraggingRowcol = getRowcol(level, location);
+ if (paintBrushTileCode === "select") selectionStart = location;
+ if (paintBrushTileCode === "resize") resizeDragAnchorRowcol = lastDraggingRowcol;
+ draggingChangeLog = [];
+ paintAtLocation(location, draggingChangeLog);
+ } else {
+ // playtime
+ var object = findObjectAtLocation(location);
+ if (object == null) return;
+ if (object.type !== SNAKE) return;
+ // active snake
+ activeSnakeId = object.id;
+ render();
+ }
+});
+canvas.addEventListener("dblclick", function(event) {
+ if (event.altKey) return;
+ if (event.button !== 0) return;
+ event.preventDefault();
+ if (persistentState.showEditor && paintBrushTileCode === "select") {
+ // double click with select tool
+ var location = getLocationFromEvent(event);
+ var object = findObjectAtLocation(location);
+ if (object == null) return;
+ stopDragging();
+ if (object.type === SNAKE) {
+ // edit snakes of this color
+ paintBrushTileCode = SNAKE;
+ paintBrushSnakeColorIndex = object.id % snakeColors.length;
+ } else if (object.type === BLOCK) {
+ // edit this particular block
+ paintBrushTileCode = BLOCK;
+ paintBrushBlockId = object.id;
+ } else if (object.type === FRUIT) {
+ // edit fruits, i guess
+ paintBrushTileCode = FRUIT;
+ } else throw unreachable();
+ paintBrushTileCodeChanged();
+ }
+});
+document.addEventListener("mouseup", function(event) {
+ stopDragging();
+});
+function stopDragging() {
+ if (lastDraggingRowcol != null) {
+ // release the draggin'
+ lastDraggingRowcol = null;
+ paintBrushObject = null;
+ resizeDragAnchorRowcol = null;
+ pushUndo(uneditStuff, draggingChangeLog);
+ draggingChangeLog = null;
+ }
+}
+canvas.addEventListener("mousemove", function(event) {
+ if (!persistentState.showEditor) return;
+ var location = getLocationFromEvent(event);
+ var mouseRowcol = getRowcol(level, location);
+ if (lastDraggingRowcol != null) {
+ // Dragging Force - Through the Fruit and Flames
+ var lastDraggingLocation = getLocation(level, lastDraggingRowcol.r, lastDraggingRowcol.c);
+ // we need to get rowcols for everything before we start dragging, because dragging might resize the world.
+ var path = getNaiveOrthogonalPath(lastDraggingLocation, location).map(function(location) {
+ return getRowcol(level, location);
+ });
+ path.forEach(function(rowcol) {
+ // convert to location at the last minute in case each of these steps is changing the coordinate system.
+ paintAtLocation(getLocation(level, rowcol.r, rowcol.c), draggingChangeLog);
+ });
+ lastDraggingRowcol = mouseRowcol;
+ hoverLocation = null;
+ } else {
+ // hovering
+ if (hoverLocation !== location) {
+ hoverLocation = location;
+ render();
+ }
+ }
+});
+canvas.addEventListener("mouseout", function() {
+ if (hoverLocation !== location) {
+ // turn off the hover when the mouse leaves
+ hoverLocation = null;
+ render();
+ }
+});
+function getLocationFromEvent(event) {
+ var r = Math.floor(eventToMouseY(event, canvas) / tileSize);
+ var c = Math.floor(eventToMouseX(event, canvas) / tileSize);
+ // since the canvas is centered, the bounding client rect can be half-pixel aligned,
+ // resulting in slightly out-of-bounds mouse events.
+ r = clamp(r, 0, level.height);
+ c = clamp(c, 0, level.width);
+ return getLocation(level, r, c);
+}
+function eventToMouseX(event, canvas) { return event.clientX - canvas.getBoundingClientRect().left; }
+function eventToMouseY(event, canvas) { return event.clientY - canvas.getBoundingClientRect().top; }
+
+function selectAll() {
+ selectionStart = 0;
+ selectionEnd = level.map.length - 1;
+ setPaintBrushTileCode("select");
+}
+
+function setPaintBrushTileCode(tileCode) {
+ if (tileCode === "paste") {
+ // make sure we have something to paste
+ if (clipboardData == null) return;
+ }
+ if (paintBrushTileCode === "select" && tileCode !== "select" && selectionStart != null && selectionEnd != null) {
+ // usually this means to fill in the selection
+ if (tileCode == null) {
+ // cancel selection
+ selectionStart = null;
+ selectionEnd = null;
+ return;
+ }
+ if (typeof tileCode === "number" && tileCode !== PORTAL) {
+ // fill in the selection
+ fillSelection(tileCode);
+ selectionStart = null;
+ selectionEnd = null;
+ return;
+ }
+ // ok, just select something else then.
+ selectionStart = null;
+ selectionEnd = null;
+ }
+ if (tileCode === SNAKE) {
+ if (paintBrushTileCode === SNAKE) {
+ // next snake color
+ paintBrushSnakeColorIndex = (paintBrushSnakeColorIndex + 1) % snakeColors.length;
+ }
+ } else if (tileCode === BLOCK) {
+ var blocks = getBlocks();
+ if (paintBrushTileCode === BLOCK && blocks.length > 0) {
+ // cycle through block ids
+ blocks.sort(compareId);
+ if (paintBrushBlockId != null) {
+ (function() {
+ for (var i = 0; i < blocks.length; i++) {
+ if (blocks[i].id === paintBrushBlockId) {
+ i += 1;
+ if (i < blocks.length) {
+ // next block id
+ paintBrushBlockId = blocks[i].id;
+ } else {
+ // new block id
+ paintBrushBlockId = null;
+ }
+ return;
+ }
+ }
+ throw unreachable()
+ })();
+ } else {
+ // first one
+ paintBrushBlockId = blocks[0].id;
+ }
+ } else {
+ // new block id
+ paintBrushBlockId = null;
+ }
+ } else if (tileCode == null) {
+ // escape
+ if (paintBrushTileCode === BLOCK && paintBrushBlockId != null) {
+ // stop editing this block, but keep the block brush selected
+ tileCode = BLOCK;
+ paintBrushBlockId = null;
+ }
+ }
+ paintBrushTileCode = tileCode;
+ paintBrushTileCodeChanged();
+}
+function paintBrushTileCodeChanged() {
+ paintButtonIdAndTileCodes.forEach(function(pair) {
+ var id = pair[0];
+ var tileCode = pair[1];
+ var backgroundStyle = "";
+ if (tileCode === paintBrushTileCode) {
+ if (tileCode === SNAKE) {
+ // show the color of the active snake in the color of the button
+ backgroundStyle = snakeColors[paintBrushSnakeColorIndex];
+ } else {
+ backgroundStyle = "#fdc122";
+ }
+ }
+ document.getElementById(id).style.background = backgroundStyle;
+ });
+
+ var isSelectionMode = paintBrushTileCode === "select";
+ ["cutButton", "copyButton"].forEach(function (id) {
+ document.getElementById(id).disabled = !isSelectionMode;
+ });
+ document.getElementById("pasteButton").disabled = clipboardData == null;
+
+ render();
+}
+
+function cutSelection() {
+ copySelection();
+ fillSelection(SPACE);
+ render();
+}
+function copySelection() {
+ var selectedLocations = getSelectedLocations();
+ if (selectedLocations.length === 0) return;
+ var selectedObjects = [];
+ selectedLocations.forEach(function(location) {
+ var object = findObjectAtLocation(location);
+ if (object != null) addIfNotPresent(selectedObjects, object);
+ });
+ setClipboardData({
+ level: JSON.parse(JSON.stringify(level)),
+ selectedLocations: selectedLocations,
+ selectedObjects: JSON.parse(JSON.stringify(selectedObjects)),
+ });
+}
+function setClipboardData(data) {
+ // find the center
+ var minR = Infinity;
+ var maxR = -Infinity;
+ var minC = Infinity;
+ var maxC = -Infinity;
+ data.selectedLocations.forEach(function(location) {
+ var rowcol = getRowcol(data.level, location);
+ if (rowcol.r < minR) minR = rowcol.r;
+ if (rowcol.r > maxR) maxR = rowcol.r;
+ if (rowcol.c < minC) minC = rowcol.c;
+ if (rowcol.c > maxC) maxC = rowcol.c;
+ });
+ var offsetR = Math.floor((minR + maxR) / 2);
+ var offsetC = Math.floor((minC + maxC) / 2);
+
+ clipboardData = data;
+ clipboardOffsetRowcol = {r:offsetR, c:offsetC};
+ paintBrushTileCodeChanged();
+}
+function fillSelection(tileCode) {
+ var changeLog = [];
+ var locations = getSelectedLocations();
+ locations.forEach(function(location) {
+ if (level.map[location] !== tileCode) {
+ changeLog.push(["m", location, level.map[location], tileCode]);
+ level.map[location] = tileCode;
+ }
+ removeAnyObjectAtLocation(location, changeLog);
+ });
+ pushUndo(uneditStuff, changeLog);
+}
+function getSelectedLocations() {
+ if (selectionStart == null || selectionEnd == null) return [];
+ var rowcol1 = getRowcol(level, selectionStart);
+ var rowcol2 = getRowcol(level, selectionEnd);
+ var r1 = rowcol1.r;
+ var c1 = rowcol1.c;
+ var r2 = rowcol2.r;
+ var c2 = rowcol2.c;
+ if (r2 < r1) {
+ var tmp = r1;
+ r1 = r2;
+ r2 = tmp;
+ }
+ if (c2 < c1) {
+ var tmp = c1;
+ c1 = c2;
+ c2 = tmp;
+ }
+ var objects = [];
+ var locations = [];
+ for (var r = r1; r <= r2; r++) {
+ for (var c = c1; c <= c2; c++) {
+ var location = getLocation(level, r, c);
+ locations.push(location);
+ var object = findObjectAtLocation(location);
+ if (object != null) addIfNotPresent(objects, object);
+ }
+ }
+ // select the rest of any partially-selected objects
+ objects.forEach(function(object) {
+ object.locations.forEach(function(location) {
+ addIfNotPresent(locations, location);
+ });
+ });
+ return locations;
+}
+
+function setHeight(newHeight, changeLog) {
+ if (newHeight < level.height) {
+ // crop
+ for (var r = newHeight; r < level.height; r++) {
+ for (var c = 0; c < level.width; c++) {
+ var location = getLocation(level, r, c);
+ removeAnyObjectAtLocation(location, changeLog);
+ // also delete non-space tiles
+ paintTileAtLocation(location, SPACE, changeLog);
+ }
+ }
+ level.map.splice(newHeight * level.width);
+ } else {
+ // expand
+ for (var r = level.height; r < newHeight; r++) {
+ for (var c = 0; c < level.width; c++) {
+ level.map.push(SPACE);
+ }
+ }
+ }
+ changeLog.push(["h", level.height, newHeight]);
+ level.height = newHeight;
+}
+function setWidth(newWidth, changeLog) {
+ if (newWidth < level.width) {
+ // crop
+ for (var r = level.height - 1; r >= 0; r--) {
+ for (var c = level.width - 1; c >= newWidth; c--) {
+ var location = getLocation(level, r, c);
+ removeAnyObjectAtLocation(location, changeLog);
+ paintTileAtLocation(location, SPACE, changeLog);
+ level.map.splice(location, 1);
+ }
+ }
+ } else {
+ // expand
+ for (var r = level.height - 1; r >= 0; r--) {
+ var insertionPoint = level.width * (r + 1);
+ for (var c = level.width; c < newWidth; c++) {
+ // boy is this inefficient. ... YOLO!
+ level.map.splice(insertionPoint, 0, SPACE);
+ }
+ }
+ }
+
+ var transformLocation = makeScaleCoordinatesFunction(level.width, newWidth);
+ level.objects.forEach(function(object) {
+ object.locations = object.locations.map(transformLocation);
+ });
+
+ changeLog.push(["w", level.width, newWidth]);
+ level.width = newWidth;
+}
+
+function newSnake(color, location) {
+ var snakes = findSnakesOfColor(color);
+ snakes.sort(compareId);
+ for (var i = 0; i < snakes.length; i++) {
+ if (snakes[i].id !== i * snakeColors.length + color) break;
+ }
+ return {
+ type: SNAKE,
+ id: i * snakeColors.length + color,
+ dead: false,
+ locations: [location],
+ };
+}
+function newBlock(location) {
+ var blocks = getBlocks();
+ blocks.sort(compareId);
+ for (var i = 0; i < blocks.length; i++) {
+ if (blocks[i].id !== i) break;
+ }
+ return {
+ type: BLOCK,
+ id: i,
+ dead: false, // unused
+ locations: [location],
+ };
+}
+function newFruit(location) {
+ var fruits = getObjectsOfType(FRUIT);
+ fruits.sort(compareId);
+ for (var i = 0; i < fruits.length; i++) {
+ if (fruits[i].id !== i) break;
+ }
+ return {
+ type: FRUIT,
+ id: i,
+ dead: false, // unused
+ locations: [location],
+ };
+}
+function paintAtLocation(location, changeLog) {
+ if (typeof paintBrushTileCode === "number") {
+ removeAnyObjectAtLocation(location, changeLog);
+ paintTileAtLocation(location, paintBrushTileCode, changeLog);
+ } else if (paintBrushTileCode === "resize") {
+ var toRowcol = getRowcol(level, location);
+ var dr = toRowcol.r - resizeDragAnchorRowcol.r;
+ var dc = toRowcol.c - resizeDragAnchorRowcol.c;
+ resizeDragAnchorRowcol = toRowcol;
+ if (dr !== 0) setHeight(level.height + dr, changeLog);
+ if (dc !== 0) setWidth(level.width + dc, changeLog);
+ } else if (paintBrushTileCode === "select") {
+ selectionEnd = location;
+ } else if (paintBrushTileCode === "paste") {
+ var hoverRowcol = getRowcol(level, location);
+ var pastedData = previewPaste(hoverRowcol.r, hoverRowcol.c);
+ pastedData.selectedLocations.forEach(function(location) {
+ var tileCode = pastedData.level.map[location];
+ removeAnyObjectAtLocation(location, changeLog);
+ paintTileAtLocation(location, tileCode, changeLog);
+ });
+ pastedData.selectedObjects.forEach(function(object) {
+ // refresh the ids so there are no collisions.
+ if (object.type === SNAKE) {
+ object.id = newSnake(object.id % snakeColors.length).id;
+ } else if (object.type === BLOCK) {
+ object.id = newBlock().id;
+ } else if (object.type === FRUIT) {
+ object.id = newFruit().id;
+ } else throw unreachable();
+ level.objects.push(object);
+ changeLog.push([object.type, object.id, [0,[]], serializeObjectState(object)]);
+ });
+ } else if (paintBrushTileCode === SNAKE) {
+ var oldSnakeSerialization = serializeObjectState(paintBrushObject);
+ if (paintBrushObject != null) {
+ // keep dragging
+ if (paintBrushObject.locations[0] === location) return; // we just did that
+ // watch out for self-intersection
+ var selfIntersectionIndex = paintBrushObject.locations.indexOf(location);
+ if (selfIntersectionIndex !== -1) {
+ // truncate from here back
+ paintBrushObject.locations.splice(selfIntersectionIndex);
+ }
+ }
+
+ // make sure there's space behind us
+ paintTileAtLocation(location, SPACE, changeLog);
+ removeAnyObjectAtLocation(location, changeLog);
+ if (paintBrushObject == null) {
+ var thereWereNoSnakes = countSnakes() === 0;
+ paintBrushObject = newSnake(paintBrushSnakeColorIndex, location);
+ level.objects.push(paintBrushObject);
+ if (thereWereNoSnakes) activateAnySnakePlease();
+ } else {
+ // extend le snake
+ paintBrushObject.locations.unshift(location);
+ }
+ changeLog.push([paintBrushObject.type, paintBrushObject.id, oldSnakeSerialization, serializeObjectState(paintBrushObject)]);
+ } else if (paintBrushTileCode === BLOCK) {
+ var objectHere = findObjectAtLocation(location);
+ if (paintBrushBlockId == null && objectHere != null && objectHere.type === BLOCK) {
+ // just start editing this block
+ paintBrushBlockId = objectHere.id;
+ } else {
+ // make a change
+ // make sure there's space behind us
+ paintTileAtLocation(location, SPACE, changeLog);
+ var thisBlock = null;
+ if (paintBrushBlockId != null) {
+ thisBlock = findBlockById(paintBrushBlockId);
+ }
+ var oldBlockSerialization = serializeObjectState(thisBlock);
+ if (thisBlock == null) {
+ // create new block
+ removeAnyObjectAtLocation(location, changeLog);
+ thisBlock = newBlock(location);
+ level.objects.push(thisBlock);
+ paintBrushBlockId = thisBlock.id;
+ } else {
+ var existingIndex = thisBlock.locations.indexOf(location);
+ if (existingIndex !== -1) {
+ // reclicking part of this object means to delete just part of it.
+ if (thisBlock.locations.length === 1) {
+ // goodbye
+ removeObject(thisBlock, changeLog);
+ paintBrushBlockId = null;
+ } else {
+ thisBlock.locations.splice(existingIndex, 1);
+ }
+ } else {
+ // add a tile to the block
+ removeAnyObjectAtLocation(location, changeLog);
+ thisBlock.locations.push(location);
+ }
+ }
+ changeLog.push([thisBlock.type, thisBlock.id, oldBlockSerialization, serializeObjectState(thisBlock)]);
+ delete blockSupportRenderCache[thisBlock.id];
+ }
+ } else if (paintBrushTileCode === FRUIT) {
+ paintTileAtLocation(location, SPACE, changeLog);
+ removeAnyObjectAtLocation(location, changeLog);
+ var object = newFruit(location)
+ level.objects.push(object);
+ changeLog.push([object.type, object.id, serializeObjectState(null), serializeObjectState(object)]);
+ } else throw unreachable();
+ render();
+}
+
+function paintTileAtLocation(location, tileCode, changeLog) {
+ if (level.map[location] === tileCode) return;
+ changeLog.push(["m", location, level.map[location], tileCode]);
+ level.map[location] = tileCode;
+}
+
+function pushUndo(undoStuff, changeLog) {
+ // changeLog = [
+ // ["i", 0, -1, 0, animationQueue, freshlyRemovedAnimatedObjects],
+ // // player input for snake 0, dr:-1, dc:0. has no effect on state.
+ // // "i" is always the first change in normal player movement.
+ // // if a changeLog does not start with "i", then it is an editor action.
+ // // animationQueue and freshlyRemovedAnimatedObjects
+ // // are used for animating re-move.
+ // ["m", 21, 0, 1], // map at location 23 changed from 0 to 1
+ // ["s", 0, [false, [1,2]], [false, [2,3]]], // snake id 0 moved from alive at [1, 2] to alive at [2, 3]
+ // ["s", 1, [false, [11,12]], [true, [12,13]]], // snake id 1 moved from alive at [11, 12] to dead at [12, 13]
+ // ["b", 1, [false, [20,30]], [false, []]], // block id 1 was deleted from location [20, 30]
+ // ["f", 0, [false, [40]], [false, []]], // fruit id 0 was deleted from location [40]
+ // ["h", 25, 10], // height changed from 25 to 10. all cropped tiles are guaranteed to be SPACE.
+ // ["w", 8, 10], // width changed from 8 to 10. a change in the coordinate system.
+ // ["m", 23, 2, 0], // map at location 23 changed from 2 to 0 in the new coordinate system.
+ // 10, // the last change is always a declaration of the final width of the map.
+ // ];
+ reduceChangeLog(changeLog);
+ if (changeLog.length === 0) return;
+ changeLog.push(level.width);
+ undoStuff.undoStack.push(changeLog);
+ undoStuff.redoStack = [];
+ paradoxes = [];
+
+ if (undoStuff === uneditStuff) editorHasBeenTouched = true;
+
+ undoStuffChanged(undoStuff);
+}
+function reduceChangeLog(changeLog) {
+ for (var i = 0; i < changeLog.length - 1; i++) {
+ var change = changeLog[i];
+ if (change[0] === "i") {
+ continue; // don't reduce player input
+ } else if (change[0] === "h") {
+ for (var j = i + 1; j < changeLog.length; j++) {
+ var otherChange = changeLog[j];
+ if (otherChange[0] === "h") {
+ // combine
+ change[2] = otherChange[2];
+ changeLog.splice(j, 1);
+ j--;
+ continue;
+ } else if (otherChange[0] === "w") {
+ continue; // no interaction between height and width
+ } else break; // no more reduction possible
+ }
+ if (change[1] === change[2]) {
+ // no change
+ changeLog.splice(i, 1);
+ i--;
+ }
+ } else if (change[0] === "w") {
+ for (var j = i + 1; j < changeLog.length; j++) {
+ var otherChange = changeLog[j];
+ if (otherChange[0] === "w") {
+ // combine
+ change[2] = otherChange[2];
+ changeLog.splice(j, 1);
+ j--;
+ continue;
+ } else if (otherChange[0] === "h") {
+ continue; // no interaction between height and width
+ } else break; // no more reduction possible
+ }
+ if (change[1] === change[2]) {
+ // no change
+ changeLog.splice(i, 1);
+ i--;
+ }
+ } else if (change[0] === "m") {
+ for (var j = i + 1; j < changeLog.length; j++) {
+ var otherChange = changeLog[j];
+ if (otherChange[0] === "m" && otherChange[1] === change[1]) {
+ // combine
+ change[3] = otherChange[3];
+ changeLog.splice(j, 1);
+ j--;
+ } else if (otherChange[0] === "w" || otherChange[0] === "h") {
+ break; // can't reduce accros resizes
+ }
+ }
+ if (change[2] === change[3]) {
+ // no change
+ changeLog.splice(i, 1);
+ i--;
+ }
+ } else if (change[0] === SNAKE || change[0] === BLOCK || change[0] === FRUIT) {
+ for (var j = i + 1; j < changeLog.length; j++) {
+ var otherChange = changeLog[j];
+ if (otherChange[0] === change[0] && otherChange[1] === change[1]) {
+ // combine
+ change[3] = otherChange[3];
+ changeLog.splice(j, 1);
+ j--;
+ } else if (otherChange[0] === "w" || otherChange[0] === "h") {
+ break; // can't reduce accros resizes
+ }
+ }
+ if (deepEquals(change[2], change[3])) {
+ // no change
+ changeLog.splice(i, 1);
+ i--;
+ }
+ } else throw unreachable();
+ }
+}
+function undo(undoStuff) {
+ if (undoStuff.undoStack.length === 0) return; // already at the beginning
+ animationQueue = [];
+ animationQueueCursor = 0;
+ paradoxes = [];
+ undoOneFrame(undoStuff);
+ undoStuffChanged(undoStuff);
+}
+function reset(undoStuff) {
+ animationQueue = [];
+ animationQueueCursor = 0;
+ paradoxes = [];
+ while (undoStuff.undoStack.length > 0) {
+ undoOneFrame(undoStuff);
+ }
+ undoStuffChanged(undoStuff);
+}
+function undoOneFrame(undoStuff) {
+ var doThis = undoStuff.undoStack.pop();
+ var redoChangeLog = [];
+ undoChanges(doThis, redoChangeLog);
+ if (redoChangeLog.length > 0) {
+ redoChangeLog.push(level.width);
+ undoStuff.redoStack.push(redoChangeLog);
+ }
+
+ if (undoStuff === uneditStuff) editorHasBeenTouched = true;
+}
+function redo(undoStuff) {
+ if (undoStuff.redoStack.length === 0) return; // already at the beginning
+ animationQueue = [];
+ animationQueueCursor = 0;
+ paradoxes = [];
+ redoOneFrame(undoStuff);
+ undoStuffChanged(undoStuff);
+}
+function unreset(undoStuff) {
+ animationQueue = [];
+ animationQueueCursor = 0;
+ paradoxes = [];
+ while (undoStuff.redoStack.length > 0) {
+ redoOneFrame(undoStuff);
+ }
+ undoStuffChanged(undoStuff);
+
+ // don't animate the last frame
+ animationQueue = [];
+ animationQueueCursor = 0;
+ freshlyRemovedAnimatedObjects = [];
+}
+function redoOneFrame(undoStuff) {
+ var doThis = undoStuff.redoStack.pop();
+ var undoChangeLog = [];
+ undoChanges(doThis, undoChangeLog);
+ if (undoChangeLog.length > 0) {
+ undoChangeLog.push(level.width);
+ undoStuff.undoStack.push(undoChangeLog);
+ }
+
+ if (undoStuff === uneditStuff) editorHasBeenTouched = true;
+}
+function undoChanges(changes, changeLog) {
+ var widthContext = changes.pop();
+ var transformLocation = widthContext === level.width ? identityFunction : makeScaleCoordinatesFunction(widthContext, level.width);
+ for (var i = changes.length - 1; i >= 0; i--) {
+ var paradoxDescription = undoChange(changes[i]);
+ if (paradoxDescription != null) paradoxes.push(paradoxDescription);
+ }
+
+ var lastChange = changes[changes.length - 1];
+ if (lastChange[0] === "i") {
+ // replay animation
+ animationQueue = lastChange[4];
+ animationQueueCursor = 0;
+ freshlyRemovedAnimatedObjects = lastChange[5];
+ animationStart = new Date().getTime();
+ }
+
+ function undoChange(change) {
+ // note: everything here is going backwards: to -> from
+ if (change[0] === "i") {
+ // no state change, but preserve the intention.
+ changeLog.push(change);
+ return null;
+ } else if (change[0] === "h") {
+ // change height
+ var fromHeight = change[1];
+ var toHeight = change[2];
+ if (level.height !== toHeight) return "Impossible";
+ setHeight(fromHeight, changeLog);
+ } else if (change[0] === "w") {
+ // change width
+ var fromWidth = change[1];
+ var toWidth = change[2];
+ if (level.width !== toWidth) return "Impossible";
+ setWidth(fromWidth, changeLog);
+ } else if (change[0] === "m") {
+ // change map tile
+ var location = transformLocation(change[1]);
+ var fromTileCode = change[2];
+ var toTileCode = change[3];
+ if (location >= level.map.length) return "Can't turn " + describe(toTileCode) + " into " + describe(fromTileCode) + " out of bounds";
+ if (level.map[location] !== toTileCode) return "Can't turn " + describe(toTileCode) + " into " + describe(fromTileCode) + " because there's " + describe(level.map[location]) + " there now";
+ paintTileAtLocation(location, fromTileCode, changeLog);
+ } else if (change[0] === SNAKE || change[0] === BLOCK || change[0] === FRUIT) {
+ // change object
+ var type = change[0];
+ var id = change[1];
+ var fromDead = change[2][0];
+ var toDead = change[3][0];
+ var fromLocations = change[2][1].map(transformLocation);
+ var toLocations = change[3][1].map(transformLocation);
+ if (fromLocations.filter(function(location) { return location >= level.map.length; }).length > 0) {
+ return "Can't move " + describe(type, id) + " out of bounds";
+ }
+ var object = findObjectOfTypeAndId(type, id);
+ if (toLocations.length !== 0) {
+ // should exist at this location
+ if (object == null) return "Can't move " + describe(type, id) + " because it doesn't exit";
+ if (!deepEquals(object.locations, toLocations)) return "Can't move " + describe(object) + " because it's in the wrong place";
+ if (object.dead !== toDead) return "Can't move " + describe(object) + " because it's alive/dead state doesn't match";
+ // doit
+ if (fromLocations.length !== 0) {
+ var oldState = serializeObjectState(object);
+ object.locations = fromLocations;
+ object.dead = fromDead;
+ changeLog.push([object.type, object.id, oldState, serializeObjectState(object)]);
+ } else {
+ removeObject(object, changeLog);
+ }
+ } else {
+ // shouldn't exist
+ if (object != null) return "Can't create " + describe(type, id) + " because it already exists";
+ // doit
+ object = {
+ type: type,
+ id: id,
+ dead: fromDead,
+ locations: fromLocations,
+ };
+ level.objects.push(object);
+ changeLog.push([object.type, object.id, [0,[]], serializeObjectState(object)]);
+ }
+ } else throw unreachable();
+ }
+}
+function describe(arg1, arg2) {
+ // describe(0) -> "Space"
+ // describe(SNAKE, 0) -> "Snake 0 (Red)"
+ // describe(object) -> "Snake 0 (Red)"
+ // describe(BLOCK, 1) -> "Block 1"
+ // describe(FRUIT) -> "Fruit"
+ if (typeof arg1 === "number") {
+ switch (arg1) {
+ case SPACE: return "Space";
+ case WALL: return "a Wall";
+ case SPIKE: return "Spikes";
+ case EXIT: return "an Exit";
+ case PORTAL: return "a Portal";
+ default: throw unreachable();
+ }
+ }
+ if (arg1 === SNAKE) {
+ var color = (function() {
+ switch (snakeColors[arg2 % snakeColors.length]) {
+ case "#fd0c0b": return " (Red)";
+ case "#18d11f": return " (Green)";
+ case "#004cff": return " (Blue)";
+ case "#fdc122": return " (Yellow)";
+ default: throw unreachable();
+ }
+ })();
+ return "Snake " + arg2 + color;
+ }
+ if (arg1 === BLOCK) {
+ return "Block " + arg2;
+ }
+ if (arg1 === FRUIT) {
+ return "Fruit";
+ }
+ if (typeof arg1 === "object") return describe(arg1.type, arg1.id);
+ throw unreachable();
+}
+
+function undoStuffChanged(undoStuff) {
+ var movesText = undoStuff.undoStack.length + "+" + undoStuff.redoStack.length;
+ document.getElementById(undoStuff.spanId).textContent = movesText;
+ document.getElementById(undoStuff.undoButtonId).disabled = undoStuff.undoStack.length === 0;
+ document.getElementById(undoStuff.redoButtonId).disabled = undoStuff.redoStack.length === 0;
+
+ // render paradox display
+ var uniqueParadoxes = [];
+ var paradoxCounts = [];
+ paradoxes.forEach(function(paradoxDescription) {
+ var index = uniqueParadoxes.indexOf(paradoxDescription);
+ if (index !== -1) {
+ paradoxCounts[index] += 1;
+ } else {
+ uniqueParadoxes.push(paradoxDescription);
+ paradoxCounts.push(1);
+ }
+ });
+ var paradoxDivContent = "";
+ uniqueParadoxes.forEach(function(paradox, i) {
+ if (i > 0) paradoxDivContent += "
\n";
+ if (paradoxCounts[i] > 1) paradoxDivContent += "(" + paradoxCounts[i] + "x) ";
+ paradoxDivContent += "Time Travel Paradox! " + uniqueParadoxes[i];
+ });
+ document.getElementById("paradoxDiv").innerHTML = paradoxDivContent;
+
+ updateDirtyState();
+
+ if (unmoveStuff.redoStack.length === 0) {
+ document.getElementById("removeButton").classList.remove("click-me");
+ }
+}
+
+var CLEAN_NO_TIMELINES = 0;
+var CLEAN_WITH_REDO = 1;
+var REPLAY_DIRTY = 2;
+var EDITOR_DIRTY = 3;
+var dirtyState = CLEAN_NO_TIMELINES;
+var editorHasBeenTouched = false;
+function updateDirtyState() {
+ if (haveCheatcodesBeenUsed() || editorHasBeenTouched) {
+ dirtyState = EDITOR_DIRTY;
+ } else if (unmoveStuff.undoStack.length > 0) {
+ dirtyState = REPLAY_DIRTY;
+ } else if (unmoveStuff.redoStack.length > 0) {
+ dirtyState = CLEAN_WITH_REDO;
+ } else {
+ dirtyState = CLEAN_NO_TIMELINES;
+ }
+
+ var saveLevelButton = document.getElementById("saveLevelButton");
+ // the save button clears your timelines
+ saveLevelButton.disabled = dirtyState === CLEAN_NO_TIMELINES;
+ if (dirtyState >= EDITOR_DIRTY) {
+ // you should save
+ saveLevelButton.classList.add("click-me");
+ saveLevelButton.textContent = "*" + "Save Level";
+ } else {
+ saveLevelButton.classList.remove("click-me");
+ saveLevelButton.textContent = "Save Level";
+ }
+
+ var saveProgressButton = document.getElementById("saveProgressButton");
+ // you can't save a replay if your level is dirty
+ if (dirtyState === CLEAN_WITH_REDO) {
+ saveProgressButton.textContent = "Forget Progress";
+ } else {
+ saveProgressButton.textContent = "Save Progress";
+ }
+ saveProgressButton.disabled = dirtyState >= EDITOR_DIRTY || dirtyState === CLEAN_NO_TIMELINES;
+}
+function haveCheatcodesBeenUsed() {
+ return !unmoveStuff.undoStack.every(function(changeLog) {
+ // normal movement always starts with "i".
+ return changeLog[0][0] === "i";
+ });
+}
+
+var persistentState = {
+ showEditor: false,
+ showGrid: false,
+};
+function savePersistentState() {
+ localStorage.snakefall = JSON.stringify(persistentState);
+}
+function loadPersistentState() {
+ try {
+ persistentState = JSON.parse(localStorage.snakefall);
+ } catch (e) {
+ }
+ persistentState.showEditor = !!persistentState.showEditor;
+ persistentState.showGrid = !!persistentState.showGrid;
+ showEditorChanged();
+}
+var isGravityEnabled = true;
+function isGravity() {
+ return isGravityEnabled || !persistentState.showEditor;
+}
+var isCollisionEnabled = true;
+function isCollision() {
+ return isCollisionEnabled || !persistentState.showEditor;
+}
+function isAnyCheatcodeEnabled() {
+ return persistentState.showEditor && (
+ !isGravityEnabled || !isCollisionEnabled
+ );
+}
+var background = [
+ "sky",
+ "gradient",
+];
+
+
+function showEditorChanged() {
+ document.getElementById("showHideEditor").textContent = (persistentState.showEditor ? "Hide" : "Show") + " Editor Stuff";
+ ["editorDiv", "editorPane"].forEach(function(id) {
+ document.getElementById(id).style.display = persistentState.showEditor ? "block" : "none";
+ });
+ document.getElementById("wasdSpan").textContent = persistentState.showEditor ? "" : "/WASD";
+
+ render();
+}
+
+function move(dr, dc) {
+ if (!isAlive()) return;
+ animationQueue = [];
+ animationQueueCursor = 0;
+ freshlyRemovedAnimatedObjects = [];
+ animationStart = new Date().getTime();
+ var activeSnake = findActiveSnake();
+ var headRowcol = getRowcol(level, activeSnake.locations[0]);
+ var newRowcol = {r:headRowcol.r + dr, c:headRowcol.c + dc};
+ if (!isInBounds(level, newRowcol.r, newRowcol.c)) return;
+ var newLocation = getLocation(level, newRowcol.r, newRowcol.c);
+ var changeLog = [];
+
+ // The changeLog for a player movement starts with the input
+ // when playing normally.
+ if (!isAnyCheatcodeEnabled()) {
+ changeLog.push(["i", activeSnake.id, dr, dc, animationQueue, freshlyRemovedAnimatedObjects]);
+ }
+
+ var ate = false;
+ var pushedObjects = [];
+
+ if (isCollision()) {
+ var newTile = level.map[newLocation];
+ if (!isTileCodeAir(newTile)) return; // can't go through that tile
+ var otherObject = findObjectAtLocation(newLocation);
+ if (otherObject != null) {
+ if (otherObject === activeSnake) return; // can't push yourself
+ if (otherObject.type === FRUIT) {
+ // eat
+ removeObject(otherObject, changeLog);
+ ate = true;
+ } else {
+ // push objects
+ if (!checkMovement(activeSnake, otherObject, dr, dc, pushedObjects)) return false;
+ }
+ }
+ }
+
+ // slither forward
+ var activeSnakeOldState = serializeObjectState(activeSnake);
+ var size1 = activeSnake.locations.length === 1;
+ var slitherAnimations = [
+ 70,
+ [
+ // size-1 snakes really do more of a move than a slither
+ size1 ? MOVE_SNAKE : SLITHER_HEAD,
+ activeSnake.id,
+ dr,
+ dc,
+ ]
+ ];
+ activeSnake.locations.unshift(newLocation);
+ if (!ate) {
+ // drag your tail forward
+ var oldRowcol = getRowcol(level, activeSnake.locations[activeSnake.locations.length - 1]);
+ var newRowcol = getRowcol(level, activeSnake.locations[activeSnake.locations.length - 2]);
+ if (!size1) {
+ slitherAnimations.push([
+ SLITHER_TAIL,
+ activeSnake.id,
+ newRowcol.r - oldRowcol.r,
+ newRowcol.c - oldRowcol.c,
+ ]);
+ }
+ activeSnake.locations.pop();
+ }
+ changeLog.push([activeSnake.type, activeSnake.id, activeSnakeOldState, serializeObjectState(activeSnake)]);
+
+ // did you just push your face into a portal?
+ var portalLocations = getActivePortalLocations();
+ var portalActivationLocations = [];
+ if (portalLocations.indexOf(newLocation) !== -1) {
+ portalActivationLocations.push(newLocation);
+ }
+ // push everything, too
+ moveObjects(pushedObjects, dr, dc, portalLocations, portalActivationLocations, changeLog, slitherAnimations);
+ animationQueue.push(slitherAnimations);
+
+ // gravity loop
+ var stateToAnimationIndex = {};
+ if (isGravity()) for (var fallHeight = 1;; fallHeight++) {
+ var serializedState = serializeObjects(level.objects);
+ var infiniteLoopStartIndex = stateToAnimationIndex[serializedState];
+ if (infiniteLoopStartIndex != null) {
+ // infinite loop
+ animationQueue.push([0, [INFINITE_LOOP, animationQueue.length - infiniteLoopStartIndex]]);
+ break;
+ } else {
+ stateToAnimationIndex[serializedState] = animationQueue.length;
+ }
+ // do portals separate from falling logic
+ if (portalActivationLocations.length === 1) {
+ var portalAnimations = [500];
+ if (activatePortal(portalLocations, portalActivationLocations[0], portalAnimations, changeLog)) {
+ animationQueue.push(portalAnimations);
+ }
+ portalActivationLocations = [];
+ }
+ // now do falling logic
+ var didAnything = false;
+ var fallingAnimations = [
+ 70 / Math.sqrt(fallHeight),
+ ];
+ var exitAnimationQueue = [];
+
+ // check for exit
+ if (!isUneatenFruit()) { //Gooby
+ var snakes = getSnakes();
+ for (var i = 0; i < snakes.length; i++) {
+ var snake = snakes[i];
+ if (level.map[snake.locations[0]] === EXIT) {
+ // (one of) you made it!
+ removeAnimatedObject(snake, changeLog);
+ exitAnimationQueue.push([
+ 200,
+ [EXIT_SNAKE, snake.id, 0, 0],
+ ]);
+ didAnything = true;
+ }
+ }
+ }
+
+ // fall
+ var dyingObjects = [];
+ var fallingObjects = level.objects.filter(function(object) {
+ if (object.type === FRUIT) return; // can't fall
+ var theseDyingObjects = [];
+ if (!checkMovement(null, object, 1, 0, [], theseDyingObjects)) return false;
+ // this object can fall. maybe more will fall with it too. we'll check those separately.
+ theseDyingObjects.forEach(function(object) {
+ addIfNotPresent(dyingObjects, object);
+ });
+ return true;
+ });
+ if (dyingObjects.length > 0) {
+ var anySnakesDied = false;
+ dyingObjects.forEach(function(object) {
+ if (object.type === SNAKE) {
+ // look what you've done
+ var oldState = serializeObjectState(object);
+ object.dead = true;
+ changeLog.push([object.type, object.id, oldState, serializeObjectState(object)]);
+ anySnakesDied = true;
+ } else if (object.type === BLOCK) {
+ // a box fell off the world
+ removeAnimatedObject(object, changeLog);
+ removeFromArray(fallingObjects, object);
+ exitAnimationQueue.push([
+ 200,
+ [
+ DIE_BLOCK,
+ object.id,
+ 0, 0
+ ],
+ ]);
+ didAnything = true;
+ } else throw unreachable();
+ });
+ if (anySnakesDied) break;
+ }
+ if (fallingObjects.length > 0) {
+ moveObjects(fallingObjects, 1, 0, portalLocations, portalActivationLocations, changeLog, fallingAnimations);
+ didAnything = true;
+ }
+
+ if (!didAnything) break;
+ Array.prototype.push.apply(animationQueue, exitAnimationQueue);
+ if (fallingAnimations.length > 1) animationQueue.push(fallingAnimations);
+ }
+
+ pushUndo(unmoveStuff, changeLog);
+ render();
+}
+
+function checkMovement(pusher, pushedObject, dr, dc, pushedObjects, dyingObjects) {
+ // pusher can be null (for gravity)
+ pushedObjects.push(pushedObject);
+ // find forward locations
+ var forwardLocations = [];
+ for (var i = 0; i < pushedObjects.length; i++) {
+ pushedObject = pushedObjects[i];
+ for (var j = 0; j < pushedObject.locations.length; j++) {
+ var rowcol = getRowcol(level, pushedObject.locations[j]);
+ var forwardRowcol = {r:rowcol.r + dr, c:rowcol.c + dc};
+ if (!isInBounds(level, forwardRowcol.r, forwardRowcol.c)) {
+ if (dyingObjects == null) {
+ // can't push things out of bounds
+ return false;
+ } else {
+ // this thing is going to fall out of bounds
+ addIfNotPresent(dyingObjects, pushedObject);
+ addIfNotPresent(pushedObjects, pushedObject);
+ continue;
+ }
+ }
+ var forwardLocation = getLocation(level, forwardRowcol.r, forwardRowcol.c);
+ var yetAnotherObject = findObjectAtLocation(forwardLocation);
+ if (yetAnotherObject != null) {
+ if (yetAnotherObject.type === FRUIT) {
+ // not pushable
+ return false;
+ }
+ if (yetAnotherObject === pusher) {
+ // indirect pushing ourselves.
+ // special check for when we're indirectly pushing the tip of our own tail.
+ if (forwardLocation === pusher.locations[pusher.locations.length -1]) {
+ // for some reason this is ok.
+ continue;
+ }
+ return false;
+ }
+ addIfNotPresent(pushedObjects, yetAnotherObject);
+ } else {
+ addIfNotPresent(forwardLocations, forwardLocation);
+ }
+ }
+ }
+ // check forward locations
+ for (var i = 0; i < forwardLocations.length; i++) {
+ var forwardLocation = forwardLocations[i];
+ // many of these locations can be inside objects,
+ // but that means the tile must be air,
+ // and we already know pushing that object.
+ var tileCode = level.map[forwardLocation];
+ if (!isTileCodeAir(tileCode)) {
+ if (dyingObjects != null) {
+ if (tileCode === SPIKE) {
+ // uh... which object was this again?
+ var deadObject = findObjectAtLocation(offsetLocation(forwardLocation, -dr, -dc));
+ if (deadObject.type === SNAKE) {
+ // ouch!
+ addIfNotPresent(dyingObjects, deadObject);
+ continue;
+ }
+ }
+ }
+ // can't push into something solid
+ return false;
+ }
+ }
+ // the push is go
+ return true;
+}
+
+function activateAnySnakePlease() {
+ var snakes = getSnakes();
+ if (snakes.length === 0) return; // nope.avi
+ activeSnakeId = snakes[0].id;
+}
+
+function moveObjects(objects, dr, dc, portalLocations, portalActivationLocations, changeLog, animations) {
+ objects.forEach(function(object) {
+ var oldState = serializeObjectState(object);
+ var oldPortals = getSetIntersection(portalLocations, object.locations);
+ for (var i = 0; i < object.locations.length; i++) {
+ object.locations[i] = offsetLocation(object.locations[i], dr, dc);
+ }
+ changeLog.push([object.type, object.id, oldState, serializeObjectState(object)]);
+ animations.push([
+ "m" + object.type, // MOVE_SNAKE | MOVE_BLOCK
+ object.id,
+ dr,
+ dc,
+ ]);
+
+ var newPortals = getSetIntersection(portalLocations, object.locations);
+ var activatingPortals = newPortals.filter(function(portalLocation) {
+ return oldPortals.indexOf(portalLocation) === -1;
+ });
+ if (activatingPortals.length === 1) {
+ // exactly one new portal we're touching. activate it
+ portalActivationLocations.push(activatingPortals[0]);
+ }
+ });
+}
+
+function activatePortal(portalLocations, portalLocation, animations, changeLog) {
+ var otherPortalLocation = portalLocations[1 - portalLocations.indexOf(portalLocation)];
+ var portalRowcol = getRowcol(level, portalLocation);
+ var otherPortalRowcol = getRowcol(level, otherPortalLocation);
+ var delta = {r:otherPortalRowcol.r - portalRowcol.r, c:otherPortalRowcol.c - portalRowcol.c};
+
+ var object = findObjectAtLocation(portalLocation);
+ var newLocations = [];
+ for (var i = 0; i < object.locations.length; i++) {
+ var rowcol = getRowcol(level, object.locations[i]);
+ var r = rowcol.r + delta.r;
+ var c = rowcol.c + delta.c;
+ if (!isInBounds(level, r, c)) return false; // out of bounds
+ newLocations.push(getLocation(level, r, c));
+ }
+
+ for (var i = 0; i < newLocations.length; i++) {
+ var location = newLocations[i];
+ if (!isTileCodeAir(level.map[location])) return false; // blocked by tile
+ var otherObject = findObjectAtLocation(location);
+ if (otherObject != null && otherObject !== object) return false; // blocked by object
+ }
+
+ // zappo presto!
+ var oldState = serializeObjectState(object);
+ object.locations = newLocations;
+ changeLog.push([object.type, object.id, oldState, serializeObjectState(object)]);
+ animations.push([
+ "t" + object.type, // TELEPORT_SNAKE | TELEPORT_BLOCK
+ object.id,
+ delta.r,
+ delta.c,
+ ]);
+ return true;
+}
+
+function isTileCodeAir(tileCode) {
+ return tileCode === SPACE || tileCode === EXIT || tileCode === PORTAL;
+}
+
+function addIfNotPresent(array, element) {
+ if (array.indexOf(element) !== -1) return;
+ array.push(element);
+}
+function removeAnyObjectAtLocation(location, changeLog) {
+ var object = findObjectAtLocation(location);
+ if (object != null) removeObject(object, changeLog);
+}
+function removeAnimatedObject(object, changeLog) {
+ removeObject(object, changeLog);
+ freshlyRemovedAnimatedObjects.push(object);
+}
+function removeObject(object, changeLog) {
+ removeFromArray(level.objects, object);
+ changeLog.push([object.type, object.id, [object.dead, copyArray(object.locations)], [0,[]]]);
+ if (object.type === SNAKE && object.id === activeSnakeId) {
+ activateAnySnakePlease();
+ }
+ if (object.type === BLOCK && paintBrushTileCode === BLOCK && paintBrushBlockId === object.id) {
+ // no longer editing an object that doesn't exit
+ paintBrushBlockId = null;
+ }
+ if (object.type === BLOCK) {
+ delete blockSupportRenderCache[object.id];
+ }
+}
+function removeFromArray(array, element) {
+ var index = array.indexOf(element);
+ if (index === -1) throw unreachable();
+ array.splice(index, 1);
+}
+function findActiveSnake() {
+ var snakes = getSnakes();
+ for (var i = 0; i < snakes.length; i++) {
+ if (snakes[i].id === activeSnakeId) return snakes[i];
+ }
+ throw unreachable();
+}
+function findBlockById(id) {
+ return findObjectOfTypeAndId(BLOCK, id);
+}
+function findSnakesOfColor(color) {
+ return level.objects.filter(function(object) {
+ if (object.type !== SNAKE) return false;
+ return object.id % snakeColors.length === color;
+ });
+}
+function findObjectOfTypeAndId(type, id) {
+ for (var i = 0; i < level.objects.length; i++) {
+ var object = level.objects[i];
+ if (object.type === type && object.id === id) return object;
+ }
+ return null;
+}
+function findObjectAtLocation(location) {
+ for (var i = 0; i < level.objects.length; i++) {
+ var object = level.objects[i];
+ if (object.locations.indexOf(location) !== -1)
+ return object;
+ }
+ return null;
+}
+function isUneatenFruit() {
+ return getObjectsOfType(FRUIT).length > 0;
+}
+function getActivePortalLocations() {
+ var portalLocations = getPortalLocations();
+ if (portalLocations.length !== 2) return []; // nice try
+ return portalLocations;
+}
+function getPortalLocations() {
+ var result = [];
+ for (var i = 0; i < level.map.length; i++) {
+ if (level.map[i] === PORTAL) result.push(i);
+ }
+ return result;
+}
+function countSnakes() {
+ return getSnakes().length;
+}
+function getSnakes() {
+ return getObjectsOfType(SNAKE);
+}
+function getBlocks() {
+ return getObjectsOfType(BLOCK);
+}
+function getObjectsOfType(type) {
+ return level.objects.filter(function(object) {
+ return object.type == type;
+ });
+}
+function isDead() {
+ if (animationQueue.length > 0 && animationQueue[animationQueue.length - 1][1][0] === INFINITE_LOOP) return true;
+ return getSnakes().filter(function(snake) {
+ return !!snake.dead;
+ }).length > 0;
+}
+function isAlive() {
+ return countSnakes() > 0 && !isDead();
+}
+
+var snakeColors = [
+ "#fd0c0b",
+ "#18d11f",
+ "#004cff",
+ "#fdc122",
+];
+var snakeAltColors = [
+ "#ff6666",
+ "#66ff66",
+ "#6666ff",
+ "#ffff66",
+];
+var blockForeground = ["#de5a6d","#fa65dd","#c367e3","#9c62fa","#625ff0"];
+var blockBackground = ["#853641","#963c84","#753d88","#5d3a96","#3a3990"];
+
+var activeSnakeId = null;
+
+var SLITHER_HEAD = "sh";
+var SLITHER_TAIL = "st";
+var MOVE_SNAKE = "ms";
+var MOVE_BLOCK = "mb";
+var TELEPORT_SNAKE = "ts";
+var TELEPORT_BLOCK = "tb";
+var EXIT_SNAKE = "es";
+var DIE_SNAKE = "ds";
+var DIE_BLOCK = "db";
+var INFINITE_LOOP = "il";
+var animationQueue = [
+ // // sequence of disjoint animation groups.
+ // // each group completes before the next begins.
+ // [
+ // 70, // duration of this animation group
+ // // multiple things to animate simultaneously
+ // [
+ // SLITHER_HEAD | SLITHER_TAIL | MOVE_SNAKE | MOVE_BLOCK | TELEPORT_SNAKE | TELEPORT_BLOCK,
+ // objectId,
+ // dr,
+ // dc,
+ // ],
+ // [
+ // INFINITE_LOOP,
+ // loopSizeNotIncludingThis,
+ // ],
+ // ],
+];
+var animationQueueCursor = 0;
+var animationStart = null; // new Date().getTime()
+var animationProgress; // 0.0 <= x < 1.0
+var freshlyRemovedAnimatedObjects = [];
+
+// render the support beams for blocks into a temporary buffer, and remember it.
+// this is due to stencil buffers causing slowdown on some platforms. see #25.
+var blockSupportRenderCache = {
+ // id: canvas,
+ // "0": document.createElement("canvas"),
+};
+
+function render() {
+ if (level == null) return;
+ if (animationQueueCursor < animationQueue.length) {
+ var animationDuration = animationQueue[animationQueueCursor][0];
+ animationProgress = (new Date().getTime() - animationStart) / animationDuration;
+ if (animationProgress >= 1.0) {
+ // animation group complete
+ animationProgress -= 1.0;
+ animationQueueCursor++;
+ if (animationQueueCursor < animationQueue.length && animationQueue[animationQueueCursor][1][0] === INFINITE_LOOP) {
+ var infiniteLoopSize = animationQueue[animationQueueCursor][1][1];
+ animationQueueCursor -= infiniteLoopSize;
+ }
+ animationStart = new Date().getTime();
+ }
+ }
+ if (animationQueueCursor === animationQueue.length) animationProgress = 1.0;
+ canvas.width = tileSize * level.width;
+ canvas.height = tileSize * level.height;
+ var context = canvas.getContext("2d"); //Gooby
+
+ if(background=="gradient"){
+ for(var i = 0; i maxR) maxR = rowcol.r;
+ if (rowcol.c < minC) minC = rowcol.c;
+ if (rowcol.c > maxC) maxC = rowcol.c;
+ });
+ var image = blockSupportRenderCache[object.id];
+ if (image == null) {
+ // render the support beams to a buffer
+ blockSupportRenderCache[object.id] = image = document.createElement("canvas");
+ image.width = (maxC - minC + 1) * tileSize;
+ image.height = (maxR - minR + 1) * tileSize;
+ var bufferContext = image.getContext("2d");
+ // Make a stencil that excludes the insides of blocks.
+ // Then when we render the support beams, we won't see the supports inside the block itself.
+ bufferContext.beginPath();
+ // Draw a path around the whole screen in the opposite direction as the rectangle paths below.
+ // This means that the below rectangles will be removing area from the greater rectangle.
+ bufferContext.rect(image.width, 0, -image.width, image.height);
+ for (var i = 0; i < object.locations.length; i++) {
+ var rowcol = getRowcol(level, object.locations[i]);
+ var r = rowcol.r - minR;
+ var c = rowcol.c - minC;
+ bufferContext.rect(c * tileSize, r * tileSize, tileSize, tileSize);
+ }
+ bufferContext.clip();
+ for (var i = 0; i < object.locations.length - 1; i++) {
+ var rowcol1 = getRowcol(level, object.locations[i]);
+ rowcol1.r -= minR;
+ rowcol1.c -= minC;
+ var rowcol2 = getRowcol(level, object.locations[i + 1]);
+ rowcol2.r -= minR;
+ rowcol2.c -= minC;
+ var cornerRowcol = {r:rowcol1.r, c:rowcol2.c};
+ drawConnector(bufferContext, rowcol1.r, rowcol1.c, cornerRowcol.r, cornerRowcol.c, blockBackground[object.id % blockBackground.length]);
+ drawConnector(bufferContext, rowcol2.r, rowcol2.c, cornerRowcol.r, cornerRowcol.c, blockBackground[object.id % blockBackground.length]);
+ }
+ }
+ var r = minR + animationDisplacementRowcol.r;
+ var c = minC + animationDisplacementRowcol.c;
+ context.drawImage(image, c * tileSize, r * tileSize);
+ });
+
+ // terrain
+ if (onlyTheseObjects == null) {
+ for (var r = 0; r < level.height; r++) {
+ for (var c = 0; c < level.width; c++) {
+ var location = getLocation(level, r, c);
+ var tileCode = level.map[location];
+ drawTile(tileCode, r, c, level, location);
+ }
+ }
+ }
+
+ // objects
+ objects.forEach(drawObject);
+
+ // banners
+ if (countSnakes() === 0) {
+ context.fillStyle = "#fdc122";
+ context.font = "150px Impact";
+ context.shadowOffsetX = 5;
+ context.shadowOffsetY = 5;
+ context.shadowColor = "rgba(0,0,0,0.5)";
+ context.shadowBlur = 4;
+ var textString = "WIN";
+ var textWidth = context.measureText(textString).width;
+ context.fillText(textString, (canvas.width/2) - (textWidth/2), canvas.height/2);
+ }
+ if (isDead()) {
+ context.fillStyle = "#fd0c0b";
+ context.font = "150px Impact";
+ context.shadowOffsetX = 5;
+ context.shadowOffsetY = 5;
+ context.shadowColor = "rgba(0,0,0,0.5)";
+ context.shadowBlur = 4;
+ textString = "LOSE";
+ textWidth = context.measureText(textString).width;
+ context.fillText(textString, (canvas.width/2) - (textWidth/2), canvas.height/2);
+ }
+
+ // editor hover
+ if (persistentState.showEditor && paintBrushTileCode != null && hoverLocation != null && hoverLocation < level.map.length) {
+
+ var savedContext = context;
+ var buffer = document.createElement("canvas");
+ buffer.width = canvas.width;
+ buffer.height = canvas.height;
+ context = buffer.getContext("2d");
+
+ var hoverRowcol = getRowcol(level, hoverLocation);
+ var objectHere = findObjectAtLocation(hoverLocation);
+ if (typeof paintBrushTileCode === "number") {
+ if (level.map[hoverLocation] !== paintBrushTileCode) {
+ drawTile(paintBrushTileCode, hoverRowcol.r, hoverRowcol.c, level, hoverLocation);
+ }
+ } else if (paintBrushTileCode === SNAKE) {
+ if (!(objectHere != null && objectHere.type === SNAKE && objectHere.id === paintBrushSnakeColorIndex)) {
+ drawObject(newSnake(paintBrushSnakeColorIndex, hoverLocation));
+ }
+ } else if (paintBrushTileCode === BLOCK) {
+ if (!(objectHere != null && objectHere.type === BLOCK && objectHere.id === paintBrushBlockId)) {
+ drawObject(newBlock(hoverLocation));
+ }
+ } else if (paintBrushTileCode === FRUIT) {
+ if (!(objectHere != null && objectHere.type === FRUIT)) {
+ drawObject(newFruit(hoverLocation));
+ }
+ } else if (paintBrushTileCode === "resize") {
+ void 0; // do nothing
+ } else if (paintBrushTileCode === "select") {
+ void 0; // do nothing
+ } else if (paintBrushTileCode === "paste") {
+ // show what will be pasted if you click
+ var pastedData = previewPaste(hoverRowcol.r, hoverRowcol.c);
+ pastedData.selectedLocations.forEach(function(location) {
+ var tileCode = pastedData.level.map[location];
+ var rowcol = getRowcol(level, location);
+ drawTile(tileCode, rowcol.r, rowcol.c, pastedData.level, location);
+ });
+ pastedData.selectedObjects.forEach(drawObject);
+ } else throw unreachable();
+
+ context = savedContext;
+ context.save();
+ context.globalAlpha = 0.2;
+ context.drawImage(buffer, 0, 0);
+ context.restore();
+ }
+ }
+ function drawTile(tileCode, r, c, level, location) {
+ switch (tileCode) {
+ case SPACE:
+ break;
+ case WALL:
+ drawWall(r, c, getAdjacentTiles());
+ break;
+ case SPIKE:
+ drawSpikes(r, c, getAdjacentTiles(), level);
+ break;
+ case EXIT:
+ drawExit(r, c);
+ /*var radiusFactor = isUneatenFruit() ? 0.7 : 1.2;
+ drawQuarterPie(r, c, radiusFactor, "#fd0c0b", 0);
+ drawQuarterPie(r, c, radiusFactor, "#18d11f", 1);
+ drawQuarterPie(r, c, radiusFactor, "#004cff", 2);
+ drawQuarterPie(r, c, radiusFactor, "#fdc122", 3);*/
+ break;
+ case PORTAL:
+ drawCircle(r, c, 0.8, "#888");
+ drawCircle(r, c, 0.6, "#111");
+ if (activePortalLocations.indexOf(location) !== -1) drawCircle(r, c, 0.3, "#666");
+ break;
+ default: throw unreachable();
+ }
+ function getAdjacentTiles() {
+ return [
+ [getTile(r - 1, c - 1),
+ getTile(r - 1, c + 0),
+ getTile(r - 1, c + 1)],
+ [getTile(r + 0, c - 1),
+ null,
+ getTile(r + 0, c + 1)],
+ [getTile(r + 1, c - 1),
+ getTile(r + 1, c + 0),
+ getTile(r + 1, c + 1)],
+ ];
+ }
+ function getTile(r, c) {
+ if (!isInBounds(level, r, c)) return null;
+ return level.map[getLocation(level, r, c)];
+ }
+ }
+
+ function drawObject(object) {
+ switch (object.type) {
+ case SNAKE:
+ var animationDisplacementRowcol = findAnimationDisplacementRowcol(object.type, object.id);
+ var lastRowcol = null
+ var color = snakeColors[object.id % snakeColors.length];
+ //var altColor = snakeAltColors[object.id % snakeAltColors.length];
+ var headRowcol;
+ for (var i = 0; i <= object.locations.length; i++) {
+ var animation;
+ var rowcol;
+ if (i === 0 && (animation = findAnimation([SLITHER_HEAD], object.id)) != null) {
+ // animate head slithering forward
+ rowcol = getRowcol(level, object.locations[i]);
+ rowcol.r += animation[2] * (animationProgress - 1);
+ rowcol.c += animation[3] * (animationProgress - 1);
+ } else if (i === object.locations.length) {
+ // animated tail?
+ if ((animation = findAnimation([SLITHER_TAIL], object.id)) != null) {
+ // animate tail slithering to catch up
+ rowcol = getRowcol(level, object.locations[i - 1]);
+ rowcol.r += animation[2] * (animationProgress - 1);
+ rowcol.c += animation[3] * (animationProgress - 1);
+ } else {
+ // no animated tail needed
+ break;
+ }
+ } else {
+ rowcol = getRowcol(level, object.locations[i]);
+ }
+ if (object.dead) rowcol.r += 0.5;
+ rowcol.r += animationDisplacementRowcol.r;
+ rowcol.c += animationDisplacementRowcol.c;
+ if (i === 0) {
+ // head
+ headRowcol = rowcol;
+ drawDiamond(rowcol.r, rowcol.c, color);
+ } else {
+ // middle
+ var cx = (rowcol.c + 0.5) * tileSize;
+ var cy = (rowcol.r + 0.5) * tileSize;
+ /*if(i % 2 == 0)*/ context.fillStyle = color;
+ //else context.fillStyle = altColor;
+ var orientation;
+ if (lastRowcol.r < rowcol.r) {
+ orientation = 0;
+ context.beginPath();
+ context.moveTo((lastRowcol.c + 0) * tileSize, (lastRowcol.r + 0.5) * tileSize);
+ context.lineTo((lastRowcol.c + 1) * tileSize, (lastRowcol.r + 0.5) * tileSize);
+ context.arc(cx, cy, tileSize/2, 0, Math.PI);
+ context.fill();
+ } else if (lastRowcol.r > rowcol.r) {
+ orientation = 2;
+ context.beginPath();
+ context.moveTo((lastRowcol.c + 1) * tileSize, (lastRowcol.r + 0.5) * tileSize);
+ context.lineTo((lastRowcol.c + 0) * tileSize, (lastRowcol.r + 0.5) * tileSize);
+ context.arc(cx, cy, tileSize/2, Math.PI, 0);
+ context.fill();
+ } else if (lastRowcol.c < rowcol.c) {
+ orientation = 3;
+ context.beginPath();
+ context.moveTo((lastRowcol.c + 0.5) * tileSize, (lastRowcol.r + 1) * tileSize);
+ context.lineTo((lastRowcol.c + 0.5) * tileSize, (lastRowcol.r + 0) * tileSize);
+ context.arc(cx, cy, tileSize/2, 1.5 * Math.PI, 2.5 * Math.PI);
+ context.fill();
+ } else if (lastRowcol.c > rowcol.c) {
+ orientation = 1;
+ context.beginPath();
+ context.moveTo((lastRowcol.c + 0.5) * tileSize, (lastRowcol.r + 0) * tileSize);
+ context.lineTo((lastRowcol.c + 0.5) * tileSize, (lastRowcol.r + 1) * tileSize);
+ context.arc(cx, cy, tileSize/2, 2.5 * Math.PI, 1.5 * Math.PI);
+ context.fill();
+ }
+ }
+ lastRowcol = rowcol;
+ }
+ // eye
+ if (object.id === activeSnakeId) {
+ drawCircle(headRowcol.r, headRowcol.c, 0.5, "#fff");
+ drawCircle(headRowcol.r, headRowcol.c, 0.2, "#000");
+ }
+ break;
+ case BLOCK:
+ drawBlock(object);
+ break;
+ case FRUIT:
+ var rowcol = getRowcol(level, object.locations[0]);
+ //drawCircle(rowcol.r, rowcol.c, 1, "#f0f");
+
+ context.drawImage(img3,rowcol.c*tileSize+(tileSize*.1), rowcol.r*tileSize+(tileSize*.1), tileSize*.8, tileSize*.8);
+ break;
+ default: throw unreachable();
+ }
+ }
+
+ function drawExit(r, c) { //Gooby
+ /*var cx = c+.5;
+ var rx = r+.5;
+
+ var grd = context.createRadialGradient(cx*tileSize, rx*tileSize, 1, cx*tileSize, rx*tileSize, 13);
+ grd.addColorStop(1, "red");
+ grd.addColorStop(.8, "orange");
+ grd.addColorStop(.6, "yellow");
+ grd.addColorStop(.4, "green");
+ grd.addColorStop(.2, "blue");
+ grd.addColorStop(0, "violet");
+ context.fillStyle = grd;
+
+ context.arc(cx*tileSize,rx*tileSize,tileSize/2,0,2*Math.PI);
+ context.fill();
+ context.stroke();*/
+
+ var img2=document.createElement('img');
+ img2.src='/Snakefall/Snakebird Images/pinwheel.png';
+
+ if(isUneatenFruit()==0)
+ context.drawImage(img2,c*tileSize-tileSize/2,r*tileSize-tileSize/2,2*tileSize, 2*tileSize);
+ else
+ context.drawImage(img2,c*tileSize,r*tileSize,tileSize, tileSize);
+ }
+
+ function drawWall(r, c, adjacentTiles) { //GOOBY
+ //drawRect(r, c, "#976537"); // dirt
+ drawTileNew(r, c, isWall, 0.2, "#976537");
+ context.fillStyle = "#95ff45"; // grass
+ drawTileOutlines(r, c, isWall, 0.2, true);
+ context.fillStyle = "#895C33"; // dirt edge
+ drawTileOutlines(r, c, isWall, 0.2, false);
+
+ function isWall(dc, dr) {
+ var tileCode = adjacentTiles[1 + dr][1 + dc];
+ return tileCode == null || tileCode === WALL;
+ }
+ }
+
+ function drawTileNew(r, c, isOccupied, outlineThickness, fillStyle){
+ context.fillStyle = fillStyle;
+ if (isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)) roundRect(context, c*tileSize, r*tileSize, tileSize, tileSize, {bl:10,br:10}, true, false);
+ else if (!isOccupied(0, -1) && isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)) roundRect(context, c*tileSize, r*tileSize, tileSize, tileSize, {tl:10,bl:10}, true, false);
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0)) roundRect(context, c*tileSize, r*tileSize, tileSize, tileSize, {tl:10,tr:10}, true, false);
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && isOccupied(-1, 0)) roundRect(context, c*tileSize, r*tileSize, tileSize, tileSize, {tr:10,br:10}, true, false);
+ else if (isOccupied(0, -1) && isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)) roundRect(context, c*tileSize, r*tileSize, tileSize, tileSize, {bl:10}, true, false);
+ else if (isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && isOccupied(-1, 0)) roundRect(context, c*tileSize, r*tileSize, tileSize, tileSize, {br:10}, true, false);
+ else if (!isOccupied(0, -1) && isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0)) roundRect(context, c*tileSize, r*tileSize, tileSize, tileSize, {tl:10}, true, false);
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && isOccupied(-1, 0)) roundRect(context, c*tileSize, r*tileSize, tileSize, tileSize, {tr:10}, true, false);
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)) roundRect(context, c*tileSize, r*tileSize, tileSize, tileSize, 10, true, false);
+ else roundRect(context, c*tileSize, r*tileSize, tileSize, tileSize, 0, true, false);
+ }
+
+ function drawTileOutlines(r, c, isOccupied, outlineThickness, grass) {
+ var complement = 1 - outlineThickness;
+ var outlinePixels = outlineThickness * tileSize;
+ var complementPixels = (1 - 2 * outlineThickness) * tileSize;
+
+
+ if (grass && !isOccupied( 0, -1)) context.fillRect((c) * tileSize, (r) * tileSize, tileSize, outlinePixels); //grass
+
+ /*if (!grass && !isOccupied(-1, -1)) context.fillRect((c) * tileSize, (r) * tileSize, outlinePixels, outlinePixels);
+ if (!grass && !isOccupied( 1, -1)) context.fillRect((c+complement) * tileSize, (r) * tileSize, outlinePixels, outlinePixels);
+ if (!grass && !isOccupied(-1, 1)) context.fillRect((c) * tileSize, (r+complement) * tileSize, outlinePixels, outlinePixels);
+ if (!grass && !isOccupied( 1, 1)) context.fillRect((c+complement) * tileSize, (r+complement) * tileSize, outlinePixels, outlinePixels);
+ if (!grass && !isOccupied( 0, 1)) context.fillRect((c) * tileSize, (r+complement) * tileSize, tileSize, outlinePixels);
+ if (!grass && !isOccupied(-1, 0)) context.fillRect((c) * tileSize, (r) * tileSize, outlinePixels, tileSize);
+ if (!grass && !isOccupied( 1, 0)) context.fillRect((c+complement) * tileSize, (r) * tileSize, outlinePixels, tileSize);*/
+ }
+
+ function drawTileOutlines2(r, c, isOccupied, outlineThickness) {
+ var complement = 1 - outlineThickness;
+ var outlinePixels = outlineThickness * tileSize;
+ var complementPixels = (1 - 2 * outlineThickness) * tileSize;
+ if (!isOccupied(-1, -1)) context.fillRect((c) * tileSize, (r) * tileSize, outlinePixels, outlinePixels);
+ if (!isOccupied( 1, -1)) context.fillRect((c+complement) * tileSize, (r) * tileSize, outlinePixels, outlinePixels);
+ if (!isOccupied(-1, 1)) context.fillRect((c) * tileSize, (r+complement) * tileSize, outlinePixels, outlinePixels);
+ if (!isOccupied( 1, 1)) context.fillRect((c+complement) * tileSize, (r+complement) * tileSize, outlinePixels, outlinePixels);
+ if (!isOccupied( 0, -1)) context.fillRect((c) * tileSize, (r) * tileSize, tileSize, outlinePixels);
+ if (!isOccupied( 0, 1)) context.fillRect((c) * tileSize, (r+complement) * tileSize, tileSize, outlinePixels);
+ if (!isOccupied(-1, 0)) context.fillRect((c) * tileSize, (r) * tileSize, outlinePixels, tileSize);
+ if (!isOccupied( 1, 0)) context.fillRect((c+complement) * tileSize, (r) * tileSize, outlinePixels, tileSize);
+ }
+
+ function drawSpikes(r, c, adjacentTiles) {
+ var x = c * tileSize;
+ var y = r * tileSize;
+ context.fillStyle = "#666";
+ context.beginPath();
+ context.moveTo(x + tileSize * 0.2, y + tileSize * 0.3); //top spikes
+ context.lineTo(x + tileSize * 0.35, y + tileSize * 0.0);
+ context.lineTo(x + tileSize * 0.5, y + tileSize * 0.3);
+ context.lineTo(x + tileSize * 0.65, y + tileSize * 0.0);
+ context.lineTo(x + tileSize * 0.8, y + tileSize * 0.3);
+
+ context.moveTo(x + tileSize * 0.7, y + tileSize * 0.2); //right spikes
+ context.lineTo(x + tileSize * 1.0, y + tileSize * 0.35);
+ context.lineTo(x + tileSize * 0.7, y + tileSize * 0.5);
+ context.lineTo(x + tileSize * 1.0, y + tileSize * 0.65);
+ context.lineTo(x + tileSize * 0.7, y + tileSize * 0.8);
+
+ context.moveTo(x + tileSize * 0.8, y + tileSize * 0.7); //bottom spikes
+ context.lineTo(x + tileSize * 0.65, y + tileSize * 1.0);
+ context.lineTo(x + tileSize * 0.5, y + tileSize * 0.7);
+ context.lineTo(x + tileSize * 0.35, y + tileSize * 1.0);
+ context.lineTo(x + tileSize * 0.2, y + tileSize * 0.7);
+
+ context.moveTo(x + tileSize * 0.3, y + tileSize * 0.8); //left spikes
+ context.lineTo(x + tileSize * 0.0, y + tileSize * 0.65);
+ context.lineTo(x + tileSize * 0.3, y + tileSize * 0.5);
+ context.lineTo(x + tileSize * 0.0, y + tileSize * 0.35);
+ context.lineTo(x + tileSize * 0.3, y + tileSize * 0.2);
+ context.closePath();
+
+ /*context.lineTo(x + tileSize * 1.0, y + tileSize * 0.4);
+ context.lineTo(x + tileSize * 0.7, y + tileSize * 0.5);
+ context.lineTo(x + tileSize * 1.0, y + tileSize * 0.6);
+ context.lineTo(x + tileSize * 0.7, y + tileSize * 0.7);
+ context.lineTo(x + tileSize * 0.6, y + tileSize * 1.0);
+ context.lineTo(x + tileSize * 0.5, y + tileSize * 0.7);
+ context.lineTo(x + tileSize * 0.4, y + tileSize * 1.0);
+ context.lineTo(x + tileSize * 0.3, y + tileSize * 0.7);
+ context.lineTo(x + tileSize * 0.0, y + tileSize * 0.6);
+ context.lineTo(x + tileSize * 0.3, y + tileSize * 0.5);
+ context.lineTo(x + tileSize * 0.0, y + tileSize * 0.4);
+ context.lineTo(x + tileSize * 0.3, y + tileSize * 0.3);*/
+ context.fill();
+ drawSpikeSupports(r, c, isSpike, isWall);
+
+ function isSpike(dc, dr) {
+ var tileCode = adjacentTiles[1 + dr][1 + dc];
+ return tileCode == null || tileCode === SPIKE;
+ }
+ function isWall(dc, dr) {
+ var tileCode = adjacentTiles[1 + dr][1 + dc];
+ return tileCode == null || tileCode === WALL;
+ }
+ }
+
+ function drawSpikeSupports(r, c, isOccupied, canConnect){
+
+ var boltBool = false;
+ if(canConnect(0, 1)){
+ context.fillStyle = "#444";
+ context.fillRect(c*tileSize+(tileSize*.3), r*tileSize+(tileSize*.8), tileSize*.4, tileSize*.4);
+ boltBool = true;
+ }
+ if(canConnect(0, -1) && !canConnect(0, 1)){
+ context.fillStyle = "#444";
+ context.fillRect(c*tileSize+(tileSize*.3), r*tileSize, tileSize*.4, tileSize*.4);
+ boltBool = true;
+ }
+ if(canConnect(-1, 0) && !canConnect(0, 1)){
+ context.fillStyle = "#444";
+ context.fillRect(c*tileSize, r*tileSize+(tileSize*.3), tileSize*.4, tileSize*.4);
+ boltBool = true;
+ }
+ if(canConnect(1, 0) && !canConnect(0, 1)){
+ context.fillStyle = "#444";
+ context.fillRect(c*tileSize+(tileSize*.8), r*tileSize+(tileSize*.3), tileSize*.4, tileSize*.4);
+ boltBool = true;
+ }
+
+ context.fillStyle = "#555";
+ if (isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)){ //TOUCHING ONE
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize, tileSize*.6, tileSize*.8, {bl:4,br:4}, true, false);
+ boltBool = true;
+ }
+ else if (!isOccupied(0, -1) && isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)){
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize+(tileSize*.2), tileSize, tileSize*.6, {tl:4,bl:4}, true, false);
+ boltBool = true;
+ }
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0)){
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize+(tileSize*.2), tileSize*.6, tileSize*.8, {tl:4,tr:4}, true, false);
+ boltBool = true;
+ }
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && isOccupied(-1, 0)){
+ roundRect(context, c*tileSize, r*tileSize+(tileSize*.2), tileSize*.8, tileSize*.6, {tr:4,br:4}, true, false);
+ boltBool = true;
+ }
+ else if (isOccupied(0, -1) && isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)){ //TOUCHING TWO (CORNERS)
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize, tileSize*.6, tileSize*.8, {bl:4}, true, false);
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize+(tileSize*.2), tileSize*.8, tileSize*.6, {bl:4}, true, false);
+ }
+ else if (isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && isOccupied(-1, 0)){
+ roundRect(context, c*tileSize, r*tileSize+(tileSize*.2), tileSize*.8, tileSize*.6, {br:4}, true, false);
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize, tileSize*.6, tileSize*.8, {br:4}, true, false);
+ }
+ else if (!isOccupied(0, -1) && isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0)){
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize+(tileSize*.2), tileSize*.8, tileSize*.6, {tl:4}, true, false);
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize+(tileSize*.2), tileSize*.6, tileSize*.8, {tl:4}, true, false);
+ }
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && isOccupied(-1, 0)){
+ roundRect(context, c*tileSize, r*tileSize+(tileSize*.2), tileSize*.8, tileSize*.6, {tr:4}, true, false);
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize+(tileSize*.2), tileSize*.6, tileSize*.8, {tr:4}, true, false);
+ }
+ else if (isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0)){ //TOUCHING TWO (OPPOSITES)
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize, tileSize*.6, tileSize, 0, true, false);
+ }
+ else if (!isOccupied(0, -1) && isOccupied(1, 0) && !isOccupied(0, 1) && isOccupied(-1, 0)){
+ roundRect(context, c*tileSize, r*tileSize+(tileSize*.2), tileSize, tileSize*.6, 0, true, false);
+ }
+ else if (isOccupied(0, -1) && isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0)){ //TOUCHING THREE
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize, tileSize*.6, tileSize, 0, true, false);
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize+(tileSize*.2), tileSize*.8, tileSize*.6, 0, true, false);
+ }
+ else if (isOccupied(0, -1) && isOccupied(1, 0) && !isOccupied(0, 1) && isOccupied(-1, 0)){
+ roundRect(context, c*tileSize, r*tileSize+(tileSize*.2), tileSize, tileSize*.6, 0, true, false);
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize, tileSize*.6, tileSize*.8, 0, true, false);
+ }
+ else if (isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && isOccupied(-1, 0)){
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize, tileSize*.6, tileSize, 0, true, false);
+ roundRect(context, c*tileSize, r*tileSize+(tileSize*.2), tileSize*.8, tileSize*.6, 0, true, false);
+ }
+ else if (!isOccupied(0, -1) && isOccupied(1, 0) && isOccupied(0, 1) && isOccupied(-1, 0)){
+ roundRect(context, c*tileSize, r*tileSize+(tileSize*.2), tileSize, tileSize*.6, 0, true, false);
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize+(tileSize*.2), tileSize*.6, tileSize*.8, 0, true, false);
+ }
+ else if (isOccupied(0, -1) && isOccupied(1, 0) && isOccupied(0, 1) && isOccupied(-1, 0)){
+ roundRect(context, c*tileSize, r*tileSize+(tileSize*.2), tileSize, tileSize*.6, 0, true, false);
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize, tileSize*.6, tileSize, 0, true, false);
+ }
+ else{
+ roundRect(context, c*tileSize+(tileSize*.2), r*tileSize+(tileSize*.2), tileSize*.6, tileSize*.6, 0, true, false);
+ boltBool = true;
+ }
+
+ if (boltBool) drawBolt(r, c);
+ }
+
+ function drawBolt(r, c){
+ context.strokeStyle = "#777";
+ context.beginPath();
+ context.arc(c*tileSize+(tileSize*.55), r*tileSize+(tileSize*.45), 4, -.7*Math.PI, .2*Math.PI);
+ context.lineTo(c*tileSize+(tileSize*.45),r*tileSize+(tileSize*.35));
+ context.closePath();
+ context.fillStyle = "#777";
+ context.fill();
+ context.stroke();
+
+ context.beginPath();
+ context.moveTo(c*tileSize+(tileSize*.43),r*tileSize+(tileSize*.47));
+ context.arc(c*tileSize+(tileSize*.48), r*tileSize+(tileSize*.52), 4, .2*Math.PI, -.75*Math.PI);
+ //context.lineTo(c*tileSize+(tileSize*.4),r*tileSize+(tileSize*.6));
+ context.closePath();
+ context.fillStyle = "#777";
+ context.fill();
+ context.stroke();
+ }
+
+ function drawConnector(context, r1, c1, r2, c2, color) {
+ // either r1 and r2 or c1 and c2 must be equal
+ if (r1 > r2 || c1 > c2) {
+ var rTmp = r1;
+ var cTmp = c1;
+ r1 = r2;
+ c1 = c2;
+ r2 = rTmp;
+ c2 = cTmp;
+ }
+ var xLo = (c1 + 0.4) * tileSize;
+ var yLo = (r1 + 0.4) * tileSize;
+ var xHi = (c2 + 0.6) * tileSize;
+ var yHi = (r2 + 0.6) * tileSize;
+ context.fillStyle = color;
+ context.fillRect(xLo, yLo, xHi - xLo, yHi - yLo);
+ }
+ function drawBlock(block) {
+ var animationDisplacementRowcol = findAnimationDisplacementRowcol(block.type, block.id);
+ var rowcols = block.locations.map(function(location) {
+ return getRowcol(level, location);
+ });
+ rowcols.forEach(function(rowcol) {
+ var r = rowcol.r + animationDisplacementRowcol.r;
+ var c = rowcol.c + animationDisplacementRowcol.c;
+ context.fillStyle = blockForeground[block.id % blockForeground.length];
+ drawTileOutlines2(r, c, isAlsoThisBlock, 0.3);
+ function isAlsoThisBlock(dc, dr) {
+ for (var i = 0; i < rowcols.length; i++) {
+ var otherRowcol = rowcols[i];
+ if (rowcol.r + dr === otherRowcol.r && rowcol.c + dc === otherRowcol.c) return true;
+ }
+ return false;
+ }
+ });
+ }
+ function drawQuarterPie(r, c, radiusFactor, fillStyle, quadrant) {
+ var cx = (c + 0.5) * tileSize;
+ var cy = (r + 0.5) * tileSize;
+ context.fillStyle = fillStyle;
+ context.beginPath();
+ context.moveTo(cx, cy);
+ context.arc(cx, cy, radiusFactor * tileSize/2, quadrant * Math.PI/2, (quadrant + 1) * Math.PI/2);
+ context.fill();
+ }
+ function drawDiamond(r, c, fillStyle) {
+ var x = c * tileSize;
+ var y = r * tileSize;
+ context.fillStyle = fillStyle;
+ context.beginPath();
+ context.moveTo(x + tileSize/2, y);
+ context.lineTo(x + tileSize, y + tileSize/2);
+ context.lineTo(x + tileSize/2, y + tileSize);
+ context.lineTo(x, y + tileSize/2);
+ context.lineTo(x + tileSize/2, y);
+ context.fill();
+ }
+ function drawCircle(r, c, radiusFactor, fillStyle) {
+ context.fillStyle = fillStyle;
+ context.beginPath();
+ context.arc((c + 0.5) * tileSize, (r + 0.5) * tileSize, tileSize/2 * radiusFactor, 0, 2*Math.PI);
+ context.fill();
+ }
+ function drawRect(r, c, fillStyle) {
+ context.fillStyle = fillStyle;
+ context.fillRect(c * tileSize, r * tileSize, tileSize, tileSize);
+ }
+
+ function roundRect(ctx, x, y, width, height, radius, fill, stroke) { //Gooby
+ if (typeof stroke === 'undefined') {
+ stroke = true;
+ }
+ if (typeof radius === 'undefined') {
+ radius = 5;
+ }
+ if (typeof radius === 'number') {
+ radius = {tl: radius, tr: radius, br: radius, bl: radius};
+ } else {
+ var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0};
+ for (var side in defaultRadius) {
+ radius[side] = radius[side] || defaultRadius[side];
+ }
+ }
+ ctx.beginPath();
+ ctx.moveTo(x + radius.tl, y);
+ ctx.lineTo(x + width - radius.tr, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
+ ctx.lineTo(x + width, y + height - radius.br);
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
+ ctx.lineTo(x + radius.bl, y + height);
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
+ ctx.lineTo(x, y + radius.tl);
+ ctx.quadraticCurveTo(x, y, x + radius.tl, y);
+ ctx.closePath();
+ if (fill) {
+ ctx.fill();
+ }
+ if (stroke) {
+ ctx.stroke();
+ }
+ }
+
+
+ function drawR(r,c,fillStyle){ //Gooby
+ context.fillStyle = fillStyle;
+ var cornerRadius = 20;
+ context.lineJoin = "round";
+ context.lineWidth = 1;
+ context.strokeRect(c*tileSize, r*tileSize, tileSize, tileSize);
+ //context.fillRect(c*tileSize, r*tileSize, tileSize, tileSize);
+ }
+
+ function drawGrid() {
+ var buffer = document.createElement("canvas");
+ buffer.width = canvas.width;
+ buffer.height = canvas.height;
+ var localContext = buffer.getContext("2d");
+
+ localContext.strokeStyle = "#fff";
+ localContext.beginPath();
+ for (var r = 0; r < level.height; r++) {
+ localContext.moveTo(0, tileSize*r);
+ localContext.lineTo(tileSize*level.width, tileSize*r);
+ }
+ for (var c = 0; c < level.width; c++) {
+ localContext.moveTo(tileSize*c, 0);
+ localContext.lineTo(tileSize*c, tileSize*level.height);
+ }
+ localContext.stroke();
+
+ context.save();
+ context.globalAlpha = 0.4;
+ context.drawImage(buffer, 0, 0);
+ context.restore();
+ }
+}
+
+function findAnimation(animationTypes, objectId) {
+ if (animationQueueCursor === animationQueue.length) return null;
+ var currentAnimation = animationQueue[animationQueueCursor];
+ for (var i = 1; i < currentAnimation.length; i++) {
+ var animation = currentAnimation[i];
+ if (animationTypes.indexOf(animation[0]) !== -1 &&
+ animation[1] === objectId) {
+ return animation;
+ }
+ }
+}
+function findAnimationDisplacementRowcol(objectType, objectId) {
+ var dr = 0;
+ var dc = 0;
+ var animationTypes = [
+ "m" + objectType, // MOVE_SNAKE | MOVE_BLOCK
+ "t" + objectType, // TELEPORT_SNAKE | TELEPORT_BLOCK
+ ];
+ // skip the current one
+ for (var i = animationQueueCursor + 1; i < animationQueue.length; i++) {
+ var animations = animationQueue[i];
+ for (var j = 1; j < animations.length; j++) {
+ var animation = animations[j];
+ if (animationTypes.indexOf(animation[0]) !== -1 &&
+ animation[1] === objectId) {
+ dr += animation[2];
+ dc += animation[3];
+ }
+ }
+ }
+ var movementAnimation = findAnimation(animationTypes, objectId);
+ if (movementAnimation != null) {
+ dr += movementAnimation[2] * (1 - animationProgress);
+ dc += movementAnimation[3] * (1 - animationProgress);
+ }
+ return {r: -dr, c: -dc};
+}
+function hasFutureRemoveAnimation(object) {
+ var animationTypes = [
+ EXIT_SNAKE,
+ DIE_BLOCK,
+ ];
+ for (var i = animationQueueCursor; i < animationQueue.length; i++) {
+ var animations = animationQueue[i];
+ for (var j = 1; j < animations.length; j++) {
+ var animation = animations[j];
+ if (animationTypes.indexOf(animation[0]) !== -1 &&
+ animation[1] === object.id) {
+ return true;
+ }
+ }
+ }
+}
+
+function previewPaste(hoverR, hoverC) {
+ var offsetR = hoverR - clipboardOffsetRowcol.r;
+ var offsetC = hoverC - clipboardOffsetRowcol.c;
+
+ var newLevel = JSON.parse(JSON.stringify(level));
+ var selectedLocations = [];
+ var selectedObjects = [];
+ clipboardData.selectedLocations.forEach(function(location) {
+ var tileCode = clipboardData.level.map[location];
+ var rowcol = getRowcol(clipboardData.level, location);
+ var r = rowcol.r + offsetR;
+ var c = rowcol.c + offsetC;
+ if (!isInBounds(newLevel, r, c)) return;
+ var newLocation = getLocation(newLevel, r, c);
+ newLevel.map[newLocation] = tileCode;
+ selectedLocations.push(newLocation);
+ });
+ clipboardData.selectedObjects.forEach(function(object) {
+ var newLocations = [];
+ for (var i = 0; i < object.locations.length; i++) {
+ var rowcol = getRowcol(clipboardData.level, object.locations[i]);
+ rowcol.r += offsetR;
+ rowcol.c += offsetC;
+ if (!isInBounds(newLevel, rowcol.r, rowcol.c)) {
+ // this location is oob
+ if (object.type === SNAKE) {
+ // snakes must be completely in bounds
+ return;
+ }
+ // just skip it
+ continue;
+ }
+ var newLocation = getLocation(newLevel, rowcol.r, rowcol.c);
+ newLocations.push(newLocation);
+ }
+ if (newLocations.length === 0) return; // can't have a non-present object
+ var newObject = JSON.parse(JSON.stringify(object));
+ newObject.locations = newLocations;
+ selectedObjects.push(newObject);
+ });
+ return {
+ level: newLevel,
+ selectedLocations: selectedLocations,
+ selectedObjects: selectedObjects,
+ };
+}
+
+function getNaiveOrthogonalPath(a, b) {
+ // does not include a, but does include b.
+ var rowcolA = getRowcol(level, a);
+ var rowcolB = getRowcol(level, b);
+ var path = [];
+ if (rowcolA.r < rowcolB.r) {
+ for (var r = rowcolA.r; r < rowcolB.r; r++) {
+ path.push(getLocation(level, r + 1, rowcolA.c));
+ }
+ } else {
+ for (var r = rowcolA.r; r > rowcolB.r; r--) {
+ path.push(getLocation(level, r - 1, rowcolA.c));
+ }
+ }
+ if (rowcolA.c < rowcolB.c) {
+ for (var c = rowcolA.c; c < rowcolB.c; c++) {
+ path.push(getLocation(level, rowcolB.r, c + 1));
+ }
+ } else {
+ for (var c = rowcolA.c; c > rowcolB.c; c--) {
+ path.push(getLocation(level, rowcolB.r, c - 1));
+ }
+ }
+ return path;
+}
+function identityFunction(x) {
+ return x;
+}
+function compareId(a, b) {
+ return operatorCompare(a.id, b.id);
+}
+function operatorCompare(a, b) {
+ return a < b ? -1 : a > b ? 1 : 0;
+}
+function clamp(value, min, max) {
+ if (value < min) return min;
+ if (value > max) return max;
+ return value;
+}
+function copyArray(array) {
+ return array.map(identityFunction);
+}
+function getSetIntersection(array1, array2) {
+ if (array1.length * array2.length === 0) return [];
+ return array1.filter(function(x) { return array2.indexOf(x) !== -1; });
+}
+function makeScaleCoordinatesFunction(width1, width2) {
+ return function(location) {
+ return location + (width2 - width1) * Math.floor(location / width1);
+ };
+}
+
+var expectHash;
+window.addEventListener("hashchange", function() {
+ if (location.hash === expectHash) {
+ // We're in the middle of saveLevel() or saveReplay().
+ // Don't react to that event.
+ expectHash = null;
+ return;
+ }
+ // The user typed into the url bar or used Back/Forward browser buttons, etc.
+ loadFromLocationHash();
+});
+function loadFromLocationHash() {
+ var hashSegments = location.hash.split("#");
+ hashSegments.shift(); // first element is always ""
+ if (!(1 <= hashSegments.length && hashSegments.length <= 2)) return false;
+ var hashPairs = hashSegments.map(function(segment) {
+ var equalsIndex = segment.indexOf("=");
+ if (equalsIndex === -1) return ["", segment]; // bad
+ return [segment.substring(0, equalsIndex), segment.substring(equalsIndex + 1)];
+ });
+
+ if (hashPairs[0][0] !== "level") return false;
+ try {
+ var level = parseLevel(hashPairs[0][1]);
+ } catch (e) {
+ alert(e);
+ return false;
+ }
+ loadLevel(level);
+ if (hashPairs.length > 1) {
+ try {
+ if (hashPairs[1][0] !== "replay") throw new Error("unexpected hash pair: " + hashPairs[1][0]);
+ parseAndLoadReplay(hashPairs[1][1]);
+ } catch (e) {
+ alert(e);
+ return false;
+ }
+ }
+ return true;
+}
+
+// run test suite
+var testTime = new Date().getTime();
+if (compressSerialization(stringifyLevel(parseLevel(testLevel_v0))) !== testLevel_v0_converted) throw new Error("v0 level conversion is broken");
+// ask the debug console for this variable if you're concerned with how much time this wastes.
+testTime = new Date().getTime() - testTime;
+
+loadPersistentState();
+if (!loadFromLocationHash()) {
+ loadLevel(parseLevel(exampleLevel));
+}
+
From 9bf343ea806d1bd4a0f346eaedcaa274beb7787d Mon Sep 17 00:00:00 2001
From: Gooby <34070642+jmdiamond3@users.noreply.github.com>
Date: Wed, 22 Jan 2020 16:01:07 -0500
Subject: [PATCH 010/577] Add files via upload
---
Snakebird Images/Apple.png | Bin 0 -> 27977 bytes
Snakebird Images/Apple2.png | Bin 0 -> 21375 bytes
Snakebird Images/Banana.png | Bin 0 -> 18537 bytes
Snakebird Images/Cherry.png | Bin 0 -> 39242 bytes
Snakebird Images/Cherry2.png | Bin 0 -> 155539 bytes
Snakebird Images/Grapes.png | Bin 0 -> 48826 bytes
Snakebird Images/Peach.png | Bin 0 -> 22131 bytes
Snakebird Images/Peach2.png | Bin 0 -> 25501 bytes
Snakebird Images/Pear.png | Bin 0 -> 24856 bytes
Snakebird Images/Pineapple.png | Bin 0 -> 71082 bytes
Snakebird Images/Slice.png | Bin 0 -> 88690 bytes
Snakebird Images/Strawberry.png | Bin 0 -> 33997 bytes
Snakebird Images/g16093-512.png | Bin 0 -> 25878 bytes
Snakebird Images/pinwheel.png | Bin 0 -> 38782 bytes
Snakebird Images/sky.jpg | Bin 0 -> 121572 bytes
Snakebird Images/sky2.jpeg | Bin 0 -> 162621 bytes
16 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 Snakebird Images/Apple.png
create mode 100644 Snakebird Images/Apple2.png
create mode 100644 Snakebird Images/Banana.png
create mode 100644 Snakebird Images/Cherry.png
create mode 100644 Snakebird Images/Cherry2.png
create mode 100644 Snakebird Images/Grapes.png
create mode 100644 Snakebird Images/Peach.png
create mode 100644 Snakebird Images/Peach2.png
create mode 100644 Snakebird Images/Pear.png
create mode 100644 Snakebird Images/Pineapple.png
create mode 100644 Snakebird Images/Slice.png
create mode 100644 Snakebird Images/Strawberry.png
create mode 100644 Snakebird Images/g16093-512.png
create mode 100644 Snakebird Images/pinwheel.png
create mode 100644 Snakebird Images/sky.jpg
create mode 100644 Snakebird Images/sky2.jpeg
diff --git a/Snakebird Images/Apple.png b/Snakebird Images/Apple.png
new file mode 100644
index 0000000000000000000000000000000000000000..d674834a6bbddd661b4aa12cc9657c61c8db5ae7
GIT binary patch
literal 27977
zcmZ@=2|U#4{~vdVicsXLM2=KKattX#84=1gBDs&;XX8kUM9vaM3CVqjVc2q)AvcAX
z3Ax8HhMDj8{~f#A?r(Shuh+iX-DaN8^Lakc=XigPXC52qYjd#iv%z364xKBP3}G;O
z=s)RUdzhhrt^4)ki~i{bHej+y3)rP=jQyg
zU0mE>-{cx#7YhRQS5Np(U7PZsK0mVl?KlAUbP?Bg88Laqrt?@({EgMz}>_Iqt<@bGSL2q_BV38vJ-_Lw6Y%xR9~#v_vhD2D42`)y*33-
z{m}YM51yS2V-fO){;)b65J~w_b#CG7Tgyo#^Pe=Nj+Mkuq*EX2y}yn8pT9=jbqWzx
zww%qn-o~-H!NCNy{-Q(=7#bQd=mLw7zixt-_RO1&5U
z`}RWqb_AIzHx3DL*q0c=ufkax+shY$vuE?@esRK%P*Z>ZTo1;3vSGMeMs?3~mp{*W
z9MLEruLJ8p8!6_sDRg?|4^Hf*Gb8Hdo48;vM}@*N^k6^#`s94(f`<~_+v{fAH9CjG
z(_!cT_K^_E+4`|fugIK%I!7vO_{=YIS2CN#V7^tFs9^fic+beKUlJ`>QKIvZ*tQ5X
zlK0czPh{D>j(sZAM8<3OTkjd0n8^{ppEC1%z^L2uJysZg->%Sw{7t^glxFWgA@Ij3
z_jZ54hyEcEd|U_JAC%gK6FL4^5D)+LWf9b$9PInG=gPVS>T?bF*Kyb~Ew+(f&qTz3
zE!!)LdYQ`@IxjA5UEchM>hus(cc{P1{!1S=w1fGNjX!i^O@3~gXW!gL29{X{Zu@yQ
zME)fZ=}b%h7mVsMMmK*GgXv#Ypy?6B=HaI^@xiCWVG32M^vbEQ{@cH{@lYB0?n!Tu
zUAgX_c$N_o+Fu-as03_S>BF3o@kkjvmY7;9{a^Ir#64I;Ef;%)(Xf7}Vy*|j*lSqD
zGd8`N^_sgj@KsQwUHA_vKI!5l@m{VDa!jn7kEf`gZHDqi_nH
zVEJRU2>8~T309A%H6Z|b-z%Mb2(3K?4%RKo-+je!~
zOZ#z{(~IA@PSJVT{7t-3crPWo()`5(!?lcAoPTXI-NBj49m=1RgL;`H+Rhrv@b`}j
zNYI@W90#3FD&PFH$oJQs_^X8J!4{mNfGx%>6R%{EU+m4rii^WIFQfI`-~CwHa}oH{
z;4Ct;=JpChM)ONGjySLMeZ$nk438OWd!cc&(gP`qOccZ9V^RW&4P2cm6OL
zA;=8=)W%;I+!XOAxqT|CoyfIC_%GI8go~#FLl(!|AOBkRp=%=dB;DjTEBmk8l&ho=
z?!!A-L)UGh*4JhKI&vwoB)N_{D3JoBTDhcV0vQ(Ww|~(-9AlrVb%M^I>tG6`klcTh
zq0(`57i;J>*SMx`F__O^+Z;jozE6ksOUO{i-t5)ThaLI*M^+8cotTM{P4z~aZQQW1
zU-Vk1SuNuw1&qHN(DoURA>W;_sH3kK_H9tVHnZ&9Cp2j`)TlJbZidNz
zt@0&lVlh?RYmf=r5jt2yHG5~iynI)y9KNfhd7aAZ>d37g%`)5(e~AnCW?#)JM+yDB
z)-f#-D^A+T+3fSDgf%WxKB!U^Zv#yXB`zu&eTUUf(Uoe@t~ZlQ*4Vh}QRWhAwW-k|
zobUcAa@X`~P`G&O?0>syOgbh)_;D+z>acQx9;FtKYrH?j>G!ikSp^k=
zg94QIN2wbWnk*`q-6xI4Tv>#pL!vXcCr(`ga4P$CRDI`v_D}oOX|D!A-q4JZrVGrI
zf&mg=?yoeDYly8%b)cyfKv~6xz7+H|J%VQOpZuN8?f_iKaekFb+3n7Yc+@0NDuGhS
z#PpFa2Ma-`*frb>KLjZrF5HVG6iSqsN6O0_p6@&kLrf6H9?@9aRb504F>dKiA^aLwfwt7v9}uc+85cdjt4WQ+QIEt*fuvm>P|(FCH*etSc+>J
z7pxdvy6efkh@2?@#ps9~n-ZSOKn)T_L`^B+*l`QrH-`bH&r0jIq6ba0xEB2|Hj_;bTnblY5hO`DRCu}`nDh?g^G~b>Odin
zSImH0Mb6_PL?N(^cA1&Q{=SZPxL{RRZ?mSt&eNQuy^zrStTsJJ{0*@aC6`BaTn1y?
zmV7p*c2B)}BmqZYY|5?Hww~b%y+E_Y7lj2A5e?X~$smB#tBTSA`sdv5OcpQt((X7<
z>c2v}{aNzz{5LHt((?p_QAYK_r6@}Jm##qy&eE5OiWb^D-e2~YU7&lOUXCYplwZ#6h$?l1XTUo>M;M~;23fFCtnUde6IMu_trA^|9ra8
zH2x~2jBhOg9xJ}&kyEnsZBFSSjr=E>KLL>ak@4nzq{S|6V9EzupTi9X1vM7fyA%hY9N13DU-pK`+{L?HSFWzhqF|azem*`ZF|@vUmEQz`Z>q7<@X=PmP#2)Yk~6
zdB2!Y4naMoZGBY+RXY*qNiWhcEUHS~jW&IX2o*9`Ht>1@Wn@^^cpK849pgWU4(Wco
z?|e`X)RV2&LxB3lHK%E&=iRBi^?mjlxgDGfig=L&+Ap&*uFx#UG2ow;ljlDEiX|w{
zF&S)Tv7OUIEzE(ZmQ&B5AePNCHHo$9u;KpAJqs4}2D`WLt|=y8
zFE#>cGR3X@7c4`77)Z^)5)(4q3;8H$o1_{_hR~uB5kC+FL1*IxR8^9o^p8z;8rID7
z8lOMMzUb+7=ki!p1TMT8kW_a1bZLECA4ET7=GW9g=oKAm79CDn?a@g|01`ufU8^Ft^
z>@;+x25h5YREl#ats3W3zQQF(T%t@H5#R`nk->j^eQ;AEElA>uAy_11P-AAG^P!_w@ZN*i!#Fka0@YNuTFpwQ+-$r3A=5d
z|L&at?O-fIH0a7qpspby*Y=S`(HyXG3YEOaYfyLDBot;!$2rBF&rb+_u#D5{%?MYgI
zP5e24tq&=u7$F-5cDQIPi=c}CX&o%fJ&ogWC&gi9{kHu?(oM
zXhA>ukrrIl{BL}niA;|~Ij98Y$tJ=e#Dc5YRUXIxG7{*dvw<2q0AgwWnyP1kR3c2J
ze28>tm!+;mK`5`0*VO|RPHM(;!&qnryZ)ie2XSSU9
zWg!fwODst#k7)UNpKjN}KPsClJ%U_4J3y@W8R3HM{=o&gQ}x{5Bd%z>rz9sQ{62RW*-&vJI;Ue@CaB)<&=q4^YC|E;MT6Q
zK?P$}DYNmD4d$n?W+DW&50Kz
zV~>T;jFvIulp}mp;R5gG$-VK}Gxd2t1Fh5u-q$|PTkdH83`Q#%FE`FzF@D1d0!rQF1%
zl(UgnD=%mAJ9CK5VM`KLCTwhm&ia$(R)w8ceu&N6bRoNRf9nk#LU9fT?TgzJhImrV
zWU)nJ$!z{xdLKZ%@>=nuIcDRS;Iqw++@{V;&YwPKCbXF!YvBIeyOAweR)i>;4VeIV
zLa-}${FY@C9N;VK(T)3_RBD6lK?y0^fvn!w>J>K8`V{ed?=i2yGJY3>;IVg3-#$
z;u|&{s8X5q3xlFAC!n{S7}TxQki!%Oh{|1aTzYL<4G>D6xPR6kG9sWro_M7tUcXmDepr2fI%W>
zrRdH#iWxM*g&!;GBc@xDq=M4y?OB+|js<{ZtvQ_GW7h}M;UpGKjz)&;*v7>zfQeR@
z!LY3kXR35ehIn73qQpmko34BSx+&RNvW@q0lob-~r=c)k!4>+7C$sHp}K-1>2mka_fJv-t6wtxDI)p+q!o8eY;_d8x%k~2K}S{Cs$48d_Z+h!
zYo4Nf-Ny|6{6(D>zLEoN9*b%HkHw1n?`a1f`O4cViBi~{xJQLe0{dzY640~Jxw&_N4B!W
z^C)d88;_@;{4UY^H`J@MM;ur3OqDt}0t~pC+{9*oO1|!+c0bE)$}f>#o(T7cS&89G
z$eY3^1Tea5YNfV%AEu?Y-7bvt?6g`3tzT1J>b0MFw$dUVJUUT5!`qlf85z=cW3x{N
zU$NL$Y=Y<=r3n>sTy>ck2!s<*_sMlPUN_f>9Dds8)HagiBJfI9{@aLN$EC_AXqIoU
z_p8(uYI+oQkYsVbjQ6Q}ss*l(SSFLh{=&T_To_H)-jr;;Y^?Sd)O8%o`j5_s-w?t@
z$L5BLdyj|6lPl10V%P#}Q&>oaJ1l(|kCs7=E~Dc&ci5j{-{Xs*I^R$ybMaCEJv_I99;JM1U|C%SR`P&YnlI&=tYf%gQI{a
z{p}K8oKV4gd@?x3G2(3Ig)3#7C@Gq7w9f
zu9csvri}P6RYL;kBYke9KR^B*3*fQkE8<|sphxrBJ6%-1**P^~vi;6lMqYA+T&3k{
z4OWd_HRKuAm02U8V#3Sy{df@NhLH!;6ubZBcvMmUVvSbWI(FTretmrzAINT7-Ymr`
zJHq?K(BuB71c`{~9S2}CuIQ}!fRQ_vF64?ehN$I7cGqB;b>i{QAk@Bj1p!3L?#SJ%
zaDEioE~Wpt=c+0CEHax?k)io~E)Bi+;5xbdxJa9CsD3eL_eqTfWu?(0mJhe+t-@65
z&z)rrHOno1pa)Bb(5r5gKKWw!JOwbE#&Cxbg0>=fjC%
zjp?jS8ROn8>I!a~J;vB^dII*8{ODwtxz&et`#U_0#-_Z{$5EMzIx_-OdbaG_!GseR
z15?0-@jXj5XlM5OODRQ)3ZjjndEfUU1)(zZ{}kb(JCqSj23IJ;dE-R)JBb?xrqVp(
zVX=ibU+R`p=k4YV`_Ae2aZen~&_ZWXC-2Q|-@Jv#EiLvLu9jL_4(23bpToW;s?rT3
zSYe5vsr#IcDxiV7P0S-~99Z&5HwlO}toqHXYslmhpdwp_DOtfk&G7N4;}8Y2E80j8
z(<0z;(3Noh2Ry{QUcHh#CNXK1&i7!<_F&UcZogR<_p-6A|6du!i(29|eW3Lq2LFra2rBHw&e-y&VTGCyRle?Z_V)
zuh!_8&~py^1atuv_nRv%iJwo=t+HX1{$0nARH^=oV7k|MUU5bLOCM!Dn&b6C+xu51
zQ1?H1C&Fe&pQrCRcp2~x59$9tC_QfoCri1~m3D?A>#2eC9xF4N0n6l>9Xu|DGP2PW
zNnVUYo5hE*Cs4)Dz96{6LCr^|lb>s=W|B;ZeEALeY4ykUkRxiWt=_$J
zI>0on%q{b3`4bPgSL$@d&sL@mw6MK{tpAhESA3G-od#JWOdc1}o(e1PK*xUYYx&WE
z-eBGu9iL}ydiL!ZMt^6feg=18Ge3h^@_;h(s_CQ?ASWu%Bf0*hy<;%+J04yiTTVJZ
zFBj_1l(`Q2n6U#l^z3P+6W&j@rYUcEmanT#K26NR#e07gCmroX{)jCac^=(2N4khd
zzv-i1Z6vqX&6-jj>ccWNaW7yVLohq?G9Ne0{RG{iao#>@l*ubu;gKgjxAw^FPzfpE
zE!`_0ecQgMINoA1Fgy9hOyWvO4a=-N_{@cqaes;QU!@z~PuYo7>?Ms&_}QsE&7g(-
z6C+CkdM6-Vzo^$7pi1tuf+NdoprhNq?oBlUT#k$gyc9>N(OI+5X)A6zaY@#fr^dP5
z6b@zQ-`wev0ph8^of4LYm@ABOZTfzos*
zLL9tO9i?cv`2rqc(aEWaHllfdAVq%~spJSB`s@Gy(
z6F)}WFZzxYc4lw*AWKH{4V3G4B83w5cDyWB*XAz*E>9GX9O=x1V`3R8Ni$^jOT$Jvj>QE$Jn>8!~Bp
zgwx8$yxjtpjrUkPK(xK!diMP7g-eY>c(e}vg_((c!rw1!8XE$vQRa!1TLBaHpq@)6
zr7tp61#++H&X(PpsAJ)q(SBwnp!VKer0_|-azEH3`k0J0^ax)Fqw
z>g0Eo!y|P$kUswMJEQe;CD%D^<}Fp*Q3=M8Z@%DcOf_YQd?et5*gRA71r~lQfH;4o
zSop-Krr_@f!(jZqFaK1#k3~u^Xo}smH<6)qfRvw~^Vgge@=g_w90gKCLfkfTUyE_~
zd8-;&(Y>vt6=BW-&C!Ei}$v2iD!Y(RF*2WEujM>^{w20wC07bWHH}
z>T-!C@G8MK?5ct1Jxuc!5o`6ica5r(m&db;t0N8t@9cwGGRh0uRTPw;l@jv8vyA1$
zZKV@CnKJn?#jg2Xje_}nlzKy1#_>cii(Djnb@|G*fSQ~W7BBfK6HZ@d`Tm?y-*Yio
zN)$C{DNw77VFsN4@@t
z092M4UC%t%0A5^QCX>S6E&6_hcdh>FDh?7<&3wG%GYWw*sJwebbpT0pL0Dctpaw7Q
zInNi2`04UUAj~p9X36!%Kv;0i0j=4)O(KH41k-6fpqSJoSv3#DL*
z-u&Zxi0FePa`qa~m^`{>Iwk15lBNlKc{CK%ZRCk!m_p~^-TRVY9@Ga+SF7a%Of8Dd
z*dD||9h;dTg*^1Sye?7)SeTc3F|?p-NnF}r!%kGyJbhIrie-+5dvUGLC%g23sg3I7
z=0escIa1_Vdsuf#UE?}pV6vY?k&*z{ZhR8&cnpW|RQ){Ci5$ZbVHT1540TsoeMRUJ
zrQ*pyT6RTk+)_JLk@}yOr}C}=iIoue?@26zqNE2f9Z6?ptP!S)+FJKc!*n-%D(
zgXCLRNxk*H;*zk@yX$-Jv8WVEsi(l^i6=6{G{xqX5qg_uFoo`&+wN-|?XiERhj)HIIVU?NZ?=hn^ZEj7Anged;QSxy=7S@)>=x
zw>%!0>PjMdjHrk_EJQYNv7-0zK%Ps4J#m}8Cg{E{*%_A1a4MB@>*cuI`m)xksciD{
zSi+7YHi4MhRyuhmmSQ(v#yo)lKi2s2Oy$q{b9T&l(pwvi9V_)>RTrc&NNj4p^5}`i~B(Kk$d_igXiTXms?HeOf^<|$-G1o~+sDm?(edmSItCZmt?{olj%@&lW0`5;;w9r<
ztm;Jxl!BZE>oYaT(2nZPOw@p{08>@o49nOao}LaFkoCfMIQQ3lJ#v?HO8V8FT1FWk
zhMU~kw@|1H8>+gN42bj5rsp1s@kX{IztRtfH2g%X3@5^&x)>64udUmTjFv>5pj6y|
zIi&$1SF0Wn-#JLc-3y#}e|IjAUH_U9g2CjTr|9KZxv3@SDoKM`g^R@%Fs+h9SHKXz
zaxuK7io6P9k_%Qp`Ua&=+(_KllQDRt|m>lf4AEq<@{nokS@#Zi)`6D3z+gB0Ev#
zFTeHWx@$`IWX
z<8$!SaqyEz9a4kF;r|pXWj^XvN5V5@ny$$#8YQ9f?^vD{%3$v?lbe)QwbSoa^HhK~
zUM+kIUU9u`%EH3rt^q-=k38)sQ2{H>!4FdM^K@?5Um2HMU}ljT`*f%K=lMt3iaseD
z_p*?-B|2YF+H=e2brYT%2?D`juonS$=);Xczekc3!ZFj$2Wr=L$ia{24A)bx7;ZEh
zS+YAGJa`&S$vI=TyrcQiNOSGi@Q`_oHsXw;J-bU`ZCpMcCq~Ejc5m6>YYm-1exY2=
zySE5U-K++JVZCC-m|{-O{vh@QR2@uTU7Gw-4_KICG5coN;Agh&PUKs8BP6|r6E7d0
z3mMd^2^F8eXBKdK>~xC@V~z=*O;R_PG=4MmFhO=8+F#tXPXuCOC7qnKQ}mEh)FR-{
z?)G}#-Mu>zuDpt#?r=`;fWPm0PMm2Ch&(Rx(j}piq>}5AmM+MDwfs(K(cZUTh0beq
zwXz44-=~XR#y!#>P^p$jhTHDxBV^hIr*-64MuPZ{ufjJ2>7<6aMfuKu(>mG`PR
zDqv6M^^Zo0YqTci*n5?Cl0%6OvB7>;B5p53rch8up>ct`9Vs+oLZ5W!SmyCOs)K@d
zw4rr}x~)h^hKCMfaExJ^AN{GKW4jDV{E-9~Kulcfs`2j5xVyE6S>~ov%UYh&v_do`0HsfkF=a&M?YDu
z8b+y{lWmDuZj{VhppOGx3EXFlR-#ZqGql>EkOCi
zk(h_zDIYDD4OKJo|eo3i?d
za+eJoj0|QK{(W$pb4QPkDyRRT(T
zQ+3#3iP)G~l$#6zeQ(#3Hx;B0oWCYMIAbXY!Oj;FlUIH~nZJ{G?P;a9%Q{hp;wsJk
z$7TtT3Yx`bx{h+-`A&y^qN*YYOp`zr>6trCl7xK^Tf+QW6E-7e^>Lj{M1>jX+*NliMx+J0LARd
zpB>ZM4)}(Y7GSF8Hd<}4=GnQf9`2SHF`Gi56iG+=m5G~4f$PPt&v#XK=+?Mk*oZA`y~$h
zpScMnGY
zf{yEynQ<8bfdjkZZ}va<22h)C#ClJ_%}diae++#gT~1I7dX3PdG%$6bmicGWw>L
z*kpmKqcKd5DWA3OMMiG$OI)^1(_LwGsJBh3R^_r#1$5i%ZeVH^>Cb%@E(kfDcQ$J-
zkm+0?ihZ^J$nQAT)kTOY58Y+uDs%Ky?_Ks3qe}5&<6_xoS;&_gjL0M8HI>%$e5Y`S
z=?8TT+{7)|MoN|JgQgZvH2Vi#9U|JZkcLN?O%zhU?Y|u&I?P?3dB>7X$ec&&4p8aB
zaH!V70MkuX{b>JT?H4?4#a~m{P&BX(RCUfkmGwJqS_h2O81)%~1-EDwo;6I&t8aEn
z>g@?InFfYvF&ce+|1RoQRXG2oW{SpRi~qz7BdBy)|)cX%%a77YaG4j^3a7_1UeYvo}4cETcW3*kU5w+4fH>2&4
zEZUeJ3-q_MnU&|giTieQfBnzizF@VT4U=bBm0<<#Mk(=r(+~Ybz7j^ZC8d
z5AGv3))^M{;>)OY~O^csSf7U
z@9`=g&nm9CVnyCSnAh`ShBRZ~#fMN1VB%u%pz6OT=cZRBV5%9{fCcY>V(S<4lfqK1
z+4khFh;LUUw0lKr%|!4{9OF05D@%Esy#;$d5&8BpTu_xgsy3hYZ~UZi2&%e~$wGSzLSET&D&Yf!sTO97#Vyc{-*@we=nn3rnrH&8gx-N{#1A$tmu0xQhq89^j~`3am>qx_iXD^c@p*Vl!PwKu1twSyH-ml&+tmirt9y?fDMJMR-Mll7Us
zZflu9#9Ar<6$KSeMf#>%0L1sRqinTl&+}4X5*ut+Dj!^xMLipu+%f%VPn#EI#$OlN
zUR-FFIH5+JVRJM6p+d!~%*;(ZBHAOJi}v3vtfQ+A8QJ9BvoSTq_(y|!41XSPxm$RgL@sBA;I~$4i$9j*P!$RnCaQ`Dw
zHBDeL`LW{gVWnhfbMKFy%;$Wq7j;vdQFjxu7Kck!hzP9vXg!Cg{9$(|M`SaRZ(@k6
z`T?bdw|Qe6C#oi(4Wzk)x63!$cDB=ijoR?uIl{arb7id^YNQG-*NMzG_Q>yK!u5n&
zd~UIg3}s73{Wt*|_@r1*Y4L)E@32i(i28fZp*~*@hZ+$ft55w}gu3~kj2>g7bKU%o
zEok;`K*@E&y*Nc~3X!`Cbx6GJ1D8|=*{wy1e98=iL04r#Z?CM(n*C@F(Z6QdH$Lpl
zw)zM%WPDdFXUc|>;LlOu%?@NmC%CN?`i(XKU{6sNG38#>a-dO~ai@QN3mAII5ODMn
zsqPt2zC}FC1+vXF8Vi0cAsg=g*^uKR@jxq*(%U5urhy
zCr~f^UfHwbow&vgOkH+~=7x&lWlzBLNsLnsr~WbjwLMwZZ61-)>qoQb%HyU6^^>D#
z^{jB_>cEtSIzIsdzNzImm>*cgcG$8uUBu(Y{obkGVI}Krz{b@vLxMO_GL!+kI0Z5K
zo}zpE;m&-NU!~k8+>0su#Zs&igMieHV_oyX{xuK>#EU^tt98|9mdk-_yp^d7siHnC
z1S!O2s|fLp-u9=QAA`QF+=+s6)Yvayu-7{GLu2`29fN#v;krl7OK;~TC#QlXUGoV^
zu;*5QXV9bv8INkv*pGMe9v^-e%btooCoPJ4ymkYs?nCGFhoB)eG%eYIHS6T$_1a;2
z?jpFVUlm#B#gyLH#%RridIq=G{~
z_PTKwe})FeZV})y9iV(MuR1hD-AYhmp%XQoUj5-q4~>6L88%
z4llsX!muk&)R$84Exp~l_MAi$P;wo}E;jZ2K&H~cruc$00rm%-V0bCen|^=!6H3mi
z
zG*sXlpXxyJn`hW=GwJAYf5XW+AI
zfTms$PsEs*6DY;r*$G*}WjCfQ3!*(7l^@CoD=$`Y&)sQ#-G$VL6&^*co(SzD8ADp#
z`Rl#KLMN2bswBu9iY=Tq-RqVbQh!Oq()bFjPy~{>!F@8;K%E|TYd*qH%ZdI5RHNgD
z)!5->pE8s7Of@Xw|F>Chd040j_UR70R<_g-gc_g@_u%!>?dXG-R{<(pXBLAYF|{
zF*CkwoWCUn9wh9{>cRt+N_!_rJP=bI|Mil|4$pDJwMNYRsQX=Ztqx;I?ur1+x_SpR
zfpjW49;L<#yHg}9G*4#$_2Nm;_?MX$us~fWQR$`{-2O4iPM8rhvt6=49Mx@(g~PVY
zdPa$;WUXEQy{-gl%``_JN2pB~`rcFdKuFi0^-a=*&ZqK^H#?SxtuV)kVNiv5`O!({
zGQG@ox~2lJpP)Ged_z({1W6u+VA8rvm_=->d5EnJEj;vd4NF2%e>bmOmg@Z1kYc?j
zCfl+M0Ugn(@>2f6_Ds^!l_79u6D*QIZ324Ge6T*b0D2o$r4~?Ft#uNz=*P^?9mucD
z4o}}hPNg-`beRCX0MY>l41G(VkI6ROF^%^tl-u^Gw)PGjxzJ+m+afCr`)cb*mg{a=
zS&D{+?ABBv>yB8}GA)#O3@P0Q{hA!e``xc>CTV=!iC$%d-jx&xZcjt+Up^a$pB@pL
z7F>=N>gIhiET3c1EFHoyxlN}o1p!C)B-o9MfQ|B`{4TwRsT;|cprY0^({b3t5&@Ad
zwJ?9=nM~*b-@%Ymf3%P1L;cE+1)nrOop!7CjJ>)tc6*y6)YOrwiKf3Qt`RhP^y(dl
z@J=E^i(slQurRb{Nq1VaH|ts8@(k^~zg~}(2#;IaZ^gPaUpe_UBQM1MmX%2=e5cNk
zXwTp&&l_i1B$G_(V-0OHBqCB7Hq|0&yvU(Fu>o>j(lSiwv>=JAc
zjnL}(FoToiHRuH(6tDWrtHfwM&i;BCF;v-+(n+18G4b2?j86NA9kAad&oqx`xrD0A><3+W2+tv
z-eo^;q_+c^?_tpQx*4A)v>dc>4ef%0R0S;J5bx&H6#
z6xgZ|kwq3^^;+IjuUh16LvVwQhcfrqlb!Pcb8J~qa}~fsa$A16=@wzZ@$seEsHq`2
zhE@G356q}$k8j=mhcq{sDlR+16>{2qzfy%w1higYRIGD8Im?Ft1`6$fIZg-9&5c08
z`H|xI0hxS?R}t*F88An}#{9XK7x1qZ`E3Z+&inOFKtoCndzLX?b_rzhHN6Tvk((Q*
zujPN>cbD)mp$k%KJm}@GW
zi&N>U%PB}>h(Z-*miI{`j_Jk2sn5S2(&towfP)6
zKK4i8;mILsXv)pX