diff --git a/Editor.css b/Editor.css
new file mode 100644
index 00000000..9f01bd9d
--- /dev/null
+++ b/Editor.css
@@ -0,0 +1,793 @@
+@keyframes click_me {
+ 0% {
+ background: linear-gradient(white, #e6e6e6);
+ }
+ 50% {
+ background: yellow;
+ }
+ 100% {
+ background: linear-gradient(white, #e6e6e6);
+ }
+ 50% {
+ background: yellow;
+ }
+ 0% {
+ background: linear-gradient(white, #e6e6e6);
+ }
+}
+
+@keyframes gradient {
+ 0% {
+ background-position: 0% 50%
+ }
+ 50% {
+ background-position: 100% 50%
+ }
+ 100% {
+ background-position: 0% 50%
+ }
+}
+
+button.click-me {
+ animation: click_me 1s infinite;
+ font-weight: bold;
+}
+
+/* ----------------------------------------------------------------------------------------------------------------- */
+
+table {
+ border-collapse: collapse;
+}
+
+td {
+ text-align: center;
+ line-height: 16px;
+}
+
+tr {
+ text-align: center;
+}
+
+body {
+ font-family: Trebuchet MS, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+ background-color: aliceblue;
+}
+
+button {
+ background-image: linear-gradient(white, #e6e6e6);
+ border: none;
+ border-radius: 3px;
+ box-shadow: 0.5px 0.5px 1px black;
+ font-family: Trebuchet MS;
+ margin: 4px 0 0 0;
+}
+
+button:hover:enabled {
+ box-shadow: 0 0 0 1px #1e90ff;
+}
+
+button:focus {
+ background-image: linear-gradient(#4b91ff, #055ce4);
+ color: white;
+}
+
+button:active:enabled {
+ transform: scale(.98);
+ color: black;
+}
+
+/* CANVAS----------------------------------------------------------------------------------------------------------------- */
+
+#levelTable {
+ margin: 0 auto;
+}
+
+#canvasContainer {
+ position: relative;
+}
+
+canvas {
+ border-radius: 20px;
+ position: absolute;
+ left: 0;
+ top: 0;
+}
+
+#canvas1 {
+ z-index: 1;
+}
+
+#canvas2 {
+ z-index: 2;
+}
+
+#canvas3 {
+ z-index: 3;
+}
+
+#canvas4 {
+ z-index: 4;
+}
+
+#canvas5 {
+ z-index: 5;
+}
+
+#canvas6 {
+ z-index: 6;
+}
+
+#canvas7 {
+ display: none;
+ z-index: 7;
+}
+
+/* EDITOR----------------------------------------------------------------------------------------------------------------- */
+
+#ghostEditorPane {
+ height: 500px;
+ width: 100%;
+ border-radius: 20px;
+ border: 5px solid transparent;
+}
+
+#ghostEditorPane:hover {
+ border: 5px solid red;
+}
+
+#editorPane {
+ display: none;
+ padding-left: 5px;
+}
+
+#controls {
+ background-color: #deb0b0;
+ width: 200px;
+ margin: 0 2px 0 2px;
+ border-radius: 20px;
+ position: relative;
+}
+
+#uneditButton {
+ margin-right: 10px;
+}
+
+#reeditButton {
+ margin-left: 10px;
+}
+
+#editsSpan {
+ font-size: 10pt;
+}
+
+.editorButton {
+ margin: -1px;
+}
+
+.buttonHeader {
+ font-size: 10pt;
+ font-weight: 900;
+}
+
+.spacer {
+ height: 10px;
+}
+
+.standard {
+ background-color: #ded2b0;
+}
+
+.enhanced {
+ background-color: #c7deb0;
+}
+
+.hotkeySpacer {
+ height: 1px;
+}
+
+.controlsHeader {
+ line-height: 20px;
+}
+
+.header {
+ font-weight: 900;
+}
+
+#selectHolder {
+ display: inline-block;
+}
+
+#controlsRow button {
+ vertical-align: top;
+}
+
+.hotkey {
+ font-size: 8pt;
+ color: rgba(0, 0, 0, 0.5);
+ font-family: Trebuchet MS;
+}
+
+.buttonDiv {
+ display: inline-block;
+}
+
+#controlButtons {
+ margin: 0 auto;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ display: table;
+}
+
+/* BOTTOM CONTROLS----------------------------------------------------------------------------------------------------------------- */
+
+#returnButton {
+ display: table;
+ text-decoration: none;
+ color: white;
+ background-color: #1e90ff;
+ border-radius: 50%;
+ width: 60px;
+ height: 60px;
+ text-align: center;
+ font-size: 8pt;
+ margin: 0 auto;
+ position: relative;
+}
+
+#returnButton:hover {
+ background-image: linear-gradient(45deg, #1e90ff, #1e90ff, #1e90ff, #1e90ff, #fdc122, #dc6a3f, #d55945, #dc6a3f, #fdc122);
+ background-size: 500% 500%;
+ animation: gradient 2s ease infinite;
+}
+
+#returnButton a {
+ display: table-cell;
+ text-decoration: none;
+ vertical-align: middle;
+ margin: auto;
+}
+
+#returnButton a:visited {
+ text-decoration: none;
+ color: white;
+}
+
+#returnArrow {
+ position: absolute;
+ top: 10%;
+ left: 40%;
+ font-size: 12pt;
+}
+
+#levelTypeContainer {
+ display: flex;
+ align-items: center;
+ width: 280px;
+ position: relative;
+}
+
+#levelType {
+ flex-grow: 1;
+ font-size: 16pt;
+ color: #1e90ff;
+ font-weight: 900;
+}
+
+#levelTypeSpan {
+ font-size: 8pt;
+ font-weight: 100;
+ margin-bottom: 5px;
+}
+
+#switchSnakesButton {
+ min-height: 30px;
+ margin-right: 20px;
+}
+
+#arrowsHolder {
+ display: flex;
+ align-items: center;
+}
+
+.arrow {
+ min-height: 30px;
+ min-width: 30px;
+ font-size: 14pt;
+}
+
+#arrowLeft {
+ float: left;
+ margin-right: 5px;
+}
+
+#verticalArrowsHolder {
+ float: left;
+}
+
+#arrowRight {
+ float: left;
+ margin-left: 5px;
+}
+
+.bigButton {
+ font-size: 20pt;
+ padding: 10px;
+}
+
+#bigButtonButton {
+ display: none;
+}
+
+#bigButtonButton:hover {
+ cursor: pointer;
+}
+
+#bigButtonButton:active {
+ transform: scale(.97);
+}
+
+#bottomBlock {
+ display: table;
+ width: 900px;
+ margin: 0 auto;
+ margin-top: 20px;
+ text-align: center;
+ position: relative;
+}
+
+.separator {
+ background-color: black;
+ height: 190px;
+ width: 1px;
+ margin: 0 5px 0 5px;
+ display: inline-block;
+}
+
+.optionsHeader {
+ margin-top: 10px;
+ font-family: Trebuchet MS;
+}
+
+#keyboardInstructions {
+ font-size: 8pt;
+ width: 220px;
+ text-align: center;
+ display: inline-block;
+}
+
+#optionsStack {
+ display: inline-block;
+}
+
+.optionsHeaders {
+ font-weight: 900;
+}
+
+.bottomBlockBox {
+ width: 33%;
+ text-align: center;
+ font-size: 10pt;
+ font-family: Trebuchet MS;
+ display: table-cell;
+ vertical-align: top;
+}
+
+#firstBottomBlock {
+ position: absolute;
+ top: -12px;
+ left: -300px;
+}
+
+#highlighters {
+ display: inline-block;
+ border-radius: 5px;
+ background-color: rgba(0, 0, 0, 0.05);
+ vertical-align: top;
+ padding: 5px 10px 5px 10px;
+}
+
+#levelSize {
+ display: inline-block;
+ border-radius: 5px;
+ background-color: rgba(0, 0, 0, 0.05);
+ vertical-align: top;
+ padding: 5px 10px 5px 10px;
+}
+
+#levelSizeInsider {
+ display: table;
+}
+
+#levelSizeText {
+ position: relative;
+}
+
+#fitButtonContainer {
+ vertical-align: middle;
+ padding: 0 5px 0 5px;
+ border-collapse: separate;
+}
+
+.radioContainer {
+ position: relative;
+ cursor: pointer;
+ float: right;
+ width: 15px;
+ height: 15px;
+ margin-top: 5px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+/* Hide the browser's default radio button */
+
+.radioContainer input {
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+ height: 0;
+ width: 0;
+}
+
+/* Create a custom radio button */
+
+.radioButton {
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 15px;
+ width: 15px;
+ background-color: white;
+ border: 2px solid #555;
+ border-radius: 3px;
+ box-sizing: border-box;
+}
+
+/* On mouse-over, add a grey background color */
+
+/* .radioContainer:hover input~.radioButton {
+ background-color: white;
+} */
+
+/* When the radio button is checked, add a blue background */
+
+.radioContainer input:checked~.radioButton {
+ background-color: #2196F3;
+}
+
+/* Create the indicator (the dot/circle - hidden when not checked) */
+
+.radioButton:after {
+ content: "";
+ position: absolute;
+ display: none;
+ box-sizing: border-box;
+}
+
+/* Show the indicator (dot/circle) when checked */
+
+.radioContainer input:checked~.radioButton:after {
+ display: block;
+}
+
+/* Style the indicator (dot/circle) */
+
+.radioContainer .radioButton:after {
+ top: 4.5px;
+ left: 4.5px;
+ width: 2px;
+ height: 2px;
+ border-radius: 50%;
+ background: white;
+}
+
+/* .radioContainer .radioButton:after {
+ top: 2px;
+ left: 2px;
+ width: 11px;
+ height: 11px;
+ border-radius: 1.5px;
+ background: white;
+} */
+
+.fitButton {
+ float: left;
+ margin-right: 5px;
+}
+
+#fitCanvas {
+ margin-bottom: 2.5px;
+}
+
+.plusMinus {
+ text-align: center;
+ font-size: 4pt;
+ line-height: 13px;
+ font-weight: 900;
+ border-radius: 3px;
+ width: 12px;
+ height: 12px;
+ background-color: #1e90ff;
+ color: white;
+}
+
+.plusMinus:hover {
+ cursor: pointer;
+ border-radius: 3px;
+ background-color: white;
+}
+
+.colorFix {
+ color: transparent;
+ text-shadow: 0 0 0 white;
+}
+
+.plusMinus:hover .colorFix {
+ color: transparent;
+ text-shadow: 0 0 0 #1e90ff;
+}
+
+#minus {
+ position: absolute;
+ top: 0;
+ left: -5px;
+}
+
+#plus {
+ position: absolute;
+ top: 0;
+ right: -5px;
+}
+
+#paradoxDiv {
+ background: #f88;
+ text-align: center;
+}
+
+#cycleDiv {
+ background: yellow;
+ text-align: center;
+}
+
+#additions {
+ display: none;
+ font-family: Trebuchet MS;
+ color: white;
+ font-size: 8pt;
+ width: 200px;
+ border-radius: 10px;
+ background-color: rgba(255, 0, 0, .3);
+ padding: 5px;
+ position: absolute;
+ top: 52px;
+ right: 0;
+}
+
+#editorDiv {
+ width: 100%;
+ text-align: center;
+}
+
+#shareLink {
+ padding-right: 5px;
+ display: table-cell;
+ font-size: 10pt;
+}
+
+#shareLinkTextbox {
+ padding: 5px;
+ display: table-cell;
+ border-radius: 2px;
+ border: 1px solid black;
+ background-color: #1e90ff;
+ color: white;
+}
+
+#link2 {
+ padding-right: 5px;
+ display: table-cell;
+ font-size: 10pt;
+}
+
+#link2Textbox {
+ padding: 5px;
+ display: table-cell;
+ border-radius: 2px;
+ border: 1px solid black;
+ background-color: #1e90ff;
+ color: white;
+}
+
+#submitSerializationButton {
+ padding: 5px;
+ border-radius: 2px;
+ border: 1px solid black;
+ background-image: none;
+ background-color: #1e90ff;
+ float: left;
+ color: white;
+}
+
+#progressButtons {
+ display: table;
+ margin: 0 auto;
+ margin-bottom: 5px;
+}
+
+#unmoveButton {
+ transform: scale(-1, 1);
+}
+
+.progressButton {
+ font-size: 10pt;
+ line-height: 10pt;
+ display: table-cell;
+ vertical-align: top;
+ padding: 1px 0 0 10px;
+}
+
+.progressButton:hover {
+ cursor: pointer;
+ color: #1e90ff;
+}
+
+#movesSpan {
+ display: table-cell;
+ vertical-align: bottom;
+}
+
+.sliderStack {
+ background-color: rgba(0, 0, 0, 0.05);
+ padding: 5px 10px 5px 10px;
+ display: inline-block;
+ border-radius: 4px;
+ margin-bottom: 10px;
+}
+
+.sliderContainer {
+ display: block;
+ border-radius: 4px;
+ padding: 2px;
+}
+
+.sliderLabel {
+ font-family: Trebuchet MS;
+ font-size: 8pt;
+ line-height: 12pt;
+ margin-right: 10px;
+}
+
+.switch {
+ position: relative;
+ display: table-cell;
+ vertical-align: top;
+ width: 30px;
+ height: 17px;
+ float: right;
+}
+
+/* Hide default HTML checkbox */
+
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #555;
+ border-radius: 4px;
+ -webkit-transition: .2s;
+ transition: .2s;
+}
+
+.slider:before {
+ position: absolute;
+ content: "";
+ height: 13px;
+ width: 13px;
+ left: 2px;
+ bottom: 2px;
+ background-color: white;
+ border-radius: 2px;
+ -webkit-transition: .2s;
+ transition: .2s;
+}
+
+input:checked+.slider {
+ background-color: #1e90ff;
+}
+
+input:focus+.slider {
+ box-shadow: 0 0 1px #1e90ff;
+}
+
+input:checked+.slider:before {
+ -webkit-transform: translateX(13px);
+ -ms-transform: translateX(13px);
+ transform: translateX(13px);
+}
+
+.small {
+ font-size: 6pt;
+}
+
+#csButton {
+ margin: 0 auto;
+ margin-top: 20px;
+ font-size: 30pt;
+ left: 50%;
+ display: none;
+}
+
+#csText {
+ box-sizing: border-box;
+ width: 100%;
+ font-size: 24pt;
+ font-family: Trebuchet MS;
+ text-align: center;
+ padding: 20px;
+ display: none;
+}
+
+.stackHeader {
+ margin-bottom: 6px;
+ font-weight: 900;
+}
+
+button #clearHighlightsButton {
+ background-image: linear-gradient(black, #e6e6e6);
+}
+
+#openEditorButton {
+ width: 100px;
+ height: 100px;
+ border-radius: 50%;
+ background-color: #66903c;
+ border: 10px solid white;
+ text-align: center;
+ color: white;
+ font-size: 60pt;
+ line-height: 78pt;
+ position: absolute;
+ top: 10;
+ right: 10;
+ z-index: 100;
+}
+
+#openEditorButton:hover {
+ cursor: pointer;
+}
+
+#openEditorButton:active {
+ transform: scale(.97);
+}
+
+#closeEditorButton {
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background-color: #b44b4b;
+ border: 2px solid white;
+ text-align: center;
+ color: white;
+ font-size: 12pt;
+ line-height: 16pt;
+ position: absolute;
+ top: -6;
+ right: -6;
+ opacity: 100%;
+}
+
+#closeEditorButton:hover {
+ cursor: pointer;
+}
diff --git a/Format.css b/Format.css
new file mode 100644
index 00000000..fdbd9ec1
--- /dev/null
+++ b/Format.css
@@ -0,0 +1,255 @@
+body {
+ font-family: Trebuchet MS;
+ padding-bottom: 50px;
+ background-image: linear-gradient(to bottom right, #fdc122, #dc6a3f, #d55945, #dc6a3f, #fdc122);
+ counter-reset: rowNumber 0;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+th {
+ padding-bottom: 20px;
+}
+
+tr {
+ height: 30px;
+}
+
+table tr td:first-child::before {
+ counter-increment: rowNumber;
+ content: counter(rowNumber);
+ color: rgba(0, 0, 0, .3);
+ cursor: default;
+}
+
+table tr td:not(:nth-child(3)) {
+ text-align: center;
+}
+
+table tr td:nth-child(3) {
+ padding-left: 10px;
+}
+
+table tr td:nth-child(1) {
+ width: 5%;
+}
+
+table tr td:nth-child(2) {
+ width: 5%;
+}
+
+table tr td:nth-child(3) {
+ width: 30%;
+}
+
+table tr td:nth-child(4) {
+ width: 15%;
+}
+
+table tr td:nth-child(5) {
+ width: 40%;
+}
+
+table tr td:nth-child(6) {
+ width: 5%;
+}
+
+td {
+ /* border-radius: 10px;
+ border: 2px solid transparent; */
+ padding: 6px;
+}
+
+/*
+td:hover {
+ border: 2px solid red;
+}
+td:hover:first-child {
+ border-left: 3px solid red;
+}
+td:hover:nth-child(3n) {
+ border-right: 4px solid red;
+}
+tr:last-child td:hover {
+ border-bottom: 4px solid red;
+}
+td:hover::before,
+.row:hover::before,
+.ff-fix:hover::before {
+ background-color: #ffa;
+ content: '\00a0';
+ height: 100%;
+ left: -5000px;
+ position: absolute;
+ top: 0;
+ width: 10000px;
+ z-index: -1;
+}
+td:hover::after,
+.col:hover::after,
+.ff-fix:hover::after {
+ background-color: #ffa;
+ content: '\00a0';
+ height: 10000px;
+ left: 0;
+ position: absolute;
+ top: -5000px;
+ width: 100%;
+ z-index: -1;
+} */
+
+tbody tr:hover {
+ background-color: rgba(0, 0, 0, .05);
+}
+
+/* tr td.noteColumn{
+ font-size: 10pt !important;
+ vertical-align: middle;
+} */
+
+a {
+ color: black;
+ text-decoration: none;
+}
+
+td a {
+ display: block;
+ width: 100%;
+}
+
+#title {
+ width: 100%;
+ text-align: center;
+ font-size: 100px;
+ margin-bottom: 20px;
+}
+
+.tableTitle {
+ font-size: 40px;
+ text-align: center;
+ width: 100%;
+ margin-bottom: 20px;
+}
+
+#buttonContainer {
+ width: 36%;
+ margin-left: 32%;
+ margin-right: 32%;
+ text-align: center;
+ color: white;
+ margin-bottom: 60px;
+}
+
+.topButton {
+ display: inline-block;
+ margin-bottom: 20px;
+ padding: 10px;
+ border-radius: 10px;
+ color: white;
+ background-color: #730c39;
+ width: 40%;
+ margin: 10px;
+ border: 2px solid black;
+ text-transform: uppercase;
+ font-size: 12pt;
+}
+
+.topButton:active {
+ transform: scale(.98);
+}
+
+.tableContainer {
+ width: 56%;
+ margin: 0 auto;
+ border-spacing: 0;
+ border: 10px solid black;
+ border-radius: 20px;
+ padding: 20px 30px 20px 30px;
+ background-color: rgba(255, 255, 255, .2);
+ margin-bottom: 50px;
+}
+
+.hiddenItem {
+ filter: blur(8px);
+ pointer-events: none;
+ cursor: default;
+}
+
+#showButtonContainer {
+ width: 56%;
+ margin-left: 22%;
+ margin-right: 22%;
+ position: relative;
+ padding-bottom: 1px;
+}
+
+#showButton {
+ border: 1px solid black;
+ border-radius: 5px;
+ display: inline-block;
+ padding: 5px;
+ font-size: 12px;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+}
+
+#showButton:hover {
+ cursor: pointer;
+}
+
+#showButton:active {
+ transform: scale(.98);
+}
+
+.disabled {
+ pointer-events: none;
+ cursor: default;
+}
+
+#note {
+ font-size: 12px;
+ float: right;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+}
+
+#passWindow {
+ height: 100px;
+ width: 300px;
+ margin: auto;
+ background-color: darkslategray;
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ color: white;
+ visibility: hidden;
+ border: 10px solid black;
+ border-radius: 10px;
+}
+
+form {
+ width: 50%;
+ margin: 0 auto;
+ text-align: center;
+ padding: 10px;
+}
+
+#backgroundBox {
+ width: 100%;
+ height: 100%;
+ /*background-color: rgba(0,0,0,.5);
+ filter: blur(8px);
+ visibility: hidden;*/
+}
+
+.vColumn {
+ font-size: 10pt;
+ font-family: Trebuchet MS;
+}
diff --git a/Framework.html b/Framework.html
new file mode 100644
index 00000000..5c454b12
--- /dev/null
+++ b/Framework.html
@@ -0,0 +1,298 @@
+
+
+
+ Snakefall Redesign
+
+
+
+
+
+
+
+
+
+ ➕
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ ✖
+
+ |
+ |
+
+ 0+0
+
+ |
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ |
+
+
+ |
+
+
+ |
+ |
+
+ |
+ | Standard Elements |
+
+
+ |
+ |
+ W
+ S
+ Shift E
+ P
+ |
+ |
+
+
+ |
+ |
+ Shift S
+ F
+ B
+ |
+ |
+
+ |
+
+
+
+ |
+ |
+ M
+ Spacebar
+ |
+ |
+
+
+ |
+ |
+ Shift F
+ C
+ Shift B
+ |
+ |
+
+
+ |
+ |
+ Shift R
+ T
+ O
+ |
+ |
+
+
+ |
+ |
+ X
+ Shift X
+ |
+ |
+
+
+ |
+ |
+ L
+ Shift L
+ |
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
Standard Level
+
contains only original Snakebird elements
+
+
*
+
+
+
+
+
Use the arrows or WASD keys on your keyboard to move, or use the buttons below
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
This game is a clone of
Snakebird by Noumenon Games.
+
+
+ Press any key to verify the solution
+
+
+
+
+
+
diff --git a/Main.js b/Main.js
new file mode 100644
index 00000000..a37b8b19
--- /dev/null
+++ b/Main.js
@@ -0,0 +1,6802 @@
+function unreachable() { return new Error("unreachable"); }
+if (typeof VERSION !== "undefined") {
+ document.getElementById("versionSpan").innerHTML =
+ '' + VERSION.tag + '';
+}
+
+$(document).ready(function () {
+ $(window).on("mousemove", function (e) {
+ $("#openEditorButton").css({ opacity: 1 });
+ clearTimeout(window.myTimeout);
+ window.myTimeout = setTimeout(function () {
+ $("#openEditorButton").css({ opacity: .2 });
+ }, 5000);
+ });
+});
+
+var sv = false;
+var didResize = false;
+var canvas1 = document.getElementById("canvas1");
+var canvas2 = document.getElementById("canvas2");
+var canvas3 = document.getElementById("canvas3");
+var canvas4 = document.getElementById("canvas4");
+var canvas5 = document.getElementById("canvas5");
+var canvas6 = document.getElementById("canvas6");
+var canvas7 = document.getElementById("canvas7");
+
+var SPACE = "0".charCodeAt(0);
+var WALL = "1".charCodeAt(0);
+var SPIKE = "2".charCodeAt(0);
+var FRUIT_v0 = "3".charCodeAt(0); //legacy
+var EXIT = "4".charCodeAt(0);
+var PORTAL = "5".charCodeAt(0);
+var RAINBOW = "P".charCodeAt(0);
+var TRELLIS = "p".charCodeAt(0);
+var ONEWAYWALLU = "u".charCodeAt(0);
+var ONEWAYWALLD = "d".charCodeAt(0);
+var ONEWAYWALLL = "l".charCodeAt(0);
+var ONEWAYWALLR = "r".charCodeAt(0);
+var CLOSEDLIFT = "c".charCodeAt(0);
+var OPENLIFT = "o".charCodeAt(0);
+var CLOUD = "C".charCodeAt(0);
+var BUBBLE = "b".charCodeAt(0);
+var LAVA = "v".charCodeAt(0);
+var WATER = "w".charCodeAt(0);
+var validTileCodes = [SPACE, WALL, SPIKE, EXIT, PORTAL, RAINBOW, TRELLIS, ONEWAYWALLU, ONEWAYWALLD, ONEWAYWALLL, ONEWAYWALLR, CLOSEDLIFT, OPENLIFT, CLOUD, BUBBLE, LAVA, WATER];
+var OWWCounter = 0;
+
+// object types
+var SNAKE = "s";
+var BLOCK = "b";
+var MIKE = "x";
+var FRUIT = "f";
+var POISONFRUIT = "p";
+
+var newSpikeDeath = [];
+var lowDeath = false;
+var dieOnSplock = false;
+var theseDyingLocations = [];
+var infiniteDeath = false;
+
+var checkResult = false;
+var cr = false;
+var cs = false;
+var cs2 = false;
+var dont = false;
+var fruitLog = [];
+var poisonFruitLog = [];
+
+var postPortalSnakeOutline = [];
+var portalConflicts = [];
+var portalFailure = false;
+var portalOutOfBounds = false;
+var cycle = false;
+var cycleId = -1;
+var multiDiagrams = false;
+
+var tileSize = 34;
+var borderRadiusFactor = 3.4;
+var borderRadius = tileSize / borderRadiusFactor;
+var blockRadiusFactor = 5;
+var blockRadius = tileSize / blockRadiusFactor;
+
+var level;
+var unmoveStuff = { undoStack: [], redoStack: [], spanId: "movesSpan", undoButtonId: "unmoveButton", redoButtonId: "removeButton" };
+var uneditStuff = { undoStack: [], redoStack: [], spanId: "editsSpan", undoButtonId: "uneditButton", redoButtonId: "reeditButton" };
+var paradoxes = [];
+var enhanced = false;
+
+var oldRowcols = [];
+var animationsOn = true; //defaults
+var defaultOn = true;
+var replayAnimationsOn = false;
+var blockFixOn = false;
+
+function updateSwitches() {
+ var fitDefault = "";
+ if ((fitDefault = localStorage.getItem("cachedFitDefault")) !== null) {
+ if (fitDefault === "fitCanvas") document.getElementById("fitCanvasDefault").checked = true;
+ else if (fitDefault === "fitControls") document.getElementById("fitControlsDefault").checked = true;
+ }
+
+ if (localStorage.getItem("cachedAO") !== null) animationsOn = JSON.parse(localStorage.getItem("cachedAO"));
+ if (localStorage.getItem("cachedDO") !== null) {
+ defaultOn = JSON.parse(localStorage.getItem("cachedDO"));
+ document.getElementById("defaultSlider").checked = defaultOn;
+ }
+ if (localStorage.getItem("cachedRAO") !== null) {
+ replayAnimationsOn = JSON.parse(localStorage.getItem("cachedRAO"));
+ document.getElementById("replayAnimationSlider").checked = replayAnimationsOn;
+ }
+ if (defaultOn && persistentState.showEditor) animationsOn = false;
+}
+
+var cursor = 0;
+var cursorOffset = 0;
+var replayString = false;
+var replayLength = 0;
+var switchSnakesArray = [];
+
+function loadLevel(newLevel) {
+ level = newLevel;
+ currentSerializedLevel = compressSerialization(stringifyLevel(newLevel));
+ var string = stringifyLevel(newLevel);
+ var levelString = string.substring(string.indexOf("?") + 1, string.indexOf("/")); //everything before objects
+ if (levelString.match(/[a-z]/i)) enhanced = true;
+
+ // don't know why this was defaulting to true
+ persistentState.highlightFruits = false;
+ var snakes = getSnakes();
+ var fruits = getObjectsOfType(FRUIT);
+ var poisonFruits = getObjectsOfType(POISONFRUIT);
+ fruits.sort(compareLocations);
+ for (var i = 0; i < fruits.length; i++) {
+ fruitLog.push([fruits[i], i + 1]);
+ }
+ poisonFruits.sort(compareLocations);
+ for (var i = 0; i < poisonFruits.length; i++) {
+ poisonFruitLog.push([poisonFruits[i], i + 1]);
+ }
+ if (snakes.length === 0) document.getElementById("highlightSnakesButton").disabled = true;
+ else document.getElementById("highlightSnakesButton").disabled = false;
+ if (fruits.length === 0) document.getElementById("highlightFruitsButton").disabled = true;
+ else document.getElementById("highlightFruitsButton").disabled = false;
+ if (poisonFruits.length === 0) document.getElementById("highlightPoisonFruitsButton").disabled = true;
+ else document.getElementById("highlightPoisonFruitsButton").disabled = false;
+
+ activateAnySnakePlease();
+ unmoveStuff.undoStack = [];
+ unmoveStuff.redoStack = [];
+ undoStuffChanged(unmoveStuff);
+ uneditStuff.undoStack = [];
+ uneditStuff.redoStack = [];
+ undoStuffChanged(uneditStuff);
+ blockRenderCache = {};
+ mikeRenderCache = {};
+
+ // alert(document.getElementById("editorPane").style.offsetHeight);
+ if (!persistentState.showEditor) document.getElementById("ghostEditorPane").style.display = "none";
+ // else openEditorButtonLocation(false, localStorage.getItem("editorLocation"));
+ // document.getElementById("ghostEditorPane").style.height = document.getElementById("editorPane").style.offsetHeight;
+ // document.getElementById("ghostEditorPane").style.height = tileSize * level.height; // doesn't add up
+
+ recalculateBorderRadius();
+ recalculateBlockRadius();
+ updateSwitches();
+ drawStaticCanvases(level);
+ render();
+ if (sv) {
+ fitCanvas(2);
+ toggleTheme(0);
+ render();
+ }
+ else {
+ var fitDefault = localStorage.getItem("cachedFitDefault") !== null ? localStorage.getItem("cachedFitDefault") : fitDefault = "";
+ if (fitDefault == "" || fitDefault === "fitControls") { fitCanvas(1); fitDefault = "fitControls" }
+ else { fitCanvas(0); fitDefault === "fitCanvas" }
+ localStorage.setItem("cachedFitDefault", fitDefault);
+ }
+}
+
+function drawStaticCanvases(level) {
+ resizeCanvasContainer();
+ [canvas1, canvas3, canvas5, canvas7].forEach(function (canvas) {
+ canvas.width = tileSize * level.width;
+ canvas.height = tileSize * level.height;
+ });
+ var context = canvas1.getContext("2d");
+ populateThemeVars();
+ context.fillStyle = "white";
+ drawBackground(context, canvas1);
+
+ context = canvas3.getContext("2d");
+ var rng = new Math.seedrandom("b");
+ 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];
+ if (tileCode === SPIKE || tileCode === RAINBOW || tileCode === ONEWAYWALLU || tileCode === ONEWAYWALLD) drawTile(context, tileCode, r, c, level, location, rng, true, true);
+ }
+ }
+ context = canvas5.getContext("2d");
+ 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];
+ if (tileCode === WATER || tileCode === LAVA) drawTile(context, tileCode, r, c, level, rng, location, false);
+ }
+ }
+ 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];
+ if (tileCode === WALL) drawTile(context, tileCode, r, c, level, location, rng, true, true);
+ }
+ }
+ 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];
+ if (tileCode === WALL) drawTile(context, tileCode, r, c, level, location, rng, false, true);
+ }
+ }
+ 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];
+ if (tileCode === TRELLIS) drawTile(context, tileCode, r, c, level, location, rng, false, true);
+ }
+ }
+
+}
+
+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 plCursor = 0;
+ skipWhitespace();
+ var versionTag = string.substr(plCursor, magicNumber.length);
+ switch (versionTag) {
+ case magicNumber_v0:
+ case magicNumber: break;
+ default: throw new Error("not a snakefall level");
+ }
+ plCursor += 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;
+ var tileCounter = 0;
+ for (var i = 0; i < mapData.length; i++) {
+ var tileCode = mapData[i].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],
+ splocks: []
+ });
+ tileCode = SPACE;
+ }
+ if (validTileCodes.indexOf(tileCode) === -1) throw parserError("invalid tilecode: " + JSON.stringify(mapData[i]));
+ if (tileCode === RAINBOW || tileCode === TRELLIS || tileCode === ONEWAYWALLU || tileCode === ONEWAYWALLD || tileCode === ONEWAYWALLL || tileCode === ONEWAYWALLR || tileCode === CLOSEDLIFT || tileCode === OPENLIFT || tileCode === CLOUD || tileCode === BUBBLE || tileCode === LAVA || tileCode === WATER) tileCounter++;
+ level.map.push(tileCode);
+ }
+
+ // objects
+ skipWhitespace();
+ while (plCursor < string.length) {
+ var object = {
+ type: "?",
+ id: -1,
+ dead: false,
+ locations: [],
+ splocks: []
+ };
+
+ // type
+ object.type = string[plCursor];
+ var locationsLimit;
+ if (object.type === SNAKE || object.type === BLOCK || object.type === MIKE) locationsLimit = -1;
+ else if (object.type === FRUIT || object.type === POISONFRUIT) locationsLimit = 1;
+ else throw parserError("expected object type code");
+ plCursor += 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);
+ });
+
+ // splocks
+ if (object.type === BLOCK && string.substring(plCursor, plCursor + 1) === "?") {
+ var splockData = readRun();
+ var splockStrings = splockData.split("&");
+
+ splockStrings.forEach(function (splockString) {
+ var location = parseInt(splockString);
+ if (!(0 <= location && location < level.map.length)) throw parserError("splock out of bounds: " + JSON.stringify(splockString));
+ object.splocks.push(location);
+ });
+ }
+
+ level.objects.push(object);
+ skipWhitespace();
+ }
+
+ //describe level type
+ if (enhanced) {
+ document.getElementById("levelType").innerHTML = "Enhanced Level";
+ document.getElementById("levelTypeSpan").innerHTML = "contains new user-created elements";
+ document.getElementById("additions").style.display = "none";
+ if (persistentState.showEditor && tileCounter === 0) {
+ document.getElementById("additions").innerHTML = "all initial enhanced elements have been removed but the level is not saved";
+ document.getElementById("additions").style.display = "block";
+ }
+ }
+ else {
+ document.getElementById("levelType").innerHTML = "Standard Level";
+ document.getElementById("levelTypeSpan").innerHTML = "contains only original Snakebird elements";
+ if (persistentState.showEditor && tileCounter > 0) {
+ document.getElementById("additions").innerHTML = "enhanced elements have been added to this level but the level is not saved";
+ document.getElementById("additions").style.display = "block";
+ }
+ else document.getElementById("additions").style.display = "none";
+ }
+
+ for (var i = 0; i < upconvertedObjects.length; i++) {
+ level.objects.push(upconvertedObjects[i]);
+ }
+
+ return level;
+
+ function skipWhitespace() {
+ while (" \n\t\r".indexOf(string[plCursor]) !== -1) {
+ plCursor += 1;
+ }
+ }
+ function consumeKeyword(keyword) {
+ skipWhitespace();
+ if (string.indexOf(keyword, plCursor) !== plCursor) throw parserError("expected " + JSON.stringify(keyword));
+ plCursor += 1;
+ }
+ function readInt() {
+ skipWhitespace();
+ for (var i = plCursor; i < string.length; i++) {
+ if ("0123456789".indexOf(string[i]) === -1) break;
+ }
+ var substring = string.substring(plCursor, i);
+ if (substring.length === 0) throw parserError("expected int");
+ plCursor = i;
+ return parseInt(substring, 10);
+ }
+ function readRun() {
+ consumeKeyword("?");
+ var endIndex = string.indexOf("/", plCursor);
+ var substring = string.substring(plCursor, endIndex);
+ plCursor = endIndex + 1;
+ return substring;
+ }
+ function parserError(message) {
+ return new Error("parse error at position " + plCursor + ": " + message);
+ }
+}
+
+function serializeTileCode(tileCode) {
+ return String.fromCharCode(tileCode);
+}
+
+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).map(serializeTileCode).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("&");
+ if (object.splocks.length != 0) output += "/?" + object.splocks.join("&");
+ output += "/\n";
+ }
+ return output;
+}
+function serializeObjectState(object) {
+ if (object == null) return [0, [], []];
+ return [object.dead, copyArray(object.locations), copyArray(object.splocks)];
+}
+
+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 advance() {
+ var expectedPrefix = replayMagicNumber + "&";
+ if (cursor <= expectedPrefix.length) {
+ cursor = expectedPrefix.length;
+ activeSnakeId = 0;
+ }
+ var snakeIdStr = "";
+ var c = replayString.charAt(cursor);
+ cursor++;
+
+ if ('0' <= c && c <= '9') {
+ //check if number has up to 3 digits (999)
+ var d = replayString.charAt(cursor);
+ var e = replayString.charAt(cursor + 1);
+ if ('0' <= d && d <= '9') {
+ c = c + d;
+ cursor++;
+ cursorOffset++;
+ if ('0' <= e && e <= '9') {
+ c = c + e;
+ cursor++;
+ cursorOffset++;
+ }
+ }
+
+ //add cursor location of snake switch (location of last digit in multi-digit numbers)
+ if (!switchSnakesArray.includes(cursor)) switchSnakesArray.push(cursor);
+ snakeIdStr += c;
+ if (cursor >= replayString.length) throw new Error("replay string has unexpected end of input");
+ c = replayString.charAt(cursor);
+ cursor++;
+ }
+ if (snakeIdStr.length > 0) {
+ activeSnakeId = parseInt(snakeIdStr);
+ cursorOffset++;
+ // 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, replayAnimationsOn); break;
+ case 'u': move(-1, 0, replayAnimationsOn); break;
+ case 'r': move(0, 1, replayAnimationsOn); break;
+ case 'd': move(1, 0, replayAnimationsOn); break;
+ default: throw new Error("replay string has invalid direction: " + c);
+ }
+ var pre = cursor - expectedPrefix.length - cursorOffset;
+ var post = replayLength - cursor + expectedPrefix.length + cursorOffset;
+ var movesText = pre + "\xa0\xa0✾\xa0\xa0" + post;
+ document.getElementById("movesSpan").textContent = movesText;
+}
+function parseAndLoadReplay(string) {
+ replayString = decompressSerialization(string);
+ var expectedPrefix = replayMagicNumber + "&";
+ if (replayString.substring(0, expectedPrefix.length) !== expectedPrefix) throw new Error("unrecognized replay string");
+ cursor = expectedPrefix.length;
+ if (!switchSnakesArray.includes(cursor)) switchSnakesArray.push(cursor);
+ replayLength = 0;
+
+ while (cursor < replayString.length) {
+ var c = replayString.charAt(cursor);
+ switch (c) {
+ case 'l':
+ case 'u':
+ case 'r':
+ case 'd': replayLength++; break;
+ }
+ cursor++;
+ }
+
+ var movesText = "0\xa0\xa0✾\xa0\xa0" + replayLength;
+ document.getElementById("movesSpan").textContent = movesText;
+
+ cursor = expectedPrefix.length;
+
+ // the starting snakeid is 0, which may not exist, but we only validate it when doing a move.
+
+ // 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 a snake is 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);
+ location.reload();
+}
+
+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 CMD = 3;
+var ALT = 4;
+document.addEventListener("keydown", function (event) {
+ var modifierMask = (
+ (event.shiftKey ? SHIFT : 0) |
+ (event.ctrlKey ? CTRL : 0) |
+ (event.metaKey ? CMD : 0) |
+ (event.altKey ? ALT : 0)
+ );
+ if (!sv) {
+ switch (event.keyCode) {
+ case 37: // left
+ if (modifierMask === 0) { replayString = false; move(0, -1); break; }
+ return;
+ case 38: // up
+ if (modifierMask === 0) { replayString = false; move(-1, 0); break; }
+ return;
+ case 39: // right
+ if (modifierMask === 0) { replayString = false; move(0, 1); break; }
+ return;
+ case 40: // down
+ if (modifierMask === 0) { replayString = false; move(1, 0); break; }
+ return;
+ case 8: // backspace
+ if (modifierMask === 0) { undo(unmoveStuff); break; }
+ if (modifierMask === SHIFT) { redo(unmoveStuff); break; }
+ return;
+ case 48: //zero
+ fitCanvas(1);
+ return;
+ case 187: //equals and plus
+ changeCanvasSize(2);
+ return;
+ case 189: //minus
+ changeCanvasSize(-2);
+ 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 && !replayString) { redo(unmoveStuff); break; }
+ if (modifierMask === SHIFT && replayString) { advance(); break; }
+ if (persistentState.showEditor && modifierMask === (CTRL | CMD)) { undo(uneditStuff); break; }
+ if (persistentState.showEditor && modifierMask === (CTRL | CMD | SHIFT)) { redo(uneditStuff); break; }
+ return;
+ case "Y".charCodeAt(0):
+ if (modifierMask === 0 && !replayString) { redo(unmoveStuff); break; }
+ if (modifierMask === 0 && replayString) { advance(); break; }
+ if (persistentState.showEditor && modifierMask === (CTRL | CMD)) { redo(uneditStuff); break; }
+ return;
+ case "R".charCodeAt(0):
+ if (persistentState.showEditor && modifierMask === SHIFT) { setPaintBrushTileCode(RAINBOW); break; }
+ if (persistentState.showEditor && modifierMask === CTRL) { setPaintBrushTileCode("resize"); break; }
+ if (modifierMask === 0) { reset(unmoveStuff); break; }
+ if (modifierMask === SHIFT) { unreset(unmoveStuff); break; }
+ return;
+ case 220: // backslash
+ if (modifierMask === 0) {
+ if (dirtyState != EDITOR_DIRTY) { openEditorButton(); break; }
+ else {
+ if (confirm("Hide editor and cancel changes without saving?\n\nNote: To avoid seeing this prompt in the future, save changes before closing the editor")) {
+ openEditorButton();
+ location.reload();
+ }
+ break;
+ }
+ }
+ return;
+ case "A".charCodeAt(0):
+ if (!persistentState.showEditor && modifierMask === 0) { replayString = false; move(0, -1); break; }
+ if (persistentState.showEditor && modifierMask === SHIFT) { setPaintBrushTileCode("select"); break; }
+ if (persistentState.showEditor && modifierMask === (CTRL | CMD)) { selectAll(); break; }
+ return;
+ case "E".charCodeAt(0):
+ if (persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(SPACE); break; }
+ if (persistentState.showEditor && modifierMask === SHIFT) { setPaintBrushTileCode(EXIT); break; }
+ return;
+ case 46: // delete
+ if (persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(SPACE); break; }
+ return;
+ case "W".charCodeAt(0):
+ if (!persistentState.showEditor && modifierMask === 0) { replayString = false; move(-1, 0); break; }
+ if (persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(WALL); break; }
+ if (persistentState.showEditor && modifierMask === SHIFT) { setPaintBrushTileCode(WATER); break; }
+ return;
+ case "S".charCodeAt(0):
+ if (!persistentState.showEditor && modifierMask === 0) { replayString = false; move(1, 0); break; }
+ if (persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(SPIKE); break; }
+ if (persistentState.showEditor && modifierMask === SHIFT) { setPaintBrushTileCode(SNAKE); break; }
+ if (persistentState.showEditor && modifierMask === (CTRL | CMD)) { saveLevel(); break; }
+ if (!persistentState.showEditor && modifierMask === (CTRL | CMD)) { saveReplay(); break; }
+ if (modifierMask === (CTRL | SHIFT)) { saveReplay(); break; }
+ return;
+ case "X".charCodeAt(0):
+ if (persistentState.showEditor && modifierMask === (CTRL | CMD)) { cutSelection(); break; }
+ if (persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(CLOSEDLIFT); break; }
+ if (persistentState.showEditor && modifierMask === SHIFT) { setPaintBrushTileCode(OPENLIFT); break; }
+ return;
+ case "F".charCodeAt(0):
+ if (persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(FRUIT); break; }
+ if (persistentState.showEditor && modifierMask === SHIFT) { setPaintBrushTileCode(POISONFRUIT); break; }
+ return;
+ case "D".charCodeAt(0):
+ if (!persistentState.showEditor && modifierMask === 0) { replayString = false; move(0, 1); break; }
+ return;
+ case "B".charCodeAt(0):
+ if (persistentState.showEditor && modifierMask === 0 && !splockIsActive) { setPaintBrushTileCode(BLOCK); break; }
+ if (persistentState.showEditor && modifierMask === SHIFT) { setPaintBrushTileCode(BUBBLE); break; }
+ if (persistentState.showEditor && modifierMask === 0 && paintBrushTileCode === BLOCK && blockIsInFocus && splockIsActive) { toggleSplockButton(); break; }
+ return;
+ case "P".charCodeAt(0):
+ if (!persistentState.showEditor && modifierMask === 0) { replayString = false; move(-1, 0); break; }
+ if (persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(PORTAL); break; }
+ return;
+ case "U".charCodeAt(0):
+ if (!persistentState.showEditor && modifierMask === 0) { replayString = false; move(-1, 0); break; }
+ return;
+ case "L".charCodeAt(0):
+ if (!persistentState.showEditor && modifierMask === 0) { replayString = false; move(-1, 0); break; }
+ if (persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(LAVA); break; }
+ if (persistentState.showEditor && modifierMask === SHIFT) { setPaintBrushTileCode(WATER); 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 === 0) { setPaintBrushTileCode(CLOUD); break; }
+ if (persistentState.showEditor && modifierMask === SHIFT) { toggleCollision(); break; }
+ if (persistentState.showEditor && modifierMask === (CTRL | CMD)) { copySelection(); break; }
+ return;
+ case "V".charCodeAt(0):
+ if (persistentState.showEditor && modifierMask === (CTRL | CMD)) { setPaintBrushTileCode("paste"); break; }
+ return;
+ case "H".charCodeAt(0):
+ if (persistentState.showEditor && modifierMask === 0) { toggleHotkeys(); break; }
+ return;
+ case "T".charCodeAt(0):
+ if (persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(TRELLIS); break; }
+ if ((!persistentState.showEditor) || (persistentState.showEditor && modifierMask === SHIFT)) { toggleTheme(); break; }
+ return;
+ case "O".charCodeAt(0):
+ if (persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode([ONEWAYWALLU, ONEWAYWALLD, ONEWAYWALLL, ONEWAYWALLR]); break; }
+ return;
+ case "M".charCodeAt(0):
+ if (persistentState.showEditor && modifierMask === 0) { setPaintBrushTileCode(MIKE); break; }
+ return;
+ case 190:
+ openEditorButtonLocation(true);
+ return;
+ case 53: // 5
+ if (modifierMask === 0) { highlightSnakes(); break; }
+ case 54: // 6
+ if (modifierMask === 0) { highlightFruits(); break; }
+ case 55: // 7
+ if (modifierMask === 0) { highlightPoisonFruits(); break; }
+ case 56: // 8
+ if (modifierMask === 0) { clearHighlights(); break; }
+ case 57: // 9
+ if (modifierMask === 0) { fitCanvas(0); break; }
+ case 191:
+ if (modifierMask === 0) { if (multiDiagrams) { cycle = true; cycleId++; render(); } break; }
+ case 13:
+ if (modifierMask === 0 && !replayString) { redo(unmoveStuff); break; }
+ if (modifierMask === 0 && replayString) { advance(); break; }
+ case 32: // spacebar
+ if (persistentState.showEditor && modifierMask === 0 && paintBrushTileCode === BLOCK && blockIsInFocus) { toggleSplockButton(); break; }
+ if (modifierMask === 0) { switchSnakes(1); break; }
+ if (modifierMask === SHIFT) { switchSnakes(-1); break; }
+ return;
+ 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;
+ }
+ }
+ else if (!cs2) advanceAll();
+
+ event.preventDefault();
+ render();
+});
+function changeCanvasSize(delta) {
+ if (delta !== 34) tileSize += delta;
+ else tileSize = 34;
+ recalculateBorderRadius();
+ recalculateBlockRadius();
+ textStyle.fontSize = tileSize * 5;
+ blockRenderCache = {};
+ mikeRenderCache = {};
+
+ drawStaticCanvases(getLevel());
+ resizeCanvasContainer();
+ 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("arrowUp").addEventListener("click", function () {
+ replayString = false;
+ move(-1, 0);
+ return;
+});
+document.getElementById("arrowDown").addEventListener("click", function () {
+ replayString = false;
+ move(1, 0);
+ return;
+});
+document.getElementById("arrowLeft").addEventListener("click", function () {
+ replayString = false;
+ move(0, -1);
+ return;
+});
+document.getElementById("arrowRight").addEventListener("click", function () {
+ replayString = false;
+ move(0, 1);
+ return;
+});
+document.getElementById("minus").addEventListener("click", function () {
+ changeCanvasSize(-2);
+ return;
+});
+document.getElementById("plus").addEventListener("click", function () {
+ changeCanvasSize(2);
+ return;
+});
+document.getElementById("fitControls").addEventListener("click", function () {
+ fitCanvas(1);
+ return;
+});
+document.getElementById("fitCanvas").addEventListener("click", function () {
+ fitCanvas(0);
+ return;
+});
+document.getElementById("fitControlsDefault").addEventListener("click", function () {
+ document.getElementById("fitCanvasDefault").checked = false;
+ localStorage.setItem("cachedFitDefault", "fitControls");
+});
+document.getElementById("fitCanvasDefault").addEventListener("click", function () {
+ document.getElementById("fitControlsDefault").checked = false;
+ localStorage.setItem("cachedFitDefault", "fitCanvas");
+});
+document.getElementById("paintSplockButton").addEventListener("click", function () {
+ toggleSplockButton();
+});
+document.getElementById("highlightSnakesButton").addEventListener("click", function () {
+ highlightSnakes();
+});
+document.getElementById("highlightFruitsButton").addEventListener("click", function () {
+ highlightFruits();
+});
+document.getElementById("highlightPoisonFruitsButton").addEventListener("click", function () {
+ highlightPoisonFruits();
+});
+document.getElementById("clearHighlightsButton").addEventListener("click", function () {
+ clearHighlights();
+});
+document.getElementById("showGridButton").addEventListener("click", function () {
+ toggleGrid();
+});
+document.getElementById("openEditorButton").addEventListener("click", function () {
+ openEditorButton();
+});
+document.getElementById("closeEditorButton").addEventListener("click", function () {
+ openEditorButton();
+});
+document.getElementById("hideHotkeyButton").addEventListener("click", function () {
+ toggleHotkeys();
+});
+document.getElementById("saveProgressButton").addEventListener("click", function () {
+ saveReplay();
+});
+document.getElementById("copySVButton").addEventListener("click", function () {
+ redoAll(unmoveStuff);
+});
+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("animationSlider").addEventListener("click", function () {
+ animationsOn = document.getElementById("animationSlider").checked;
+ localStorage.setItem("cachedAO", animationsOn);
+});
+document.getElementById("defaultSlider").addEventListener("click", function () {
+ defaultOn = document.getElementById("defaultSlider").checked;
+ localStorage.setItem("cachedDO", defaultOn);
+ if (defaultOn && persistentState.showEditor) document.getElementById("animationSlider").checked = false;
+ animationsOn = false;
+});
+document.getElementById("replayAnimationSlider").addEventListener("click", function () {
+ replayAnimationsOn = document.getElementById("replayAnimationSlider").checked;
+ localStorage.setItem("cachedRAO", replayAnimationsOn);
+});
+document.getElementById("blockSlider").addEventListener("click", function () {
+ blockFixOn = document.getElementById("blockSlider").checked;
+ localStorage.setItem("cachedBFO", blockFixOn);
+ // if (blockFixOn && persistentState.showEditor) document.getElementById("animationSlider").checked = false;
+ // animationsOn = false;
+});
+$(document).ready(function () {
+ $("body").on("click", "#ghostEditorPane", function () {
+ openEditorButtonLocation(true);
+ });
+});
+function resizeCanvasContainer(cc) {
+ cc = document.getElementById("canvasContainer");
+ cc.style.width = tileSize * level.width;
+ cc.style.height = tileSize * level.height;
+}
+function resetCanvases() {
+ ["canvas1", "canvas2", "canvas3", "canvas4", "canvas5", "canvas6"].forEach(function (id) {
+ var canvas = document.getElementById(id);
+ var context = canvas.getContext("2d");
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ });
+ if (!loadFromLocationHash()) {
+ loadLevel(parseLevel(exampleLevel));
+ }
+ loadFromLocationHash();
+ return;
+}
+function highlightSnakes() {
+ if (document.getElementById("highlightSnakesButton").disabled === false) {
+ persistentState.highlightSnakes = !persistentState.highlightSnakes;
+ var context = canvas7.getContext("2d");
+ var button = document.getElementById("highlightSnakesButton");
+ if (persistentState.highlightSnakes) {
+ button.style.color = "white";
+ button.style.backgroundImage = "linear-gradient(#4b91ff, #055ce4)";
+ document.getElementById("clearHighlightsButton").disabled = false;
+
+ if (!persistentState.highlightFruits && !persistentState.highlightPoisonFruits) {
+ canvas7.style.display = "block";
+ context.fillStyle = "rgba(0,0,0,.8)";
+ context.fillRect(0, 0, level.width * tileSize, level.height * tileSize);
+ }
+
+ var snakes = getSnakes();
+ snakes.forEach(function (snake) {
+ drawObject(context, snake);
+ });
+ }
+ else {
+ button.style.color = "";
+ button.style.backgroundImage = "";
+ context.clearRect(0, 0, level.width * tileSize, level.height * tileSize);
+ canvas7.style.display = "none";
+ if (persistentState.highlightFruits) {
+ persistentState.highlightFruits = !persistentState.highlightFruits;
+ highlightFruits();
+ }
+ else document.getElementById("clearHighlightsButton").disabled = true;
+ if (persistentState.highlightPoisonFruits) {
+ persistentState.highlightPoisonFruits = !persistentState.highlightPoisonFruits;
+ highlightPoisonFruits();
+ }
+ else document.getElementById("clearHighlightsButton").disabled = true;
+ }
+ }
+}
+function highlightFruits() {
+ if (document.getElementById("highlightFruitsButton").disabled === false) {
+ persistentState.highlightFruits = !persistentState.highlightFruits;
+ var context = canvas7.getContext("2d");
+ var button = document.getElementById("highlightFruitsButton");
+ if (persistentState.highlightFruits) {
+ button.style.color = "white";
+ button.style.backgroundImage = "linear-gradient(#4b91ff, #055ce4)";
+ document.getElementById("clearHighlightsButton").disabled = false;
+
+ if (!persistentState.highlightSnakes && !persistentState.highlightPoisonFruits) {
+ canvas7.style.display = "block";
+ context.fillStyle = "rgba(0,0,0,.8)";
+ context.fillRect(0, 0, level.width * tileSize, level.height * tileSize);
+ }
+
+ var fruits = getObjectsOfType(FRUIT);
+ var counter = 0;
+ var eatenCounter = 0;
+ fruitLog.forEach(function (fruit) {
+ if (fruits.includes(fruit[0])) drawObject(context, fruit[0]);
+ else {
+ eatenCounter++;
+ drawFruit(context, fruit[0], false, null, true);
+ }
+
+ var rowcol = getRowcol(level, fruit[0].locations[0]);
+ var fontSize = tileSize / 2;
+ context.font = fontSize + "px Trebuchet MS";
+ context.fillStyle = "black";
+ context.textAlign = "center";
+ context.textBaseline = "middle";
+ context.fillText(fruitLog[counter][1], rowcol.c * tileSize + tileSize / 2, rowcol.r * tileSize + tileSize / 2.1);
+ counter++;
+ });
+ var eatenText = "Eaten: " + eatenCounter;
+ var remainingText = "Remaining: " + fruits.length;
+ context.fillStyle = "rgba(255,255,255,.5)";
+ context.textAlign = "left";
+ context.textBaseline = "middle";
+
+ var fontSize = tileSize;
+ context.font = fontSize + "px Trebuchet MS";
+ context.fillText("FRUIT", tileSize / 2, tileSize * .8);
+
+ var fontSize = tileSize / 2;
+ context.font = fontSize + "px Trebuchet MS";
+ context.fillText(eatenText, tileSize / 2, tileSize * 1.6);
+ context.fillText(remainingText, tileSize / 2, tileSize * 2.2);
+ }
+ else {
+ button.style.color = "";
+ button.style.backgroundImage = "";
+ context.clearRect(0, 0, level.width * tileSize, level.height * tileSize);
+ canvas7.style.display = "none";
+ if (persistentState.highlightSnakes) {
+ persistentState.highlightSnakes = !persistentState.highlightSnakes;
+ highlightSnakes();
+ }
+ else document.getElementById("clearHighlightsButton").disabled = true;
+ if (persistentState.highlightPoisonFruits) {
+ persistentState.highlightPoisonFruits = !persistentState.highlightPoisonFruits;
+ highlightPoisonFruits();
+ }
+ else document.getElementById("clearHighlightsButton").disabled = true;
+ }
+ }
+}
+function highlightPoisonFruits() {
+ if (document.getElementById("highlightPoisonFruitsButton").disabled === false) {
+ persistentState.highlightPoisonFruits = !persistentState.highlightPoisonFruits;
+ var context = canvas7.getContext("2d");
+ var button = document.getElementById("highlightPoisonFruitsButton");
+ if (persistentState.highlightPoisonFruits) {
+ button.style.color = "white";
+ button.style.backgroundImage = "linear-gradient(#4b91ff, #055ce4)";
+ document.getElementById("clearHighlightsButton").disabled = false;
+
+ if (!persistentState.highlightSnakes && !persistentState.highlightFruits) {
+ canvas7.style.display = "block";
+ context.fillStyle = "rgba(0,0,0,.8)";
+ context.fillRect(0, 0, level.width * tileSize, level.height * tileSize);
+ }
+
+ var fruits = getObjectsOfType(POISONFRUIT);
+ var counter = 0;
+ var eatenCounter = 0;
+ var tempRng = new Math.seedrandom("b");
+ poisonFruitLog.forEach(function (fruit) {
+ if (fruits.includes(fruit[0])) drawObject(context, fruit[0], tempRng);
+ else {
+ eatenCounter++;
+ drawFruit(context, fruit[0], true, tempRng, true);
+ }
+
+ var rowcol = getRowcol(level, fruit[0].locations[0]);
+ var fontSize = tileSize / 2;
+ context.font = fontSize + "px Trebuchet MS";
+ context.fillStyle = "black";
+ context.textAlign = "center";
+ context.textBaseline = "middle";
+ context.fillText(poisonFruitLog[counter][1], rowcol.c * tileSize + tileSize / 2, rowcol.r * tileSize + tileSize / 2.1);
+ counter++;
+ });
+ var eatenText = "Eaten: " + eatenCounter;
+ var remainingText = "Remaining: " + fruits.length;
+ context.fillStyle = "rgba(255,255,255,.5)";
+ context.textAlign = "right";
+ context.textBaseline = "middle";
+
+ var fontSize = tileSize;
+ context.font = fontSize + "px Trebuchet MS";
+ context.fillText("POISON FRUIT", level.width * tileSize - tileSize / 2, tileSize * .8);
+
+ var fontSize = tileSize / 2;
+ context.font = fontSize + "px Trebuchet MS";
+ context.fillText(eatenText, level.width * tileSize - tileSize / 2, tileSize * 1.6);
+ context.fillText(remainingText, level.width * tileSize - tileSize / 2, tileSize * 2.2);
+ }
+ else {
+ button.style.color = "";
+ button.style.backgroundImage = "";
+ context.clearRect(0, 0, level.width * tileSize, level.height * tileSize);
+ canvas7.style.display = "none";
+ if (persistentState.highlightSnakes) {
+ persistentState.highlightSnakes = !persistentState.highlightSnakes;
+ highlightSnakes();
+ }
+ else document.getElementById("clearHighlightsButton").disabled = true;
+ if (persistentState.highlightFruits) {
+ persistentState.highlightFruits = !persistentState.highlightFruits;
+ highlightFruits();
+ }
+ else document.getElementById("clearHighlightsButton").disabled = true;
+ }
+ }
+}
+function clearHighlights() {
+ var button1 = document.getElementById("highlightSnakesButton");
+ var button2 = document.getElementById("highlightFruitsButton");
+ var button3 = document.getElementById("highlightPoisonFruitsButton");
+ [button1, button2, button3].forEach(function (button) {
+ button.style.color = "";
+ button.style.backgroundImage = "";
+ });
+
+ var context = canvas7.getContext("2d");
+ context.clearRect(0, 0, level.width * tileSize, level.height * tileSize);
+ canvas7.style.display = "none";
+ persistentState.highlightSnakes = false;
+ persistentState.highlightFruits = false;
+ persistentState.highlightPoisonFruits = false;
+ document.getElementById("clearHighlightsButton").disabled = true;
+}
+function fitCanvas(type) {
+ clearHighlights();
+ var offset = 0;
+ switch (type) {
+ case 0: offset = 0; break;
+ case 1: offset = document.getElementById("bottomBlock").offsetHeight + 20; break;
+ case 2: offset = document.getElementById("csText").offsetHeight + 50; break;
+ }
+ var maxW = window.innerWidth / level.width;
+ var maxH = (window.innerHeight - offset) / level.height;
+ tileSize = Math.round(Math.min(maxW, maxH) * .97);
+ recalculateBorderRadius();
+ recalculateBlockRadius();
+ textStyle.fontSize = tileSize * 5;
+ blockRenderCache = {};
+ mikeRenderCache = {};
+ drawStaticCanvases(getLevel());
+ render();
+ // location.reload(); //without this, tiles appear to have borders (comment added before static canvases added)
+}
+function toggleSplockButton() {
+ splockIsActive = !splockIsActive;
+ var button = document.getElementById("paintSplockButton");
+ button.disabled = splockIsActive ? false : true;
+ if (splockIsActive) {
+ button.style.background = "linear-gradient(#4b91ff, #055ce4)";
+ button.style.color = "white";
+ }
+ else {
+ button.style.background = "";
+ button.style.color = "";
+ }
+}
+function recalculateBorderRadius() {
+ borderRadius = tileSize / borderRadiusFactor;
+}
+function recalculateBlockRadius() {
+ blockRadius = tileSize / blockRadiusFactor;
+}
+function openEditorButton() {
+ persistentState.showEditor = !persistentState.showEditor;
+ savePersistentState();
+ showEditorChanged();
+ // resetCanvases();
+ // resizeCanvasContainer();
+}
+function openEditorButtonLocation(clicked, cached) {
+ cached = JSON.parse(cached);
+ if (clicked) {
+ persistentState.editorLeft = !persistentState.editorLeft;
+ localStorage.setItem("editorLocation", persistentState.editorLeft);
+ } else {
+ if (cached == undefined) cached = false;
+ else persistentState.editorLeft = cached ? true : false;
+ }
+ savePersistentState();
+
+ // get row, editor, and ghost editor
+ var levelRow = document.getElementById("levelRow");
+ var ghostEditorPane = document.getElementById("ghostEditorPane");
+ var editorPane = document.getElementById("editorPane");
+
+ // clone editor and ghost editor
+ var ghostEditorPaneClone = ghostEditorPane.cloneNode(true);
+ var editorPaneClone = editorPane.cloneNode(true);
+
+ // remove original editor and ghost editor
+ document.getElementById("ghostEditorPane").remove();
+ document.getElementById("editorPane").remove();
+
+ // determine the order to rearrange them in
+ var td1 = persistentState.editorLeft ? editorPaneClone : ghostEditorPaneClone;
+ var td2 = persistentState.editorLeft ? ghostEditorPaneClone : editorPaneClone;
+
+ // insert
+ levelRow.insertBefore(td1, levelRow.childNodes[0]);
+ levelRow.appendChild(td2);
+}
+function toggleGrid() {
+ persistentState.showGrid = !persistentState.showGrid;
+ savePersistentState();
+ render();
+}
+function toggleHotkeys() {
+ persistentState.hideHotkeys = !persistentState.hideHotkeys;
+ savePersistentState();
+ var hotkeys = document.getElementsByClassName("hotkey");
+ var spacers = document.getElementsByClassName("hotkeySpacer");
+ var spacers2 = document.getElementsByClassName("hotkeySpacer2");
+ for (var i = 0; i < hotkeys.length; i++) {
+ if (persistentState.hideHotkeys) hotkeys[i].style.display = "none";
+ else hotkeys[i].style.display = "block";
+ }
+ for (var i = 0; i < spacers.length; i++) {
+ if (persistentState.hideHotkeys) spacers[i].style.height = "10px";
+ else spacers[i].style.height = "1px";
+ }
+ for (var i = 0; i < spacers2.length; i++) {
+ if (persistentState.hideHotkeys) spacers2[i].style.height = "0";
+ else spacers2[i].style.height = "3px";
+ }
+ render();
+}
+
+["serializationTextarea", "shareLinkTextbox", "link2Textbox"].forEach(function (id) {
+ document.getElementById(id).addEventListener("keydown", function (event) {
+ // let things work normally
+ event.stopPropagation();
+ });
+});
+document.getElementById("submitSerializationButton").addEventListener("click", function () {
+ var newLevel = getLevel();
+ loadLevel(newLevel);
+});
+function getLevel() {
+ var string = document.getElementById("serializationTextarea").value;
+ try {
+ var level = parseLevel(string);
+ } catch (e) {
+ alert(e);
+ return;
+ }
+ return level;
+}
+document.getElementById("shareLinkTextbox").addEventListener("focus", function () {
+ setTimeout(function () {
+ document.getElementById("shareLinkTextbox").select();
+ }, 0);
+});
+document.getElementById("link2Textbox").addEventListener("focus", function () {
+ setTimeout(function () {
+ document.getElementById("link2Textbox").select();
+ }, 0);
+});
+
+var paintBrushTileCode = null;
+var splockIsActive = false;
+var blockIsInFocus = false;
+var paintBrushSnakeColorIndex = 0;
+var paintBrushBlockId = 0;
+var paintBrushMikeId = 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],
+ ["paintPortalButton", PORTAL],
+ ["paintRainbowButton", RAINBOW],
+ ["paintTrellisButton", TRELLIS],
+ ["paintOneWayWallButton", [ONEWAYWALLU, ONEWAYWALLD, ONEWAYWALLL, ONEWAYWALLR]],
+ ["paintClosedLiftButton", CLOSEDLIFT],
+ ["paintOpenLiftButton", OPENLIFT],
+ ["paintCloudButton", CLOUD],
+ ["paintBubbleButton", BUBBLE],
+ ["paintLavaButton", LAVA],
+ ["paintWaterButton", WATER],
+ ["paintSnakeButton", SNAKE],
+ ["paintBlockButton", BLOCK],
+ ["paintMikeButton", MIKE],
+ ["paintFruitButton", FRUIT],
+ ["paintPoisonFruitButton", POISONFRUIT],
+];
+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("themeButton").addEventListener("click", function () {
+ toggleTheme();
+});
+function toggleTheme(theme) {
+ (themeCounter < themes.length - 1) ? themeCounter++ : themeCounter = 0;
+ localStorage.setItem("cachedTheme", themeCounter);
+ if (theme != undefined) themeCounter = theme;
+ blockRenderCache = [];
+ mikeRenderCache = [];
+ document.getElementById("themeButton").innerHTML = "Theme: " + themes[themeCounter][0] + "";
+ drawStaticCanvases(getLevel());
+}
+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 ? "" : "red";
+
+ document.getElementById("cheatCollisionButton").textContent = isCollisionEnabled ? "Collision: ON" : "Collision: OFF";
+ document.getElementById("cheatCollisionButton").style.background = isCollisionEnabled ? "" : "red";
+}
+
+// be careful with location vs rowcol, because this variable is used when resizing
+var lastDraggingRowcol = null;
+var hoverLocation = null;
+var draggingChangeLog = null;
+canvas4.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();
+ }
+});
+canvas4.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 === MIKE) {
+ // edit this particular mike
+ paintBrushTileCode = MIKE;
+ paintBrushMikeId = object.id;
+ } else if (object.type === FRUIT) {
+ // edit fruits, i guess
+ paintBrushTileCode = FRUIT;
+ } else if (object.type === POISONFRUIT) {
+ // edit poison fruits, i guess
+ paintBrushTileCode = POISONFRUIT;
+ }
+ 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;
+ }
+}
+canvas4.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();
+ }
+ }
+});
+canvas4.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, canvas4) / tileSize);
+ var c = Math.floor(eventToMouseX(event, canvas4) / tileSize);
+ // since the canvas4 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, canvas4) { return event.clientX - canvas4.getBoundingClientRect().left; }
+function eventToMouseY(event, canvas4) { return event.clientY - canvas4.getBoundingClientRect().top; }
+
+function selectAll() {
+ selectionStart = 0;
+ selectionEnd = level.map.length - 1;
+ setPaintBrushTileCode("select");
+}
+
+function setPaintBrushTileCode(tileCode) {
+ if (tileCode !== MIKE && tileCode !== BLOCK) {
+ blockIsInFocus = false;
+ splockIsActive = false;
+ }
+
+ if (tileCode === "paste" && 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) {
+ if (!splockIsActive) {
+ var blocks = getBlocks();
+ if (paintBrushTileCode === BLOCK && blocks.length > 0) {
+ // cycle through block ids
+ blocks.sort(compareId);
+ if (paintBrushBlockId != null) {
+ blockIsInFocus = true;
+ (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
+ blockIsInFocus = true;
+ paintBrushBlockId = blocks[0].id;
+ }
+ } else {
+ // new block id
+ paintBrushBlockId = null;
+ }
+ }
+ else {
+ splockIsActive = false;
+ toggleSplockButton();
+ }
+ } else if (tileCode === MIKE) {
+ var mikes = getMikes();
+ if (paintBrushTileCode === MIKE && mikes.length > 0) {
+ // cycle through mikes ids
+ mikes.sort(compareId);
+ if (paintBrushMikeId != null) {
+ (function () {
+ for (var i = 0; i < mikes.length; i++) {
+ if (mikes[i].id === paintBrushMikeId) {
+ i += 1;
+ if (i < mikes.length) {
+ // next mikes id
+ paintBrushMikeId = mikes[i].id;
+ } else {
+ // new mikes id
+ paintBrushMikeId = null;
+ }
+ return;
+ }
+ }
+ throw unreachable()
+ })();
+ } else {
+ // first one
+ paintBrushMikeId = mikes[0].id;
+ }
+ } else {
+ // new mikes id
+ paintBrushMikeId = null;
+ }
+ } else if (Array.isArray(tileCode)) {
+ if (paintBrushTileCode === tileCode[OWWCounter]) {
+ OWWCounter++;
+ if (OWWCounter > 3) OWWCounter = 0;
+ }
+ } else if (tileCode == null) {
+ // escape
+ if (paintBrushTileCode === BLOCK && paintBrushBlockId != null) {
+ // stop editing this block, but keep the block brush selected
+ tileCode = BLOCK;
+ paintBrushBlockId = null;
+ }
+ if (paintBrushTileCode === MIKE && paintBrushMikeId != null) {
+ // stop editing this mike, but keep the mike brush selected
+ tileCode = MIKE;
+ paintBrushMikeId = null;
+ }
+ }
+ paintBrushTileCode = Array.isArray(tileCode) ? tileCode[OWWCounter] : tileCode;
+ paintBrushTileCodeChanged();
+}
+function paintBrushTileCodeChanged() {
+ paintButtonIdAndTileCodes.forEach(function (pair) {
+ var id = pair[0];
+ var tileCode = pair[1];
+ var backgroundStyle = "";
+ var textColor = "";
+ if (Array.isArray(tileCode)) {
+ var direction = tileCode[OWWCounter];
+ switch (direction) {
+ case 117: direction = "Up"; break;
+ case 100: direction = "Down"; break;
+ case 108: direction = "Left"; break;
+ case 114: direction = "Right"; break;
+ }
+ document.getElementById("paintOneWayWallButton").textContent = "One-Way " + direction;
+ }
+ if (tileCode === paintBrushTileCode || tileCode[OWWCounter] === paintBrushTileCode) {
+ if (tileCode === SNAKE) {
+ // show the color of the active snake in the color of the button
+ backgroundStyle = snakeColors[paintBrushSnakeColorIndex];
+ } else {
+ backgroundStyle = "linear-gradient(#4b91ff, #055ce4)";
+ textColor = "white";
+ }
+ }
+ document.getElementById(id).style.background = backgroundStyle;
+ document.getElementById(id).style.color = textColor;
+ });
+
+ 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) addIfAbsent(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) addIfAbsent(objects, object);
+ }
+ }
+ // select the rest of any partially-selected objects
+ objects.forEach(function (object) {
+ object.locations.forEach(function (location) {
+ addIfAbsent(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],
+ splocks: [] //unused
+ };
+}
+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],
+ splocks: []
+ };
+}
+function newMike(location) {
+ var mikes = getMikes();
+ mikes.sort(compareId);
+ for (var i = 0; i < mikes.length; i++) {
+ if (mikes[i].id !== i) break;
+ }
+ return {
+ type: MIKE,
+ id: i,
+ dead: false, // unused
+ locations: [location],
+ splocks: [] //unused
+ };
+}
+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],
+ splocks: [] //unused
+ };
+}
+function newPoisonFruit(location) {
+ var fruits = getObjectsOfType(POISONFRUIT);
+ fruits.sort(compareId);
+ for (var i = 0; i < fruits.length; i++) {
+ if (fruits[i].id !== i) break;
+ }
+ return {
+ type: POISONFRUIT,
+ id: i,
+ dead: false, // unused
+ locations: [location],
+ splocks: [] //unused
+ };
+}
+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);
+ if (dr !== 0 || dc !== 0) didResize = true;
+ } 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 === MIKE) {
+ object.id = newMike().id;
+ } else if (object.type === FRUIT) {
+ object.id = newFruit().id;
+ } else if (object.type === POISONFRUIT) {
+ object.id = newPoisonFruit().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) {
+ blockIsInFocus = true;
+ 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);
+ var existingSplockIndex = thisBlock.splocks.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);
+ if (splockIsActive) thisBlock.splocks.push(location);
+ }
+ } else if (existingSplockIndex !== -1) {
+ thisBlock.splocks.splice(existingSplockIndex, 1);
+ if (!splockIsActive) thisBlock.locations.push(location);
+ }
+ else {
+ if (!splockIsActive) {
+ // add a tile to the block
+ removeAnyObjectAtLocation(location, changeLog);
+ thisBlock.locations.push(location);
+ }
+ else {
+ //add a spike to the block
+ removeAnyObjectAtLocation(location, changeLog);
+ thisBlock.splocks.push(location);
+ }
+ }
+ }
+ changeLog.push([thisBlock.type, thisBlock.id, oldBlockSerialization, serializeObjectState(thisBlock)]);
+ delete blockRenderCache[thisBlock.id];
+ }
+ } else if (paintBrushTileCode === MIKE) {
+ var objectHere = findObjectAtLocation(location);
+ if (paintBrushMikeId == null && objectHere != null && objectHere.type === MIKE) {
+ // just start editing this mike
+ paintBrushMikeId = objectHere.id;
+ } else {
+ // make a change
+ // make sure there's space behind us
+ paintTileAtLocation(location, SPACE, changeLog);
+ var thisMike = null;
+ if (paintBrushMikeId != null) {
+ thisMike = findMikeById(paintBrushMikeId);
+ }
+ var oldMikeSerialization = serializeObjectState(thisMike);
+ if (thisMike == null) {
+ // create new mike
+ removeAnyObjectAtLocation(location, changeLog);
+ thisMike = newMike(location);
+ level.objects.push(thisMike);
+ paintBrushMikeId = thisMike.id;
+ } else {
+ var existingIndex = thisMike.locations.indexOf(location);
+ if (existingIndex !== -1) {
+ // reclicking part of this object means to delete just part of it.
+ if (thisMike.locations.length === 1) {
+ // goodbye
+ removeObject(thisMike, changeLog);
+ paintBrushMikeId = null;
+ } else {
+ thisMike.locations.splice(existingIndex, 1);
+ }
+ } else {
+ // add a tile to the mike
+ removeAnyObjectAtLocation(location, changeLog);
+ thisMike.locations.push(location);
+ }
+ }
+ changeLog.push([thisMike.type, thisMike.id, oldMikeSerialization, serializeObjectState(thisMike)]);
+ delete mikeRenderCache[thisMike.id];
+ }
+ } else if (paintBrushTileCode === FRUIT || paintBrushTileCode === POISONFRUIT) {
+ paintTileAtLocation(location, SPACE, changeLog);
+ removeAnyObjectAtLocation(location, changeLog);
+ var object = paintBrushTileCode == FRUIT ? newFruit(location) : newPoisonFruit(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] === MIKE || change[0] === FRUIT || change[0] === POISONFRUIT) {
+ 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) {
+ infiniteDeath = false;
+ canvas7.style.display = "none";
+ if (replayString) {
+ var expectedPrefix = replayMagicNumber + "&";
+ if (cursor > expectedPrefix.length) cursor--;
+ if (cursor == expectedPrefix.length) activeSnakeId = 0;
+ var c = replayString.charAt(cursor);
+
+ if ('0' <= c && c <= '9') {
+ var d = replayString.charAt(cursor - 1);
+ var e = replayString.charAt(cursor - 2);
+ if ('0' <= d && d <= '9') {
+ c = d + c;
+ cursor--;
+ cursorOffset--;
+ if ('0' <= e && e <= '9') {
+ c = e + c;
+ cursor--;
+ cursorOffset--;
+ }
+ }
+
+ // var previousSnakeChanges = switchSnakesArray.slice(0,);
+ var previousSnake = Math.max.apply(Math, switchSnakesArray.filter(function (x) { return x < cursor }));
+ if (previousSnake == expectedPrefix.length) activeSnakeId = 0;
+ else {
+ var snakeIdStr = replayString.charAt(previousSnake - 1);
+ activeSnakeId = parseInt(snakeIdStr);
+ }
+ cursor--;
+ cursorOffset--;
+ }
+ }
+
+ postPortalSnakeOutline = [];
+ portalConflicts = [];
+ portalOutOfBounds = false;
+ if (undoStuff.undoStack.length === 0) return; // already at the beginning
+ animationQueue = [];
+ animationQueueCursor = 0;
+ paradoxes = [];
+ undoOneFrame(undoStuff);
+ undoStuffChanged(undoStuff);
+}
+function reset(undoStuff) {
+ canvas7.style.display = "none";
+ cursor = 0;
+ portalFailure = false;
+ 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 redoAll(undoStuff) {
+ if (dirtyState === EDITOR_DIRTY) return alert("Can't save a replay with unsaved editor changes.");
+ // preserve the level in the url bar.
+ var hash = "#sv=" + currentSerializedLevel;
+ if (dirtyState === REPLAY_DIRTY) {
+ // there is a replay to save
+ hash += "#replay=" + compressSerialization(stringifyReplay());
+ }
+
+ var svURL = "https://jmdiamond3.github.io/Snakefall-Redesign/Framework.html" + hash;
+ copyToClipboard(svURL);
+}
+function advanceAll() {
+ cs2 = true;
+ context = canvas7.getContext("2d");
+ context.fillStyle = "rgba(0,0,0,.5)";
+ context.fillRect(0, 0, level.width * tileSize, level.height * tileSize);
+
+ context.fillStyle = "orange";
+ context.font = "100px Impact";
+ context.shadowOffsetX = 5;
+ context.shadowOffsetY = 5;
+ context.shadowColor = "rgba(0,0,0,0.5)";
+ context.shadowBlur = 4;
+ var textString = "Loading";
+ context.textBaseline = "middle";
+ var textWidth = context.measureText(textString).width;
+ context.fillText(textString, (canvas7.width / 2) - (textWidth / 2), canvas7.height / 2);
+
+ setTimeout(function () {
+ cs = true;
+ while (cursor < replayString.length) advance();
+ context.clearRect(0, 0, canvas7.width, canvas7.height);
+ if (checkResult) {
+ cr = true;
+ dont = true;
+ render();
+ }
+ else {
+ dont = true;
+ render();
+ }
+ }, 2000);
+
+}
+function copyToClipboard(text) {
+ var dummy = document.createElement("textarea");
+ // to avoid breaking orgain page when copying more words
+ // cant copy when adding below this code
+ // dummy.style.display = 'none'
+ document.body.appendChild(dummy);
+ //Be careful if you use texarea. setAttribute('value', value), which works with "input" does not work with "textarea". – Eduard
+ dummy.value = text;
+ dummy.select();
+ document.execCommand("copy");
+ document.body.removeChild(dummy);
+}
+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);
+ }
+ // can't get to here
+ 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] === MIKE || change[0] === FRUIT || change[0] === POISONFRUIT) {
+ // change object
+ blockRenderCache = {};
+ 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);
+ var fromSplocks = change[2][2].map(transformLocation);
+ var toSplocks = change[3][2].map(transformLocation);
+ if (fromLocations.filter(function (location) { return location >= level.map.length; }).length > 0) {
+ return "Can't move " + describe(type, id) + " out of bounds";
+ }
+ if (fromSplocks.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 || toSplocks.length !== 0) {
+ // should exist at this location
+ if (object == null) return "Can't move " + describe(type, id) + " because it doesn't exist";
+ if (!deepEquals(object.locations, toLocations)) return "Can't move " + describe(object) + " because it's in the wrong place";
+ if (!deepEquals(object.splocks, toSplocks)) 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 || fromSplocks.length !== 0) {
+ var oldState = serializeObjectState(object);
+ object.locations = fromLocations;
+ object.splocks = fromSplocks;
+ 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,
+ splocks: fromSplocks
+ };
+ 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";
+ case RAINBOW: return "a Rainbow";
+ case TRELLIS: return "a Trellis";
+ case ONEWAYWALLU: return "a One Way Wall (facing U)";
+ case ONEWAYWALLD: return "a One Way Wall (facing D)";
+ case ONEWAYWALLL: return "a One Way Wall (facing L)";
+ case ONEWAYWALLR: return "a One Way Wall (facing R)";
+ case CLOSEDLIFT: return "a Closed Lift";
+ case OPENLIFT: return "an Open Lift";
+ case CLOUD: return "a Cloud";
+ case BUBBLE: return "a Bubble";
+ case LAVA: return "Lava";
+ case WATER: return "Water";
+ 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 === MIKE) {
+ return "Mike " + arg2;
+ }
+ if (arg1 === FRUIT) {
+ return "Fruit";
+ }
+ if (arg1 === POISONFRUIT) {
+ return "Poison Fruit";
+ }
+ if (typeof arg1 === "object") return describe(arg1.type, arg1.id);
+ throw unreachable();
+}
+
+function undoStuffChanged(undoStuff) {
+ if (replayString) {
+ var expectedPrefix = replayMagicNumber + "&";
+ var pre = cursor - expectedPrefix.length - cursorOffset;
+ var post = replayLength - cursor + expectedPrefix.length + cursorOffset;
+ var movesText = pre + "\xa0\xa0✾\xa0\xa0" + post;
+ document.getElementById("movesSpan").textContent = movesText;
+ } else {
+ var movesText = undoStuff.undoStack.length + "\xa0\xa0✾\xa0\xa0" + 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");
+ } else {
+ saveLevelButton.classList.remove("click-me");
+ }
+
+ 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,
+ editorLeft: false,
+ showGrid: false,
+ hideHotkeys: false,
+ highlightSnakes: false,
+ highlightFruits: false,
+ highlightPoisonFruits: false
+};
+function savePersistentState() {
+ localStorage.snakefall = JSON.stringify(persistentState);
+}
+function loadPersistentState() {
+ try {
+ persistentState = JSON.parse(localStorage.snakefall);
+ } catch (e) {
+ }
+ persistentState.showEditor = !!persistentState.showEditor;
+ persistentState.editorLeft = !!persistentState.editorLeft;
+ persistentState.showGrid = !!persistentState.showGrid;
+ persistentState.hideHotkeys = !!persistentState.hideHotkeys;
+ persistentState.highlightSnakes = !!persistentState.highlightSnakes;
+ persistentState.highlightFruits = !!persistentState.highlightFruits;
+ persistentState.highlightPoisonFruits = !!persistentState.highlightPoisonFruits;
+ 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, wall, snakeColors, blockColors, spikeColors, fruitColors, textStyle, experimentalColors;
+
+var bg1 = ["fade", "rgba(145, 198, 254", "rgba(133, 192, 255"]; // 91c6fe
+var bg2 = ["fade", "rgba(254, 198, 145", "rgba(255, 192, 133"];
+var bg3 = ["fade", "rgba(145, 254, 198", "rgba(117, 255, 192"];
+var bg4 = ["fade", "rgba(7, 7, 83", "rgba(0, 0, 70"];
+var bg5 = ["fade", "rgba(140, 190, 190", "rgba(135, 185, 185"];
+
+var wall1 = { base: "#976537", surface: "#96fe45", curvedWalls: true, surfaceShape: "grass", grass: true, flowers: true, baseSpots: true, randomColors: false };
+var wall2 = { base: "#30455B", surface: "white", curvedWalls: true, surfaceShape: "snow", grass: false, flowers: false, baseSpots: true, randomColors: false };
+var wall3 = { base: "#734d26", surface: "#009933", curvedWalls: true, surfaceShape: "grass", grass: true, flowers: true, baseSpots: true, randomColors: false };
+var wall4 = { base: "#844204", surface: "#282", curvedWalls: false, surfaceShape: "stripe", grass: false, flowers: false, baseSpots: false, randomColors: false };
+var wall5 = { base: "#00aaff", surface: "#ffb3ec", curvedWalls: true, surfaceShape: "grass", grass: false, flowers: false, baseSpots: false, randomColors: false };
+var wall6 = { base: "black", surface: "rainbow", curvedWalls: false, surfaceShape: "stripe", grass: false, flowers: false, baseSpots: false, randomColors: false };
+var wall7 = { base: "transparent", surface: "green", curvedWalls: false, surfaceShape: "algae", grass: true, flowers: false, baseSpots: false, randomColors: true };
+
+// must be full length to satisfy tint function
+var snakeColors1 = ["#fd0c0b", "#18d11f", "#004dff", "#fdc122"];
+var snakeColors2 = ["#ff0000", "#00ff00", "#0000ff", "#ffff00"];
+var snakeColors3 = ["#BA145C", "#E91624", "#F75802", "#FEFE28"];
+var snakeColors4 = ["#000000", "#000000", "#000000", "#000000"];
+
+var fruitColors1 = ["#ff0066", "#ff36a6", "#ff6b1f", "#ff9900", "#ff2600"];
+var fruitColors2 = ["black", "black", "black", "black", "black"];
+var fruitColors3 = ["#ffcc00", "#ffa700", "#ff9380", "#ff5439"];
+var fruitColors4 = ["#9900cc", "#6600cc", "#0033cc", "#0099cc", "#00cccc"];
+
+var spikeColors1 = { spokes: "#999", support: "#444", box: "#555", bolt: "#777" };
+var spikeColors2 = { spokes: "#888", support: "#111", box: "#333", bolt: "#111" };
+var spikeColors3 = { spokes: "#333", support: "#333", box: "#333", bolt: "#777" };
+var spikeColors4 = { spokes: "#595959", support: "#3b2f2b", box: "#3b2f2b", bolt: "#595959" };
+
+// must be 6-digit hex colors to satisfy tint function
+var blockColors1 = ["#de5a6d", "#fa65dd", "#c367e3", "#9c62fa", "#625ff0"];
+var blockColors2 = ["#00FFFB", "#66ffd9", "#00e673", "#bfff00", "#ecff00"];
+var blockColors3 = ["#de7913", "#7d46a0", "#39868b", "#41ccc2", "#ccc500"];
+var blockColors4 = ["#660050", "#990033", "#b35900", "#e6b800 ", "#008000"];
+var blockColors5 = ["#ffccff", "#ffc2b3", "#ffffcc", "#ccffe6", "#ccffff"];
+var blockColors6 = ["#bf4053", "#ec799f", "#a679d2", "#6a45a1", "#4d6dcb"];
+
+var fontSize = tileSize * 5;
+var textStyle1 = { fontSize: fontSize, fontFamily: "American Typewriter", win: "#fdc122", lose: "#fd0c0b" };
+var textStyle2 = { fontSize: fontSize, fontFamily: "American Typewriter", win: "#400080", lose: "#ff6600" };
+var textStyle3 = { fontSize: fontSize, fontFamily: "American Typewriter", win: "#BA145C", lose: "#F75802" };
+var textStyle4 = { fontSize: fontSize, fontFamily: "Arial", win: "#ff0", lose: "#f00" };
+var textStyle5 = { fontSize: fontSize, fontFamily: "American Typewriter", win: "#80FF00", lose: "#00FFFB" };
+
+var experimentalColors1 = ["white", "#ffccff"];
+var experimentalColors2 = ["white", "#FEFE28"];
+
+var themes = [ //name, background, wall, snakeColors, blockColors, spikeColors, fruitColors, stemColor, textStyle, experimentalColors
+ ["Spring", bg1, wall1, snakeColors1, blockColors6, spikeColors1, fruitColors1, "green", textStyle1, experimentalColors1],
+ ["Winter", bg1, wall2, snakeColors1, blockColors1, spikeColors3, fruitColors1, "green", textStyle1, experimentalColors1],
+ ["Summer", bg2, wall3, snakeColors3, blockColors3, spikeColors4, fruitColors1, "green", textStyle3, experimentalColors2],
+ ["Submerged", bg5, wall7, snakeColors1, blockColors2, spikeColors4, fruitColors4, "green", textStyle5, experimentalColors1],
+ // ["Dream", bg3, wall5, snakeColors1, blockColors4, spikeColors4, fruitColors2, "white", textStyle2, experimentalColors2],
+ ["Midnight Rainbow", bg4, wall6, snakeColors1, blockColors1, spikeColors2, "white", "white", textStyle1, experimentalColors1],
+ ["Classic", "#8888ff", wall4, snakeColors2, blockColors1, spikeColors3, fruitColors1, "green", textStyle4, experimentalColors1],
+];
+
+var themeCounter = 0;
+var cachedTheme = localStorage.getItem("cachedTheme");
+if (cachedTheme == null) themeCounter = 0;
+else themeCounter = cachedTheme;
+var themeName = themes[themeCounter][0];
+document.getElementById("themeButton").innerHTML = "Theme: " + themeName + "";
+function populateThemeVars() {
+ themeName = themes[themeCounter][0];
+ background = themes[themeCounter][1];
+ wall = themes[themeCounter][2];
+ snakeColors = themes[themeCounter][3];
+ blockColors = themes[themeCounter][4];
+ spikeColors = themes[themeCounter][5];
+ fruitColors = themes[themeCounter][6];
+ textStyle = themes[themeCounter][8];
+ experimentalColors = themes[themeCounter][9];
+}
+
+function showEditorChanged() {
+ ["editorDiv", "editorPane"].forEach(function (id) {
+ document.getElementById(id).style.display = persistentState.showEditor ? "inline-block" : "none";
+ });
+
+ ["canvas3", "canvas5", "canvas6"].forEach(function (canvas) {
+ document.getElementById(canvas).style.display = persistentState.showEditor ? "none" : "block";
+ });
+
+ document.getElementById("wasdSpan").textContent = persistentState.showEditor ? "" : " or WASD";
+ document.getElementById("hideHotkeyButton").disabled = persistentState.showEditor ? false : true;
+
+ if (persistentState.showEditor) {
+ document.getElementById("ghostEditorPane").style.display = "inline-block";
+ document.getElementById("openEditorButton").style.display = "none";
+ if (defaultOn) document.getElementById("animationSlider").checked = animationsOn = false;
+ else document.getElementById("animationSlider").checked = animationsOn = JSON.parse(localStorage.getItem("cachedAO"));
+ }
+ if (!persistentState.showEditor) {
+ document.getElementById("ghostEditorPane").style.display = "none";
+ document.getElementById("openEditorButton").style.display = "block";
+ document.getElementById("animationSlider").checked = animationsOn = JSON.parse(localStorage.getItem("cachedAO"));
+ }
+ render();
+}
+
+function move(dr, dc, doAnimations) {
+ clearHighlights();
+ if (!isDead()) newSpikeDeath = [];
+ lowDeath = false;
+ if (!isDead()) theseDyingLocations = [];
+
+ document.getElementById("cycleDiv").innerHTML = "";
+ postPortalSnakeOutline = [];
+ portalConflicts = [];
+ portalOutOfBounds = false;
+ portalFailure = false;
+ cycle = false;
+ cycleId = -1;
+ multiDiagrams = false;
+
+ 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 atePoison = false;
+ var pushedObjects = [];
+
+ //track ClosedLifts that had objects on them
+ var occupiedClosedLift = getOccupiedClosedLiftLocations();
+
+ if (isCollision()) {
+ var newTile = level.map[newLocation];
+ if (newTile === BUBBLE || newTile === CLOUD) {
+ paintTileAtLocation(newLocation, SPACE, changeLog);
+ }
+ else if (!isTileCodeAir(activeSnake, null, newTile, dr, dc)) 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 if (otherObject.type === POISONFRUIT) {
+ // eat poison
+ removeObject(otherObject, changeLog);
+ atePoison = true;
+ } else if (isTileCodeAir(activeSnake, null, newTile, dr, dc)) {
+ otherObject = findObjectAtLocation(newLocation);
+ if (otherObject != null) {
+ if (otherObject === activeSnake) return; // can't push yourself
+ if (otherObject.splocks.includes(newLocation) && !otherObject.locations.includes(newLocation)) return false; // can't push splock
+ if (otherObject.type === MIKE && otherObject.locations.includes(newLocation)) return false; // can't push mike
+ // push objects
+ if (!checkMovement(activeSnake, otherObject, dr, dc, pushedObjects)) return false;
+ }
+ } else return; // can't go through that tile
+ }
+ }
+
+ // slither forward
+ var activeSnakeOldState = serializeObjectState(activeSnake);
+ var snakeLength = activeSnake.locations.length;
+ var size1 = snakeLength === 1;
+ doAnimations = doAnimations == undefined && animationsOn ? true : false;
+ var speed = doAnimations ? 70 : 1;
+ var slitherAnimations = [
+ speed,
+ [
+ // size-1 snakes really do more of a move than a slither
+ size1 ? MOVE_SNAKE : SLITHER,
+ activeSnake.id,
+ dr,
+ dc,
+ ]
+ ];
+
+ // drag your tail forward based on what was/wasn't eaten
+ var times = 1;
+ if (ate) { times--; }
+ if (atePoison) { times++; }
+ //if we're going to shrink out of existence, prevent it
+ var poisonKill = false;
+ if (times > snakeLength) {
+ times = snakeLength;
+ activeSnake.dead = true;
+ //make the snake appear to vanish into non-existence
+ activeSnake.locations.unshift({ r: -99, c: -99 });
+ poisonKill = true;
+ }
+ for (var t = 0; t < times; ++t) {
+ for (var i = 1; i < snakeLength; i++) {
+ // drag your tail forward
+ var oldRowcol = getRowcol(level, activeSnake.locations[i]);
+ newRowcol = getRowcol(level, activeSnake.locations[i - 1]);
+ if (!size1) {
+ slitherAnimations.push(
+ [
+ SLITHER + i,
+ activeSnake.id,
+ newRowcol.r - oldRowcol.r,
+ newRowcol.c - oldRowcol.c,
+ ]
+ );
+ }
+ }
+ activeSnake.locations.pop();
+ }
+ if (!poisonKill) activeSnake.locations.unshift(newLocation);
+ 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);
+
+ occupiedClosedLift = combineOldAndNewLiftOccupations(occupiedClosedLift);
+
+ // 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]]);
+ infiniteDeath = true;
+ break;
+ } else {
+ stateToAnimationIndex[serializedState] = animationQueue.length;
+ }
+ // do portals separate from falling logic
+ if (portalActivationLocations.length === 1) {
+ var portalAnimations = [500];
+ var result = activatePortal(portalLocations, portalActivationLocations[0], portalAnimations, changeLog);
+ if (result === "works") {
+ portalFailure = false;
+ portalOutOfBounds = false;
+ animationQueue.push(portalAnimations);
+ } else if (result === "blocked") {
+ portalFailure = true;
+ portalOutOfBounds = false;
+ } else if (result === "outside") {
+ portalFailure = true;
+ portalOutOfBounds = true;
+ } else if (result === "blockedBySnake") {
+ portalFailure = false;
+ portalOutOfBounds = false;
+ }
+ portalActivationLocations = [];
+ }
+ // now do falling logic
+ var didAnything = false;
+ var snakeExited = 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) {
+ snakeExited = true;
+ var exitedSnake = snake;
+ // (one of) you made it!
+
+ // var snakeLength = snakeLength;
+ // var shrinkingSnake = snake;
+ // for (var t = 0; t < snakeLength; t++) {
+ // for (var i = 1; i < shrinkingSnake.locations.length; i++) {
+ // var oldRowcol = getRowcol(level, shrinkingSnake.locations[i]);
+ // newRowcol = getRowcol(level, shrinkingSnake.locations[i - 1]);
+ // if (!size1) {
+ // slitherAnimations.push(
+ // [
+ // SLITHER + i,
+ // snake.id,
+ // newRowcol.r - oldRowcol.r,
+ // newRowcol.c - oldRowcol.c,
+ // ]
+ // );
+ // }
+ // }
+ // shrinkingSnake.locations.pop();
+ // }
+ // alert(JSON.stringify(slitherAnimations));
+
+ removeAnimatedObject(snake, changeLog);
+ exitAnimationQueue.push([
+ 200,
+ [EXIT_SNAKE, snake.id, 0, 0],
+ ]);
+ didAnything = true;
+ }
+ }
+ }
+
+ occupiedClosedLift = combineOldAndNewLiftOccupations(occupiedClosedLift);
+
+ // fall
+ var dyingObjects = [];
+ var fallingObjects = level.objects.filter(function (object) {
+ if (object.type === FRUIT || object.type === POISONFRUIT) return; // can't fall
+ var theseDyingObjects = [];
+ if (!checkMovement(null, object, 1, 0, [], theseDyingObjects, theseDyingLocations)) return false;
+ // alert(JSON.stringify(theseDyingLocations));
+ // this object can fall. maybe more will fall with it too. we'll check those separately.
+ theseDyingObjects.forEach(function (object) {
+ addIfAbsent(dyingObjects, object);
+ });
+ // when falling with splocks, animation doesn't show fall because this code is never reached (works with blocks without splocks)
+ return true;
+ });
+
+ // this code doesn't work with splocks
+ 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 || object.type === MIKE) {
+ // 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;
+ }
+
+ occupiedClosedLift = openLift(occupiedClosedLift, changeLog);
+
+ if (!didAnything) break;
+ Array.prototype.push.apply(animationQueue, exitAnimationQueue);
+ if (fallingAnimations.length > 1) animationQueue.push(fallingAnimations);
+
+ // if (snakeExited) {
+ // if (exitedSnake.length > 0) move(dr, dc);
+ // // else removeAnimatedObject(exitedSnake, changeLog);
+ // exitedSnake.locations.pop();
+ // }
+ }
+
+ pushUndo(unmoveStuff, changeLog);
+ render();
+}
+
+function combineOldAndNewLiftOccupations(oldOccupiedClosedLift) {
+ var newOccupiedClosedLift = getOccupiedClosedLiftLocations();
+ var newlyOccupiedClosedLift = getSetSubtract(newOccupiedClosedLift, oldOccupiedClosedLift);
+ return oldOccupiedClosedLift.concat(newlyOccupiedClosedLift);
+}
+
+function openLift(oldOccupiedClosedLift, changeLog) {
+ var newOccupiedClosedLift = getOccupiedClosedLiftLocations();
+ var nowUnoccupiedClosedLift = getSetSubtract(oldOccupiedClosedLift, newOccupiedClosedLift);
+ for (var i = 0; i < nowUnoccupiedClosedLift.length; i++) {
+ paintTileAtLocation(nowUnoccupiedClosedLift[i], OPENLIFT, changeLog);
+ }
+ return newOccupiedClosedLift;
+}
+
+function getSetSubtract(array1, array2) {
+ if (array1.length === 0) return [];
+ return array1.filter(function (x) { return array2.indexOf(x) == -1; });
+}
+
+function checkMovement(pusher, pushedObject, dr, dc, pushedObjects, dyingObjects, dyingLocations) {
+ // pusher can be null (for gravity)
+ // pushedObjects include snake itself when making any move
+ // gravity pushes every object on every move
+ // find forward locations
+ pushedObjects.push(pushedObject);
+ var forwardLocations = [];
+ for (var i = 0; i < pushedObjects.length; i++) {
+ pushedObject = pushedObjects[i];
+ //splocks
+ for (var j = 0; j < pushedObject.splocks.length; j++) {
+ var rowcol = getRowcol(level, pushedObject.splocks[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
+ addIfAbsent(dyingObjects, pushedObject);
+ addIfAbsent(pushedObjects, pushedObject);
+ if (dyingLocations != undefined) addIfAbsent(dyingLocations, getLocation(level, rowcol.r, rowcol.c));
+ continue;
+ }
+ }
+ var forwardLocation = getLocation(level, forwardRowcol.r, forwardRowcol.c);
+ var yetAnotherObject = findObjectAtLocation(forwardLocation);
+ if (yetAnotherObject != null) {
+ if (yetAnotherObject.type === FRUIT || yetAnotherObject.type === POISONFRUIT) return false;
+ if (yetAnotherObject.type === SNAKE) {
+ var counter = 0;
+ pushedObject.locations.forEach(function (loc) {
+ var blockRowcol = getRowcol(level, loc);
+ var blockForwardRowcol = { r: blockRowcol.r + dr, c: blockRowcol.c + dc };
+ if (isInBounds(level, blockForwardRowcol.r, blockForwardRowcol.c)) {
+ var blockForwardLocation = getLocation(level, blockForwardRowcol.r, blockForwardRowcol.c);
+ var blockYetAnotherObject = findObjectAtLocation(blockForwardLocation);
+ if (blockYetAnotherObject != null) {
+ if (blockYetAnotherObject.type === SNAKE && blockYetAnotherObject.id === yetAnotherObject.id) counter++;
+ }
+ }
+ });
+ if (counter === 0) {
+ newSpikeDeath = [pushedObject.type, pushedObject.id, yetAnotherObject];
+ addIfAbsent(dyingObjects, yetAnotherObject);
+ if (dyingLocations != undefined) addIfAbsent(dyingLocations, getLocation(level, rowcol.r, rowcol.c));
+ continue;
+ }
+ }
+ if (yetAnotherObject.type === BLOCK && yetAnotherObject.splocks.includes(forwardLocation)) {
+ var object = findObjectAtLocation(offsetLocation(forwardLocation, -dr, -dc));
+ if (object.type === SNAKE) {
+ newSpikeDeath = [pushedObject.type, pushedObject.id];
+ addIfAbsent(dyingObjects, object);
+ if (dyingLocations != undefined) addIfAbsent(dyingLocations, getLocation(level, rowcol.r, rowcol.c));
+ continue;
+ }
+ }
+ 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. ------------ THIS IS THE TAIL GLITCH
+ continue;
+ }
+ return false;
+ }
+ // for (var k = 0; k < pushedObject.locations.length; k++) { // check
+ // var rowcol = getRowcol(level, pushedObject.locations[k]);
+ addIfAbsent(pushedObjects, yetAnotherObject);
+ // }
+ if (level.map[forwardLocation] === TRELLIS || level.map[forwardLocation] === ONEWAYWALLU) addIfAbsent(forwardLocations, forwardLocation);
+ } else addIfAbsent(forwardLocations, forwardLocation);
+ }
+
+ //locations
+ 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
+ addIfAbsent(dyingObjects, pushedObject);
+ addIfAbsent(pushedObjects, pushedObject);
+ if (dyingLocations != undefined) addIfAbsent(dyingLocations, getLocation(level, rowcol.r, rowcol.c));
+ continue;
+ }
+ }
+
+ var forwardLocation = getLocation(level, forwardRowcol.r, forwardRowcol.c);
+ if (dr === 1 && level.map[forwardLocation] === RAINBOW) {
+ // this rainbow holds us, unless we're going through it
+ var neighborLocations;
+ if (pushedObject.type === SNAKE) {
+ neighborLocations = [];
+ if (j > 0) neighborLocations.push(pushedObject.locations[j - 1]);
+ if (j < pushedObject.locations.length - 1) neighborLocations.push(pushedObject.locations[j + 1]);
+ } else if (pushedObject.type === BLOCK) {
+ neighborLocations = pushedObject.locations;
+ } else throw asdf;
+ if (neighborLocations.indexOf(forwardLocation) === -1) return false; // flat surface
+ // we slip right past it
+ }
+
+ var yetAnotherObject = findObjectAtLocation(forwardLocation);
+ if (yetAnotherObject != null) {
+ var object = findObjectAtLocation(offsetLocation(forwardLocation, -dr, -dc));
+ if (yetAnotherObject.type === FRUIT || yetAnotherObject.type === POISONFRUIT) {
+ // not pushable
+ return false;
+ }
+ if (yetAnotherObject.type === SNAKE && pushedObject.type === MIKE) {
+ newSpikeDeath = [pushedObject.type, pushedObject.id, yetAnotherObject];
+ addIfAbsent(dyingObjects, yetAnotherObject);
+ if (dyingLocations != undefined) addIfAbsent(dyingLocations, getLocation(level, rowcol.r, rowcol.c));
+ continue;
+ }
+ if (yetAnotherObject.type === MIKE && object.type === SNAKE) {
+ // newSpikeDeath = [yetAnotherObject.type, yetAnotherObject.id];
+ addIfAbsent(dyingObjects, object);
+ if (dyingLocations != undefined) addIfAbsent(dyingLocations, getLocation(level, rowcol.r, rowcol.c));
+ continue;
+ }
+ // adding pusher == null allows snakes to lift up splocks in certain instances
+ if (pusher == null && yetAnotherObject.type === BLOCK && yetAnotherObject.splocks.includes(forwardLocation) && object.type === SNAKE) {
+ dieOnSplock = object.id;
+ addIfAbsent(dyingObjects, object);
+ if (dyingLocations != undefined) addIfAbsent(dyingLocations, getLocation(level, rowcol.r, rowcol.c));
+ continue;
+ }
+ 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. ------------ THIS IS THE TAIL GLITCH
+ continue;
+ }
+ return false;
+ }
+ addIfAbsent(pushedObjects, yetAnotherObject);
+ if (level.map[forwardLocation] === TRELLIS || level.map[forwardLocation] === ONEWAYWALLU) addIfAbsent(forwardLocations, forwardLocation);
+ } else addIfAbsent(forwardLocations, forwardLocation);
+ }
+ }
+
+ // check forward locations
+ for (var i = 0; i < forwardLocations.length; i++) {
+ 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];
+ object = findObjectAtLocation(offsetLocation(forwardLocation, -dr, -dc));
+ deadObject = offsetLocation(forwardLocation, -dr, -dc);
+ if (!isTileCodeAir(pusher, object, tileCode, dr, dc)) {
+ if (dyingObjects != null) {
+ if (tileCode === SPIKE) {
+ if (object.type === SNAKE) {
+ addIfAbsent(dyingObjects, object);
+ if (dyingLocations != undefined) addIfAbsent(dyingLocations, deadObject);
+ continue;
+ }
+ }
+ else if (tileCode === LAVA) {
+ if (object.type === SNAKE || object.type === BLOCK || object.type === MIKE) {
+ addIfAbsent(dyingObjects, object);
+ if (dyingLocations != undefined) addIfAbsent(dyingLocations, deadObject);
+ continue;
+ }
+ }
+ else if (tileCode === WATER) {
+ if (object.type === BLOCK || object.type === MIKE) {
+ addIfAbsent(dyingObjects, object);
+ if (dyingLocations != undefined) addIfAbsent(dyingLocations, deadObject);
+ continue;
+ }
+ }
+ }
+ // can't push into something solid
+ return false;
+ }
+ }
+ // the push is a 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);
+ if (level.map[object.locations[i]] == BUBBLE || level.map[object.locations[i]] == CLOUD)
+ paintTileAtLocation(object.locations[i], SPACE, changeLog);
+ }
+ for (var i = 0; i < object.splocks.length; i++) {
+ object.splocks[i] = offsetLocation(object.splocks[i], dr, dc);
+ }
+ changeLog.push([object.type, object.id, oldState, serializeObjectState(object)]);
+ animations.push([
+ "m" + object.type, // MOVE_SNAKE | MOVE_BLOCK | MOVE_MIKE
+ 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 locationsLength = object.locations.length;
+ var totalLocations = object.locations.concat(object.splocks);
+ var objectLength = totalLocations.length;
+ var newLocations = [];
+ for (var i = 0; i < objectLength; i++) {
+ var rowcol = getRowcol(level, totalLocations[i]);
+ var r = rowcol.r + delta.r;
+ var c = rowcol.c + delta.c;
+
+ var outlineID = Math.floor(postPortalSnakeOutline.length / objectLength);
+ if (r >= 0 && c >= 0) {
+ postPortalSnakeOutline.push({
+ id: outlineID,
+ r: r,
+ c: c,
+ });
+ newLocations.push(getLocation(level, r, c));
+ }
+ if (!isInBounds(level, r, c)) return "outside"; // out of bounds
+ }
+
+ var blockedBySnake = false;
+ for (var i = 0; i < newLocations.length; i++) {
+ var location = newLocations[i];
+ if (!isTileCodeAir(object, null, level.map[location], 0, 0)) portalConflicts.push(getRowcol(level, location)); // blocked by tile
+ var otherObject = findObjectAtLocation(location);
+ if (otherObject != null && otherObject !== object) {
+ if (otherObject.type !== SNAKE) portalConflicts.push(getRowcol(level, location)); // blocked by object
+ else blockedBySnake = true;
+ }
+ }
+ if (blockedBySnake) {
+ portalConflicts = [];
+ return "blockedBySnake";
+ }
+ if (portalConflicts.length > 0) return "blocked";
+
+ // zappo presto!
+ var oldState = serializeObjectState(object);
+ object.locations = newLocations.slice(0, locationsLength);
+ object.splocks = newLocations.slice(locationsLength);
+ changeLog.push([object.type, object.id, oldState, serializeObjectState(object)]);
+ animations.push([
+ "t" + object.type, // TELEPORT_SNAKE | TELEPORT_BLOCK | TELEPORT_MIKE
+ object.id,
+ delta.r,
+ delta.c,
+ ]);
+ return "works";
+}
+
+function isTileCodeAir(pusher, pushedObject, tileCode, dr, dc) {
+ switch (tileCode) {
+ case SPACE: case EXIT: case PORTAL: case CLOSEDLIFT: return true;
+ case TRELLIS: case BUBBLE: return pusher != null;
+ case RAINBOW: return dr != 1;
+ case ONEWAYWALLU: return dr != 1;
+ case ONEWAYWALLD: return dr != -1;
+ case ONEWAYWALLL: return dc != 1;
+ case ONEWAYWALLR: return dc != -1;
+ default: return false;
+ }
+}
+
+function addIfAbsent(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), copyArray(object.splocks)], [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 exist
+ paintBrushBlockId = null;
+ }
+ if (object.type === MIKE && paintBrushTileCode === MIKE && paintBrushMikeId === object.id) {
+ // no longer editing an object that doesn't exist
+ paintBrushMikeId = null;
+ }
+ // below causes blocks to render as just supports
+ // if (object.type === BLOCK) {
+ // delete blockRenderCache[object.id];
+ // }
+ // if (object.type === MIKE) {
+ // delete mikeRenderCache[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 findMikeById(id) {
+ return findObjectOfTypeAndId(MIKE, 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 || object.splocks.indexOf(location) !== -1) //new code to erase splocks by clicking the spikes
+ // if (object.locations.indexOf(location) !== -1)
+ return object;
+ }
+ return null;
+}
+function isUneatenFruit() {
+ return getObjectsOfType(FRUIT).length > 0 || getObjectsOfType(POISONFRUIT).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 isSnakeOnPortal() {
+ var portalLocations = getPortalLocations();
+ if (portalLocations.length !== 2) return false;
+ var o1 = findObjectAtLocation(portalLocations[0]);
+ var o2 = findObjectAtLocation(portalLocations[1]);
+ if ((o1 != null && o1.type === SNAKE) || (o2 != null && o2.type === SNAKE)) return true;
+ return false;
+}
+function countSnakes() {
+ return getSnakes().length;
+}
+function getSnakes() {
+ return getObjectsOfType(SNAKE);
+}
+function getBlocks() {
+ return getObjectsOfType(BLOCK);
+}
+function getMikes() {
+ return getObjectsOfType(MIKE);
+}
+function getOccupiedClosedLiftLocations() {
+ var result = [];
+ for (var i = 0; i < level.map.length; i++) {
+ if (level.map[i] === CLOSEDLIFT) {
+ if (findObjectAtLocation(i))
+ result.push(i);
+ }
+ }
+ return result;
+}
+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 activeSnakeId = null;
+
+var SLITHER = "ss";
+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 | MOVE_MIKE | TELEPORT_SNAKE | TELEPORT_BLOCK | TELEPORT_MIKE,
+ // 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 blockRenderCache = {
+ // id: canvas4,
+ // "0": document.createElement("canvas"),
+};
+var mikeRenderCache = {
+ // id: canvas4,
+ // "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;
+
+ [canvas2, canvas4, canvas6].forEach(function (canvas) {
+ canvas.width = tileSize * level.width;
+ canvas.height = tileSize * level.height;
+ });
+ var canvasContainer = document.getElementById("canvasContainer");
+ canvasContainer.style.height = tileSize * level.height;
+ canvasContainer.style.width = tileSize * level.width;
+
+ var context;
+ // draw background if resizing occurs
+ if (false) {
+ context = canvas2.getContext("2d");
+ drawBackground(context, canvas2);
+ }
+ context = canvas4.getContext("2d");
+
+ // if (snakes.length === 0) document.getElementById("highlightSnakesButton").disabled = true;
+ // else document.getElementById("highlightSnakesButton").disabled = false;
+ // if (fruits.length === 0) document.getElementById("highlightFruitsButton").disabled = true;
+ // else document.getElementById("highlightFruitsButton").disabled = false;
+ // if (poisonFruits.length === 0) document.getElementById("highlightPoisonFruitsButton").disabled = true;
+ // else document.getElementById("highlightPoisonFruitsButton").disabled = false;
+
+ themeName = themes[themeCounter][0];
+ background = themes[themeCounter][1];
+ wall = themes[themeCounter][2];
+ snakeColors = themes[themeCounter][3];
+ blockColors = themes[themeCounter][4];
+ spikeColors = themes[themeCounter][5];
+ fruitColors = themes[themeCounter][6];
+ textStyle = themes[themeCounter][8];
+ experimentalColors = themes[themeCounter][9];
+
+ if (persistentState.showGrid && !persistentState.showEditor) {
+ drawGrid();
+ }
+
+ // normal render
+ renderLevel();
+
+ if (persistentState.showGrid && persistentState.showEditor) {
+ drawGrid();
+ }
+
+ if (persistentState.showEditor) {
+ if (paintBrushTileCode === BLOCK && paintBrushBlockId != null) {
+ // darken background
+ context.fillStyle = "rgba(0, 0, 0, 0.8)";
+ context.fillRect(0, 0, canvas4.width, canvas4.height);
+ // and render just this object in focus
+ var activeBlock = findBlockById(paintBrushBlockId);
+ renderLevel([activeBlock]);
+ } else if (paintBrushTileCode === MIKE && paintBrushMikeId != null) {
+ // darken background
+ context.fillStyle = "rgba(0, 0, 0, 0.8)";
+ context.fillRect(0, 0, canvas4.width, canvas4.height);
+ // and render just this object in focus
+ var activeMike = findMikeById(paintBrushMikeId);
+ renderLevel([activeMike]);
+ } else if (paintBrushTileCode === "select") {
+ getSelectedLocations().forEach(function (location) {
+ var rowcol = getRowcol(level, location);
+ context.fillStyle = "rgba(128, 128, 128, 0.3)";
+ context.fillRect(rowcol.c * tileSize, rowcol.r * tileSize, tileSize, tileSize);
+ });
+ }
+ }
+
+ // 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;
+ var link2 = window.location.href.split("#");
+ var replay = link2[2] != undefined ? "#" + link2[2] : "";
+ document.getElementById("link2Textbox").value = "#" + link2[1] + replay;
+ }
+
+ // throw this in there somewhere
+ document.getElementById("showGridButton").textContent = (persistentState.showGrid ? "Hide" : "Show") + " Grid";
+ document.getElementById("hideHotkeyButton").textContent = (persistentState.hideHotkeys ? "Show" : "Hide") + " Hotkeys";
+
+ if (animationProgress < 1.0) requestAnimationFrame(render);
+ return; // this is the end of the function proper
+
+ function renderLevel(onlyTheseObjects) {
+ var rng = new Math.seedrandom("b");
+ 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
+ if (!dont) {
+ 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;
+
+
+
+
+
+
+ //combine locations and splocks because they're treated the same
+ var locations = object.locations.concat(object.splocks);
+ var minDistance = Infinity;
+ var connected = [];
+ var connectedPoints = [];
+ for (var i = 0; i < locations.length; i++) {
+ var rowcol = getRowcol(level, locations[i]);
+ for (var j = 0; j < locations.length; j++) {
+ if (j != i) {
+ var rowcolComparison = getRowcol(level, locations[j]);
+ var distance = Math.abs(rowcol.r - rowcolComparison.r) + Math.abs(rowcol.c - rowcolComparison.c);
+ if (distance < minDistance) {
+ minDistance = distance;
+ connected = [i, j];
+ connectedPoints = [rowcol, rowcolComparison];
+ }
+ }
+ }
+ }
+ // new array omitting blocks that are already connected
+ // somehow this ruins locations for the bottom section
+ var remainingLocations = locations;
+ remainingLocations.splice(connected[0], 1);
+ remainingLocations.splice(connected[1], 1);
+
+ connectedPoints.forEach(function (rowcol) {
+ 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;
+ });
+
+ // add the horizontal connector locations to the connected blocks array
+ for (var i = 0; i < maxC - minC; i++) {
+ var connectorRowcol = { r: minR, c: maxC - i };
+ addIfAbsent(connectedPoints, connectorRowcol);
+ }
+
+ // add the vertical connector locations to the connected blocks array
+ for (var i = 1; i < maxR - minR; i++) {
+ var connectorRowcol = { r: maxR - i, c: maxC };
+ addIfAbsent(connectedPoints, connectorRowcol);
+ }
+
+ // find the next closest blocks and the point to which it's closest
+ minDistance = Infinity;
+ for (var i = 0; i < remainingLocations.length; i++) {
+ var rowcol = getRowcol(level, remainingLocations[i]);
+ for (var j = 0; j < connectedPoints.length; j++) {
+ var distance = Math.abs(rowcol.r - connectedPoints.r) + Math.abs(rowcol.c - connectedPoints.c);
+ if (distance < minDistance) {
+ minDistance = distance;
+ connected = [i, j];
+ }
+ }
+ }
+ var locations = object.locations.concat(object.splocks);
+
+
+
+
+
+
+
+
+
+ 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 = blockRenderCache[object.id];
+ if (image == null) {
+ // render the support beams to a buffer
+ blockRenderCache[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 };
+ var connectorColor = tint(blockColors[object.id % blockColors.length], .5);
+ drawConnector(bufferContext, rowcol1.r, rowcol1.c, cornerRowcol.r, cornerRowcol.c, connectorColor);
+ drawConnector(bufferContext, rowcol2.r, rowcol2.c, cornerRowcol.r, cornerRowcol.c, connectorColor);
+ }
+ if (!blockFixOn) drawBlock(bufferContext, object, minR, minC, rng, false);
+ }
+ var r = minR + animationDisplacementRowcol.r;
+ var c = minC + animationDisplacementRowcol.c;
+ if (isDead() && newSpikeDeath[0] === BLOCK && newSpikeDeath[1] === object.id && newSpikeDeath[2].dead) r += .5;
+ var savedContext2 = context;
+ context = canvas2.getContext("2d");
+ context.drawImage(image, c * tileSize, r * tileSize);
+ context = savedContext2;
+ });
+
+ objects.forEach(function (object) {
+ if (object.type !== MIKE) 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 = mikeRenderCache[object.id];
+ if (image == null) {
+ // render the support beams to a buffer
+ mikeRenderCache[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 mikes.
+ // Then when we render the support beams, we won't see the supports inside the mike 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 };
+ var connectorColor = tint(blockColors[blockColors.length - 1 - object.id % blockColors.length], .5);
+ drawConnector(bufferContext, rowcol1.r, rowcol1.c, cornerRowcol.r, cornerRowcol.c, connectorColor);
+ drawConnector(bufferContext, rowcol2.r, rowcol2.c, cornerRowcol.r, cornerRowcol.c, connectorColor);
+ }
+ drawMike(bufferContext, object, minR, minC, rng);
+ }
+ var r = minR + animationDisplacementRowcol.r;
+ var c = minC + animationDisplacementRowcol.c;
+ var savedContext2 = context;
+ context = canvas2.getContext("2d");
+ context.drawImage(image, c * tileSize, r * tileSize);
+ context = savedContext2;
+ });
+ }
+
+ var exitExists = false;
+ var tileCounter = 0;
+ if (!dont) {
+ if (onlyTheseObjects == null) {
+ for (var r = 0; r < level.height; r++) { //draws wall underside curves and/or grass
+ for (var c = 0; c < level.width; c++) {
+ var location = getLocation(level, r, c);
+ var tileCode = level.map[location];
+ if (persistentState.showEditor || (!persistentState.showEditor && tileCode !== WALL && tileCode !== SPIKE)) drawTile(context, tileCode, r, c, level, location, rng, true, true);
+ if (tileCode === EXIT) exitExists = true;
+ if (tileCode === RAINBOW || tileCode === TRELLIS || tileCode === ONEWAYWALLU || tileCode === ONEWAYWALLD || tileCode === ONEWAYWALLL || tileCode === ONEWAYWALLR || tileCode === CLOSEDLIFT || tileCode === OPENLIFT || tileCode === CLOUD || tileCode === BUBBLE || tileCode === LAVA || tileCode === WATER) tileCounter++;
+ }
+ }
+ }
+
+ if (onlyTheseObjects != null || blockFixOn) {
+ for (var i = 0; i < objects.length; i++) {
+ var object = objects[i];
+ if (object.type === BLOCK || object.type === MIKE) drawObject(context, object, rng, false);
+ }
+ }
+
+ for (var i = 0; i < objects.length; i++) {
+ var object = objects[i];
+ if (object.type === SNAKE) drawObject(context, object);
+ if (object.splocks.length > 0 || object.type === MIKE) tileCounter++;
+ }
+
+ if (persistentState.showEditor && 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];
+ if (tileCode === WATER || tileCode === LAVA) drawTile(context, tileCode, r, c, level, location, rng, false);
+ }
+ }
+ }
+
+ if (persistentState.showEditor && onlyTheseObjects == null) {
+ for (var r = 0; r < level.height; r++) {
+ for (var c = 0; c < level.width; c++) {
+ location = getLocation(level, r, c);
+ tileCode = level.map[location];
+ if (tileCode === WALL) drawTile(context, tileCode, r, c, level, location, rng, false, false); //draws only walls
+ }
+ }
+ }
+
+ if (!persistentState.showEditor) context = canvas6.getContext("2d");
+ if (onlyTheseObjects == null) {
+ for (var r = 0; r < level.height; r++) {
+ for (var c = 0; c < level.width; c++) {
+ location = getLocation(level, r, c);
+ tileCode = level.map[location];
+ if ((persistentState.showEditor && (tileCode === ONEWAYWALLR || tileCode === ONEWAYWALLL || tileCode === TRELLIS)) || tileCode === CLOUD || tileCode === BUBBLE) drawTile(context, tileCode, r, c, level, location, rng, false);
+ }
+ }
+ }
+
+ for (var i = 0; i < objects.length; i++) {
+ var object = objects[i];
+ if (object.type === FRUIT || object.type === POISONFRUIT) drawObject(context, object, rng); //draws fruit
+ }
+
+ //describe level type
+ if (enhanced) {
+ document.getElementById("additions").style.display = "none";
+ if (persistentState.showEditor && tileCounter === 0) {
+ document.getElementById("additions").innerHTML = "all initial enhanced elements have been removed but the level is not saved";
+ document.getElementById("additions").style.display = "block";
+ }
+ }
+ else {
+ document.getElementById("additions").style.display = "none";
+ if (persistentState.showEditor && tileCounter > 0) {
+ document.getElementById("additions").innerHTML = "enhanced elements have been added to this level but the level is not saved";
+ document.getElementById("additions").style.display = "block";
+ }
+ }
+
+ if (portalFailure && countSnakes() != 0) {
+ drawSnakeOutline(postPortalSnakeOutline, portalConflicts); //failed portal diagram
+ if (portalOutOfBounds) {
+ context.strokeStyle = "rgba(255,0,0,.5)";
+ context.lineWidth = tileSize / 2;
+ roundRect(context, 0, 0, canvas4.width, canvas4.height, 0, false, true);
+ }
+ }
+
+ // banners
+ if (countSnakes() === 0 && exitExists) {
+ if (!cs) {
+ context.fillStyle = "rgba(0,0,0,.8)";
+ context.fillRect(0, 0, level.width * tileSize, level.height * tileSize);
+
+ context.fillStyle = textStyle.win;
+ context.font = textStyle.fontSize + "px " + textStyle.fontFamily;
+ context.shadowOffsetX = 5;
+ context.shadowOffsetY = 5;
+ context.shadowColor = "rgba(255,255,255,0.1)";
+ context.shadowBlur = 4;
+ context.textBaseline = "middle";
+ fitTextOnCanvas(context, "Well done!", textStyle.fontFamily);
+ document.getElementById("copySVButton").disabled = false;
+ }
+ else checkResult = true;
+ }
+ if (isDead()) {
+ if (!cs && !infiniteDeath) {
+ canvas7.style.display = "block"
+ context = canvas7.getContext("2d");
+ context.fillStyle = "rgba(0,0,0,.8)";
+ context.globalCompositeOperation = "source-out";
+ context.clearRect(0, 0, level.width * tileSize, level.height * tileSize);
+ context.fillRect(0, 0, level.width * tileSize, level.height * tileSize);
+ theseDyingLocations.forEach(function (loc) {
+ var localRowcol = getRowcol(level, loc);
+ context.globalCompositeOperation = "destination-out";
+ context.fillStyle = "red";
+ context.beginPath();
+ context.arc((localRowcol.c + .5) * tileSize, (localRowcol.r + 1) * tileSize, tileSize * 2, 0, 2 * Math.PI);
+ context.fill();
+ });
+ } else if (!cs && infiniteDeath) {
+ canvas7.style.display = "block"
+ context = canvas7.getContext("2d");
+ context.fillStyle = "rgba(0,0,0,.8)";
+ context.globalCompositeOperation = "source-out";
+ context.clearRect(0, 0, level.width * tileSize, level.height * tileSize);
+ context.fillRect(0, 0, level.width * tileSize, level.height * tileSize);
+
+ // doesn't work if done as forEach
+ var portalLocations = getPortalLocations();
+ var localRowcol = getRowcol(level, portalLocations[0]);
+ context.globalCompositeOperation = "destination-out";
+ context.fillStyle = "red";
+ context.beginPath();
+ context.arc((localRowcol.c + .5) * tileSize, (localRowcol.r + .5) * tileSize, tileSize * 2, 0, 2 * Math.PI);
+ context.fill();
+
+ localRowcol = getRowcol(level, portalLocations[1]);
+ context.globalCompositeOperation = "destination-out";
+ context.fillStyle = "red";
+ context.beginPath();
+ context.arc((localRowcol.c + .5) * tileSize, (localRowcol.r + .5) * tileSize, tileSize * 2, 0, 2 * Math.PI);
+ context.fill();
+ }
+ else checkResult = false;
+ }
+ }
+
+ if (cs && cr) {
+ context = canvas6.getContext("2d");
+ drawBackground(context, canvas6);
+ context.fillStyle = "green";
+ context.font = textStyle.fontSize + "px Impact";
+ context.shadowOffsetX = 5;
+ context.shadowOffsetY = 5;
+ context.shadowColor = "rgba(0,0,0,0.5)";
+ context.shadowBlur = 4;
+ var textString = "\u2713";
+ context.textBaseline = "middle";
+ var textWidth = context.measureText(textString).width;
+ context.fillText(textString, (canvas6.width / 2) - (textWidth / 2), canvas6.height / 2);
+ }
+ else if (cs && !cr) {
+ context = canvas6.getContext("2d");
+ drawBackground(context, canvas6);
+ context.fillStyle = "red";
+ context.font = textStyle.fontSize + "px Impact";
+ context.shadowOffsetX = 5;
+ context.shadowOffsetY = 5;
+ context.shadowColor = "rgba(0,0,0,0.5)";
+ context.shadowBlur = 4;
+ var textString = "\u2716";
+ context.textBaseline = "middle";
+ var textWidth = context.measureText(textString).width;
+ context.fillText(textString, (canvas6.width / 2) - (textWidth / 2), canvas6.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 = canvas4.width;
+ buffer.height = canvas4.height;
+ context = buffer.getContext("2d");
+
+ var hoverRowcol = getRowcol(level, hoverLocation);
+ var objectHere = findObjectAtLocation(hoverLocation);
+ if (typeof paintBrushTileCode === "number") {
+ if (level.map[hoverLocation] !== paintBrushTileCode) {
+ drawTile(context, paintBrushTileCode, hoverRowcol.r, hoverRowcol.c, level, hoverLocation, rng);
+ }
+ } else if (paintBrushTileCode === SNAKE) {
+ if (!(objectHere != null && objectHere.type === SNAKE && objectHere.id === paintBrushSnakeColorIndex)) {
+ drawObject(context, newSnake(paintBrushSnakeColorIndex, hoverLocation));
+ }
+ } else if (paintBrushTileCode === BLOCK) {
+ if (!(objectHere != null && objectHere.type === BLOCK && objectHere.id === paintBrushBlockId) && !splockIsActive) {
+ drawObject(context, newBlock(hoverLocation), rng, true);
+ }
+ else if (!(objectHere != null && objectHere.type === BLOCK && objectHere.id === paintBrushBlockId) && splockIsActive && paintBrushBlockId != null) {
+ drawTile(context, SPIKE, hoverRowcol.r, hoverRowcol.c, level, hoverLocation, rng);
+ }
+ } else if (paintBrushTileCode === MIKE) {
+ if (!(objectHere != null && objectHere.type === MIKE && objectHere.id === paintBrushMikeId)) {
+ drawObject(context, newMike(hoverLocation), rng);
+ }
+ } else if (paintBrushTileCode === FRUIT) {
+ if (!(objectHere != null && objectHere.type === FRUIT)) {
+ drawObject(context, newFruit(hoverLocation), rng);
+ }
+ } else if (paintBrushTileCode === POISONFRUIT) {
+ if (!(objectHere != null && objectHere.type === POISONFRUIT)) {
+ drawObject(context, newPoisonFruit(hoverLocation), rng);
+ }
+ } 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(context, tileCode, rowcol.r, rowcol.c, pastedData.level, location, rng);
+ });
+ pastedData.selectedObjects.forEach(drawObject, rng);
+ } else throw unreachable();
+
+ context = savedContext;
+ context.save();
+ context.globalAlpha = .2;
+ context.drawImage(buffer, 0, 0);
+ context.restore();
+ }
+ didResize = false;
+ }
+
+ function hexToRgb(hex) {
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16)
+ } : null;
+ }
+
+ // function hslToRgb(h, s, l) {
+ // var r, g, b;
+
+ // if (s == 0) {
+ // r = g = b = l; // achromatic
+ // } else {
+ // var hue2rgb = function hue2rgb(p, q, t) {
+ // if (t < 0) t += 1;
+ // if (t > 1) t -= 1;
+ // if (t < 1 / 6) return p + (q - p) * 6 * t;
+ // if (t < 1 / 2) return q;
+ // if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+ // return p;
+ // }
+
+ // var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ // var p = 2 * l - q;
+ // r = hue2rgb(p, q, h + 1 / 3);
+ // g = hue2rgb(p, q, h);
+ // b = hue2rgb(p, q, h - 1 / 3);
+ // }
+
+ // return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
+ // }
+
+ function drawGrid() {
+ var buffer = document.createElement("canvas");
+ buffer.width = canvas4.width;
+ buffer.height = canvas4.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 drawSnakeOutline(outline, conflicts) {
+ if (multiDiagrams) document.getElementById("cycleDiv").innerHTML = "Press the / (forward slash) key to cycle through diagrams";
+
+ var xSpots = [];
+ var maxID = -1;
+ for (var i = 0; i < outline.length; i++) {
+ maxID = outline[i].id;
+ }
+ if (maxID > 0) multiDiagrams = true;
+
+ var buffer = document.createElement("canvas");
+ buffer.width = canvas4.width;
+ buffer.height = canvas4.height;
+ var localContext = buffer.getContext("2d");
+
+ for (var i = 0; i < outline.length; i++) {
+ if (!(cycle && outline[i].id != cycleId)) {
+ localContext.strokeStyle = "white";
+ localContext.lineWidth = tileSize / 6;
+ var c = outline[i].c;
+ var r = outline[i].r;
+ localContext.beginPath();
+ localContext.moveTo((c + .35) * tileSize, r * tileSize);
+ localContext.lineTo((c + .65) * tileSize, r * tileSize);
+ localContext.moveTo((c + .9) * tileSize, r * tileSize);
+ localContext.arcTo((c + 1) * tileSize, r * tileSize, (c + 1) * tileSize, (r + .1) * tileSize, tileSize / 6);
+ localContext.moveTo((c + 1) * tileSize, (r + .35) * tileSize);
+ localContext.lineTo((c + 1) * tileSize, (r + .65) * tileSize);
+ localContext.moveTo((c + 1) * tileSize, (r + .9) * tileSize);
+ localContext.arcTo((c + 1) * tileSize, (r + 1) * tileSize, (c + .9) * tileSize, (r + 1) * tileSize, tileSize / 6);
+ localContext.moveTo((c + .65) * tileSize, (r + 1) * tileSize);
+ localContext.lineTo((c + .35) * tileSize, (r + 1) * tileSize);
+ localContext.moveTo((c + .1) * tileSize, (r + 1) * tileSize);
+ localContext.arcTo(c * tileSize, (r + 1) * tileSize, c * tileSize, (r + .9) * tileSize, tileSize / 6);
+ localContext.moveTo(c * tileSize, (r + .65) * tileSize);
+ localContext.lineTo(c * tileSize, (r + .35) * tileSize);
+ localContext.moveTo(c * tileSize, (r + .1) * tileSize);
+ localContext.arcTo(c * tileSize, r * tileSize, (c + .1) * tileSize, r * tileSize, tileSize / 6);
+ localContext.stroke();
+
+ for (var j = 0; j < conflicts.length; j++) {
+ if (conflicts[j].c === c && conflicts[j].r === r) {
+ xSpots.push({
+ c: conflicts[j].c,
+ r: conflicts[j].r
+ });
+ }
+ }
+ }
+ }
+ for (var i = 0; i < xSpots.length; i++) {
+ c = xSpots[i].c;
+ r = xSpots[i].r;
+ localContext.strokeStyle = "red";
+ localContext.lineWidth = tileSize / 2.8;
+ localContext.beginPath();
+ localContext.moveTo((c - .12) * tileSize, (r - .12) * tileSize);
+ localContext.lineTo((c + 1.12) * tileSize, (r + 1.12) * tileSize);
+ localContext.moveTo((c + 1.12) * tileSize, (r - .12) * tileSize);
+ localContext.lineTo((c - .12) * tileSize, (r + 1.12) * tileSize);
+ localContext.stroke();
+ }
+
+ if (cycleId > maxID) cycleId = 0;
+ context.save();
+ context.globalAlpha = 1;
+ context.drawImage(buffer, 0, 0);
+ context.restore();
+ }
+
+ function fitTextOnCanvas(context, text, fontface) {
+ var fontsize = 1000;
+ while (context.measureText(text).width > (canvas7.width / 1.2) || context.measureText(text).height > (canvas7.height / 1.2)) {
+ fontsize--;
+ context.font = fontsize + "px " + fontface;
+ }
+ var textWidth = context.measureText(text).width;
+ context.fillText(text, (canvas7.width / 2) - (textWidth / 2), canvas7.height / 2);
+ }
+}
+
+// DRAWING FUNCTIONS
+// -----------------------------------------------------------------------------------------------------------------------------
+
+function tint(hex, delta) {
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+
+ var r = parseInt(result[1], 16);
+ var g = parseInt(result[2], 16);
+ var b = parseInt(result[3], 16);
+
+ r /= 255, g /= 255, b /= 255;
+ var max = Math.max(r, g, b), min = Math.min(r, g, b);
+ var h, s, l = (max + min) / 2;
+
+ if (max == min) {
+ h = s = 0; // achromatic
+ } else {
+ var d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch (max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+ h /= 6;
+ }
+
+ s = s * 100;
+ s = Math.round(s);
+ l = l * 100 * delta;
+ l = Math.round(l);
+ h = Math.round(360 * h);
+
+ return 'hsl(' + h + ', ' + s + '%, ' + l + '%)';
+}
+
+function drawObject(context, object, rng, hover) {
+ var r, c;
+ switch (object.type) {
+ case SNAKE:
+ themeName !== "Classic" ? drawNewSnake(context) : drawOriginalSnake(context);
+ break;
+ case BLOCK:
+ drawBlock(context, object, 0, 0, rng, hover);
+ break;
+ case MIKE:
+ drawMike(context, object, 0, 0, rng);
+ break;
+ case FRUIT:
+ case POISONFRUIT:
+ var isPoison = object.type == POISONFRUIT;
+ drawFruit(context, object, isPoison, rng, false);
+ 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 drawNewSnake(context) {
+ var falling = false;
+ var animationDisplacementRowcol = findAnimationDisplacementRowcol(object.type, object.id);
+ if (animationDisplacementRowcol.r != 0) falling = true;
+ var lastRowcol = null
+ var nextRowcol = null
+ var color = snakeColors[object.id % snakeColors.length];
+ var colorIndex = object.id % snakeColors.length;
+ var altColor = tint(color, 1.2);
+ if (snakeColors === snakeColors2) altColor = color;
+ var headRowcol;
+ var orientation = 10;
+ for (var stage = 1; stage <= 2; stage++) {
+ for (var i = 0; i < object.locations.length; i++) {
+ if (stage === 1 && i === 0) continue;
+ var animation;
+ var rowcol = getRowcol(level, object.locations[i]);
+ var up, down;
+ if (stage === 2) {
+ if (i === 0 && (animation = findAnimation([SLITHER], object.id)) != null) {
+ rowcol.r += animation[2] * (animationProgress - 1);
+ rowcol.c += animation[3] * (animationProgress - 1);
+ up = (1 - animationProgress);
+ down = (animationProgress - 1);
+ } else if ((animation = findAnimation([SLITHER + i], object.id)) != null) {
+ rowcol.r += animation[2] * (animationProgress - 1);
+ rowcol.c += animation[3] * (animationProgress - 1);
+ up = (1 - animationProgress);
+ down = (animationProgress - 1);
+ }
+ }
+
+ lastRowcol = getRowcol(level, object.locations[i - 1]); //closer to head
+ nextRowcol = getRowcol(level, object.locations[i + 1]); //closer to tail
+
+ if (object.dead && (!dieOnSplock || dieOnSplock === object.id)) {
+ if (newSpikeDeath[2] != null && newSpikeDeath[2].type === SNAKE && newSpikeDeath[2].id === object.id) lowDeath = false;
+ else {
+ lowDeath = true;
+ rowcol.r += .5;
+ lastRowcol.r += .5;
+ nextRowcol.r += .5;
+ falling = true;
+ }
+ }
+ rowcol.r += animationDisplacementRowcol.r;
+ rowcol.c += animationDisplacementRowcol.c;
+ lastRowcol.r += animationDisplacementRowcol.r;
+ lastRowcol.c += animationDisplacementRowcol.c;
+ nextRowcol.r += animationDisplacementRowcol.r;
+ nextRowcol.c += animationDisplacementRowcol.c;
+
+ var cx = rowcol.c * tileSize;
+ var cy = rowcol.r * tileSize;
+
+ if (i === 0) {
+ context.fillStyle = color;
+ headRowcol = rowcol;
+
+ //determines orientation of face
+ if (!falling) nextRowcol = getRowcol(level, object.locations[1]);
+ if (nextRowcol.r < rowcol.r) { //last move down
+ roundRect(context, cx, cy, tileSize, tileSize, { tl: 0, tr: 0, bl: borderRadius, br: borderRadius }, true, false); //draw head
+ if (colorIndex === 0) orientation = 2;
+ else if (colorIndex === 1) orientation = 6;
+ else if (colorIndex === 2) orientation = 3;
+ else if (colorIndex === 3) orientation = 5;
+ }
+ else if (nextRowcol.r > rowcol.r) { //last move up
+ roundRect(context, cx, cy, tileSize, tileSize, { tl: borderRadius, tr: borderRadius }, true, false); //draw head
+ if (colorIndex === 0) orientation = 0;
+ else if (colorIndex === 1) orientation = 4;
+ else if (colorIndex === 2) orientation = 1;
+ else if (colorIndex === 3) orientation = 7;
+ }
+ else if (nextRowcol.c < rowcol.c) { //last move right
+ roundRect(context, cx, cy, tileSize, tileSize, { tr: borderRadius, br: borderRadius }, true, false); //draw head
+ if (colorIndex === 0) orientation = 1;
+ else if (colorIndex === 1) orientation = 5;
+ else if (colorIndex === 2) orientation = 2;
+ else if (colorIndex === 3) orientation = 4;
+ }
+ else if (nextRowcol.c > rowcol.c) { //last move left
+ roundRect(context, cx, cy, tileSize, tileSize, { tl: borderRadius, bl: borderRadius }, true, false); //draw head
+ if (colorIndex === 0) orientation = 3;
+ else if (colorIndex === 1) orientation = 7;
+ else if (colorIndex === 2) orientation = 0;
+ else if (colorIndex === 3) orientation = 6;
+ }
+ else {
+ roundRect(context, cx, cy, tileSize, tileSize, borderRadius, true, false); //draw head
+ orientation = 10;
+ }
+ } else {
+ if (i % 2 == 0) context.fillStyle = color;
+ else context.fillStyle = altColor;
+ var br = buildSegment(rowcol, lastRowcol, nextRowcol);
+ roundRect(context, cx, cy, tileSize, tileSize, br, true, false);
+
+ function buildSegment(rowcol, lastRowcol, nextRowcol) {
+ var br = {}; // border radius
+ if (i === object.locations.length - 1) {
+ // tail
+ if (lastRowcol.r > rowcol.r) br = { tl: borderRadius, tr: borderRadius };
+ else if (lastRowcol.r < rowcol.r) br = { bl: borderRadius, br: borderRadius };
+ else if (lastRowcol.c < rowcol.c) br = { tr: borderRadius, br: borderRadius };
+ else if (lastRowcol.c > rowcol.c) br = { tl: borderRadius, bl: borderRadius };
+ }
+ else if (i < object.locations.length - 1) {
+ // middle
+ if (lastRowcol.r > rowcol.r && nextRowcol.c < rowcol.c) br = { tr: borderRadius };
+ else if (lastRowcol.r > rowcol.r && nextRowcol.c > rowcol.c) br = { tl: borderRadius };
+ else if (lastRowcol.r < rowcol.r && nextRowcol.c < rowcol.c) br = { br: borderRadius };
+ else if (lastRowcol.r < rowcol.r && nextRowcol.c > rowcol.c) br = { bl: borderRadius };
+
+ else if (lastRowcol.c > rowcol.c && nextRowcol.r < rowcol.r) br = { bl: borderRadius };
+ else if (lastRowcol.c > rowcol.c && nextRowcol.r > rowcol.r) br = { tl: borderRadius };
+ else if (lastRowcol.c < rowcol.c && nextRowcol.r < rowcol.r) br = { br: borderRadius };
+ else if (lastRowcol.c < rowcol.c && nextRowcol.r > rowcol.r) br = { tr: borderRadius };
+
+ else if (lastRowcol.c < rowcol.c && nextRowcol.c > rowcol.c || lastRowcol.c > rowcol.c && nextRowcol.c < rowcol.c || lastRowcol.r < rowcol.r && nextRowcol.r > rowcol.r || lastRowcol.r > rowcol.r && nextRowcol.r < rowcol.r) br = 0;
+ }
+ else roundRect(context, cx, cy, tileSize, tileSize, borderRadius, true, false); // don't think it ever hits this code
+ return br;
+ }
+ }
+ }
+ }
+ r = headRowcol.r;
+ c = headRowcol.c;
+ drawFace(context, object.id, c, r, orientation, getAdjacentTiles());
+ }
+
+ function drawOriginalSnake(context) {
+ 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], 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 + (i - 1)], 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 && (!dieOnSplock || dieOnSplock === object.id)) {
+ if (newSpikeDeath[2] != null && newSpikeDeath[2].type === SNAKE && newSpikeDeath[2].id === object.id) lowDeath = false;
+ else {
+ lowDeath = true;
+ rowcol.r += .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(context, headRowcol.r, headRowcol.c, 0.5, "#fff");
+ drawCircle(context, headRowcol.r, headRowcol.c, 0.2, "#000");
+ }
+
+ 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 drawFace(context, snake, headCol, headRow, orientation, adjacentTiles) {
+ drawFace2(context, snake, headCol, headRow, orientation, isNotSpace);
+ function isNotSpace(dr, dc) {
+ var tileCode = adjacentTiles[1 + dr][1 + dc];
+ var result = tileCode === WALL || tileCode === SPIKE || tileCode === CLOUD || tileCode === BUBBLE || tileCode === LAVA || tileCode === WATER;
+ if (dr == 1 || dr == -1) return result || tileCode === OPENLIFT;
+ else return result;
+ }
+}
+
+function drawFace2(context, snake, headCol, headRow, orientation, isOccupied) {
+ var forwardLocation;
+ var forwardObject = null;
+ var straight;
+
+ var x = headCol * tileSize;
+ var y = headRow * tileSize;
+
+ var scaleFactor = 1.5;
+ var scale1;
+ var scale2;
+ var eye1 = tileSize * .8;
+ var eye2 = tileSize * .4;
+
+ var eyeSize = tileSize / 5;
+ var eyeRotation = 2;
+ var z1, z2, z3, z4, z5, z6, z7, z8;
+ var a1, a2, a3, a4, a5, a6, a7, a8;
+ var b1, b2, b3, b4, b5, b6, b7, b8, b9, b10;
+ var beakRotation = 1.5;
+ var arcDirection = false;
+
+ switch (orientation) {
+ case 0: //red up and blue left
+ z1 = eye2;
+ z2 = tileSize - eye1;
+ z3 = eye2;
+ z4 = tileSize - eye2;
+ z5 = eye2;
+ z6 = tileSize - eye1;
+ z7 = eye2;
+ z8 = tileSize - eye2
+ eyeRotation = 1.5;
+ scale1 = scaleFactor;
+ scale2 = 1;
+
+ if (1 <= headRow && headRow < level.height - 1) {
+ forwardLocation = getLocation(level, headRow - 1, headCol);
+ forwardObject = findObjectAtLocation(forwardLocation);
+ }
+ if (isOccupied(-1, 0) || (forwardObject != null && forwardObject.type !== FRUIT)) {
+ straight = false;
+ b1 = tileSize * .6;
+ b2 = tileSize * .05;
+ b3 = tileSize * .7;
+ b4 = -tileSize * .05;
+ b5 = tileSize;
+ b6 = tileSize * .1;
+ b7 = tileSize * .2;
+ b8 = tileSize * .1;
+ }
+ else straight = true;
+
+ a1 = tileSize * .7;
+ a2 = tileSize * .3;
+ a3 = tileSize * .7;
+ a4 = -tileSize * .3;
+ a5 = tileSize * .7;
+ a6 = tileSize * .3;
+ a7 = tileSize / 6;
+ a8 = 0;
+ beakRotation = 1;
+ arcDirection = false;
+ break;
+ case 1: //red right and blue up
+ case 10:
+ z1 = eye1;
+ z2 = eye2;
+ z3 = eye2;
+ z4 = eye2;
+ z5 = eye1;
+ z6 = eye2;
+ z7 = eye2;
+ z8 = eye2;
+ eyeRotation = 2;
+ scale1 = 1;
+ scale2 = scaleFactor;
+
+ if (1 <= headCol && headCol < level.width - 1) {
+ forwardLocation = getLocation(level, headRow, headCol + 1);
+ forwardObject = findObjectAtLocation(forwardLocation);
+ }
+ if (isOccupied(0, 1) || (forwardObject != null && forwardObject.type !== FRUIT)) {
+ straight = false;
+ b1 = tileSize * .95;
+ b2 = tileSize * .6;
+ b3 = tileSize * 1.05;
+ b4 = tileSize * .7;
+ b5 = tileSize * .9;
+ b6 = tileSize;
+ b7 = -tileSize * .1;
+ b8 = tileSize * .2;
+ }
+ else straight = true;
+
+ a1 = tileSize * .7;
+ a2 = tileSize * .7;
+ a3 = tileSize * 1.3;
+ a4 = tileSize * .7;
+ a5 = tileSize * .7;
+ a6 = tileSize * .7;
+ a7 = 0;
+ a8 = tileSize / 6;
+ beakRotation = 1.5;
+ arcDirection = false;
+ break;
+ case 2: //red down and blue right
+ z1 = tileSize - eye2;
+ z2 = eye1;
+ z3 = tileSize - eye2;
+ z4 = eye2;
+ z5 = tileSize - eye2;
+ z6 = eye1;
+ z7 = tileSize - eye2;
+ z8 = eye2;
+ eyeRotation = 2.5;
+ scale1 = scaleFactor;
+ scale2 = 1;
+
+ if (1 <= headRow && headRow < level.height - 1) {
+ forwardLocation = getLocation(level, headRow + 1, headCol);
+ forwardObject = findObjectAtLocation(forwardLocation);
+ }
+ if (isOccupied(1, 0) || (forwardObject != null && forwardObject.type !== FRUIT)) {
+ straight = false;
+ b1 = tileSize * .4;
+ b2 = tileSize * .95;
+ b3 = tileSize * .3;
+ b4 = tileSize * 1.05;
+ b5 = 0;
+ b6 = tileSize * .9;
+ b7 = -tileSize * .2;
+ b8 = -tileSize * .1;
+ }
+ else straight = true;
+
+ a1 = tileSize * .3;
+ a2 = tileSize * .7;
+ a3 = tileSize * .3;
+ a4 = tileSize * 1.3;
+ a5 = tileSize * .3;
+ a6 = tileSize * .7;
+ a7 = tileSize / 6;
+ a8 = 0;
+ beakRotation = 2;
+ arcDirection = false;
+ break;
+ case 3: //red left and blue down
+ z1 = tileSize - eye1;
+ z2 = tileSize - eye2;
+ z3 = tileSize - eye2;
+ z4 = tileSize - eye2;
+ z5 = tileSize - eye1;
+ z6 = tileSize - eye2;
+ z7 = tileSize - eye2;
+ z8 = tileSize - eye2;
+ eyeRotation = 3;
+ scale1 = 1;
+ scale2 = scaleFactor;
+
+ if (1 <= headCol && headCol < level.width - 1) {
+ forwardLocation = getLocation(level, headRow, headCol - 1);
+ forwardObject = findObjectAtLocation(forwardLocation);
+ }
+ if (isOccupied(0, -1) || (forwardObject != null && forwardObject.type !== FRUIT)) {
+ straight = false;
+ b1 = tileSize * .05;
+ b2 = tileSize * .4;
+ b3 = -tileSize * .05;
+ b4 = tileSize * .3;
+ b5 = tileSize * .1;
+ b6 = 0;
+ b7 = tileSize * .1;
+ b8 = -tileSize * .2;
+ }
+ else straight = true;
+
+ a1 = tileSize * .3;
+ a2 = tileSize * .3;
+ a3 = tileSize - tileSize * 1.3;
+ a4 = tileSize * .3;
+ a5 = tileSize * .3;
+ a6 = tileSize * .3;
+ a7 = 0;
+ a8 = tileSize / 6;
+ beakRotation = 2.5;
+ arcDirection = false;
+ break;
+ case 4: //green up and yellow right
+ z1 = tileSize - eye2;
+ z2 = tileSize - eye1;
+ z3 = tileSize - eye2;
+ z4 = tileSize - eye2;
+ z5 = tileSize - eye2;
+ z6 = tileSize - eye1;
+ z7 = tileSize - eye2;
+ z8 = tileSize - eye2
+ eyeRotation = 2.5;
+ scale1 = scaleFactor;
+ scale2 = 1;
+
+ if (1 <= headRow && headRow < level.height - 1) {
+ forwardLocation = getLocation(level, headRow - 1, headCol);
+ forwardObject = findObjectAtLocation(forwardLocation);
+ }
+ if (isOccupied(-1, 0) || (forwardObject != null && forwardObject.type !== FRUIT)) {
+ straight = false;
+ b1 = tileSize * .4;
+ b2 = tileSize * .05;
+ b3 = tileSize * .3;
+ b4 = -tileSize * .05;
+ b5 = 0;
+ b6 = tileSize * .1;
+ b7 = -tileSize * .2;
+ b8 = tileSize * .1;
+ }
+ else straight = true;
+
+ a1 = tileSize * .3;
+ a2 = tileSize * .3;
+ a3 = tileSize * .3;
+ a4 = -tileSize * .3;
+ a5 = tileSize * .3;
+ a6 = tileSize * .3;
+ a7 = tileSize / 6;
+ a8 = 0;
+ beakRotation = 2;
+ arcDirection = true;
+ break;
+ case 5: //green right and yellow down
+ z1 = eye1;
+ z2 = tileSize - eye2;
+ z3 = eye2;
+ z4 = tileSize - eye2;
+ z5 = eye1;
+ z6 = tileSize - eye2;
+ z7 = eye2;
+ z8 = tileSize - eye2;
+ eyeRotation = 3;
+ scale1 = 1;
+ scale2 = scaleFactor;
+
+ if (1 <= headCol && headCol < level.width - 1) {
+ forwardLocation = getLocation(level, headRow, headCol + 1);
+ forwardObject = findObjectAtLocation(forwardLocation);
+ }
+ if (isOccupied(0, 1) || (forwardObject != null && forwardObject.type !== FRUIT)) {
+ straight = false;
+ b1 = tileSize * .95;
+ b2 = tileSize * .4;
+ b3 = tileSize * 1.05;
+ b4 = tileSize * .3;
+ b5 = tileSize * .9;
+ b6 = 0;
+ b7 = -tileSize * .1;
+ b8 = -tileSize * .2;
+ }
+ else straight = true;
+
+ a1 = tileSize * .7;
+ a2 = tileSize * .3;
+ a3 = tileSize * 1.3;
+ a4 = tileSize * .3;
+ a5 = tileSize * .7;
+ a6 = tileSize * .3;
+ a7 = 0;
+ a8 = tileSize / 6;
+ beakRotation = .5;
+ arcDirection = true;
+ break;
+ case 6: //green down and yellow left
+ z1 = eye2;
+ z2 = eye1;
+ z3 = eye2;
+ z4 = eye2;
+ z5 = eye2;
+ z6 = eye1;
+ z7 = eye2;
+ z8 = eye2;
+ eyeRotation = 1.5;
+ scale1 = scaleFactor;
+ scale2 = 1;
+
+ if (1 <= headRow && headRow < level.height - 1) {
+ forwardLocation = getLocation(level, headRow + 1, headCol);
+ forwardObject = findObjectAtLocation(forwardLocation);
+ }
+ if (isOccupied(1, 0) || (forwardObject != null && forwardObject.type !== FRUIT)) {
+ straight = false;
+ b1 = tileSize * .6;
+ b2 = tileSize * .95;
+ b3 = tileSize * .7;
+ b4 = tileSize * 1.05;
+ b5 = tileSize;
+ b6 = tileSize * .9;
+ b7 = tileSize * .2;
+ b8 = -tileSize * .1;
+ }
+ else straight = true;
+
+ a1 = tileSize * .7;
+ a2 = tileSize * .7;
+ a3 = tileSize * .7;
+ a4 = tileSize * 1.3;
+ a5 = tileSize * .7;
+ a6 = tileSize * .7;
+ a7 = tileSize / 6;
+ a8 = 0;
+ beakRotation = 1;
+ arcDirection = true;
+ break;
+ case 7: //green left and yellow up
+ z1 = tileSize - eye1;
+ z2 = eye2;
+ z3 = tileSize - eye2;
+ z4 = eye2;
+ z5 = tileSize - eye1;
+ z6 = eye2;
+ z7 = tileSize - eye2;
+ z8 = eye2;
+ eyeRotation = 2;
+ scale1 = 1;
+ scale2 = scaleFactor;
+
+ if (1 <= headCol && headCol < level.width - 1) {
+ forwardLocation = getLocation(level, headRow, headCol - 1);
+ forwardObject = findObjectAtLocation(forwardLocation);
+ }
+ if (isOccupied(0, -1) || (forwardObject != null && forwardObject.type !== FRUIT)) {
+ straight = false;
+ b1 = tileSize * .05;
+ b2 = tileSize * .6;
+ b3 = -tileSize * .05;
+ b4 = tileSize * .7;
+ b5 = tileSize * .1;
+ b6 = tileSize;
+ b7 = tileSize * .1;
+ b8 = tileSize * .2;
+ }
+ else straight = true;
+
+ a1 = tileSize * .3;
+ a2 = tileSize * .7;
+ a3 = tileSize - tileSize * 1.3;
+ a4 = tileSize * .7;
+ a5 = tileSize * .3;
+ a6 = tileSize * .7;
+ a7 = 0;
+ a8 = tileSize / 6;
+ beakRotation = 1.5;
+ arcDirection = true;
+ break;
+ }
+
+ if (snake === activeSnakeId) { //draw eyes for active snake only
+ context.fillStyle = "white";
+ context.save();
+ context.scale(scale1, scale2);
+ context.beginPath();
+ context.arc((x + z1) / scale1, (y + z2) / scale2, eyeSize, (eyeRotation - 1) * Math.PI, eyeRotation * Math.PI, true);
+ context.closePath();
+ context.restore();
+ context.fill();
+
+ context.fillStyle = "white";
+ context.save();
+ context.scale(scale1, scale2);
+ context.beginPath();
+ context.arc((x + z3) / scale1, (y + z4) / scale2, eyeSize, (eyeRotation - 1) * Math.PI, eyeRotation * Math.PI, true);
+ context.closePath();
+ context.restore();
+ context.fill();
+
+ context.fillStyle = "black";
+ context.save();
+ context.scale(scale1, scale2);
+ context.beginPath();
+ context.arc((x + z5) / scale1, (y + z6) / scale2, eyeSize / 2, (eyeRotation - 1) * Math.PI, eyeRotation * Math.PI, true);
+ context.closePath();
+ context.restore();
+ context.fill();
+
+ context.fillStyle = "black";
+ context.save();
+ context.scale(scale1, scale2);
+ context.beginPath();
+ context.arc((x + z7) / scale1, (y + z8) / scale2, eyeSize / 2, (eyeRotation - 1) * Math.PI, eyeRotation * Math.PI, true);
+ context.closePath();
+ context.restore();
+ context.fill();
+ }
+
+ //beak
+ context.fillStyle = "#F9921C";
+ context.strokeStyle = "#f98806";
+ context.lineWidth = tileSize / 24;
+ context.beginPath();
+ context.arc(x + a1, y + a2, tileSize / 6, (beakRotation - 1) * Math.PI, beakRotation * Math.PI, arcDirection);
+ if (straight) context.lineTo(x + a3, y + a4);
+ else {
+ context.lineTo(x + b1, y + b2);
+ context.bezierCurveTo(x + b1, y + b2, x + b3, y + b4, x + b5, y + b6);
+ context.lineTo(x + b1 + b7, y + b2 + b8);
+ }
+ context.closePath();
+ context.stroke();
+ 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;
+ }
+ if (themeName === "Classic") {
+ 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);
+ }
+ else {
+ var connectorSize = .38;
+ var xLo = (c1 + connectorSize) * tileSize;
+ var yLo = (r1 + connectorSize) * tileSize;
+ var xHi = (c2 + 1 - connectorSize) * tileSize;
+ var yHi = (r2 + 1 - connectorSize) * tileSize;
+ context.fillStyle = color;
+ context.fillRect(xLo, yLo, xHi - xLo, yHi - yLo);
+ }
+}
+
+function drawBlock(context, block, minR, minC, rng, hover) {
+ var animationDisplacementRowcol = findAnimationDisplacementRowcol(block.type, block.id);
+ var blockRowcols = block.locations.map(function (location) {
+ var rowcol = getRowcol(level, location);
+ rowcol.r -= minR;
+ rowcol.c -= minC;
+ return rowcol;
+ });
+ var splockRowcols = block.splocks.map(function (location) {
+ var rowcol = getRowcol(level, location);
+ rowcol.r -= minR;
+ rowcol.c -= minC;
+ return rowcol;
+ });
+
+ var blockId = hover && block.id != 0 ? block.id - 1 : block.id;
+ var color = blockColors[blockId % blockColors.length];
+ splockRowcols.forEach(function (rowcol) {
+ var r = rowcol.r + animationDisplacementRowcol.r;
+ var c = rowcol.c + animationDisplacementRowcol.c;
+ if (isDead() && newSpikeDeath[0] === BLOCK && newSpikeDeath[1] === block.id && newSpikeDeath[2].dead) r += .5;
+ drawSpikes(context, r, c, null, rng, blockRowcols, splockRowcols, color);
+ });
+
+ blockRowcols.forEach(function (rowcol) {
+ var r = rowcol.r + animationDisplacementRowcol.r;
+ var c = rowcol.c + animationDisplacementRowcol.c;
+ if (isDead() && newSpikeDeath[0] === BLOCK && newSpikeDeath[1] === block.id && newSpikeDeath[2].dead) r += .5;
+
+ context.fillStyle = color;
+ var outlineThickness = themeName === "Classic" ? .3 : .4;
+ var complement = 1 - outlineThickness;
+ var outlinePixels = outlineThickness * tileSize;
+
+ // draw curves
+ var size = .9;
+ var inverse = 1 - size;
+ var br = 0;
+
+ if (themeName != "Classic") {// top right
+ br = borderRadius
+ if (isAlsoThisBlock(1, 0) && isAlsoThisBlock(0, -1) && !isAlsoThisBlock(1, -1)) {
+ var y = r;
+ var x = c + 1;
+ context.beginPath();
+ context.moveTo((x + inverse) * tileSize, y * tileSize);
+ context.lineTo(x * tileSize, y * tileSize);
+ context.lineTo(x * tileSize, (y - inverse) * tileSize);
+ context.arc((x + inverse) * tileSize, (y - inverse) * tileSize, inverse * tileSize, Math.PI, .5 * Math.PI, true);
+ context.closePath();
+ context.fill();
+ }
+ // top left
+ else if (isAlsoThisBlock(-1, 0) && isAlsoThisBlock(0, -1) && !isAlsoThisBlock(-1, -1)) {
+ var y = r;
+ var x = c - 1;
+ context.beginPath();
+ context.moveTo((x + size) * tileSize, y * tileSize);
+ context.lineTo((x + 1) * tileSize, y * tileSize);
+ context.lineTo((x + 1) * tileSize, (y - inverse) * tileSize);
+ context.arc((x + size) * tileSize, (y - inverse) * tileSize, inverse * tileSize, 0, .5 * Math.PI, false);
+ context.closePath();
+ context.fill();
+ }
+ // top left
+ else if (isAlsoThisBlock(-1, 1) && isAlsoThisBlock(0, 1) && !isAlsoThisBlock(-1, 0)) {
+ var y = r + 1;
+ var x = c - 1;
+ context.beginPath();
+ context.moveTo((x + size) * tileSize, y * tileSize);
+ context.lineTo((x + 1) * tileSize, y * tileSize);
+ context.lineTo((x + 1) * tileSize, (y - inverse) * tileSize);
+ context.arc((x + size) * tileSize, (y - inverse) * tileSize, inverse * tileSize, 0, .5 * Math.PI, false);
+ context.closePath();
+ context.fill();
+ }
+ // bottom right
+ else if (isAlsoThisBlock(1, 0) && isAlsoThisBlock(0, 1) && !isAlsoThisBlock(1, 1)) {
+ var y = r + 1;
+ var x = c + 1;
+ context.beginPath();
+ context.moveTo((x + inverse) * tileSize, y * tileSize);
+ context.lineTo(x * tileSize, y * tileSize);
+ context.lineTo(x * tileSize, (y + inverse) * tileSize);
+ context.arc((x + inverse) * tileSize, (y + inverse) * tileSize, inverse * tileSize, Math.PI, 1.5 * Math.PI, false);
+ context.closePath();
+ context.fill();
+ }
+ // bottom right
+ else if (!isAlsoThisBlock(1, 0) && isAlsoThisBlock(0, -1) && isAlsoThisBlock(1, -1)) {
+ var y = r;
+ var x = c + 1;
+ context.beginPath();
+ context.moveTo((x + inverse) * tileSize, y * tileSize);
+ context.lineTo(x * tileSize, y * tileSize);
+ context.lineTo(x * tileSize, (y + inverse) * tileSize);
+ context.arc((x + inverse) * tileSize, (y + inverse) * tileSize, inverse * tileSize, Math.PI, 1.5 * Math.PI, false);
+ context.closePath();
+ context.fill();
+ }
+ // bottom left
+ else if (isAlsoThisBlock(-1, 0) && isAlsoThisBlock(0, 1) && !isAlsoThisBlock(-1, 1)) {
+ var y = r + 1;
+ var x = c - 1;
+ context.beginPath();
+ context.moveTo((x + size) * tileSize, y * tileSize);
+ context.lineTo((x + 1) * tileSize, y * tileSize);
+ context.lineTo((x + 1) * tileSize, (y + inverse) * tileSize);
+ context.arc((x + size) * tileSize, (y + inverse) * tileSize, inverse * tileSize, 0, 1.5 * Math.PI, true);
+ context.closePath();
+ context.fill();
+ }
+ // bottom left (why needed?)
+ else if (isAlsoThisBlock(-1, -1) && isAlsoThisBlock(0, -1) && !isAlsoThisBlock(-1, 0)) {
+ var y = r;
+ var x = c - 1;
+ context.beginPath();
+ context.moveTo((x + size) * tileSize, y * tileSize);
+ context.lineTo((x + 1) * tileSize, y * tileSize);
+ context.lineTo((x + 1) * tileSize, (y + inverse) * tileSize);
+ context.arc((x + size) * tileSize, (y + inverse) * tileSize, inverse * tileSize, 0, 1.5 * Math.PI, true);
+ context.closePath();
+ context.fill();
+ }
+
+ var c1, c2, c3, c4;
+ c1 = c2 = c3 = c4 = blockRadius;
+ if (isAlsoThisBlock(-1, 0)) c1 = c3 = 0;
+ if (isAlsoThisBlock(1, 0)) c2 = c4 = 0;
+ if (isAlsoThisBlock(0, -1)) c1 = c2 = 0;
+ if (isAlsoThisBlock(0, 1)) c3 = c4 = 0;
+ }
+
+ if (!isAlsoThisBlock(-1, -1) && isAlsoThisBlock(0, -1))
+ roundRect(context, c * tileSize, r * tileSize, outlinePixels, outlinePixels, { tl: 0, tr: 0, bl: 0, br: br }, true, false);
+ if (!isAlsoThisBlock(1, -1) && isAlsoThisBlock(0, -1))
+ roundRect(context, (c + complement) * tileSize, r * tileSize, outlinePixels, outlinePixels, { tl: 0, tr: 0, bl: br, br: 0 }, true, false);
+ if (!isAlsoThisBlock(-1, 1) && isAlsoThisBlock(0, 1))
+ roundRect(context, c * tileSize, (r + complement) * tileSize, outlinePixels, outlinePixels, { tl: 0, tr: br, bl: 0, br: 0 }, true, false);
+ if (!isAlsoThisBlock(1, 1) && isAlsoThisBlock(0, 1))
+ roundRect(context, (c + complement) * tileSize, (r + complement) * tileSize, outlinePixels, outlinePixels, { tl: br, tr: 0, bl: 0, br: 0 }, true, false);
+
+ var test = { tl: c1, tr: c2, bl: c3, br: c4 };
+ if (!isAlsoThisBlock(0, -1)) roundRect(context, c * tileSize, r * tileSize, tileSize, outlinePixels, test, true, false);
+ if (!isAlsoThisBlock(0, 1)) roundRect(context, c * tileSize, (r + complement) * tileSize, tileSize, outlinePixels, test, true, false);
+ if (!isAlsoThisBlock(-1, 0)) roundRect(context, c * tileSize, r * tileSize, outlinePixels, tileSize, test, true, false);
+ if (!isAlsoThisBlock(1, 0)) roundRect(context, (c + complement) * tileSize, r * tileSize, outlinePixels, tileSize, test, true, false);
+
+ function isAlsoThisBlock(dc, dr) {
+ for (var i = 0; i < blockRowcols.length; i++) {
+ var otherRowcol = blockRowcols[i];
+ if (rowcol.r + dr === otherRowcol.r && rowcol.c + dc === otherRowcol.c) return true;
+ }
+ return false;
+ }
+ });
+}
+
+function drawMike(context, mike, minR, minC, rng) {
+ var animationDisplacementRowcol = findAnimationDisplacementRowcol(mike.type, mike.id);
+ var mikeRowcols = mike.locations.map(function (location) {
+ var rowcol = getRowcol(level, location);
+ rowcol.r -= minR;
+ rowcol.c -= minC;
+ return rowcol;
+ });
+ var color = blockColors[blockColors.length - 1 - mike.id % blockColors.length]
+ mikeRowcols.forEach(function (rowcol) {
+ var r = rowcol.r + animationDisplacementRowcol.r;
+ var c = rowcol.c + animationDisplacementRowcol.c;
+ if (isDead() && newSpikeDeath[0] === MIKE && newSpikeDeath[1] === mike.id) r += .5;
+ drawStar(context, (c + .5) * tileSize, (r + .5) * tileSize, 31, tileSize * .48, tileSize * .36, color);
+
+ var side = 0;
+ var size = tileSize / 8;
+ var x = (c + .5) * tileSize;
+ var y = (r + .5) * tileSize;
+
+ var rotation = rng() * 2 * Math.PI;
+ context.save();
+ context.translate(x, y);
+ context.rotate(rotation);
+ context.translate(-x, -y);
+
+ context.beginPath();
+ context.moveTo(x + size * Math.cos(0), y + size * Math.sin(0));
+ for (side; side < 7; side++) {
+ context.lineTo((x + size * Math.cos(side * 2 * Math.PI / 6)) * 1, (y + size * Math.sin(side * 2 * Math.PI / 6)) * 1);
+ }
+ context.lineWidth = 1.5;
+ context.strokeStyle = "#999";
+ context.stroke();
+ context.fillStyle = tint(color, .5);
+ context.fillStyle = "#888";
+ context.fill();
+ context.restore();
+ });
+
+ function drawStar(context, cx, cy, spikes, outerRadius, innerRadius, color) {
+ var rot = 1.5 * Math.PI;
+ var x = cx;
+ var y = cy;
+ var step = Math.PI / spikes;
+
+ context.save();
+ context.translate(x, y);
+ context.rotate(Math.PI / 12);
+ context.translate(-x, -y);
+ context.beginPath();
+ context.moveTo(cx, cy - outerRadius)
+ for (i = 0; i < spikes; i++) {
+ x = cx + Math.cos(rot) * outerRadius;
+ y = cy + Math.sin(rot) * outerRadius;
+ context.lineTo(x, y);
+ rot += step;
+
+ x = cx + Math.cos(rot) * innerRadius;
+ y = cy + Math.sin(rot) * innerRadius;
+ context.lineTo(x, y);
+ rot += step;
+ }
+ context.lineTo(cx, cy - outerRadius);
+ context.closePath();
+ context.lineWidth = 2;
+ context.strokeStyle = tint(color, .95);
+ context.stroke();
+ context.fillStyle = color;
+ context.fill();
+ context.restore();
+ }
+}
+
+function drawFruit(context, object, isPoison, rng, eaten) {
+ var isPoison = object.type === POISONFRUIT;
+ var rowcol = getRowcol(level, object.locations[0]);
+ var c = rowcol.c;
+ var r = rowcol.r;
+ var startC = c * tileSize + tileSize / 2;
+ var startR = (r + .08) * tileSize;
+ var resize = tileSize * 1.7;
+
+ var color = fruitColors[object.id % fruitColors.length];
+ var stemColor = themes[themeCounter][7];
+ if (themeName === "Classic") {
+ var circle = true;
+ color = "#f0f";
+ }
+ context.save();
+ if (isPoison) {
+ color = "#666600";
+ stemColor = "#805500";
+ }
+ if (eaten) color = stemColor = "rgba(255,255,255,.1)";
+
+ context.fillStyle = color;
+ if (circle) {
+ drawCircle(context, r, c, 1, color);
+ }
+ else {
+ if (wall.surface == "rainbow") {
+ if (isPoison) {
+ stemColor = "black";
+ var grd = context.createLinearGradient(c * tileSize, r * tileSize, (c + 1) * tileSize, (r + 1) * tileSize);
+ grd.addColorStop(0, "black");
+ grd.addColorStop(1 / 2, "gray");
+ grd.addColorStop(1, "black");
+ context.fillStyle = grd;
+ } else {
+ stemColor = "white";
+ var grd = context.createLinearGradient(c * tileSize, r * tileSize, (c + 1) * tileSize, (r + 1) * tileSize);
+ grd.addColorStop(0, "white");
+ grd.addColorStop(1 / 2, "#888");
+ grd.addColorStop(1, "white");
+ context.fillStyle = grd;
+ }
+
+ // context.lineWidth = tileSize / 8;
+ // context.strokeStyle = "white";
+ }
+ // context.transform(.8, 0, 0, .8, 100, 100);
+ context.beginPath();
+ context.moveTo(startC, startR);
+ context.bezierCurveTo(startC - resize * .1, startR - resize * .05, startC - resize * .25, startR - resize * .1, startC - resize * .3, startR + resize * .05);
+ context.bezierCurveTo(startC - resize * .35, startR + resize * .15, startC - resize * .3, startR + resize * .6, startC, startR + resize * .5);
+ context.bezierCurveTo(startC + resize * .3, startR + resize * .6, startC + resize * .35, startR + resize * .15, startC + resize * .3, startR + resize * .05);
+ context.bezierCurveTo(startC + resize * .25, startR - resize * .05, startC + resize * .1, startR - resize * .1, startC, startR);
+ context.closePath();
+ context.fill();
+
+ context.beginPath();
+ context.moveTo(startC, startR);
+ context.bezierCurveTo(startC - resize * .1, startR - resize * .05, startC, startR - resize * .1, startC - resize * .1, startR - resize * .15);
+ context.bezierCurveTo(startC, startR - resize * .1, startC + resize * .05, startR - resize * .1, startC, startR);
+ context.fillStyle = stemColor;
+ context.fill();
+ }
+ if (isPoison) {
+ for (var i = 0; i < 60; i++) {
+ var spotC = rng();
+ var spotR = rng();
+ var mod = i % 2;
+ switch (mod) {
+ case 0: context.fillStyle = "rgba(20,20,0,.2)"; break;
+ case 1: context.fillStyle = "rgba(100,200,255,.2)"; break;
+ }
+ context.globalCompositeOperation = "source-atop";
+ context.beginPath();
+ context.arc((c + spotC) * tileSize, (r + spotR) * tileSize, tileSize / 22, 0, 2 * Math.PI);
+ context.fill();
+ }
+ for (var i = 0; i < 10; i++) {
+ var spotC = rng();
+ var spotR = rng();
+ var mod = i % 2;
+ switch (mod) {
+ case 0: context.fillStyle = "rgba(20,20,0,.2)"; break;
+ case 1: context.fillStyle = "rgba(100,200,255,.1)"; break;
+ }
+ context.globalCompositeOperation = "source-atop";
+ context.beginPath();
+ context.arc((c + spotC) * tileSize, (r + spotR) * tileSize, tileSize / 10, 0, 2 * Math.PI);
+ context.fill();
+ }
+ }
+ context.restore();
+}
+
+function drawTile(context, tileCode, r, c, level, location, rng, isCurve, grass) {
+ switch (tileCode) {
+ case SPACE:
+ break;
+ case WALL:
+ if (isCurve && wall.curvedWalls) drawCurves(context, r, c, getAdjacentTiles());
+ else if (!isCurve && wall.curvedWalls) drawWall(context, r, c, getAdjacentTiles(), rng, false);
+
+ if (!wall.curvedWalls) drawWall(context, r, c, getAdjacentTiles(), rng, grass);
+ break;
+ case SPIKE:
+ drawSpikes(context, r, c, getAdjacentTiles(), rng);
+ break;
+ case EXIT:
+ var radiusFactor = isUneatenFruit() ? 0.7 : 1.2;
+ drawQuarterPie(context, r, c, radiusFactor, snakeColors[0], 0);
+ drawQuarterPie(context, r, c, radiusFactor, snakeColors[1], 1);
+ drawQuarterPie(context, r, c, radiusFactor, snakeColors[2], 2);
+ drawQuarterPie(context, r, c, radiusFactor, snakeColors[3], 3);
+ break;
+ case PORTAL:
+ drawPortal(context, r, c, location);
+ break;
+ case RAINBOW:
+ drawRainbow(context, r, c, getAdjacentTiles());
+ break;
+ case TRELLIS:
+ drawTrellis(context, r, c);
+ break;
+ case ONEWAYWALLU:
+ drawOneWayWall(context, r, c, -1, 0);
+ break;
+ case ONEWAYWALLD:
+ drawOneWayWall(context, r, c, 1, 0);
+ break;
+ case ONEWAYWALLL:
+ drawOneWayWall(context, r, c, 0, -1);
+ break;
+ case ONEWAYWALLR:
+ drawOneWayWall(context, r, c, 0, 1);
+ break;
+ case CLOSEDLIFT:
+ drawLift(context, r, c, false);
+ break;
+ case OPENLIFT:
+ drawLift(context, r, c, true);
+ break;
+ case CLOUD:
+ drawCloud(context, c * tileSize, r * tileSize, getAdjacentTiles(), rng);
+ break;
+ case BUBBLE:
+ drawBubble(context, r, c);
+ break;
+ case LAVA:
+ drawLiquid(context, r, c, LAVA, getAdjacentTiles());
+ break;
+ case WATER:
+ drawLiquid(context, r, c, WATER, getAdjacentTiles());
+ 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 drawBackground(context, canvas) {
+ //solid color background or provide color base for other backgrounds
+ context.fillStyle = Array.isArray(background) ? "white" : background;
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ if (Array.isArray(background)) {
+ //checkerboard background
+ if (background[0] == "fade") {
+ for (var i = 0; i < level.width; i++) {
+ for (var j = 0; j < level.height; j++) {
+ /* var bgColor1 = hexToRgb(background[1]);
+ var bgColor2 = tint(background[1], .8);
+
+ var h = bgColor2.substr(bgColor2.indexOf("(") + 1, bgColor2.indexOf(","));
+ var s = bgColor2.substr(bgColor2.indexOf(",") + 1, bgColor2.indexOf(","));
+ var l = bgColor2.substr(bgColor2.split(",", 1).join(",").length + 1, bgColor2.indexOf(")"));
+
+ bgColor2 = hslToRgb(h, s, l); */
+
+ var bgColor1 = background[1];
+ var bgColor2 = background[2];
+
+ var shade = (j + 1) * .03 + .5;
+ if ((i + j) % 2 == 0) context.fillStyle = bgColor1 + ", " + shade + ")";
+ else context.fillStyle = bgColor2 + ", " + shade + ")";
+ context.fillRect(i * tileSize, j * tileSize, tileSize, tileSize);
+ }
+ }
+ }
+ //gradient background
+ else if (background[0] == "gradient") {
+ var grd = context.createLinearGradient(0, 0, 0, canvas.height);
+ grd.addColorStop(0, "rgba(255,255,255,.5)");
+ grd.addColorStop(1 / 2, "rgba(0, 200, 255, .5)");
+ grd.addColorStop(1, "rgba(0, 100, 255, .5)");
+ context.fillStyle = grd;
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ }
+ }
+}
+
+function drawCurves(context, r, c, adjacentTiles) {
+ drawCurves2(context, r, c, isWall, wall.base);
+
+ function isWall(dc, dr) {
+ var tileCode = adjacentTiles[1 + dr][1 + dc];
+ return tileCode == null || tileCode === WALL;
+ }
+}
+
+function drawCurves2(context, r, c, isOccupied, base) {
+ context.fillStyle = base;
+ var size = .6;
+ var inverse = 1 - size;
+ // under side right
+ if (isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(1, 1) && wall.curvedWalls) {
+ r = r + 1;
+ c = c + 1;
+ context.beginPath();
+ context.moveTo((c + inverse) * tileSize, r * tileSize);
+ context.lineTo(c * tileSize, r * tileSize);
+ context.lineTo(c * tileSize, (r + inverse) * tileSize);
+ context.arc((c + inverse) * tileSize, (r + inverse) * tileSize, inverse * tileSize, Math.PI, 1.5 * Math.PI, false);
+ context.closePath();
+ context.fill();
+ }
+ // under side left
+ else if (isOccupied(-1, 0) && isOccupied(0, 1) && !isOccupied(-1, 1) && wall.curvedWalls) {
+ r = r + 1;
+ c = c - 1;
+ context.beginPath();
+ context.moveTo((c + size) * tileSize, r * tileSize);
+ context.lineTo((c + 1) * tileSize, r * tileSize);
+ context.lineTo((c + 1) * tileSize, (r + inverse) * tileSize);
+ context.arc((c + size) * tileSize, (r + inverse) * tileSize, inverse * tileSize, 0, 1.5 * Math.PI, true);
+ context.closePath();
+ context.fill();
+ }
+ // under side left (no idea why this is needed)
+ else if (r != 0 && isOccupied(-1, -1) && isOccupied(0, -1) && !isOccupied(-1, 0) && wall.curvedWalls) {
+ r = r;
+ c = c - 1;
+ context.beginPath();
+ context.moveTo((c + size) * tileSize, r * tileSize);
+ context.lineTo((c + 1) * tileSize, r * tileSize);
+ context.lineTo((c + 1) * tileSize, (r + inverse) * tileSize);
+ context.arc((c + size) * tileSize, (r + inverse) * tileSize, inverse * tileSize, 0, 1.5 * Math.PI, true);
+ context.closePath();
+ context.fill();
+ }
+}
+
+function drawWall(context, r, c, adjacentTiles, rng, grass) {
+ drawBase(context, r, c, isWall, rng, wall.base);
+ drawTileOutlines(context, r, c, isWall, rng);
+ drawPlant(context, r, c, isWall, rng, wall.surface);
+ drawFlower(context, r, c, isWall, rng);
+
+ function isWall(dc, dr) {
+ var tileCode = adjacentTiles[1 + dr][1 + dc];
+ return tileCode == null || tileCode === WALL;
+ }
+}
+
+function drawFlower(context, r, c, isOccupied, rng) {
+ if (wall.flowers && !isOccupied(-1, -1) && isOccupied(-1, 0) && rng() > .3) {
+ c = c - 1;
+ var offset = rng();
+ context.save();
+ context.transform(1, 0, 0, 1, offset * tileSize, 0);
+
+ context.fillStyle = "#6666ff";
+ context.beginPath();
+ context.arc((c) * tileSize, (r) * tileSize, tileSize / 12, 0, 2 * Math.PI);
+ context.fill();
+
+ context.beginPath();
+ context.arc((c - .13) * tileSize, (r + .08) * tileSize, tileSize / 12, 0, 2 * Math.PI);
+ context.fill();
+
+ context.beginPath();
+ context.arc((c - .13) * tileSize, (r + .22) * tileSize, tileSize / 12, 0, 2 * Math.PI);
+ context.fill();
+
+ context.beginPath();
+ context.arc((c) * tileSize, (r + .3) * tileSize, tileSize / 12, 0, 2 * Math.PI);
+ context.fill();
+
+ context.beginPath();
+ context.arc((c + .13) * tileSize, (r + .22) * tileSize, tileSize / 12, 0, 2 * Math.PI);
+ context.fill();
+
+ context.beginPath();
+ context.arc((c + .13) * tileSize, (r + .08) * tileSize, tileSize / 12, 0, 2 * Math.PI);
+ context.fill();
+
+ context.beginPath();
+ context.arc((c) * tileSize, (r + .15) * tileSize, tileSize / 5.5, 0, 2 * Math.PI);
+ context.fill();
+
+ context.beginPath();
+ context.arc((c) * tileSize, (r + .15) * tileSize, tileSize / 12, 0, 2 * Math.PI);
+ context.fillStyle = "yellow";
+ context.fill();
+ context.restore();
+ }
+}
+
+function drawPlant(context, r, c, isOccupied, rng, fillStyle) {
+ var tileCode = r === 0 ? 0 : level.map[getLocation(level, r - 1, c)];
+ if ((wall.randomColors && isOccupied(0, 1) && isOccupied(-1, 0) && isOccupied(-1, 1)) || (wall.surfaceShape === "grass" && !isOccupied(0, -1) && tileCode !== CLOSEDLIFT && tileCode !== OPENLIFT)) {
+ var plant = 1;
+ var startPoint = (rng() * .7 - .3) * tileSize;
+ context.fillStyle = fillStyle;
+ if (!isOccupied(0, -1) && tileCode !== CLOSEDLIFT && tileCode !== OPENLIFT) {
+ plant = 0;
+ startPoint = (rng() * .4 + .3) * tileSize;
+ }
+
+ if (rng() > .75) {
+ context.beginPath();
+ context.arc(c * tileSize + startPoint, (r + plant + .1) * tileSize, tileSize / 6, 0, 2 * Math.PI);
+ context.fill();
+
+ var basePoint = { c: c * tileSize + startPoint, r: (r + plant + .1) * tileSize };
+ for (var i = -2; i <= 2; i++) {
+ var width = tileSize / (rng() + 5);
+ var height = -tileSize * (rng() * .2 + .3);
+ var angle = Math.PI * (rng() * .08 + .14) * (i - rng() / 2);
+
+ context.save();
+ context.translate(basePoint.c, basePoint.r);
+ context.rotate(angle);
+ context.translate(-basePoint.c, -basePoint.r);
+ context.fillRect(c * tileSize + startPoint - width / 2, (r + plant) * tileSize, width, height);
+ context.beginPath();
+ context.arc(c * tileSize + startPoint, (r + plant) * tileSize + height, width / 2, 0, 2 * Math.PI);
+ context.fill();
+ context.restore();
+ }
+ }
+ }
+}
+
+function drawBase(context, r, c, isOccupied, rng, fillStyle) {
+ var x = c * tileSize;
+ var y = r * tileSize;
+
+ context.fillStyle = fillStyle;
+ if (wall.randomColors) {
+ context.fillStyle = "rgb(50,70,100)";
+ if (isOccupied(0, 1) && isOccupied(1, 0) && isOccupied(1, 1)) context.fillRect((c + .5) * tileSize, (r + .5) * tileSize, tileSize, tileSize);
+ if (r === 0 && isOccupied(1, 0)) context.fillRect((c + .5) * tileSize, (r - .5) * tileSize, tileSize, tileSize);
+ if (c === 0) context.fillRect((c - .5) * tileSize, r * tileSize, tileSize, tileSize);
+ if (r === 0 && c === 0) context.fillRect((c - .5) * tileSize, (r - .5) * tileSize, tileSize, tileSize);
+ var color = rng === 0 ? 0 : Math.floor(rng() * 4) * 10;
+ context.fillStyle = "rgb(" + (color + 50) + "," + (color + 70) + "," + (color + 100) + ")";
+ roundRect(context, x, y, tileSize, tileSize, borderRadius, true, false);
+ }
+ else {
+ if (isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)) roundRect(context, x, y, tileSize, tileSize, { bl: borderRadius, br: borderRadius }, true, false);
+ else if (!isOccupied(0, -1) && isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)) roundRect(context, x, y, tileSize, tileSize, { tl: borderRadius, bl: borderRadius }, true, false);
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0)) roundRect(context, x, y, tileSize, tileSize, { tl: borderRadius, tr: borderRadius }, true, false);
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && isOccupied(-1, 0)) roundRect(context, x, y, tileSize, tileSize, { tr: borderRadius, br: borderRadius }, true, false);
+ else if (isOccupied(0, -1) && isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)) roundRect(context, x, y, tileSize, tileSize, { bl: borderRadius }, true, false);
+ else if (isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && isOccupied(-1, 0)) roundRect(context, x, y, tileSize, tileSize, { br: borderRadius }, true, false);
+ else if (!isOccupied(0, -1) && isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0)) roundRect(context, x, y, tileSize, tileSize, { tl: borderRadius }, true, false);
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && isOccupied(-1, 0)) roundRect(context, x, y, tileSize, tileSize, { tr: borderRadius }, true, false);
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)) roundRect(context, x, y, tileSize, tileSize, borderRadius, true, false);
+ else roundRect(context, x, y, tileSize, tileSize, 0, true, false);
+
+ if (wall.baseSpots) {
+ var color = Math.floor(rng() * 2);
+ switch (color) {
+ case 0: context.fillStyle = "rgba(0,0,0,.05)"; break;
+ case 1: context.fillStyle = "rgba(255,255,255,.05)"; break;
+ }
+ var radius = rng() * tileSize / 4 + tileSize / 10;
+ x = rng() * (x + tileSize - radius - (x + radius)) + x + radius;
+ y = rng() * (y + tileSize - radius - (y + radius)) + y + radius;
+
+ context.beginPath();
+ var freq = rng() * 100;
+ if (freq < 20) context.arc(x, (r + .5) * tileSize, radius, 0, 2 * Math.PI);
+ context.fill();
+ }
+ }
+}
+
+function drawTileOutlines(context, r, c, isOccupied, rng) {
+ if (wall.surface !== "rainbow") context.fillStyle = wall.surface;
+ else {
+ context.fillStyle = "white";
+ var mod = (r + c) % 17;
+ switch (mod) {
+ case 0: context.fillStyle = "#ff004c"; break;
+ case 1: context.fillStyle = "#e30000"; break;
+ case 2: context.fillStyle = "#ff4c00"; break;
+ case 3: context.fillStyle = "#ff9900"; break;
+ case 4: context.fillStyle = "#ffe500"; break;
+ case 5: context.fillStyle = "#cbff00"; break;
+ case 6: context.fillStyle = "#7fff00"; break;
+ case 7: context.fillStyle = "#00ff19"; break;
+ case 8: context.fillStyle = "#00ff66"; break;
+ case 9: context.fillStyle = "#00ffb2"; break;
+ case 10: context.fillStyle = "#00ffff"; break;
+ case 11: context.fillStyle = "#00b2ff"; break;
+ case 12: context.fillStyle = "#3200ff"; break;
+ case 13: context.fillStyle = "#5702c6"; break;
+ case 14: context.fillStyle = "#cc00ff"; break;
+ case 15: context.fillStyle = "#ff00e5"; break;
+ case 16: context.fillStyle = "#ff0098"; break;
+ }
+ }
+
+ if (wall.surfaceShape === "algae") {
+ if (!isOccupied(0, -1)) {
+ context.beginPath();
+ context.moveTo(c * tileSize, (r - .05) * tileSize);
+ context.bezierCurveTo((c - .25) * tileSize, (r - .1) * tileSize, (c - .4) * tileSize, (r + .1) * tileSize, (c - .25) * tileSize, (r + .2) * tileSize);
+ context.bezierCurveTo((c - .3) * tileSize, (r + .55) * tileSize, (c - .1) * tileSize, (r + .6) * tileSize, (c + .1) * tileSize, (r + .55) * tileSize);
+ context.bezierCurveTo((c + .3) * tileSize, (r + .7) * tileSize, (c + .7) * tileSize, (r + .6) * tileSize, (c + .65) * tileSize, (r + .5) * tileSize);
+ context.bezierCurveTo((c + 1) * tileSize, (r + .6) * tileSize, (c + 1.3) * tileSize, (r + .6) * tileSize, (c + 1.15) * tileSize, (r + .1) * tileSize);
+ context.bezierCurveTo((c + 1.15) * tileSize, r * tileSize, (c + 1.2) * tileSize, (r - .1) * tileSize, (c + 1) * tileSize, (r - .05) * tileSize);
+ context.bezierCurveTo((c + .8) * tileSize, (r - .1) * tileSize, (c + .7) * tileSize, (r - .15) * tileSize, (c + .6) * tileSize, (r - .05) * tileSize);
+ context.bezierCurveTo((c + .4) * tileSize, (r - .1) * tileSize, (c + .3) * tileSize, (r - .15) * tileSize, (c + .2) * tileSize, (r - .05) * tileSize);
+ context.bezierCurveTo((c + .2) * tileSize, (r - .1) * tileSize, (c + .1) * tileSize, (r - .15) * tileSize, (c + 0) * tileSize, (r - .05) * tileSize);
+ context.fill();
+ } else if (isOccupied(0, -1) && isOccupied(-1, 0) && !isOccupied(-1, -1)) {
+ context.beginPath();
+ context.moveTo((c - .05) * tileSize, (r + .55) * tileSize);
+ context.bezierCurveTo((c + .1) * tileSize, (r + .5) * tileSize, (c + .3) * tileSize, (r + .55) * tileSize, (c + .2) * tileSize, (r + .25) * tileSize);
+ context.bezierCurveTo((c + .25) * tileSize, (r + .2) * tileSize, (c + .35) * tileSize, (r - .05) * tileSize, (c + .05) * tileSize, (r - .05) * tileSize);
+ context.lineTo(c * tileSize, r * tileSize);
+ context.closePath();
+ context.fill();
+ }
+ }
+ else if (wall.surfaceShape === "snow") {
+ if (!isOccupied(0, -1)) {
+ context.beginPath();
+ context.moveTo((c + 1) * tileSize, (r - .05) * tileSize);
+ context.lineTo(c * tileSize, (r - .05) * tileSize);
+ context.bezierCurveTo((c - .2) * tileSize, (r - .05) * tileSize, (c - .35) * tileSize, (r + .4) * tileSize, (c + 0) * tileSize, (r + .45) * tileSize);
+ context.bezierCurveTo((c + .1) * tileSize, (r + .55) * tileSize, (c + .45) * tileSize, (r + .6) * tileSize, (c + .6) * tileSize, (r + .45) * tileSize);
+ context.bezierCurveTo((c + .85) * tileSize, (r + .6) * tileSize, (c + 1.3) * tileSize, (r + .3) * tileSize, (c + 1.1) * tileSize, (r - .02) * tileSize);
+ context.fill();
+ if (isOccupied(-1, -1)) {
+ context.beginPath();
+ context.moveTo((c + .4) * tileSize, (r - .05) * tileSize);
+ context.bezierCurveTo((c + .15) * tileSize, (r - .15) * tileSize, (c + .1) * tileSize, (r - .4) * tileSize, (c + .05) * tileSize, (r - .45) * tileSize);
+ context.bezierCurveTo((c + .05) * tileSize, (r - .5) * tileSize, (c - .1) * tileSize, (r - .5) * tileSize, (c - .1) * tileSize, (r - .4) * tileSize);
+ context.bezierCurveTo((c - .1) * tileSize, (r - .3) * tileSize, (c - .1) * tileSize, (r - .1) * tileSize, (c - .18) * tileSize, (r + .1) * tileSize);
+ context.closePath();
+ context.fill();
+ }
+ if (isOccupied(1, -1)) {
+ context.beginPath();
+ context.moveTo((c + .6) * tileSize, (r - .05) * tileSize);
+ context.bezierCurveTo((c + .85) * tileSize, (r - .15) * tileSize, (c + .9) * tileSize, (r - .4) * tileSize, (c + .95) * tileSize, (r - .45) * tileSize);
+ context.bezierCurveTo((c + .95) * tileSize, (r - .5) * tileSize, (c + 1.1) * tileSize, (r - .5) * tileSize, (c + 1.1) * tileSize, (r - .4) * tileSize);
+ context.bezierCurveTo((c + 1.1) * tileSize, (r - .3) * tileSize, (c + 1.2) * tileSize, (r - .1) * tileSize, (c + 1.15) * tileSize, (r + .3) * tileSize);
+ context.closePath();
+ context.fill();
+ }
+ } else if (isOccupied(0, -1) && isOccupied(-1, 0) && !isOccupied(-1, -1)) {
+ context.beginPath();
+ context.moveTo((c - .05) * tileSize, (r + .38) * tileSize);
+ context.bezierCurveTo((c + .1) * tileSize, (r + .53) * tileSize, (c + .3) * tileSize, (r - .15) * tileSize, (c + .05) * tileSize, (r - .05) * tileSize);
+ context.lineTo(c * tileSize, r * tileSize);
+ context.closePath();
+ context.fill();
+ }
+ }
+ else if (wall.surfaceShape === "grass") {
+ if (!isOccupied(0, -1)) {
+ context.beginPath();
+ context.moveTo((c + 1) * tileSize, (r - .05) * tileSize);
+ context.lineTo(c * tileSize, (r - .05) * tileSize);
+ context.bezierCurveTo((c - .3) * tileSize, r * tileSize, (c - .15) * tileSize, (r + .6) * tileSize, (c + .1) * tileSize, (r + .3) * tileSize);
+ context.bezierCurveTo((c + .15) * tileSize, (r + .4) * tileSize, (c + .3) * tileSize, (r + .5) * tileSize, (c + .45) * tileSize, (r + .3) * tileSize);
+ context.bezierCurveTo((c + .6) * tileSize, (r + .5) * tileSize, (c + .7) * tileSize, (r + .45) * tileSize, (c + .8) * tileSize, (r + .3) * tileSize);
+ context.bezierCurveTo((c + 1) * tileSize, (r + .6) * tileSize, (c + 1.3) * tileSize, r * tileSize, (c + 1) * tileSize, (r - .05) * tileSize);
+ context.closePath();
+ context.fill();
+ } else if (isOccupied(0, -1) && isOccupied(-1, 0) && !isOccupied(-1, -1)) {
+ context.beginPath();
+ context.moveTo((c - .05) * tileSize, (r + .38) * tileSize);
+ context.bezierCurveTo((c + .2) * tileSize, (r + .38) * tileSize, (c + .25) * tileSize, (r - .05) * tileSize, c * tileSize, (r - .05) * tileSize);
+ context.closePath();
+ context.fill();
+ }
+ }
+ else if (wall.surfaceShape === "stripe") {
+ var outlineThickness = .2;
+ var complement = 1 - outlineThickness;
+ var outlinePixels = outlineThickness * tileSize;
+ if (!isOccupied(0, -1)) context.fillRect(c * tileSize, r * tileSize, tileSize, outlinePixels);
+ 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 + 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 drawGrass(context, r, c) {
+ count = Math.floor(rng() * 3 + 10);
+ for (var i = 0; i < count; i++) {
+ var bladeStart = rng();
+ var bladeHeight = rng() * .1 + .05;
+ var curve = rng() * .15;
+ if (i % 2 != 0) {
+ bladeStart = bladeStart * (-1) + 1;
+ curve = curve * (-1);
+ }
+ context.beginPath();
+ context.moveTo((c + bladeStart) * tileSize, (r + .1) * tileSize);
+ context.bezierCurveTo((c + bladeStart) * tileSize, r * tileSize, (c + bladeStart / 1.2 + curve) * tileSize, (r - bladeHeight) * tileSize, (c + bladeStart / 1.2 + curve * 2) * tileSize, (r - bladeHeight) * tileSize);
+ context.strokeStyle = wall.surface;
+ context.lineWidth = tileSize / 70;
+ context.stroke();
+ }
+}
+
+function drawSpikes(context, r, c, adjacentTiles, rng, blockRowcols, splockRowcols, color) {
+ var x = c * tileSize;
+ var y = r * tileSize;
+ if (themeName === "Classic") {
+ 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();
+ }
+ else {
+ var spikeWidth = 10 / 9;
+ drawSpokes(context, x, y, spikeWidth, color);
+ if (color == undefined) drawSpikeSupports(context, r, c, x, y, spikeWidth, isSpike, isWall, rng);
+ else drawSpikeSupports(context, r, c, x, y, spikeWidth, isAlsoThisSplock, isAlsoThisBlock, rng, color);
+
+ 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 isAlsoThisSplock(dc, dr) {
+ for (var i = 0; i < splockRowcols.length; i++) {
+ var otherRowcol = splockRowcols[i];
+ if (r + dr === otherRowcol.r && c + dc === otherRowcol.c) return true;
+ }
+ return false;
+ }
+ function isAlsoThisBlock(dc, dr) {
+ for (var i = 0; i < blockRowcols.length; i++) {
+ var otherRowcol = blockRowcols[i];
+ if (r + dr === otherRowcol.r && c + dc === otherRowcol.c) return true;
+ }
+ return false;
+ }
+ }
+}
+
+function drawSpokes(context, x, y, spikeWidth, color) {
+ context.fillStyle = color != undefined ? color : spikeColors.spokes;
+ context.beginPath();
+ context.moveTo(x + tileSize * .2 * spikeWidth, y + tileSize * 0.3); //top spikes
+ context.lineTo(x + tileSize * .3 * spikeWidth, y + tileSize * 0.0);
+ context.lineTo(x + tileSize * .4 * spikeWidth, y + tileSize * 0.3);
+ context.lineTo(x + tileSize * .5 * spikeWidth, y + tileSize * 0.3);
+ context.lineTo(x + tileSize * .6 * spikeWidth, y + tileSize * 0.0);
+ context.lineTo(x + tileSize * .7 * spikeWidth, y + tileSize * 0.3);
+ context.closePath();
+ context.fill();
+
+ context.beginPath();
+ context.moveTo(x + tileSize * 0.7, y + tileSize * .2 * spikeWidth); //right spikes
+ context.lineTo(x + tileSize * 1.0, y + tileSize * .3 * spikeWidth);
+ context.lineTo(x + tileSize * 0.7, y + tileSize * .4 * spikeWidth);
+ context.lineTo(x + tileSize * 0.7, y + tileSize * .5 * spikeWidth);
+ context.lineTo(x + tileSize * 1.0, y + tileSize * .6 * spikeWidth);
+ context.lineTo(x + tileSize * 0.7, y + tileSize * .7 * spikeWidth);
+ context.closePath();
+ context.fill();
+
+ context.beginPath();
+ context.moveTo(x + tileSize * .7 * spikeWidth, y + tileSize * 0.7); //bottom spikes
+ context.lineTo(x + tileSize * .6 * spikeWidth, y + tileSize * 1.0);
+ context.lineTo(x + tileSize * .5 * spikeWidth, y + tileSize * 0.7);
+ context.lineTo(x + tileSize * .4 * spikeWidth, y + tileSize * 0.7);
+ context.lineTo(x + tileSize * .3 * spikeWidth, y + tileSize * 1.0);
+ context.lineTo(x + tileSize * .2 * spikeWidth, y + tileSize * 0.7);
+ context.closePath();
+ context.fill();
+
+ context.beginPath();
+ context.moveTo(x + tileSize * 0.3, y + tileSize * .7 * spikeWidth); //left spikes
+ context.lineTo(x + tileSize * 0.0, y + tileSize * .6 * spikeWidth);
+ context.lineTo(x + tileSize * 0.3, y + tileSize * .5 * spikeWidth);
+ context.lineTo(x + tileSize * 0.3, y + tileSize * .4 * spikeWidth);
+ context.lineTo(x + tileSize * 0.0, y + tileSize * .3 * spikeWidth);
+ context.lineTo(x + tileSize * 0.3, y + tileSize * .2 * spikeWidth);
+ context.closePath();
+ context.fill();
+}
+
+function drawSpikeSupports(context, r, c, x, y, spikeWidth, isOccupied, canConnect, rng, color) {
+ var skip = false;
+ // if (isOccupied = canConnect) skip = true;
+ var boltBool = false;
+ var splock = false;
+ if (color != undefined) splock = true;
+ context.fillStyle = splock ? color : spikeColors.support;
+ var color2 = splock ? color : spikeColors.spokes;
+
+ if (!skip && canConnect(0, 1) && (!(isOccupied(-1, 0) && isOccupied(1, 0) && (canConnect(1, 1) || canConnect(-1, 1))))) {
+ context.fillRect((c + .26) * tileSize, (r + .7) * tileSize, tileSize * .48, tileSize * .4);
+ boltBool = true;
+ }
+ if (!skip && !canConnect(0, 1) || splock) {
+ if (canConnect(0, -1) && (!(!isOccupied(0, -1) && isOccupied(1, 0) && !isOccupied(0, 1) && isOccupied(-1, 0) && canConnect(-1, -1) && canConnect(1, -1)) || splock)) {
+ context.fillRect((c + .26) * tileSize, r * tileSize, tileSize * .48, tileSize * .4);
+ boltBool = true;
+ }
+ if (canConnect(-1, 0) && (!(isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0) && canConnect(-1, -1) && canConnect(-1, 1)) || splock)) {
+ context.fillRect(c * tileSize, (r + .26) * tileSize, tileSize * .4, tileSize * .48);
+ boltBool = true;
+ }
+ if (canConnect(1, 0) && (!(isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0) && canConnect(1, -1) && canConnect(1, 1)) || splock)) {
+ context.fillRect((c + .7) * tileSize, (r + .26) * tileSize, tileSize * .4, tileSize * .48);
+ boltBool = true;
+ }
+ }
+
+ var spikeSize = .2;
+ context.fillStyle = spikeColors.box;
+ if (isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)) { //TOUCHING ONE
+ roundRect(context, (c + spikeSize) * tileSize, r * tileSize, tileSize * .6, tileSize * .8, 0, true, false);
+ boltBool = true;
+ }
+ else if (!isOccupied(0, -1) && isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)) {
+ drawCenterSpikes(context, x, y, spikeWidth, color2);
+ roundRect(context, (c + spikeSize) * tileSize, (r + spikeSize) * tileSize, tileSize * 1.05, tileSize * .6, 0, true, false);
+ boltBool = true;
+ }
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0)) {
+ drawMiddleSpikes(context, x, y, spikeWidth, color2);
+ roundRect(context, (c + spikeSize) * tileSize, (r + spikeSize) * tileSize, tileSize * .6, tileSize * 1.05, 0, true, false);
+ boltBool = true;
+ }
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && isOccupied(-1, 0)) {
+ roundRect(context, c * tileSize, (r + spikeSize) * tileSize, tileSize * .8, tileSize * .6, 0, true, false);
+ boltBool = true;
+ }
+ else if (isOccupied(0, -1) && isOccupied(1, 0) && !isOccupied(0, 1) && !isOccupied(-1, 0)) { //TOUCHING TWO (CORNERS)
+ drawCenterSpikes(context, x, y, spikeWidth, color2);
+ roundRect(context, (c + spikeSize) * tileSize, r * tileSize, tileSize * .6, tileSize * .8, 0, true, false);
+ roundRect(context, (c + spikeSize) * tileSize, (r + spikeSize) * tileSize, tileSize * 1.05, tileSize * .6, 0, true, false);
+ if (!skip && !canConnect(1, -1)) boltBool = true;
+ }
+ else if (isOccupied(0, -1) && !isOccupied(1, 0) && !isOccupied(0, 1) && isOccupied(-1, 0)) {
+ roundRect(context, c * tileSize, (r + spikeSize) * tileSize, tileSize * .8, tileSize * .6, 0, true, false);
+ roundRect(context, (c + spikeSize) * tileSize, r * tileSize, tileSize * .6, tileSize * .8, 0, true, false);
+ if (!skip && !canConnect(-1, -1)) boltBool = true;
+ }
+ else if (!isOccupied(0, -1) && isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0)) {
+ drawMiddleSpikes(context, x, y, spikeWidth, color2);
+ drawCenterSpikes(context, x, y, spikeWidth, color2);
+ roundRect(context, (c + spikeSize) * tileSize, (r + spikeSize) * tileSize, tileSize * 1.05, tileSize * .6, 0, true, false);
+ roundRect(context, (c + spikeSize) * tileSize, (r + spikeSize) * tileSize, tileSize * .6, tileSize * 1.05, 0, true, false);
+ if (!skip && !canConnect(1, 1)) boltBool = true;
+ }
+ else if (!isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && isOccupied(-1, 0)) {
+ drawMiddleSpikes(context, x, y, spikeWidth, color2);
+ roundRect(context, c * tileSize, (r + spikeSize) * tileSize, tileSize * .8, tileSize * .6, 0, true, false);
+ roundRect(context, (c + spikeSize) * tileSize, (r + spikeSize) * tileSize, tileSize * .6, tileSize * 1.05, 0, true, false);
+ if (!skip && !canConnect(-1, 1)) boltBool = true;
+ }
+ else if (isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0)) { //TOUCHING TWO (OPPOSITES)
+ drawMiddleSpikes(context, x, y, spikeWidth, color2);
+ roundRect(context, (c + spikeSize) * tileSize, r * tileSize, tileSize * .6, tileSize * 1.2, 0, true, false);
+ }
+ else if (!isOccupied(0, -1) && isOccupied(1, 0) && !isOccupied(0, 1) && isOccupied(-1, 0)) {
+ drawCenterSpikes(context, x, y, spikeWidth, color2);
+ roundRect(context, c * tileSize, (r + spikeSize) * tileSize, tileSize * 1.2, tileSize * .6, 0, true, false);
+ }
+ else if (isOccupied(0, -1) && isOccupied(1, 0) && isOccupied(0, 1) && !isOccupied(-1, 0)) { //TOUCHING THREE
+ drawMiddleSpikes(context, x, y, spikeWidth, color2);
+ drawCenterSpikes(context, x, y, spikeWidth, color2);
+ context.fillStyle = spikeColors.box;
+ roundRect(context, (c + spikeSize) * tileSize, r * tileSize, tileSize * .6, tileSize * 1.1, 0, true, false);
+ roundRect(context, (c + spikeSize) * tileSize, (r + spikeSize) * tileSize, tileSize * 1.05, tileSize * .6, 0, true, false);
+ boltBool = true;
+ }
+ else if (isOccupied(0, -1) && isOccupied(1, 0) && !isOccupied(0, 1) && isOccupied(-1, 0)) {
+ drawCenterSpikes(context, x, y, spikeWidth, color2);
+ roundRect(context, c * tileSize, (r + spikeSize) * tileSize, tileSize * 1.1, tileSize * .6, 0, true, false);
+ roundRect(context, (c + spikeSize) * tileSize, r * tileSize, tileSize * .6, tileSize * .8, 0, true, false);
+ boltBool = true;
+ }
+ else if (isOccupied(0, -1) && !isOccupied(1, 0) && isOccupied(0, 1) && isOccupied(-1, 0)) {
+ drawMiddleSpikes(context, x, y, spikeWidth, color2);
+ roundRect(context, (c + spikeSize) * tileSize, r * tileSize, tileSize * .6, tileSize * 1.1, 0, true, false);
+ roundRect(context, c * tileSize, (r + spikeSize) * tileSize, tileSize * .8, tileSize * .6, 0, true, false);
+ boltBool = true;
+ }
+ else if (!isOccupied(0, -1) && isOccupied(1, 0) && isOccupied(0, 1) && isOccupied(-1, 0)) {
+ drawMiddleSpikes(context, x, y, spikeWidth, color2);
+ drawCenterSpikes(context, x, y, spikeWidth, color2);
+ roundRect(context, c * tileSize, (r + spikeSize) * tileSize, tileSize * 1.1, tileSize * .6, 0, true, false);
+ roundRect(context, (c + spikeSize) * tileSize, (r + spikeSize) * tileSize, tileSize * .6, tileSize * 1.05, 0, true, false);
+ boltBool = true;
+ }
+ else if (isOccupied(0, -1) && isOccupied(1, 0) && isOccupied(0, 1) && isOccupied(-1, 0)) { //TOUCHING FOUR
+ drawMiddleSpikes(context, x, y, spikeWidth, color2);
+ drawCenterSpikes(context, x, y, spikeWidth, color2);
+ roundRect(context, c * tileSize, (r + spikeSize) * tileSize, tileSize * 1.1, tileSize * .6, 0, true, false);
+ roundRect(context, (c + spikeSize) * tileSize, r * tileSize, tileSize * .6, tileSize * 1.1, 0, true, false);
+ //boltBool = true;
+ }
+ else {
+ roundRect(context, (c + spikeSize) * tileSize, (r + spikeSize) * tileSize, tileSize * .6, tileSize * .6, 0, true, false);
+ boltBool = true;
+ }
+ if (!skip && canConnect(1, 1) && canConnect(-1, 1) && isOccupied(-1, 0) && isOccupied(1, 0)) boltBool = false;
+
+ if (boltBool) drawBolt(context, r, c, rng, color);
+
+ function drawCenterSpikes(context, x, y, spikeWidth, fillStyle) {
+ context.save();
+ context.fillStyle = fillStyle;
+ context.beginPath();
+ context.moveTo(x + tileSize * .8 * spikeWidth, y + tileSize * 0.3);
+ context.lineTo(x + tileSize * .9 * spikeWidth, y + tileSize * 0.0);
+ context.lineTo(x + tileSize * 1 * spikeWidth, y + tileSize * 0.3);
+ context.closePath();
+ context.fill();
+
+ context.beginPath();
+ context.moveTo(x + tileSize * .8 * spikeWidth, y + tileSize * 0.7);
+ context.lineTo(x + tileSize * .9 * spikeWidth, y + tileSize * 1.0);
+ context.lineTo(x + tileSize * 1 * spikeWidth, y + tileSize * 0.7);
+ context.closePath();
+ context.fill();
+ context.restore();
+ }
+ function drawMiddleSpikes(context, x, y, spikeWidth, fillStyle) {
+ context.save();
+ context.fillStyle = fillStyle;
+ context.beginPath();
+ context.moveTo(x + tileSize * .3, y + tileSize * .8 * spikeWidth);
+ context.lineTo(x + tileSize * 0, y + tileSize * .9 * spikeWidth);
+ context.lineTo(x + tileSize * .3, y + tileSize * 1 * spikeWidth);
+ context.closePath();
+ context.fill();
+
+ context.beginPath();
+ context.moveTo(x + tileSize * .7, y + tileSize * .8 * spikeWidth);
+ context.lineTo(x + tileSize * 1, y + tileSize * .9 * spikeWidth);
+ context.lineTo(x + tileSize * .7, y + tileSize * 1 * spikeWidth);
+ context.closePath();
+ context.fill();
+ context.restore();
+ }
+}
+
+function drawBolt(context, r, c, rng, color) {
+ var splock = false;
+ if (color != undefined) splock = true;
+ context.fillStyle = splock ? color : spikeColors.bolt;
+ context.strokeStyle = spikeColors.box;
+
+ var radius = .2;
+ context.beginPath();
+ context.arc((c + .5) * tileSize, (r + .5) * tileSize, radius * tileSize, 0, 2 * Math.PI);
+ context.closePath();
+ context.fill();
+
+ context.lineWidth = tileSize / 17;
+ var x = rng() * radius + .5 - radius;
+ var y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - .5, 2)) + .5;
+ if (Math.floor(rng() * 2) == 0) y = 1 - y;
+ context.beginPath();
+ context.moveTo((c + x) * tileSize, (r + y) * tileSize);
+ context.lineTo((c + 1 - x) * tileSize, (r + 1 - y) * tileSize);
+ context.closePath();
+ context.stroke();
+}
+
+function drawRainbow(context, r, c, adjacentTiles) {
+ newRainbow(context, r, c, isRainbow);
+
+ function isRainbow(dc, dr) {
+ var tileCode = adjacentTiles[1 + dr][1 + dc];
+ return tileCode == null || tileCode === RAINBOW;
+ }
+}
+
+function newRainbow(context, r, c, isOccupied) {
+
+ var x1 = (isOccupied(-1, 0)) ? 0 : .05;
+ var x2 = (isOccupied(1, 0)) ? 0 : .05;
+
+ var rainbowColors = ["#ffcccc", "#ffe0cc", "#ffffcc", "#e6ffe6", "#e6e6ff"];
+ context.lineWidth = tileSize / 17;
+ for (var i = 0; i < 5; i++) {
+ context.beginPath();
+ context.arc((c + .5) * tileSize, (r + .5) * tileSize, tileSize / 2 - context.lineWidth * (i + .5), Math.PI, 2 * Math.PI);
+ context.strokeStyle = rainbowColors[i];
+ context.stroke();
+ }
+
+ // for (var i = 0; i < 5; i++) {
+ // var j = (i != 0) ? i - 1 : 0;
+ // context.beginPath();
+ // context.moveTo(c * tileSize + tileSize * (i * x1), r * tileSize + tileSize * (.05 + (.1 * i) - (j * .02)));
+ // context.strokeStyle = rainbowColors[i];
+ // var lw = tileSize * (.1 - (i * .02));
+ // context.lineWidth = lw;
+ // context.lineTo(c * tileSize + tileSize * (1 - (i * x2)), r * tileSize + tileSize * (.05 + (.1 * i) - (j * .02)));
+ // context.stroke();
+ // }
+}
+
+function drawTrellis(context, r, c) {
+ context.fillStyle = "white";
+ roundRect(context, (c - .05) * tileSize, r * tileSize, tileSize, tileSize * .1, 0, true, false);
+ roundRect(context, (c - .05) * tileSize, r * tileSize, tileSize * .1, tileSize, 0, true, false);
+ roundRect(context, (c + .95) * tileSize, r * tileSize, tileSize * .1, tileSize, 0, true, false);
+ roundRect(context, (c - .05) * tileSize, (r - .2) * tileSize, tileSize * .1, tileSize * .2, 0, true, false);
+ roundRect(context, (c + .95) * tileSize, (r - .2) * tileSize, tileSize * .1, tileSize * .2, 0, true, false);
+}
+
+function drawPortal(context, r, c, location) {
+ var whitePortal = isSnakeOnPortal();
+ var activePortalLocations = getActivePortalLocations();
+ context.save();
+ if (!whitePortal && activePortalLocations.indexOf(location) !== -1) {
+ context.fillStyle = "black";
+ context.strokeStyle = "purple";
+ context.shadowColor = "purple";
+ context.shadowBlur = 5;
+ context.lineWidth = 3;
+ context.beginPath();
+ context.arc(c * tileSize + tileSize / 2, r * tileSize + tileSize / 2, tileSize / 1.9, 0, 2 * Math.PI);
+ context.closePath();
+ context.fill();
+ context.stroke();
+ }
+ else if (whitePortal) {
+ context.fillStyle = "white";
+ context.strokeStyle = "purple";
+ context.shadowColor = "purple";
+ context.shadowBlur = 5;
+ context.lineWidth = 3;
+ context.beginPath();
+ context.arc(c * tileSize + tileSize / 2, r * tileSize + tileSize / 2, tileSize / 3, 0, 2 * Math.PI);
+ context.closePath();
+ context.fill();
+ context.stroke();
+ }
+ else drawCircle(context, r, c, 1, "#777");
+ context.restore();
+}
+
+function drawTurnstile(context, r, c, dc) {
+ if (dc == -1) {
+ var x1 = c * tileSize;
+ var x2 = (c + 1) * tileSize;
+ var y1 = r * tileSize;
+ var y2 = (r + 1) * tileSize;
+ var start = c * tileSize;
+ var light1 = (c + .1) * tileSize;
+ var light2 = (c + .4) * tileSize;
+ var lineStart = (c + .25) * tileSize;
+ var lineEnd = (c + .65) * tileSize;
+ }
+ else if (dc == 1) {
+ var x2 = c * tileSize;
+ var x1 = (c + 1) * tileSize;
+ var y1 = r * tileSize;
+ var y2 = (r + 1) * tileSize;
+ var start = (c + .5) * tileSize;
+ var light1 = (c + .9) * tileSize;
+ var light2 = (c + .6) * tileSize;
+ var lineStart = (c + .75) * tileSize;
+ var lineEnd = (c + .35) * tileSize;
+ }
+
+ var grd = context.createLinearGradient(x1, y1, x2, y2);
+ grd.addColorStop(0, "#d9d9d9");
+ grd.addColorStop(1 / 3, "#ffffff");
+ grd.addColorStop(1, "#d9d9d9");
+ context.fillStyle = grd;
+ context.strokeStyle = "white";
+
+ roundRect(context, start, r * tileSize, tileSize * .5, tileSize, 2, true, false);
+
+ //lights
+ context.lineWidth = 3;
+ context.beginPath();
+ context.fillStyle = "red";
+ context.arc(light1, (r + .11) * tileSize, tileSize / 15, 0, 2 * Math.PI);
+ context.fill();
+ context.beginPath();
+ context.fillStyle = "green";
+ context.arc(light2, (r + .11) * tileSize, tileSize / 14, 0, 2 * Math.PI);
+ context.fill();
+
+ //spokes
+ context.strokeStyle = context.fillStyle = "#555";
+ context.lineWidth = 1.5;
+ for (var i = .3; i < 1; i += .15) {
+ context.beginPath();
+ context.moveTo(lineStart, (r + i) * tileSize);
+ context.lineTo(lineEnd, (r + i) * tileSize);
+ context.closePath();
+ context.stroke();
+ context.arc(light1, (r + i) * tileSize, tileSize / 24, 0, 2 * Math.PI)
+ context.fill();
+ }
+}
+
+function drawOneWayWall(context, r, c, dr, dc) {
+ context.lineWidth = 2;
+ context.strokeStyle = "#333";
+ context.fillStyle = "orange";
+ context.beginPath();
+ if (dr == -1) {
+ context.moveTo(c * tileSize, r * tileSize + tileSize / 2);
+ context.lineTo(c * tileSize + tileSize / 4, r * tileSize + tileSize / 4);
+ context.stroke();
+ context.moveTo(c * tileSize + 3 * tileSize / 4, r * tileSize + tileSize / 4);
+ context.lineTo((c + 1) * tileSize, r * tileSize + tileSize / 2);
+ }
+ else if (dr == 1) {
+ context.moveTo(c * tileSize, r * tileSize + tileSize / 2);
+ context.lineTo(c * tileSize + tileSize / 4, r * tileSize + 3 * tileSize / 4);
+ context.stroke();
+ context.moveTo(c * tileSize + 3 * tileSize / 4, r * tileSize + 3 * tileSize / 4);
+ context.lineTo((c + 1) * tileSize, r * tileSize + tileSize / 2);
+ }
+ else if (dc == -1) {
+ context.moveTo(c * tileSize + tileSize / 2, r * tileSize);
+ context.lineTo(c * tileSize + tileSize / 4, r * tileSize + tileSize / 4);
+ context.stroke();
+ context.moveTo(c * tileSize + tileSize / 4, r * tileSize + 3 * tileSize / 4);
+ context.lineTo(c * tileSize + tileSize / 2, r * tileSize + tileSize);
+ }
+ else if (dc == 1) {
+ context.moveTo(c * tileSize + tileSize / 2, r * tileSize);
+ context.lineTo(c * tileSize + 3 * tileSize / 4, r * tileSize + tileSize / 4);
+ context.stroke();
+ context.moveTo(c * tileSize + 3 * tileSize / 4, r * tileSize + 3 * tileSize / 4);
+ context.lineTo(c * tileSize + tileSize / 2, r * tileSize + tileSize);
+ }
+ context.stroke();
+ context.lineWidth = 0;
+
+ if (dr == -1) roundRect(context, (c - 1 / 15) * tileSize, (r - 1 / 15) * tileSize, tileSize + 2 * tileSize / 15, tileSize / 4 + 2 * tileSize / 15, tileSize / 17, true, false);
+ else if (dr == 1) roundRect(context, (c - 1 / 15) * tileSize, (r + 1 - 1 / 15 - 1 / 4) * tileSize, tileSize + 2 * tileSize / 15, tileSize / 4 + 2 * tileSize / 15, tileSize / 17, true, false);
+ else if (dc == -1) roundRect(context, (c - 1 / 15) * tileSize, r * tileSize - tileSize / 15, tileSize / 4 + 2 * tileSize / 15, tileSize + 2 * tileSize / 15, tileSize / 17, true, false);
+ else if (dc == 1) roundRect(context, (c + 1 - 1 / 15 - 1 / 4) * tileSize, r * tileSize - tileSize / 15, tileSize / 4 + 2 * tileSize / 15, tileSize + 2 * tileSize / 15, tileSize / 17, true, false);
+}
+
+/* var grd = context.createLinearGradient(c * tileSize, r * tileSize, (c + 1) * tileSize, (r + 1) * tileSize);
+grd.addColorStop(0, "rgba(255,255,255,.4)");
+grd.addColorStop(.1, "rgba(255,255,255,.5)");
+grd.addColorStop(.2, "rgba(255,255,255,.3)");
+grd.addColorStop(.3, "rgba(255,255,255,.4)");
+grd.addColorStop(.4, "rgba(255,255,255,.6)");
+grd.addColorStop(.5, "rgba(255,255,255,.5)");
+grd.addColorStop(.6, "rgba(255,255,255,.3)");
+grd.addColorStop(.7, "rgba(255,255,255,.4)");
+grd.addColorStop(.8, "rgba(255,255,255,.5)");
+grd.addColorStop(.9, "rgba(255,255,255,.6)");
+grd.addColorStop(1, "rgba(255,255,255,.5)");
+
+context.fillStyle = grd;
+roundRect(context, c * tileSize, r * tileSize, tileSize, tileSize, 2, true, false);
+context.save();
+var color1 = color2 = color3 = color4 = "transparent";
+
+var preventColor = "rgba(255,0,0,.5)";
+
+context.fillStyle = "black";
+if (type === 0) {
+ color1 = "black";
+}
+else if (type === 1) {
+ // color1 = "red";
+ context.beginPath();
+ context.moveTo((c + .4) * tileSize, (r + .2) * tileSize);
+ context.lineTo((c + .4) * tileSize, (r + .55) * tileSize);
+ context.lineTo((c + .3) * tileSize, (r + .55) * tileSize);
+ context.lineTo((c + .5) * tileSize, (r + .8) * tileSize);
+ context.lineTo((c + .7) * tileSize, (r + .55) * tileSize);
+ context.lineTo((c + .6) * tileSize, (r + .55) * tileSize);
+ context.lineTo((c + .6) * tileSize, (r + .2) * tileSize);
+ context.lineTo((c + .4) * tileSize, (r + .2) * tileSize);
+ context.closePath();
+ context.fill();
+}
+else if (type === 2) {
+ // color3 = "red";
+ context.beginPath();
+ context.moveTo((c + .6) * tileSize, (r + .8) * tileSize);
+ context.lineTo((c + .6) * tileSize, (r + .45) * tileSize);
+ context.lineTo((c + .7) * tileSize, (r + .45) * tileSize);
+ context.lineTo((c + .5) * tileSize, (r + .2) * tileSize);
+ context.lineTo((c + .3) * tileSize, (r + .45) * tileSize);
+ context.lineTo((c + .4) * tileSize, (r + .45) * tileSize);
+ context.lineTo((c + .4) * tileSize, (r + .8) * tileSize);
+ context.lineTo((c + .6) * tileSize, (r + .8) * tileSize);
+ context.closePath();
+ context.fill();
+}
+else if (type === 3) {
+ // color4 = "red";
+ context.beginPath();
+ context.moveTo((c + .2) * tileSize, (r + .6) * tileSize);
+ context.lineTo((c + .55) * tileSize, (r + .6) * tileSize);
+ context.lineTo((c + .55) * tileSize, (r + .7) * tileSize);
+ context.lineTo((c + .8) * tileSize, (r + .5) * tileSize);
+ context.lineTo((c + .55) * tileSize, (r + .3) * tileSize);
+ context.lineTo((c + .55) * tileSize, (r + .4) * tileSize);
+ context.lineTo((c + .2) * tileSize, (r + .4) * tileSize);
+ context.lineTo((c + .2) * tileSize, (r + .6) * tileSize);
+ context.closePath();
+ context.fill();
+}
+else if (type === 4) {
+ // color2 = "red";
+ context.beginPath();
+ context.moveTo((c + .8) * tileSize, (r + .6) * tileSize);
+ context.lineTo((c + .45) * tileSize, (r + .6) * tileSize);
+ context.lineTo((c + .45) * tileSize, (r + .7) * tileSize);
+ context.lineTo((c + .2) * tileSize, (r + .5) * tileSize);
+ context.lineTo((c + .45) * tileSize, (r + .3) * tileSize);
+ context.lineTo((c + .45) * tileSize, (r + .4) * tileSize);
+ context.lineTo((c + .8) * tileSize, (r + .4) * tileSize);
+ context.lineTo((c + .8) * tileSize, (r + .6) * tileSize);
+ context.closePath();
+ context.fill();
+}
+
+context.strokeStyle = "red";
+context.lineWidth = 2;
+context.beginPath();
+context.arc((c + .5) * tileSize, (r + .5) * tileSize, tileSize / 3, 0, 2 * Math.PI);
+context.closePath();
+context.stroke();
+
+context.beginPath();
+context.moveTo((c + .3) * tileSize, (r + .3) * tileSize);
+context.lineTo((c + .7) * tileSize, (r + .7) * tileSize);
+context.closePath();
+context.stroke();
+
+context.shadowBlur = 3;
+context.lineWidth = 3;
+context.strokeStyle = color1;
+context.beginPath();
+context.moveTo(c * tileSize, r * tileSize);
+context.lineTo((c + 1) * tileSize, r * tileSize);
+context.closePath();
+context.stroke();
+context.strokeStyle = color2;
+context.beginPath();
+context.moveTo((c + 1) * tileSize, r * tileSize);
+context.lineTo((c + 1) * tileSize, (r + 1) * tileSize);
+context.closePath();
+context.stroke();
+context.strokeStyle = color3;
+context.beginPath();
+context.moveTo((c + 1) * tileSize, (r + 1) * tileSize);
+context.lineTo(c * tileSize, (r + 1) * tileSize);
+context.closePath();
+context.stroke();
+context.strokeStyle = color4;
+context.beginPath();
+context.moveTo(c * tileSize, (r + 1) * tileSize);
+context.lineTo(c * tileSize, r * tileSize);
+context.closePath();
+context.stroke();
+context.restore(); */
+
+function drawBubble(context, r, c) {
+ bubbleX = c * tileSize;
+ var grd = context.createRadialGradient(bubbleX, r * tileSize, 0, bubbleX, r * tileSize, tileSize);
+ grd.addColorStop(0, "rgba(255,255,255,1)");
+ grd.addColorStop(.5, "rgba(220,255,255,.3)");
+ grd.addColorStop(1, "rgba(0,100,255,.12)");
+ context.fillStyle = grd;
+
+ context.beginPath();
+ context.arc((c + .5) * tileSize, (r + .5) * tileSize, tileSize / 1.8, 0, 2 * Math.PI);
+ context.fill();
+}
+
+function drawLiquid(context, r, c, type, adjacentTiles) {
+ context.save();
+ var tubColor;
+ var color1;
+ var color2;
+ var color3;
+ if (type == LAVA) {
+ color1 = "red";
+ color2 = "#e69900";
+ color3 = "#111";
+ context.strokeStyle = color1;
+ context.shadowColor = color1;
+ tubColor = "black";
+ }
+ else {
+ color1 = "#80ffe5";
+ color2 = "#1a8cff";
+ color3 = "white";
+ context.strokeStyle = color1;
+ context.shadowColor = "white";
+ tubColor = "white";
+ }
+
+ context.fillStyle = color3;
+ roundRect(context, c * tileSize, r * tileSize, tileSize, tileSize, 0, true, false);
+
+ context.shadowBlur = 10;
+ context.lineWidth = 3.5;
+ for (var i = .1; i <= .8; i += .1) {
+ var mod = Math.round((i + .1) * 10) % 3;
+ switch (mod) {
+ case 0: context.strokeStyle = color2; break;
+ case 1: context.strokeStyle = color3; break;
+ case 2: context.strokeStyle = color1; break;
+ }
+ context.beginPath();
+ context.moveTo(c * tileSize, (r + i + .1) * tileSize);
+ context.bezierCurveTo((c + 1 / 6) * tileSize, (r + i) * tileSize, (c + 1 / 3) * tileSize, (r + i) * tileSize, (c + 1 / 2) * tileSize, (r + i + .1) * tileSize);
+ context.bezierCurveTo((c + 2 / 3) * tileSize, (r + i + .2) * tileSize, (c + 5 / 6) * tileSize, (r + i + .2) * tileSize, (c + 1) * tileSize, (r + i + .1) * tileSize);
+ context.stroke();
+ context.shadowBlur = 0;
+ context.shadowColor = "transparent";
+ }
+
+ context.fillStyle = color3;
+ if (!isSameLiquid(-1, 0)) {
+ if (isSameLiquid(-1, -1)) roundRect(context, (c - .1) * tileSize, r * tileSize, tileSize * .2, tileSize, 0, true, false);
+ else roundRect(context, (c - .1) * tileSize, r * tileSize, tileSize * .2, tileSize, 0, true, false);
+ }
+ if (!isSameLiquid(1, 0)) {
+ if (isSameLiquid(1, -1)) roundRect(context, (c + .9) * tileSize, r * tileSize, tileSize * .2, tileSize, 0, true, false);
+ else roundRect(context, (c + .9) * tileSize, r * tileSize, tileSize * .2, tileSize, 0, true, false);
+ }
+ if (!isSameLiquid(0, 1)) {
+ if (isSameLiquid(-1, 1) && !isSameLiquid(1, 1)) roundRect(context, (c - .1) * tileSize, (r + .8) * tileSize, tileSize * 1.2, tileSize * .25, 0, true, false);
+ else if (!isSameLiquid(-1, 1) && isSameLiquid(1, 1)) roundRect(context, (c - .1) * tileSize, (r + .8) * tileSize, tileSize * 1.2, tileSize * .25, 0, true, false);
+ else if (isSameLiquid(-1, 1) && isSameLiquid(1, 1)) roundRect(context, (c - .1) * tileSize, (r + .8) * tileSize, tileSize * 1.2, tileSize * .25, 0, true, false);
+ else roundRect(context, (c - .1) * tileSize, (r + .8) * tileSize, tileSize * 1.2, tileSize * .25, 0, true, false);
+ }
+ context.restore();
+
+ function isSameLiquid(dc, dr) {
+ var tileCode = adjacentTiles[1 + dr][1 + dc];
+ return tileCode == null || tileCode === type;
+ }
+}
+
+function drawLift(context, r, c, isOpen) {
+ context.lineWidth = .5;
+ context.strokeStyle = "#777";
+ var strokeBool = false;
+ if (!isOpen) {
+ context.fillStyle = "#e68a00";
+ roundRect(context, (c + .05) * tileSize, r * tileSize + tileSize, tileSize * .9, tileSize * .2, 2, true, strokeBool);
+ context.fillStyle = "#cc0000";
+ roundRect(context, (c + .3) * tileSize, (r + .8) * tileSize, tileSize * .4, tileSize * .2, { tl: 2, tr: 2 }, true, strokeBool);
+ }
+ else if (isOpen) {
+ context.fillStyle = "#e68a00";
+ roundRect(context, (c + .05) * tileSize, (r + .8) * tileSize, tileSize * .9, tileSize * .2, tileSize / 17, true, strokeBool);
+ roundRect(context, (c + .05) * tileSize, r * tileSize, tileSize * .9, tileSize * .2, tileSize / 17, true, strokeBool);
+
+ if (themeName != "Midnight Rainbow") context.fillStyle = "#333";
+ else context.fillStyle = "gainsboro";
+
+ context.beginPath();
+ context.moveTo((c + .9) * tileSize, (r + .8) * tileSize);
+ context.lineTo((c + .3) * tileSize, (r + .5) * tileSize);
+ context.lineTo((c + .9) * tileSize, (r + .2) * tileSize);
+ context.lineTo((c + .7) * tileSize, (r + .2) * tileSize);
+ context.lineTo((c + .2) * tileSize, (r + .45) * tileSize);
+ context.bezierCurveTo((c + .16) * tileSize, (r + .45) * tileSize, (c + .16) * tileSize, (r + .55) * tileSize, (c + .2) * tileSize, (r + .55) * tileSize);
+ context.lineTo((c + .7) * tileSize, (r + .8) * tileSize);
+ context.lineTo((c + .9) * tileSize, (r + .8) * tileSize);
+ context.closePath();
+ context.fill();
+
+ context.beginPath();
+ context.moveTo((c + .1) * tileSize, (r + .8) * tileSize);
+ context.lineTo((c + .7) * tileSize, (r + .5) * tileSize);
+ context.lineTo((c + .1) * tileSize, (r + .2) * tileSize);
+ context.lineTo((c + .3) * tileSize, (r + .2) * tileSize);
+ context.lineTo((c + .8) * tileSize, (r + .45) * tileSize);
+ context.bezierCurveTo((c + .84) * tileSize, (r + .45) * tileSize, (c + .84) * tileSize, (r + .55) * tileSize, (c + .8) * tileSize, (r + .55) * tileSize);
+ context.lineTo((c + .3) * tileSize, (r + .8) * tileSize);
+ context.lineTo((c + .1) * tileSize, (r + .8) * tileSize);
+ context.closePath();
+ context.fill();
+ }
+}
+
+function drawQuarterPie(context, 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 drawCircle(context, 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 drawCloud(c, x, y, adjacentTiles, rng) {
+ c.save();
+ c.fillStyle = experimentalColors[0];
+ c.shadowOffsetX = 5;
+ c.shadowOffsetY = 5;
+ c.shadowColor = "rgba(0,0,0, .03)";
+
+ c.beginPath();
+ c.moveTo(x + tileSize * 0, y + tileSize * 0);
+
+ c.bezierCurveTo(x + tileSize * 0, y - tileSize * .15, x + tileSize * .33, y - tileSize * .15, x + tileSize * .33, y + tileSize * 0);
+ c.bezierCurveTo(x + tileSize * .33, y - tileSize * .15, x + tileSize * .67, y - tileSize * .15, x + tileSize * .67, y + tileSize * 0);
+ c.bezierCurveTo(x + tileSize * .67, y - tileSize * .15, x + tileSize * 1, y - tileSize * .15, x + tileSize * 1, y + tileSize * 0);
+
+ c.bezierCurveTo(x + tileSize * 1.15, y + tileSize * 0, x + tileSize * 1.15, y + tileSize * .33, x + tileSize * 1, y + tileSize * .33);
+ c.bezierCurveTo(x + tileSize * 1.15, y + tileSize * .33, x + tileSize * 1.15, y + tileSize * .67, x + tileSize * 1, y + tileSize * .67);
+ c.bezierCurveTo(x + tileSize * 1.15, y + tileSize * .67, x + tileSize * 1.15, y + tileSize * 1, x + tileSize * 1, y + tileSize * 1);
+
+ c.bezierCurveTo(x + tileSize * 1, y + tileSize * 1.15, x + tileSize * .67, y + tileSize * 1.15, x + tileSize * .67, y + tileSize * 1);
+ c.bezierCurveTo(x + tileSize * .67, y + tileSize * 1.15, x + tileSize * .33, y + tileSize * 1.15, x + tileSize * .33, y + tileSize * 1);
+ c.bezierCurveTo(x + tileSize * .33, y + tileSize * 1.15, x + tileSize * 0, y + tileSize * 1.15, x + tileSize * 0, y + tileSize * 1);
+
+ c.closePath();
+ c.fill();
+ c.restore();
+ c.save();
+
+ if (!isCloud(-1, 0)) {
+ c.fillStyle = experimentalColors[0];
+ c.beginPath();
+ c.moveTo(x + tileSize * 0, y + tileSize * 1);
+ c.bezierCurveTo(x - tileSize * .15, y + tileSize * 1, x - tileSize * .15, y + tileSize * .67, x + tileSize * 0, y + tileSize * .67);
+ c.bezierCurveTo(x - tileSize * .15, y + tileSize * .67, x - tileSize * .15, y + tileSize * .33, x + tileSize * 0, y + tileSize * .33);
+ c.bezierCurveTo(x - tileSize * .15, y + tileSize * .33, x - tileSize * .15, y + tileSize * 0, x + tileSize * 0, y + tileSize * 0);
+ if (isWall(-1, 0)) {
+ c.shadowOffsetX = -2;
+ c.shadowOffsetY = 2;
+ c.shadowColor = "rgba(0,0,0, .03)";
+ }
+ c.fill();
+ }
+ c.restore();
+
+ function isCloud(dc, dr) {
+ var tileCode = adjacentTiles[1 + dr][1 + dc];
+ return tileCode == null || tileCode === CLOUD;
+ }
+ function isWall(dc, dr) {
+ var tileCode = adjacentTiles[1 + dr][1 + dc];
+ return tileCode == null || tileCode === WALL;
+ }
+}
+
+function roundRect(ctx, x, y, width, height, radius, fill, stroke) {
+ 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 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 | MOVE_MIKE
+ "t" + objectType, // TELEPORT_SNAKE | TELEPORT_BLOCK | TELEPORT_MIKE
+ ];
+ // 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 compareLocations(a, b) {
+ return operatorCompare(a.locations[0], b.locations[0]);
+}
+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" && hashPairs[0][0] !== "sv") return false;
+ if (hashPairs[0][0] === "sv") {
+ sv = true;
+ canvas7.style.display = "block";
+ document.getElementById("ghostEditorPane").style.display = "none";
+ document.getElementById("editorPane").style.display = "none";
+ document.getElementById("bottomEverything").style.display = "none";
+ document.getElementById("csText").style.display = "block";
+ }
+
+ 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));
+}
diff --git a/README.md b/README.md
deleted file mode 100644
index 67276736..00000000
--- a/README.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Snakefall
-
-A [Snakebird](http://snakebird.noumenongames.com/) clone with a level editor.
-
-If you haven't played Snakebird yet, go buy it and play it. It's great!
-This project is not trying to compete with the original game,
-but instead tries to realise even more potential inherent in the genius
-of the original game engine design.
-
-This project enables players to create their own levels and share them with others.
-
-## Demo
-
-[http://wolfesoftware.com/snakefall/](http://wolfesoftware.com/snakefall/)
-
-And check out some levels people have made:
-
-[Snakefall Wiki](https://github.com/thejoshwolfe/snakefall/wiki)
-
-## Bugs and Ideas
-
-See the [issue tracker](https://github.com/thejoshwolfe/snakefall/issues).
-
-## Version History
-
-#### 1.1.0
-
-* Ability to share replays. ([issue #9](https://github.com/thejoshwolfe/snakefall/issues/9))
-* Redoing normal movement shows the animation just like if you were playing.
-* Remove "playtest" button and overhaul dirty states.
-* Use semver-like version numbers instead of just linking to the git hash.
-* Finally add a link to the wiki on the actual game page.
-
-#### 1.0.0
-
-* Game Engine:
- * Everything from the original game (except that the left, right, and top border of the map are impassable).
- * Undo and redo movement. Repeatedly hitting redo after a reset will effectively show a replay.
- * Arbitrarily many snakes. A fourth snake color (yellow). More hotkeys for switching snakes.
- * Size-1 snakes.
-* Editor:
- * Edit the game while it's running.
- * Undo/redo edits independently of undo/redo normal movement. Can create time travel paradoxes.
- * Resize the world.
- * Select/Cut/Copy/Paste (but not between browser tabs).
- * Cheatcodes to turn off gravity and collision detection (noclip) while editing.
- * Share levels with a url that encodes the level. No server-side saving (because there's no server at all).
-
diff --git a/a.js b/a.js
deleted file mode 100644
index bdcf7822..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("You Win!", 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));
-}
diff --git a/experimental/index.html b/experimental/index.html
deleted file mode 100644
index 05d498ae..00000000
--- a/experimental/index.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
- Snakefall Experimental Index
-
-
-
- List of experimental versions of Snakefall.
-
-
-
-
diff --git a/index.html b/index.html
index 358a21d9..857464fc 100644
--- a/index.html
+++ b/index.html
@@ -1,77 +1,932 @@
-
- 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.
-
-
-
+
+ Snakefall
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Snakefall Redesign
+
+
+
+
+
+
+
+
+
+
+
+