diff --git a/assets/images/game-icons.png b/assets/images/game-icons.png index 05694aa..6ceac24 100644 Binary files a/assets/images/game-icons.png and b/assets/images/game-icons.png differ diff --git a/assets/images/game-icons.psd b/assets/images/game-icons.psd index 4217d1c..6af4c01 100644 Binary files a/assets/images/game-icons.psd and b/assets/images/game-icons.psd differ diff --git a/assets/images/ta/arm-units.png b/assets/images/ta/arm-units.png new file mode 100644 index 0000000..e957512 Binary files /dev/null and b/assets/images/ta/arm-units.png differ diff --git a/assets/images/ta/arm-units.psd b/assets/images/ta/arm-units.psd new file mode 100644 index 0000000..af31e4d Binary files /dev/null and b/assets/images/ta/arm-units.psd differ diff --git a/data/bloxorz.json b/data/bloxorz.json index 740215e..c692a53 100644 --- a/data/bloxorz.json +++ b/data/bloxorz.json @@ -1,179 +1,11 @@ { - "generatedAt": "2026-07-25T14:50:43.614Z", + "generatedAt": "2026-07-25T19:24:10.533Z", "seed": null, "count": 36, "levels": [ { "level": 1, - "name": "Warm Up", - "cols": 6, - "rows": 5, - "start": { - "x": 1, - "y": 1, - "orient": "up" - }, - "goal": { - "x": 4, - "y": 3 - }, - "tiles": [ - { - "x": 0, - "y": 0, - "type": "floor" - }, - { - "x": 1, - "y": 0, - "type": "floor" - }, - { - "x": 2, - "y": 0, - "type": "floor" - }, - { - "x": 3, - "y": 0, - "type": "floor" - }, - { - "x": 4, - "y": 0, - "type": "floor" - }, - { - "x": 5, - "y": 0, - "type": "floor" - }, - { - "x": 0, - "y": 1, - "type": "floor" - }, - { - "x": 1, - "y": 1, - "type": "floor" - }, - { - "x": 2, - "y": 1, - "type": "floor" - }, - { - "x": 3, - "y": 1, - "type": "floor" - }, - { - "x": 4, - "y": 1, - "type": "floor" - }, - { - "x": 5, - "y": 1, - "type": "floor" - }, - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 0, - "y": 3, - "type": "floor" - }, - { - "x": 1, - "y": 3, - "type": "floor" - }, - { - "x": 2, - "y": 3, - "type": "floor" - }, - { - "x": 3, - "y": 3, - "type": "floor" - }, - { - "x": 4, - "y": 3, - "type": "goal" - }, - { - "x": 5, - "y": 3, - "type": "floor" - }, - { - "x": 0, - "y": 4, - "type": "floor" - }, - { - "x": 1, - "y": 4, - "type": "floor" - }, - { - "x": 2, - "y": 4, - "type": "floor" - }, - { - "x": 3, - "y": 4, - "type": "floor" - }, - { - "x": 4, - "y": 4, - "type": "floor" - }, - { - "x": 5, - "y": 4, - "type": "floor" - } - ], - "par": 4 - }, - { - "level": 2, - "name": "Stretch Out", + "name": "First Roll", "cols": 7, "rows": 5, "start": { @@ -365,9 +197,9 @@ "par": 6 }, { - "level": 3, - "name": "Open Floor", - "cols": 8, + "level": 2, + "name": "Mind the Gap", + "cols": 9, "rows": 6, "start": { "x": 1, @@ -375,8 +207,8 @@ "orient": "up" }, "goal": { - "x": 6, - "y": 4 + "x": 7, + "y": 5 }, "tiles": [ { @@ -419,6 +251,11 @@ "y": 0, "type": "floor" }, + { + "x": 8, + "y": 0, + "type": "floor" + }, { "x": 0, "y": 1, @@ -459,6 +296,11 @@ "y": 1, "type": "floor" }, + { + "x": 8, + "y": 1, + "type": "floor" + }, { "x": 0, "y": 2, @@ -479,16 +321,6 @@ "y": 2, "type": "floor" }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, { "x": 6, "y": 2, @@ -499,6 +331,11 @@ "y": 2, "type": "floor" }, + { + "x": 8, + "y": 2, + "type": "floor" + }, { "x": 0, "y": 3, @@ -519,16 +356,6 @@ "y": 3, "type": "floor" }, - { - "x": 4, - "y": 3, - "type": "floor" - }, - { - "x": 5, - "y": 3, - "type": "floor" - }, { "x": 6, "y": 3, @@ -539,6 +366,11 @@ "y": 3, "type": "floor" }, + { + "x": 8, + "y": 3, + "type": "floor" + }, { "x": 0, "y": 4, @@ -572,59 +404,495 @@ { "x": 6, "y": 4, + "type": "floor" + }, + { + "x": 7, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 4, + "type": "floor" + }, + { + "x": 0, + "y": 5, + "type": "floor" + }, + { + "x": 1, + "y": 5, + "type": "floor" + }, + { + "x": 2, + "y": 5, + "type": "floor" + }, + { + "x": 3, + "y": 5, + "type": "floor" + }, + { + "x": 4, + "y": 5, + "type": "floor" + }, + { + "x": 5, + "y": 5, + "type": "floor" + }, + { + "x": 6, + "y": 5, + "type": "floor" + }, + { + "x": 7, + "y": 5, "type": "goal" }, { - "x": 7, - "y": 4, - "type": "floor" - }, - { - "x": 0, - "y": 5, - "type": "floor" - }, - { - "x": 1, - "y": 5, - "type": "floor" - }, - { - "x": 2, - "y": 5, - "type": "floor" - }, - { - "x": 3, - "y": 5, - "type": "floor" - }, - { - "x": 4, - "y": 5, - "type": "floor" - }, - { - "x": 5, - "y": 5, - "type": "floor" - }, - { - "x": 6, - "y": 5, - "type": "floor" - }, - { - "x": 7, + "x": 8, "y": 5, "type": "floor" } ], - "par": 6 + "par": 7 + }, + { + "level": 3, + "name": "Right Angle", + "cols": 5, + "rows": 8, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 3, + "y": 6 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 0, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 0, + "y": 5, + "type": "floor" + }, + { + "x": 1, + "y": 5, + "type": "floor" + }, + { + "x": 2, + "y": 5, + "type": "floor" + }, + { + "x": 3, + "y": 5, + "type": "floor" + }, + { + "x": 4, + "y": 5, + "type": "floor" + }, + { + "x": 0, + "y": 6, + "type": "floor" + }, + { + "x": 1, + "y": 6, + "type": "floor" + }, + { + "x": 2, + "y": 6, + "type": "floor" + }, + { + "x": 3, + "y": 6, + "type": "goal" + }, + { + "x": 4, + "y": 6, + "type": "floor" + }, + { + "x": 0, + "y": 7, + "type": "floor" + }, + { + "x": 1, + "y": 7, + "type": "floor" + }, + { + "x": 2, + "y": 7, + "type": "floor" + }, + { + "x": 3, + "y": 7, + "type": "floor" + }, + { + "x": 4, + "y": 7, + "type": "floor" + } + ], + "par": 8 }, { "level": 4, - "name": "The Corner", + "name": "Narrow Pass", + "cols": 12, + "rows": 5, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 9, + "y": 4 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 11, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, + "type": "floor" + }, + { + "x": 10, + "y": 1, + "type": "floor" + }, + { + "x": 11, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 5, + "y": 2, + "type": "floor" + }, + { + "x": 6, + "y": 2, + "type": "floor" + }, + { + "x": 7, + "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 2, + "type": "floor" + }, + { + "x": 11, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "floor" + }, + { + "x": 9, + "y": 3, + "type": "floor" + }, + { + "x": 10, + "y": 3, + "type": "floor" + }, + { + "x": 11, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 4, + "type": "floor" + }, + { + "x": 9, + "y": 4, + "type": "goal" + }, + { + "x": 10, + "y": 4, + "type": "floor" + }, + { + "x": 11, + "y": 4, + "type": "floor" + } + ], + "par": 11 + }, + { + "level": 5, + "name": "The Ring", "cols": 9, "rows": 7, "start": { @@ -662,6 +930,26 @@ "y": 0, "type": "floor" }, + { + "x": 5, + "y": 0, + "type": "floor" + }, + { + "x": 6, + "y": 0, + "type": "floor" + }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, { "x": 0, "y": 1, @@ -687,133 +975,98 @@ "y": 1, "type": "floor" }, - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 0, - "y": 3, - "type": "floor" - }, - { - "x": 1, - "y": 3, - "type": "floor" - }, - { - "x": 2, - "y": 3, - "type": "floor" - }, - { - "x": 3, - "y": 3, - "type": "floor" - }, - { - "x": 4, - "y": 3, - "type": "floor" - }, - { - "x": 0, - "y": 4, - "type": "floor" - }, - { - "x": 1, - "y": 4, - "type": "floor" - }, - { - "x": 2, - "y": 4, - "type": "floor" - }, - { - "x": 3, - "y": 4, - "type": "floor" - }, - { - "x": 4, - "y": 4, - "type": "floor" - }, - { - "x": 0, - "y": 5, - "type": "floor" - }, - { - "x": 1, - "y": 5, - "type": "floor" - }, - { - "x": 2, - "y": 5, - "type": "floor" - }, - { - "x": 3, - "y": 5, - "type": "floor" - }, - { - "x": 4, - "y": 5, - "type": "floor" - }, - { - "x": 0, - "y": 6, - "type": "floor" - }, - { - "x": 1, - "y": 6, - "type": "floor" - }, - { - "x": 2, - "y": 6, - "type": "floor" - }, - { - "x": 3, - "y": 6, - "type": "floor" - }, - { - "x": 4, - "y": 6, - "type": "floor" - }, { "x": 5, + "y": 1, + "type": "floor" + }, + { + "x": 6, + "y": 1, + "type": "floor" + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 6, + "y": 2, + "type": "floor" + }, + { + "x": 7, + "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 6, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "floor" + }, + { + "x": 0, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 2, "y": 4, "type": "floor" }, @@ -832,6 +1085,31 @@ "y": 4, "type": "floor" }, + { + "x": 0, + "y": 5, + "type": "floor" + }, + { + "x": 1, + "y": 5, + "type": "floor" + }, + { + "x": 2, + "y": 5, + "type": "floor" + }, + { + "x": 3, + "y": 5, + "type": "floor" + }, + { + "x": 4, + "y": 5, + "type": "floor" + }, { "x": 5, "y": 5, @@ -852,6 +1130,31 @@ "y": 5, "type": "floor" }, + { + "x": 0, + "y": 6, + "type": "floor" + }, + { + "x": 1, + "y": 6, + "type": "floor" + }, + { + "x": 2, + "y": 6, + "type": "floor" + }, + { + "x": 3, + "y": 6, + "type": "floor" + }, + { + "x": 4, + "y": 6, + "type": "floor" + }, { "x": 5, "y": 6, @@ -873,170 +1176,110 @@ "type": "floor" } ], - "par": 7 + "par": 11 }, { - "level": 5, - "name": "Crossroads", - "cols": 9, - "rows": 9, + "level": 6, + "name": "Zigzag", + "cols": 12, + "rows": 10, "start": { - "x": 4, + "x": 1, "y": 1, "orient": "up" }, "goal": { - "x": 1, - "y": 4 + "x": 10, + "y": 8 }, "tiles": [ - { - "x": 3, - "y": 0, - "type": "floor" - }, - { - "x": 4, - "y": 0, - "type": "floor" - }, - { - "x": 5, - "y": 0, - "type": "floor" - }, - { - "x": 3, - "y": 1, - "type": "floor" - }, - { - "x": 4, - "y": 1, - "type": "floor" - }, - { - "x": 5, - "y": 1, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 3, - "type": "floor" - }, - { - "x": 4, - "y": 3, - "type": "floor" - }, - { - "x": 5, - "y": 3, - "type": "floor" - }, - { - "x": 3, - "y": 4, - "type": "floor" - }, - { - "x": 4, - "y": 4, - "type": "floor" - }, - { - "x": 5, - "y": 4, - "type": "floor" - }, - { - "x": 3, - "y": 5, - "type": "floor" - }, - { - "x": 4, - "y": 5, - "type": "floor" - }, - { - "x": 5, - "y": 5, - "type": "floor" - }, - { - "x": 3, - "y": 6, - "type": "floor" - }, - { - "x": 4, - "y": 6, - "type": "floor" - }, - { - "x": 5, - "y": 6, - "type": "floor" - }, - { - "x": 3, - "y": 7, - "type": "floor" - }, - { - "x": 4, - "y": 7, - "type": "floor" - }, - { - "x": 5, - "y": 7, - "type": "floor" - }, - { - "x": 3, - "y": 8, - "type": "floor" - }, - { - "x": 4, - "y": 8, - "type": "floor" - }, - { - "x": 5, - "y": 8, - "type": "floor" - }, { "x": 0, - "y": 3, + "y": 0, "type": "floor" }, { "x": 1, - "y": 3, + "y": 0, "type": "floor" }, { "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 5, "y": 3, "type": "floor" }, @@ -1056,73 +1299,193 @@ "type": "floor" }, { - "x": 0, + "x": 3, "y": 4, "type": "floor" }, { - "x": 1, + "x": 4, "y": 4, + "type": "floor" + }, + { + "x": 5, + "y": 4, + "type": "floor" + }, + { + "x": 6, + "y": 4, + "type": "floor" + }, + { + "x": 7, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 4, + "type": "floor" + }, + { + "x": 3, + "y": 5, + "type": "floor" + }, + { + "x": 4, + "y": 5, + "type": "floor" + }, + { + "x": 5, + "y": 5, + "type": "floor" + }, + { + "x": 6, + "y": 5, + "type": "floor" + }, + { + "x": 7, + "y": 5, + "type": "floor" + }, + { + "x": 8, + "y": 5, + "type": "floor" + }, + { + "x": 6, + "y": 6, + "type": "floor" + }, + { + "x": 7, + "y": 6, + "type": "floor" + }, + { + "x": 8, + "y": 6, + "type": "floor" + }, + { + "x": 9, + "y": 6, + "type": "floor" + }, + { + "x": 10, + "y": 6, + "type": "floor" + }, + { + "x": 11, + "y": 6, + "type": "floor" + }, + { + "x": 6, + "y": 7, + "type": "floor" + }, + { + "x": 7, + "y": 7, + "type": "floor" + }, + { + "x": 8, + "y": 7, + "type": "floor" + }, + { + "x": 9, + "y": 7, + "type": "floor" + }, + { + "x": 10, + "y": 7, + "type": "floor" + }, + { + "x": 11, + "y": 7, + "type": "floor" + }, + { + "x": 6, + "y": 8, + "type": "floor" + }, + { + "x": 7, + "y": 8, + "type": "floor" + }, + { + "x": 8, + "y": 8, + "type": "floor" + }, + { + "x": 9, + "y": 8, + "type": "floor" + }, + { + "x": 10, + "y": 8, "type": "goal" }, { - "x": 2, - "y": 4, + "x": 11, + "y": 8, "type": "floor" }, { "x": 6, - "y": 4, + "y": 9, "type": "floor" }, { "x": 7, - "y": 4, + "y": 9, "type": "floor" }, { "x": 8, - "y": 4, + "y": 9, "type": "floor" }, { - "x": 0, - "y": 5, + "x": 9, + "y": 9, "type": "floor" }, { - "x": 1, - "y": 5, + "x": 10, + "y": 9, "type": "floor" }, { - "x": 2, - "y": 5, - "type": "floor" - }, - { - "x": 6, - "y": 5, - "type": "floor" - }, - { - "x": 7, - "y": 5, - "type": "floor" - }, - { - "x": 8, - "y": 5, + "x": 11, + "y": 9, "type": "floor" } ], - "par": 4 + "par": 11 }, { - "level": 6, - "name": "Staircase", - "cols": 10, - "rows": 9, + "level": 7, + "name": "Heavy Switch", + "cols": 12, + "rows": 5, "start": { "x": 1, "y": 1, @@ -1130,6 +1493,294 @@ }, "goal": { "x": 8, + "y": 2 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 11, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, + "type": "floor" + }, + { + "x": 10, + "y": 1, + "type": "floor" + }, + { + "x": 11, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 5, + "y": 2, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 6, + "y": 2, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 7, + "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "goal" + }, + { + "x": 9, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 2, + "type": "floor" + }, + { + "x": 11, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "floor" + }, + { + "x": 9, + "y": 3, + "type": "floor" + }, + { + "x": 10, + "y": 3, + "type": "floor" + }, + { + "x": 11, + "y": 3, + "type": "floor" + }, + { + "x": 0, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 2, + "y": 4, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 3, + "y": 4, + "type": "floor" + }, + { + "x": 4, + "y": 4, + "type": "floor" + }, + { + "x": 7, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 4, + "type": "floor" + }, + { + "x": 9, + "y": 4, + "type": "floor" + }, + { + "x": 10, + "y": 4, + "type": "floor" + }, + { + "x": 11, + "y": 4, + "type": "floor" + } + ], + "par": 9 + }, + { + "level": 8, + "name": "Light Touch", + "cols": 9, + "rows": 9, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 4, "y": 7 }, "tiles": [ @@ -1153,6 +1804,31 @@ "y": 0, "type": "floor" }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 5, + "y": 0, + "type": "floor" + }, + { + "x": 6, + "y": 0, + "type": "floor" + }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, { "x": 0, "y": 1, @@ -1173,6 +1849,31 @@ "y": 1, "type": "floor" }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 5, + "y": 1, + "type": "floor" + }, + { + "x": 6, + "y": 1, + "type": "floor" + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, { "x": 0, "y": 2, @@ -1186,23 +1887,19 @@ { "x": 2, "y": 2, - "type": "floor" + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "any", + "mode": "toggle" }, { "x": 3, "y": 2, "type": "floor" }, - { - "x": 4, - "y": 1, - "type": "floor" - }, - { - "x": 5, - "y": 1, - "type": "floor" - }, { "x": 4, "y": 2, @@ -1213,6 +1910,37 @@ "y": 2, "type": "floor" }, + { + "x": 6, + "y": 2, + "type": "switch", + "switchId": "s2", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "any", + "mode": "toggle" + }, + { + "x": 7, + "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, { "x": 2, "y": 3, @@ -1234,63 +1962,52 @@ "type": "floor" }, { - "x": 2, + "x": 6, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "floor" + }, + { + "x": 4, "y": 4, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 4, + "y": 5, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 0, + "y": 6, + "type": "floor" + }, + { + "x": 1, + "y": 6, + "type": "floor" + }, + { + "x": 2, + "y": 6, "type": "floor" }, { "x": 3, - "y": 4, - "type": "floor" - }, - { - "x": 4, - "y": 4, - "type": "floor" - }, - { - "x": 5, - "y": 4, - "type": "floor" - }, - { - "x": 6, - "y": 3, - "type": "floor" - }, - { - "x": 7, - "y": 3, - "type": "floor" - }, - { - "x": 6, - "y": 4, - "type": "floor" - }, - { - "x": 7, - "y": 4, - "type": "floor" - }, - { - "x": 4, - "y": 5, - "type": "floor" - }, - { - "x": 5, - "y": 5, - "type": "floor" - }, - { - "x": 6, - "y": 5, - "type": "floor" - }, - { - "x": 7, - "y": 5, + "y": 6, "type": "floor" }, { @@ -1313,46 +2030,86 @@ "y": 6, "type": "floor" }, - { - "x": 8, - "y": 5, - "type": "floor" - }, - { - "x": 9, - "y": 5, - "type": "floor" - }, { "x": 8, "y": 6, "type": "floor" }, { - "x": 9, - "y": 6, - "type": "floor" - }, - { - "x": 6, + "x": 0, "y": 7, "type": "floor" }, { - "x": 7, + "x": 1, "y": 7, "type": "floor" }, { - "x": 8, + "x": 2, + "y": 7, + "type": "floor" + }, + { + "x": 3, + "y": 7, + "type": "floor" + }, + { + "x": 4, "y": 7, "type": "goal" }, { - "x": 9, + "x": 5, "y": 7, "type": "floor" }, + { + "x": 6, + "y": 7, + "type": "floor" + }, + { + "x": 7, + "y": 7, + "type": "floor" + }, + { + "x": 8, + "y": 7, + "type": "floor" + }, + { + "x": 0, + "y": 8, + "type": "floor" + }, + { + "x": 1, + "y": 8, + "type": "floor" + }, + { + "x": 2, + "y": 8, + "type": "floor" + }, + { + "x": 3, + "y": 8, + "type": "floor" + }, + { + "x": 4, + "y": 8, + "type": "floor" + }, + { + "x": 5, + "y": 8, + "type": "floor" + }, { "x": 6, "y": 8, @@ -1367,278 +2124,20 @@ "x": 8, "y": 8, "type": "floor" - }, - { - "x": 9, - "y": 8, - "type": "floor" } ], - "par": 9 + "par": 7 }, { - "level": 7, - "name": "Mind the Gap", - "cols": 8, + "level": 9, + "name": "Hold the Pad", + "cols": 10, "rows": 6, "start": { "x": 1, "y": 1, "orient": "up" }, - "goal": { - "x": 6, - "y": 4 - }, - "tiles": [ - { - "x": 0, - "y": 0, - "type": "floor" - }, - { - "x": 1, - "y": 0, - "type": "floor" - }, - { - "x": 2, - "y": 0, - "type": "floor" - }, - { - "x": 3, - "y": 0, - "type": "floor" - }, - { - "x": 4, - "y": 0, - "type": "floor" - }, - { - "x": 5, - "y": 0, - "type": "floor" - }, - { - "x": 6, - "y": 0, - "type": "floor" - }, - { - "x": 7, - "y": 0, - "type": "floor" - }, - { - "x": 0, - "y": 1, - "type": "floor" - }, - { - "x": 1, - "y": 1, - "type": "floor" - }, - { - "x": 2, - "y": 1, - "type": "floor" - }, - { - "x": 3, - "y": 1, - "type": "floor" - }, - { - "x": 4, - "y": 1, - "type": "floor" - }, - { - "x": 5, - "y": 1, - "type": "floor" - }, - { - "x": 6, - "y": 1, - "type": "floor" - }, - { - "x": 7, - "y": 1, - "type": "floor" - }, - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 0, - "y": 3, - "type": "floor" - }, - { - "x": 1, - "y": 3, - "type": "floor" - }, - { - "x": 2, - "y": 3, - "type": "floor" - }, - { - "x": 3, - "y": 3, - "type": "floor" - }, - { - "x": 4, - "y": 3, - "type": "floor" - }, - { - "x": 5, - "y": 3, - "type": "floor" - }, - { - "x": 6, - "y": 3, - "type": "floor" - }, - { - "x": 7, - "y": 3, - "type": "floor" - }, - { - "x": 0, - "y": 4, - "type": "floor" - }, - { - "x": 1, - "y": 4, - "type": "floor" - }, - { - "x": 2, - "y": 4, - "type": "floor" - }, - { - "x": 3, - "y": 4, - "type": "floor" - }, - { - "x": 4, - "y": 4, - "type": "floor" - }, - { - "x": 5, - "y": 4, - "type": "floor" - }, - { - "x": 6, - "y": 4, - "type": "goal" - }, - { - "x": 7, - "y": 4, - "type": "floor" - }, - { - "x": 0, - "y": 5, - "type": "floor" - }, - { - "x": 1, - "y": 5, - "type": "floor" - }, - { - "x": 2, - "y": 5, - "type": "floor" - }, - { - "x": 3, - "y": 5, - "type": "floor" - }, - { - "x": 4, - "y": 5, - "type": "floor" - }, - { - "x": 5, - "y": 5, - "type": "floor" - }, - { - "x": 6, - "y": 5, - "type": "floor" - }, - { - "x": 7, - "y": 5, - "type": "floor" - } - ], - "par": 6 - }, - { - "level": 8, - "name": "Pinwheel", - "cols": 9, - "rows": 7, - "start": { - "x": 1, - "y": 1, - "orient": "up" - }, "goal": { "x": 7, "y": 5 @@ -1664,31 +2163,6 @@ "y": 0, "type": "floor" }, - { - "x": 4, - "y": 0, - "type": "floor" - }, - { - "x": 5, - "y": 0, - "type": "floor" - }, - { - "x": 6, - "y": 0, - "type": "floor" - }, - { - "x": 7, - "y": 0, - "type": "floor" - }, - { - "x": 8, - "y": 0, - "type": "floor" - }, { "x": 0, "y": 1, @@ -1709,83 +2183,61 @@ "y": 1, "type": "floor" }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 3, + "y": 3, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "lying", + "mode": "momentary" + }, { "x": 4, - "y": 1, - "type": "floor" + "y": 3, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false }, { "x": 5, - "y": 1, - "type": "floor" - }, - { - "x": 6, - "y": 1, - "type": "floor" - }, - { - "x": 7, - "y": 1, - "type": "floor" - }, - { - "x": 8, - "y": 1, - "type": "floor" - }, - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 0, - "y": 3, - "type": "floor" - }, - { - "x": 1, - "y": 3, - "type": "floor" - }, - { - "x": 2, "y": 3, "type": "floor" }, @@ -1805,23 +2257,8 @@ "type": "floor" }, { - "x": 0, - "y": 4, - "type": "floor" - }, - { - "x": 1, - "y": 4, - "type": "floor" - }, - { - "x": 2, - "y": 4, - "type": "floor" - }, - { - "x": 3, - "y": 4, + "x": 9, + "y": 3, "type": "floor" }, { @@ -1845,28 +2282,8 @@ "type": "floor" }, { - "x": 0, - "y": 5, - "type": "floor" - }, - { - "x": 1, - "y": 5, - "type": "floor" - }, - { - "x": 2, - "y": 5, - "type": "floor" - }, - { - "x": 3, - "y": 5, - "type": "floor" - }, - { - "x": 4, - "y": 5, + "x": 9, + "y": 4, "type": "floor" }, { @@ -1889,6 +2306,290 @@ "y": 5, "type": "floor" }, + { + "x": 9, + "y": 5, + "type": "floor" + } + ], + "par": 8 + }, + { + "level": 10, + "name": "The Long Way", + "cols": 12, + "rows": 9, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 10, + "y": 7 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 5, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 11, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 5, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, + "type": "floor" + }, + { + "x": 10, + "y": 1, + "type": "floor" + }, + { + "x": 11, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 5, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 2, + "type": "floor" + }, + { + "x": 11, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 9, + "y": 3, + "type": "floor" + }, + { + "x": 10, + "y": 3, + "type": "floor" + }, + { + "x": 11, + "y": 3, + "type": "floor" + }, + { + "x": 0, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 2, + "y": 4, + "type": "floor" + }, + { + "x": 9, + "y": 4, + "type": "floor" + }, + { + "x": 10, + "y": 4, + "type": "floor" + }, + { + "x": 11, + "y": 4, + "type": "floor" + }, + { + "x": 0, + "y": 5, + "type": "floor" + }, + { + "x": 1, + "y": 5, + "type": "floor" + }, + { + "x": 2, + "y": 5, + "type": "floor" + }, + { + "x": 3, + "y": 5, + "type": "floor" + }, + { + "x": 4, + "y": 5, + "type": "floor" + }, + { + "x": 5, + "y": 5, + "type": "floor" + }, + { + "x": 6, + "y": 5, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 7, + "y": 5, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 8, + "y": 5, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 9, + "y": 5, + "type": "floor" + }, + { + "x": 10, + "y": 5, + "type": "floor" + }, + { + "x": 11, + "y": 5, + "type": "floor" + }, { "x": 0, "y": 6, @@ -1920,28 +2621,1251 @@ "type": "floor" }, { - "x": 6, + "x": 9, "y": 6, "type": "floor" }, + { + "x": 10, + "y": 6, + "type": "floor" + }, + { + "x": 11, + "y": 6, + "type": "floor" + }, + { + "x": 0, + "y": 7, + "type": "floor" + }, + { + "x": 1, + "y": 7, + "type": "floor" + }, + { + "x": 2, + "y": 7, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 3, + "y": 7, + "type": "floor" + }, + { + "x": 4, + "y": 7, + "type": "floor" + }, + { + "x": 5, + "y": 7, + "type": "floor" + }, + { + "x": 9, + "y": 7, + "type": "floor" + }, + { + "x": 10, + "y": 7, + "type": "goal" + }, + { + "x": 11, + "y": 7, + "type": "floor" + }, + { + "x": 0, + "y": 8, + "type": "floor" + }, + { + "x": 1, + "y": 8, + "type": "floor" + }, + { + "x": 2, + "y": 8, + "type": "floor" + }, + { + "x": 3, + "y": 8, + "type": "floor" + }, + { + "x": 4, + "y": 8, + "type": "floor" + }, + { + "x": 5, + "y": 8, + "type": "floor" + }, + { + "x": 9, + "y": 8, + "type": "floor" + }, + { + "x": 10, + "y": 8, + "type": "floor" + }, + { + "x": 11, + "y": 8, + "type": "floor" + } + ], + "par": 17 + }, + { + "level": 11, + "name": "Two Gates", + "cols": 17, + "rows": 4, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 14, + "y": 2 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 6, + "y": 0, + "type": "floor" + }, { "x": 7, - "y": 6, + "y": 0, "type": "floor" }, { "x": 8, - "y": 6, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 13, + "y": 0, + "type": "floor" + }, + { + "x": 14, + "y": 0, + "type": "floor" + }, + { + "x": 15, + "y": 0, + "type": "floor" + }, + { + "x": 16, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 6, + "y": 1, + "type": "floor" + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b2" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 9, + "y": 1, + "type": "floor" + }, + { + "x": 10, + "y": 1, + "type": "floor" + }, + { + "x": 13, + "y": 1, + "type": "floor" + }, + { + "x": 14, + "y": 1, + "type": "floor" + }, + { + "x": 15, + "y": 1, + "type": "floor" + }, + { + "x": 16, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 5, + "y": 2, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 6, + "y": 2, + "type": "floor" + }, + { + "x": 7, + "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 2, + "type": "floor" + }, + { + "x": 11, + "y": 2, + "type": "bridge", + "bridgeId": "b2", + "initiallyOpen": false + }, + { + "x": 12, + "y": 2, + "type": "bridge", + "bridgeId": "b2", + "initiallyOpen": false + }, + { + "x": 13, + "y": 2, + "type": "floor" + }, + { + "x": 14, + "y": 2, + "type": "goal" + }, + { + "x": 15, + "y": 2, + "type": "floor" + }, + { + "x": 16, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "switch", + "switchId": "s2", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 6, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "floor" + }, + { + "x": 9, + "y": 3, + "type": "floor" + }, + { + "x": 10, + "y": 3, + "type": "floor" + }, + { + "x": 13, + "y": 3, + "type": "floor" + }, + { + "x": 14, + "y": 3, + "type": "floor" + }, + { + "x": 15, + "y": 3, + "type": "floor" + }, + { + "x": 16, + "y": 3, "type": "floor" } ], - "par": 7 + "par": 19 }, { - "level": 9, - "name": "Causeway", - "cols": 9, - "rows": 3, + "level": 12, + "name": "Shuttle", + "cols": 20, + "rows": 4, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 16, + "y": 2 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 11, + "y": 0, + "type": "floor" + }, + { + "x": 16, + "y": 0, + "type": "floor" + }, + { + "x": 17, + "y": 0, + "type": "floor" + }, + { + "x": 18, + "y": 0, + "type": "floor" + }, + { + "x": 19, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1", + "b2" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 10, + "y": 1, + "type": "floor" + }, + { + "x": 11, + "y": 1, + "type": "floor" + }, + { + "x": 16, + "y": 1, + "type": "floor" + }, + { + "x": 17, + "y": 1, + "type": "floor" + }, + { + "x": 18, + "y": 1, + "type": "floor" + }, + { + "x": 19, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": true + }, + { + "x": 5, + "y": 2, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": true + }, + { + "x": 6, + "y": 2, + "type": "floor" + }, + { + "x": 7, + "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 2, + "type": "floor" + }, + { + "x": 11, + "y": 2, + "type": "floor" + }, + { + "x": 12, + "y": 2, + "type": "bridge", + "bridgeId": "b2", + "initiallyOpen": false + }, + { + "x": 13, + "y": 2, + "type": "bridge", + "bridgeId": "b2", + "initiallyOpen": false + }, + { + "x": 14, + "y": 2, + "type": "floor" + }, + { + "x": 15, + "y": 2, + "type": "floor" + }, + { + "x": 16, + "y": 2, + "type": "goal" + }, + { + "x": 17, + "y": 2, + "type": "floor" + }, + { + "x": 18, + "y": 2, + "type": "floor" + }, + { + "x": 19, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "floor" + }, + { + "x": 9, + "y": 3, + "type": "floor" + }, + { + "x": 10, + "y": 3, + "type": "floor" + }, + { + "x": 11, + "y": 3, + "type": "floor" + }, + { + "x": 16, + "y": 3, + "type": "floor" + }, + { + "x": 17, + "y": 3, + "type": "floor" + }, + { + "x": 18, + "y": 3, + "type": "floor" + }, + { + "x": 19, + "y": 3, + "type": "floor" + } + ], + "par": 21 + }, + { + "level": 13, + "name": "Two at a Time", + "cols": 4, + "rows": 8, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 2, + "y": 6 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "fragile" + }, + { + "x": 3, + "y": 3, + "type": "fragile" + }, + { + "x": 2, + "y": 4, + "type": "fragile" + }, + { + "x": 3, + "y": 4, + "type": "fragile" + }, + { + "x": 0, + "y": 5, + "type": "floor" + }, + { + "x": 1, + "y": 5, + "type": "floor" + }, + { + "x": 2, + "y": 5, + "type": "floor" + }, + { + "x": 3, + "y": 5, + "type": "floor" + }, + { + "x": 0, + "y": 6, + "type": "floor" + }, + { + "x": 1, + "y": 6, + "type": "floor" + }, + { + "x": 2, + "y": 6, + "type": "goal" + }, + { + "x": 3, + "y": 6, + "type": "floor" + }, + { + "x": 0, + "y": 7, + "type": "floor" + }, + { + "x": 1, + "y": 7, + "type": "floor" + }, + { + "x": 2, + "y": 7, + "type": "floor" + }, + { + "x": 3, + "y": 7, + "type": "floor" + } + ], + "par": 11 + }, + { + "level": 14, + "name": "Cracked Gate", + "cols": 14, + "rows": 5, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 12, + "y": 2 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "fragile" + }, + { + "x": 10, + "y": 0, + "type": "fragile" + }, + { + "x": 11, + "y": 0, + "type": "fragile" + }, + { + "x": 12, + "y": 0, + "type": "floor" + }, + { + "x": 13, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, + "type": "fragile" + }, + { + "x": 10, + "y": 1, + "type": "fragile" + }, + { + "x": 11, + "y": 1, + "type": "fragile" + }, + { + "x": 12, + "y": 1, + "type": "floor" + }, + { + "x": 13, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 5, + "y": 2, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 6, + "y": 2, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 7, + "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, + "type": "fragile" + }, + { + "x": 10, + "y": 2, + "type": "fragile" + }, + { + "x": 11, + "y": 2, + "type": "fragile" + }, + { + "x": 12, + "y": 2, + "type": "goal" + }, + { + "x": 13, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "floor" + }, + { + "x": 9, + "y": 3, + "type": "fragile" + }, + { + "x": 10, + "y": 3, + "type": "fragile" + }, + { + "x": 11, + "y": 3, + "type": "fragile" + }, + { + "x": 12, + "y": 3, + "type": "floor" + }, + { + "x": 13, + "y": 3, + "type": "floor" + }, + { + "x": 0, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 2, + "y": 4, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 3, + "y": 4, + "type": "floor" + }, + { + "x": 4, + "y": 4, + "type": "floor" + }, + { + "x": 7, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 4, + "type": "floor" + }, + { + "x": 9, + "y": 4, + "type": "fragile" + }, + { + "x": 10, + "y": 4, + "type": "fragile" + }, + { + "x": 11, + "y": 4, + "type": "fragile" + }, + { + "x": 12, + "y": 4, + "type": "floor" + }, + { + "x": 13, + "y": 4, + "type": "floor" + } + ], + "par": 15 + }, + { + "level": 15, + "name": "Corner Ice", + "cols": 10, + "rows": 7, "start": { "x": 1, "y": 1, @@ -1949,6 +3873,294 @@ }, "goal": { "x": 7, + "y": 6 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "fragile" + }, + { + "x": 5, + "y": 0, + "type": "fragile" + }, + { + "x": 6, + "y": 0, + "type": "fragile" + }, + { + "x": 7, + "y": 0, + "type": "fragile" + }, + { + "x": 8, + "y": 0, + "type": "fragile" + }, + { + "x": 9, + "y": 0, + "type": "fragile" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "fragile" + }, + { + "x": 5, + "y": 1, + "type": "fragile" + }, + { + "x": 6, + "y": 1, + "type": "fragile" + }, + { + "x": 7, + "y": 1, + "type": "fragile" + }, + { + "x": 8, + "y": 1, + "type": "fragile" + }, + { + "x": 9, + "y": 1, + "type": "fragile" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "fragile" + }, + { + "x": 5, + "y": 2, + "type": "fragile" + }, + { + "x": 6, + "y": 2, + "type": "fragile" + }, + { + "x": 7, + "y": 2, + "type": "fragile" + }, + { + "x": 8, + "y": 2, + "type": "fragile" + }, + { + "x": 9, + "y": 2, + "type": "fragile" + }, + { + "x": 4, + "y": 3, + "type": "fragile" + }, + { + "x": 5, + "y": 3, + "type": "fragile" + }, + { + "x": 6, + "y": 3, + "type": "fragile" + }, + { + "x": 7, + "y": 3, + "type": "fragile" + }, + { + "x": 8, + "y": 3, + "type": "fragile" + }, + { + "x": 9, + "y": 3, + "type": "fragile" + }, + { + "x": 4, + "y": 4, + "type": "fragile" + }, + { + "x": 5, + "y": 4, + "type": "fragile" + }, + { + "x": 6, + "y": 4, + "type": "fragile" + }, + { + "x": 7, + "y": 4, + "type": "fragile" + }, + { + "x": 8, + "y": 4, + "type": "fragile" + }, + { + "x": 9, + "y": 4, + "type": "fragile" + }, + { + "x": 4, + "y": 5, + "type": "floor" + }, + { + "x": 5, + "y": 5, + "type": "floor" + }, + { + "x": 6, + "y": 5, + "type": "floor" + }, + { + "x": 7, + "y": 5, + "type": "floor" + }, + { + "x": 8, + "y": 5, + "type": "floor" + }, + { + "x": 9, + "y": 5, + "type": "floor" + }, + { + "x": 4, + "y": 6, + "type": "floor" + }, + { + "x": 5, + "y": 6, + "type": "floor" + }, + { + "x": 6, + "y": 6, + "type": "floor" + }, + { + "x": 7, + "y": 6, + "type": "goal" + }, + { + "x": 8, + "y": 6, + "type": "floor" + }, + { + "x": 9, + "y": 6, + "type": "floor" + } + ], + "par": 16 + }, + { + "level": 16, + "name": "Thin Ice", + "cols": 12, + "rows": 3, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 10, "y": 1 }, "tiles": [ @@ -1967,6 +4179,51 @@ "y": 0, "type": "floor" }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "fragile" + }, + { + "x": 5, + "y": 0, + "type": "fragile" + }, + { + "x": 6, + "y": 0, + "type": "fragile" + }, + { + "x": 7, + "y": 0, + "type": "fragile" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 11, + "y": 0, + "type": "floor" + }, { "x": 0, "y": 1, @@ -1982,21 +4239,6 @@ "y": 1, "type": "floor" }, - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, { "x": 3, "y": 1, @@ -2005,1037 +4247,87 @@ { "x": 4, "y": 1, - "type": "floor" + "type": "fragile" }, { "x": 5, "y": 1, - "type": "floor" - }, - { - "x": 6, - "y": 0, - "type": "floor" - }, - { - "x": 7, - "y": 0, - "type": "floor" - }, - { - "x": 8, - "y": 0, - "type": "floor" + "type": "fragile" }, { "x": 6, "y": 1, - "type": "floor" + "type": "fragile" }, { "x": 7, "y": 1, + "type": "fragile" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, + "type": "floor" + }, + { + "x": 10, + "y": 1, "type": "goal" }, { - "x": 8, + "x": 11, "y": 1, "type": "floor" }, { - "x": 6, + "x": 0, "y": 2, "type": "floor" }, { - "x": 7, + "x": 1, "y": 2, "type": "floor" }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "fragile" + }, + { + "x": 5, + "y": 2, + "type": "fragile" + }, + { + "x": 6, + "y": 2, + "type": "fragile" + }, + { + "x": 7, + "y": 2, + "type": "fragile" + }, { "x": 8, "y": 2, "type": "floor" - } - ], - "par": 4 - }, - { - "level": 10, - "name": "Bent Causeway", - "cols": 8, - "rows": 8, - "start": { - "x": 1, - "y": 1, - "orient": "up" - }, - "goal": { - "x": 6, - "y": 6 - }, - "tiles": [ - { - "x": 0, - "y": 0, - "type": "floor" - }, - { - "x": 1, - "y": 0, - "type": "floor" - }, - { - "x": 2, - "y": 0, - "type": "floor" - }, - { - "x": 0, - "y": 1, - "type": "floor" - }, - { - "x": 1, - "y": 1, - "type": "floor" - }, - { - "x": 2, - "y": 1, - "type": "floor" - }, - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 1, - "type": "floor" - }, - { - "x": 4, - "y": 1, - "type": "floor" - }, - { - "x": 5, - "y": 1, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 3, - "type": "floor" - }, - { - "x": 5, - "y": 3, - "type": "floor" - }, - { - "x": 4, - "y": 4, - "type": "floor" - }, - { - "x": 5, - "y": 4, - "type": "floor" - }, - { - "x": 4, - "y": 5, - "type": "floor" - }, - { - "x": 5, - "y": 5, - "type": "floor" - }, - { - "x": 3, - "y": 5, - "type": "floor" - }, - { - "x": 6, - "y": 5, - "type": "floor" - }, - { - "x": 7, - "y": 5, - "type": "floor" - }, - { - "x": 3, - "y": 6, - "type": "floor" - }, - { - "x": 4, - "y": 6, - "type": "floor" - }, - { - "x": 5, - "y": 6, - "type": "floor" - }, - { - "x": 6, - "y": 6, - "type": "goal" - }, - { - "x": 7, - "y": 6, - "type": "floor" - }, - { - "x": 3, - "y": 7, - "type": "floor" - }, - { - "x": 4, - "y": 7, - "type": "floor" - }, - { - "x": 5, - "y": 7, - "type": "floor" - }, - { - "x": 6, - "y": 7, - "type": "floor" - }, - { - "x": 7, - "y": 7, - "type": "floor" - } - ], - "par": 10 - }, - { - "level": 11, - "name": "Scattered", - "cols": 10, - "rows": 8, - "start": { - "x": 1, - "y": 1, - "orient": "up" - }, - "goal": { - "x": 8, - "y": 6 - }, - "tiles": [ - { - "x": 0, - "y": 0, - "type": "floor" - }, - { - "x": 1, - "y": 0, - "type": "floor" - }, - { - "x": 2, - "y": 0, - "type": "floor" - }, - { - "x": 3, - "y": 0, - "type": "floor" - }, - { - "x": 4, - "y": 0, - "type": "floor" - }, - { - "x": 5, - "y": 0, - "type": "floor" - }, - { - "x": 6, - "y": 0, - "type": "floor" - }, - { - "x": 7, - "y": 0, - "type": "floor" - }, - { - "x": 8, - "y": 0, - "type": "floor" - }, - { - "x": 9, - "y": 0, - "type": "floor" - }, - { - "x": 0, - "y": 1, - "type": "floor" - }, - { - "x": 1, - "y": 1, - "type": "floor" - }, - { - "x": 2, - "y": 1, - "type": "floor" - }, - { - "x": 3, - "y": 1, - "type": "floor" - }, - { - "x": 4, - "y": 1, - "type": "floor" - }, - { - "x": 5, - "y": 1, - "type": "floor" - }, - { - "x": 6, - "y": 1, - "type": "floor" - }, - { - "x": 7, - "y": 1, - "type": "floor" - }, - { - "x": 8, - "y": 1, - "type": "floor" - }, - { - "x": 9, - "y": 1, - "type": "floor" - }, - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 9, - "y": 2, - "type": "floor" - }, - { - "x": 0, - "y": 3, - "type": "floor" - }, - { - "x": 1, - "y": 3, - "type": "floor" - }, - { - "x": 2, - "y": 3, - "type": "floor" - }, - { - "x": 4, - "y": 3, - "type": "floor" - }, - { - "x": 5, - "y": 3, - "type": "floor" - }, - { - "x": 6, - "y": 3, - "type": "floor" - }, - { - "x": 7, - "y": 3, - "type": "floor" - }, - { - "x": 8, - "y": 3, - "type": "floor" - }, - { - "x": 9, - "y": 3, - "type": "floor" - }, - { - "x": 0, - "y": 4, - "type": "floor" - }, - { - "x": 1, - "y": 4, - "type": "floor" - }, - { - "x": 2, - "y": 4, - "type": "floor" - }, - { - "x": 3, - "y": 4, - "type": "floor" - }, - { - "x": 4, - "y": 4, - "type": "floor" - }, - { - "x": 5, - "y": 4, - "type": "floor" - }, - { - "x": 6, - "y": 4, - "type": "floor" - }, - { - "x": 7, - "y": 4, - "type": "floor" - }, - { - "x": 8, - "y": 4, - "type": "floor" - }, - { - "x": 9, - "y": 4, - "type": "floor" - }, - { - "x": 0, - "y": 5, - "type": "floor" - }, - { - "x": 1, - "y": 5, - "type": "floor" - }, - { - "x": 2, - "y": 5, - "type": "floor" - }, - { - "x": 3, - "y": 5, - "type": "floor" - }, - { - "x": 4, - "y": 5, - "type": "floor" - }, - { - "x": 5, - "y": 5, - "type": "floor" - }, - { - "x": 6, - "y": 5, - "type": "floor" - }, - { - "x": 8, - "y": 5, - "type": "floor" - }, - { - "x": 9, - "y": 5, - "type": "floor" - }, - { - "x": 0, - "y": 6, - "type": "floor" - }, - { - "x": 1, - "y": 6, - "type": "floor" - }, - { - "x": 3, - "y": 6, - "type": "floor" - }, - { - "x": 5, - "y": 6, - "type": "floor" - }, - { - "x": 6, - "y": 6, - "type": "floor" - }, - { - "x": 7, - "y": 6, - "type": "floor" - }, - { - "x": 8, - "y": 6, - "type": "goal" - }, - { - "x": 9, - "y": 6, - "type": "floor" - }, - { - "x": 0, - "y": 7, - "type": "floor" - }, - { - "x": 1, - "y": 7, - "type": "floor" - }, - { - "x": 2, - "y": 7, - "type": "floor" - }, - { - "x": 3, - "y": 7, - "type": "floor" - }, - { - "x": 4, - "y": 7, - "type": "floor" - }, - { - "x": 5, - "y": 7, - "type": "floor" - }, - { - "x": 6, - "y": 7, - "type": "floor" - }, - { - "x": 7, - "y": 7, - "type": "floor" - }, - { - "x": 8, - "y": 7, - "type": "floor" - }, - { - "x": 9, - "y": 7, - "type": "floor" - } - ], - "par": 10 - }, - { - "level": 12, - "name": "Tightrope", - "cols": 8, - "rows": 8, - "start": { - "x": 1, - "y": 1, - "orient": "up" - }, - "goal": { - "x": 6, - "y": 6 - }, - "tiles": [ - { - "x": 0, - "y": 0, - "type": "floor" - }, - { - "x": 1, - "y": 0, - "type": "floor" - }, - { - "x": 2, - "y": 0, - "type": "floor" - }, - { - "x": 0, - "y": 1, - "type": "floor" - }, - { - "x": 1, - "y": 1, - "type": "floor" - }, - { - "x": 2, - "y": 1, - "type": "floor" - }, - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 1, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 3, - "type": "floor" - }, - { - "x": 2, - "y": 3, - "type": "floor" - }, - { - "x": 3, - "y": 3, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 3, - "type": "floor" - }, - { - "x": 2, - "y": 4, - "type": "floor" - }, - { - "x": 3, - "y": 4, - "type": "floor" - }, - { - "x": 4, - "y": 4, - "type": "floor" - }, - { - "x": 5, - "y": 3, - "type": "floor" - }, - { - "x": 5, - "y": 4, - "type": "floor" - }, - { - "x": 3, - "y": 5, - "type": "floor" - }, - { - "x": 4, - "y": 5, - "type": "floor" - }, - { - "x": 5, - "y": 5, - "type": "floor" - }, - { - "x": 6, - "y": 4, - "type": "floor" - }, - { - "x": 6, - "y": 5, - "type": "floor" - }, - { - "x": 4, - "y": 6, - "type": "floor" - }, - { - "x": 5, - "y": 6, - "type": "floor" - }, - { - "x": 6, - "y": 6, - "type": "goal" - }, - { - "x": 7, - "y": 5, - "type": "floor" - }, - { - "x": 7, - "y": 6, - "type": "floor" - }, - { - "x": 5, - "y": 7, - "type": "floor" - }, - { - "x": 6, - "y": 7, - "type": "floor" - }, - { - "x": 7, - "y": 7, - "type": "floor" - } - ], - "par": 8 - }, - { - "level": 13, - "name": "Flip the Switch", - "cols": 10, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 9, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "switch", - "switchId": "s1", - "linkedBridgeIds": [ - "b1" - ], - "requireOrientation": "any", - "mode": "toggle" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "bridge", - "bridgeId": "b1", - "initiallyOpen": false - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 9, - "y": 2, - "type": "goal" - } - ], - "par": 6 - }, - { - "level": 14, - "name": "Weight and See", - "cols": 7, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 6, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "switch", - "switchId": "s1", - "linkedBridgeIds": [ - "b1" - ], - "requireOrientation": "lying", - "mode": "momentary" - }, - { - "x": 3, - "y": 2, - "type": "bridge", - "bridgeId": "b1", - "initiallyOpen": false - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "goal" - } - ], - "par": 4 - }, - { - "level": 15, - "name": "Double Gate", - "cols": 13, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 12, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "switch", - "switchId": "s1", - "linkedBridgeIds": [ - "b1", - "b2" - ], - "requireOrientation": "any", - "mode": "toggle" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "bridge", - "bridgeId": "b1", - "initiallyOpen": false - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "bridge", - "bridgeId": "b2", - "initiallyOpen": false }, { "x": 9, @@ -3051,963 +4343,18 @@ "x": 11, "y": 2, "type": "floor" - }, - { - "x": 12, - "y": 2, - "type": "goal" } ], - "par": 8 - }, - { - "level": 16, - "name": "Two Keys", - "cols": 16, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 15, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "switch", - "switchId": "s1", - "linkedBridgeIds": [ - "b1" - ], - "requireOrientation": "any", - "mode": "toggle" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "bridge", - "bridgeId": "b1", - "initiallyOpen": false - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "switch", - "switchId": "s2", - "linkedBridgeIds": [ - "b2" - ], - "requireOrientation": "any", - "mode": "toggle" - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 9, - "y": 2, - "type": "floor" - }, - { - "x": 10, - "y": 2, - "type": "bridge", - "bridgeId": "b2", - "initiallyOpen": false - }, - { - "x": 11, - "y": 2, - "type": "floor" - }, - { - "x": 12, - "y": 2, - "type": "floor" - }, - { - "x": 13, - "y": 2, - "type": "floor" - }, - { - "x": 14, - "y": 2, - "type": "floor" - }, - { - "x": 15, - "y": 2, - "type": "goal" - } - ], - "par": 10 + "par": 17 }, { "level": 17, - "name": "Soft Landing", - "cols": 16, + "name": "Collapse", + "cols": 15, "rows": 5, "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 15, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "switch", - "switchId": "s1", - "linkedBridgeIds": [ - "b1" - ], - "requireOrientation": "any", - "mode": "toggle" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "bridge", - "bridgeId": "b1", - "initiallyOpen": false - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "switch", - "switchId": "s2", - "linkedBridgeIds": [ - "b2" - ], - "requireOrientation": "lying", - "mode": "momentary" - }, - { - "x": 9, - "y": 2, - "type": "bridge", - "bridgeId": "b2", - "initiallyOpen": false - }, - { - "x": 10, - "y": 2, - "type": "floor" - }, - { - "x": 11, - "y": 2, - "type": "floor" - }, - { - "x": 12, - "y": 2, - "type": "floor" - }, - { - "x": 13, - "y": 2, - "type": "floor" - }, - { - "x": 14, - "y": 2, - "type": "floor" - }, - { - "x": 15, - "y": 2, - "type": "goal" - } - ], - "par": 10 - }, - { - "level": 18, - "name": "Three Gates", - "cols": 19, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 18, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "switch", - "switchId": "s1", - "linkedBridgeIds": [ - "b1" - ], - "requireOrientation": "any", - "mode": "toggle" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "bridge", - "bridgeId": "b1", - "initiallyOpen": false - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "switch", - "switchId": "s2", - "linkedBridgeIds": [ - "b2" - ], - "requireOrientation": "lying", - "mode": "momentary" - }, - { - "x": 9, - "y": 2, - "type": "bridge", - "bridgeId": "b2", - "initiallyOpen": false - }, - { - "x": 10, - "y": 2, - "type": "floor" - }, - { - "x": 11, - "y": 2, - "type": "floor" - }, - { - "x": 12, - "y": 2, - "type": "floor" - }, - { - "x": 13, - "y": 2, - "type": "switch", - "switchId": "s3", - "linkedBridgeIds": [ - "b3" - ], - "requireOrientation": "any", - "mode": "toggle" - }, - { - "x": 14, - "y": 2, - "type": "floor" - }, - { - "x": 15, - "y": 2, - "type": "floor" - }, - { - "x": 16, - "y": 2, - "type": "bridge", - "bridgeId": "b3", - "initiallyOpen": false - }, - { - "x": 17, - "y": 2, - "type": "floor" - }, - { - "x": 18, - "y": 2, - "type": "goal" - } - ], - "par": 12 - }, - { - "level": 19, - "name": "Cracked Floor", - "cols": 10, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 9, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "fragile" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 9, - "y": 2, - "type": "goal" - } - ], - "par": 6 - }, - { - "level": 20, - "name": "Eggshells", - "cols": 13, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 12, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "fragile" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "fragile" - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 9, - "y": 2, - "type": "floor" - }, - { - "x": 10, - "y": 2, - "type": "floor" - }, - { - "x": 11, - "y": 2, - "type": "floor" - }, - { - "x": 12, - "y": 2, - "type": "goal" - } - ], - "par": 8 - }, - { - "level": 21, - "name": "No Turning Back", - "cols": 16, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 15, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "fragile" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "fragile" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 9, - "y": 2, - "type": "floor" - }, - { - "x": 10, - "y": 2, - "type": "fragile" - }, - { - "x": 11, - "y": 2, - "type": "floor" - }, - { - "x": 12, - "y": 2, - "type": "floor" - }, - { - "x": 13, - "y": 2, - "type": "floor" - }, - { - "x": 14, - "y": 2, - "type": "floor" - }, - { - "x": 15, - "y": 2, - "type": "goal" - } - ], - "par": 10 - }, - { - "level": 22, - "name": "Fragile Gate", - "cols": 13, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 12, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "switch", - "switchId": "s1", - "linkedBridgeIds": [ - "b1" - ], - "requireOrientation": "any", - "mode": "toggle" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "bridge", - "bridgeId": "b1", - "initiallyOpen": false - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "fragile" - }, - { - "x": 9, - "y": 2, - "type": "floor" - }, - { - "x": 10, - "y": 2, - "type": "floor" - }, - { - "x": 11, - "y": 2, - "type": "floor" - }, - { - "x": 12, - "y": 2, - "type": "goal" - } - ], - "par": 8 - }, - { - "level": 23, - "name": "Soft and Thin", - "cols": 16, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 15, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "fragile" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "switch", - "switchId": "s1", - "linkedBridgeIds": [ - "b1" - ], - "requireOrientation": "lying", - "mode": "momentary" - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 9, - "y": 2, - "type": "bridge", - "bridgeId": "b1", - "initiallyOpen": false - }, - { - "x": 10, - "y": 2, - "type": "floor" - }, - { - "x": 11, - "y": 2, - "type": "fragile" - }, - { - "x": 12, - "y": 2, - "type": "floor" - }, - { - "x": 13, - "y": 2, - "type": "floor" - }, - { - "x": 14, - "y": 2, - "type": "floor" - }, - { - "x": 15, - "y": 2, - "type": "goal" - } - ], - "par": 10 - }, - { - "level": 24, - "name": "Every Step Counts", - "cols": 19, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 18, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "fragile" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "switch", - "switchId": "s1", - "linkedBridgeIds": [ - "b1" - ], - "requireOrientation": "any", - "mode": "toggle" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "bridge", - "bridgeId": "b1", - "initiallyOpen": false - }, - { - "x": 9, - "y": 2, - "type": "floor" - }, - { - "x": 10, - "y": 2, - "type": "floor" - }, - { - "x": 11, - "y": 2, - "type": "fragile" - }, - { - "x": 12, - "y": 2, - "type": "floor" - }, - { - "x": 13, - "y": 2, - "type": "floor" - }, - { - "x": 14, - "y": 2, - "type": "switch", - "switchId": "s2", - "linkedBridgeIds": [ - "b2" - ], - "requireOrientation": "lying", - "mode": "momentary" - }, - { - "x": 15, - "y": 2, - "type": "bridge", - "bridgeId": "b2", - "initiallyOpen": false - }, - { - "x": 16, - "y": 2, - "type": "floor" - }, - { - "x": 17, - "y": 2, - "type": "floor" - }, - { - "x": 18, - "y": 2, - "type": "goal" - } - ], - "par": 12 - }, - { - "level": 25, - "name": "Shortcut", - "cols": 14, - "rows": 5, - "start": { - "x": 0, - "y": 2, + "x": 1, + "y": 1, "orient": "up" }, "goal": { @@ -4017,226 +4364,134 @@ "tiles": [ { "x": 0, - "y": 2, + "y": 0, "type": "floor" }, { "x": 1, - "y": 2, + "y": 0, "type": "floor" }, { "x": 2, - "y": 2, + "y": 0, "type": "floor" }, { "x": 3, - "y": 2, - "type": "teleport", - "teleportId": "t1" + "y": 0, + "type": "floor" }, { "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, + "y": 0, "type": "floor" }, { "x": 7, - "y": 2, + "y": 0, "type": "floor" }, { "x": 8, - "y": 2, - "type": "floor" + "y": 0, + "type": "fragile" }, { "x": 9, - "y": 2, - "type": "floor" + "y": 0, + "type": "fragile" }, { "x": 10, - "y": 2, - "type": "teleport", - "teleportId": "t1" + "y": 0, + "type": "fragile" }, { "x": 11, - "y": 2, - "type": "floor" + "y": 0, + "type": "fragile" }, { "x": 12, - "y": 2, - "type": "floor" + "y": 0, + "type": "fragile" }, { "x": 13, - "y": 2, - "type": "goal" - } - ], - "par": 4 - }, - { - "level": 26, - "name": "Two Hops", - "cols": 22, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 21, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "teleport", - "teleportId": "t1" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 9, - "y": 2, - "type": "teleport", - "teleportId": "t1" - }, - { - "x": 10, - "y": 2, - "type": "floor" - }, - { - "x": 11, - "y": 2, - "type": "floor" - }, - { - "x": 12, - "y": 2, - "type": "teleport", - "teleportId": "t2" - }, - { - "x": 13, - "y": 2, + "y": 0, "type": "floor" }, { "x": 14, - "y": 2, + "y": 0, "type": "floor" }, { - "x": 15, - "y": 2, + "x": 0, + "y": 1, "type": "floor" }, { - "x": 16, - "y": 2, + "x": 1, + "y": 1, "type": "floor" }, { - "x": 17, - "y": 2, + "x": 2, + "y": 1, "type": "floor" }, { - "x": 18, - "y": 2, - "type": "teleport", - "teleportId": "t2" - }, - { - "x": 19, - "y": 2, + "x": 3, + "y": 1, "type": "floor" }, { - "x": 20, - "y": 2, + "x": 4, + "y": 1, "type": "floor" }, { - "x": 21, - "y": 2, - "type": "goal" - } - ], - "par": 6 - }, - { - "level": 27, - "name": "Triple Hop", - "cols": 28, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 27, - "y": 2 - }, - "tiles": [ + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "fragile" + }, + { + "x": 9, + "y": 1, + "type": "fragile" + }, + { + "x": 10, + "y": 1, + "type": "fragile" + }, + { + "x": 11, + "y": 1, + "type": "fragile" + }, + { + "x": 12, + "y": 1, + "type": "fragile" + }, + { + "x": 13, + "y": 1, + "type": "floor" + }, + { + "x": 14, + "y": 1, + "type": "floor" + }, { "x": 0, "y": 2, @@ -4255,8 +4510,7 @@ { "x": 3, "y": 2, - "type": "teleport", - "teleportId": "t1" + "type": "floor" }, { "x": 4, @@ -4266,184 +4520,16 @@ { "x": 5, "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 9, - "y": 2, - "type": "teleport", - "teleportId": "t1" - }, - { - "x": 10, - "y": 2, - "type": "floor" - }, - { - "x": 11, - "y": 2, - "type": "floor" - }, - { - "x": 12, - "y": 2, - "type": "teleport", - "teleportId": "t2" - }, - { - "x": 13, - "y": 2, - "type": "floor" - }, - { - "x": 14, - "y": 2, - "type": "floor" - }, - { - "x": 15, - "y": 2, - "type": "floor" - }, - { - "x": 16, - "y": 2, - "type": "floor" - }, - { - "x": 17, - "y": 2, - "type": "floor" - }, - { - "x": 18, - "y": 2, - "type": "teleport", - "teleportId": "t2" - }, - { - "x": 19, - "y": 2, - "type": "floor" - }, - { - "x": 20, - "y": 2, - "type": "floor" - }, - { - "x": 21, - "y": 2, - "type": "teleport", - "teleportId": "t3" - }, - { - "x": 22, - "y": 2, - "type": "floor" - }, - { - "x": 23, - "y": 2, - "type": "floor" - }, - { - "x": 24, - "y": 2, - "type": "teleport", - "teleportId": "t3" - }, - { - "x": 25, - "y": 2, - "type": "floor" - }, - { - "x": 26, - "y": 2, - "type": "floor" - }, - { - "x": 27, - "y": 2, - "type": "goal" - } - ], - "par": 8 - }, - { - "level": 28, - "name": "Gate and Go", - "cols": 16, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 15, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "switch", - "switchId": "s1", - "linkedBridgeIds": [ - "b1" - ], - "requireOrientation": "any", - "mode": "toggle" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, "type": "bridge", "bridgeId": "b1", "initiallyOpen": false }, - { - "x": 5, - "y": 2, - "type": "floor" - }, { "x": 6, "y": 2, - "type": "teleport", - "teleportId": "t1" + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false }, { "x": 7, @@ -4453,33 +4539,32 @@ { "x": 8, "y": 2, - "type": "floor" + "type": "fragile" }, { "x": 9, "y": 2, - "type": "floor" + "type": "fragile" }, { "x": 10, "y": 2, - "type": "floor" + "type": "fragile" }, { "x": 11, "y": 2, - "type": "floor" + "type": "fragile" }, { "x": 12, "y": 2, - "type": "teleport", - "teleportId": "t1" + "type": "fragile" }, { "x": 13, "y": 2, - "type": "floor" + "type": "goal" }, { "x": 14, @@ -4487,28 +4572,319 @@ "type": "floor" }, { - "x": 15, - "y": 2, - "type": "goal" + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "fragile" + }, + { + "x": 9, + "y": 3, + "type": "fragile" + }, + { + "x": 10, + "y": 3, + "type": "fragile" + }, + { + "x": 11, + "y": 3, + "type": "fragile" + }, + { + "x": 12, + "y": 3, + "type": "fragile" + }, + { + "x": 13, + "y": 3, + "type": "floor" + }, + { + "x": 14, + "y": 3, + "type": "floor" + }, + { + "x": 0, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 2, + "y": 4, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 3, + "y": 4, + "type": "floor" + }, + { + "x": 4, + "y": 4, + "type": "floor" + }, + { + "x": 7, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 4, + "type": "fragile" + }, + { + "x": 9, + "y": 4, + "type": "fragile" + }, + { + "x": 10, + "y": 4, + "type": "fragile" + }, + { + "x": 11, + "y": 4, + "type": "fragile" + }, + { + "x": 12, + "y": 4, + "type": "fragile" + }, + { + "x": 13, + "y": 4, + "type": "floor" + }, + { + "x": 14, + "y": 4, + "type": "floor" } ], - "par": 6 + "par": 19 }, { - "level": 29, - "name": "Brittle Hop", - "cols": 13, - "rows": 5, + "level": 18, + "name": "Stepping Stones", + "cols": 16, + "rows": 3, "start": { - "x": 0, - "y": 2, + "x": 1, + "y": 1, "orient": "up" }, "goal": { - "x": 12, - "y": 2 + "x": 14, + "y": 1 }, "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "fragile" + }, + { + "x": 5, + "y": 0, + "type": "fragile" + }, + { + "x": 6, + "y": 0, + "type": "floor" + }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "fragile" + }, + { + "x": 11, + "y": 0, + "type": "fragile" + }, + { + "x": 12, + "y": 0, + "type": "floor" + }, + { + "x": 13, + "y": 0, + "type": "floor" + }, + { + "x": 14, + "y": 0, + "type": "floor" + }, + { + "x": 15, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "fragile" + }, + { + "x": 5, + "y": 1, + "type": "fragile" + }, + { + "x": 6, + "y": 1, + "type": "floor" + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, + "type": "floor" + }, + { + "x": 10, + "y": 1, + "type": "fragile" + }, + { + "x": 11, + "y": 1, + "type": "fragile" + }, + { + "x": 12, + "y": 1, + "type": "floor" + }, + { + "x": 13, + "y": 1, + "type": "floor" + }, + { + "x": 14, + "y": 1, + "type": "goal" + }, + { + "x": 15, + "y": 1, + "type": "floor" + }, { "x": 0, "y": 2, @@ -4527,23 +4903,22 @@ { "x": 3, "y": 2, - "type": "fragile" + "type": "floor" }, { "x": 4, "y": 2, - "type": "floor" + "type": "fragile" }, { "x": 5, "y": 2, - "type": "floor" + "type": "fragile" }, { "x": 6, "y": 2, - "type": "teleport", - "teleportId": "t1" + "type": "floor" }, { "x": 7, @@ -4558,8 +4933,787 @@ { "x": 9, "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 2, + "type": "fragile" + }, + { + "x": 11, + "y": 2, + "type": "fragile" + }, + { + "x": 12, + "y": 2, + "type": "floor" + }, + { + "x": 13, + "y": 2, + "type": "floor" + }, + { + "x": 14, + "y": 2, + "type": "floor" + }, + { + "x": 15, + "y": 2, + "type": "floor" + } + ], + "par": 20 + }, + { + "level": 19, + "name": "Warp", + "cols": 12, + "rows": 9, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 7, + "y": 8 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 5, + "y": 0, + "type": "floor" + }, + { + "x": 6, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 5, + "y": 1, + "type": "floor" + }, + { + "x": 6, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, "type": "teleport", - "teleportId": "t1" + "teleportId": "t" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 5, + "y": 2, + "type": "floor" + }, + { + "x": 6, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 5, + "y": 3, + "type": "floor" + }, + { + "x": 6, + "y": 3, + "type": "floor" + }, + { + "x": 5, + "y": 5, + "type": "floor" + }, + { + "x": 6, + "y": 5, + "type": "floor" + }, + { + "x": 7, + "y": 5, + "type": "floor" + }, + { + "x": 8, + "y": 5, + "type": "floor" + }, + { + "x": 9, + "y": 5, + "type": "floor" + }, + { + "x": 10, + "y": 5, + "type": "floor" + }, + { + "x": 11, + "y": 5, + "type": "floor" + }, + { + "x": 5, + "y": 6, + "type": "floor" + }, + { + "x": 6, + "y": 6, + "type": "floor" + }, + { + "x": 7, + "y": 6, + "type": "floor" + }, + { + "x": 8, + "y": 6, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 9, + "y": 6, + "type": "floor" + }, + { + "x": 10, + "y": 6, + "type": "floor" + }, + { + "x": 11, + "y": 6, + "type": "floor" + }, + { + "x": 5, + "y": 7, + "type": "floor" + }, + { + "x": 6, + "y": 7, + "type": "floor" + }, + { + "x": 7, + "y": 7, + "type": "floor" + }, + { + "x": 8, + "y": 7, + "type": "floor" + }, + { + "x": 9, + "y": 7, + "type": "floor" + }, + { + "x": 10, + "y": 7, + "type": "floor" + }, + { + "x": 11, + "y": 7, + "type": "floor" + }, + { + "x": 5, + "y": 8, + "type": "floor" + }, + { + "x": 6, + "y": 8, + "type": "floor" + }, + { + "x": 7, + "y": 8, + "type": "goal" + }, + { + "x": 8, + "y": 8, + "type": "floor" + }, + { + "x": 9, + "y": 8, + "type": "floor" + }, + { + "x": 10, + "y": 8, + "type": "floor" + }, + { + "x": 11, + "y": 8, + "type": "floor" + } + ], + "par": 12 + }, + { + "level": 20, + "name": "Brittle Warp", + "cols": 14, + "rows": 9, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 11, + "y": 8 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 5, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 5, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 5, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 5, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 5, + "type": "fragile" + }, + { + "x": 8, + "y": 5, + "type": "fragile" + }, + { + "x": 9, + "y": 5, + "type": "fragile" + }, + { + "x": 10, + "y": 5, + "type": "fragile" + }, + { + "x": 11, + "y": 5, + "type": "fragile" + }, + { + "x": 12, + "y": 5, + "type": "fragile" + }, + { + "x": 13, + "y": 5, + "type": "fragile" + }, + { + "x": 7, + "y": 6, + "type": "fragile" + }, + { + "x": 8, + "y": 6, + "type": "fragile" + }, + { + "x": 9, + "y": 6, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 10, + "y": 6, + "type": "fragile" + }, + { + "x": 11, + "y": 6, + "type": "fragile" + }, + { + "x": 12, + "y": 6, + "type": "fragile" + }, + { + "x": 13, + "y": 6, + "type": "fragile" + }, + { + "x": 7, + "y": 7, + "type": "fragile" + }, + { + "x": 8, + "y": 7, + "type": "fragile" + }, + { + "x": 9, + "y": 7, + "type": "fragile" + }, + { + "x": 10, + "y": 7, + "type": "fragile" + }, + { + "x": 11, + "y": 7, + "type": "fragile" + }, + { + "x": 12, + "y": 7, + "type": "fragile" + }, + { + "x": 13, + "y": 7, + "type": "fragile" + }, + { + "x": 7, + "y": 8, + "type": "fragile" + }, + { + "x": 8, + "y": 8, + "type": "fragile" + }, + { + "x": 9, + "y": 8, + "type": "fragile" + }, + { + "x": 10, + "y": 8, + "type": "fragile" + }, + { + "x": 11, + "y": 8, + "type": "goal" + }, + { + "x": 12, + "y": 8, + "type": "floor" + }, + { + "x": 13, + "y": 8, + "type": "floor" + } + ], + "par": 13 + }, + { + "level": 21, + "name": "Crossed Wires", + "cols": 16, + "rows": 9, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 13, + "y": 7 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 5, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 11, + "y": 0, + "type": "floor" + }, + { + "x": 12, + "y": 0, + "type": "floor" + }, + { + "x": 13, + "y": 0, + "type": "floor" + }, + { + "x": 14, + "y": 0, + "type": "floor" + }, + { + "x": 15, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 5, + "y": 1, + "type": "floor" + }, + { + "x": 10, + "y": 1, + "type": "floor" + }, + { + "x": 11, + "y": 1, + "type": "floor" + }, + { + "x": 12, + "y": 1, + "type": "teleport", + "teleportId": "u" + }, + { + "x": 13, + "y": 1, + "type": "floor" + }, + { + "x": 14, + "y": 1, + "type": "floor" + }, + { + "x": 15, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 5, + "y": 2, + "type": "floor" }, { "x": 10, @@ -4574,19 +5728,3582 @@ { "x": 12, "y": 2, + "type": "floor" + }, + { + "x": 13, + "y": 2, + "type": "floor" + }, + { + "x": 14, + "y": 2, + "type": "floor" + }, + { + "x": 15, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 3, + "type": "floor" + }, + { + "x": 11, + "y": 3, + "type": "floor" + }, + { + "x": 12, + "y": 3, + "type": "floor" + }, + { + "x": 13, + "y": 3, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 14, + "y": 3, + "type": "floor" + }, + { + "x": 15, + "y": 3, + "type": "floor" + }, + { + "x": 10, + "y": 5, + "type": "floor" + }, + { + "x": 11, + "y": 5, + "type": "floor" + }, + { + "x": 12, + "y": 5, + "type": "floor" + }, + { + "x": 13, + "y": 5, + "type": "floor" + }, + { + "x": 14, + "y": 5, + "type": "floor" + }, + { + "x": 15, + "y": 5, + "type": "floor" + }, + { + "x": 10, + "y": 6, + "type": "floor" + }, + { + "x": 11, + "y": 6, + "type": "floor" + }, + { + "x": 12, + "y": 6, + "type": "teleport", + "teleportId": "u" + }, + { + "x": 13, + "y": 6, + "type": "floor" + }, + { + "x": 14, + "y": 6, + "type": "floor" + }, + { + "x": 15, + "y": 6, + "type": "floor" + }, + { + "x": 10, + "y": 7, + "type": "floor" + }, + { + "x": 11, + "y": 7, + "type": "floor" + }, + { + "x": 12, + "y": 7, + "type": "floor" + }, + { + "x": 13, + "y": 7, + "type": "goal" + }, + { + "x": 14, + "y": 7, + "type": "floor" + }, + { + "x": 15, + "y": 7, + "type": "floor" + }, + { + "x": 10, + "y": 8, + "type": "floor" + }, + { + "x": 11, + "y": 8, + "type": "floor" + }, + { + "x": 12, + "y": 8, + "type": "floor" + }, + { + "x": 13, + "y": 8, + "type": "floor" + }, + { + "x": 14, + "y": 8, + "type": "floor" + }, + { + "x": 15, + "y": 8, + "type": "floor" + } + ], + "par": 14 + }, + { + "level": 22, + "name": "Round Trip", + "cols": 16, + "rows": 10, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 13, + "y": 8 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 5, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 11, + "y": 0, + "type": "floor" + }, + { + "x": 12, + "y": 0, + "type": "floor" + }, + { + "x": 13, + "y": 0, + "type": "floor" + }, + { + "x": 14, + "y": 0, + "type": "floor" + }, + { + "x": 15, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 5, + "y": 1, + "type": "floor" + }, + { + "x": 10, + "y": 1, + "type": "floor" + }, + { + "x": 11, + "y": 1, + "type": "floor" + }, + { + "x": 12, + "y": 1, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 13, + "y": 1, + "type": "floor" + }, + { + "x": 14, + "y": 1, + "type": "floor" + }, + { + "x": 15, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 5, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 2, + "type": "floor" + }, + { + "x": 11, + "y": 2, + "type": "floor" + }, + { + "x": 12, + "y": 2, + "type": "floor" + }, + { + "x": 13, + "y": 2, + "type": "floor" + }, + { + "x": 14, + "y": 2, + "type": "floor" + }, + { + "x": 15, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 3, + "type": "floor" + }, + { + "x": 11, + "y": 3, + "type": "floor" + }, + { + "x": 12, + "y": 3, + "type": "floor" + }, + { + "x": 13, + "y": 3, + "type": "floor" + }, + { + "x": 14, + "y": 3, + "type": "teleport", + "teleportId": "u" + }, + { + "x": 15, + "y": 3, + "type": "floor" + }, + { + "x": 10, + "y": 5, + "type": "floor" + }, + { + "x": 11, + "y": 5, + "type": "floor" + }, + { + "x": 12, + "y": 5, + "type": "floor" + }, + { + "x": 13, + "y": 5, + "type": "floor" + }, + { + "x": 14, + "y": 5, + "type": "floor" + }, + { + "x": 15, + "y": 5, + "type": "floor" + }, + { + "x": 10, + "y": 6, + "type": "floor" + }, + { + "x": 11, + "y": 6, + "type": "floor" + }, + { + "x": 12, + "y": 6, + "type": "teleport", + "teleportId": "u" + }, + { + "x": 13, + "y": 6, + "type": "floor" + }, + { + "x": 14, + "y": 6, + "type": "floor" + }, + { + "x": 15, + "y": 6, + "type": "floor" + }, + { + "x": 10, + "y": 7, + "type": "floor" + }, + { + "x": 11, + "y": 7, + "type": "floor" + }, + { + "x": 12, + "y": 7, + "type": "floor" + }, + { + "x": 13, + "y": 7, + "type": "floor" + }, + { + "x": 14, + "y": 7, + "type": "floor" + }, + { + "x": 15, + "y": 7, + "type": "floor" + }, + { + "x": 10, + "y": 8, + "type": "floor" + }, + { + "x": 11, + "y": 8, + "type": "floor" + }, + { + "x": 12, + "y": 8, + "type": "floor" + }, + { + "x": 13, + "y": 8, + "type": "goal" + }, + { + "x": 14, + "y": 8, + "type": "floor" + }, + { + "x": 15, + "y": 8, + "type": "floor" + }, + { + "x": 10, + "y": 9, + "type": "floor" + }, + { + "x": 11, + "y": 9, + "type": "floor" + }, + { + "x": 12, + "y": 9, + "type": "floor" + }, + { + "x": 13, + "y": 9, + "type": "floor" + }, + { + "x": 14, + "y": 9, + "type": "floor" + }, + { + "x": 15, + "y": 9, + "type": "floor" + } + ], + "par": 18 + }, + { + "level": 23, + "name": "Warp Gate", + "cols": 13, + "rows": 9, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 11, + "y": 7 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 11, + "y": 0, + "type": "floor" + }, + { + "x": 12, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 5, + "y": 1, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 6, + "y": 1, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, + "type": "floor" + }, + { + "x": 10, + "y": 1, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 11, + "y": 1, + "type": "floor" + }, + { + "x": 12, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 7, + "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 2, + "type": "floor" + }, + { + "x": 11, + "y": 2, + "type": "floor" + }, + { + "x": 12, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "floor" + }, + { + "x": 9, + "y": 3, + "type": "floor" + }, + { + "x": 10, + "y": 3, + "type": "floor" + }, + { + "x": 11, + "y": 3, + "type": "floor" + }, + { + "x": 12, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 5, + "type": "floor" + }, + { + "x": 8, + "y": 5, + "type": "floor" + }, + { + "x": 9, + "y": 5, + "type": "floor" + }, + { + "x": 10, + "y": 5, + "type": "floor" + }, + { + "x": 11, + "y": 5, + "type": "floor" + }, + { + "x": 12, + "y": 5, + "type": "floor" + }, + { + "x": 7, + "y": 6, + "type": "floor" + }, + { + "x": 8, + "y": 6, + "type": "floor" + }, + { + "x": 9, + "y": 6, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 10, + "y": 6, + "type": "floor" + }, + { + "x": 11, + "y": 6, + "type": "floor" + }, + { + "x": 12, + "y": 6, + "type": "floor" + }, + { + "x": 7, + "y": 7, + "type": "floor" + }, + { + "x": 8, + "y": 7, + "type": "floor" + }, + { + "x": 9, + "y": 7, + "type": "floor" + }, + { + "x": 10, + "y": 7, + "type": "floor" + }, + { + "x": 11, + "y": 7, + "type": "goal" + }, + { + "x": 12, + "y": 7, + "type": "floor" + }, + { + "x": 7, + "y": 8, + "type": "floor" + }, + { + "x": 8, + "y": 8, + "type": "floor" + }, + { + "x": 9, + "y": 8, + "type": "floor" + }, + { + "x": 10, + "y": 8, + "type": "floor" + }, + { + "x": 11, + "y": 8, + "type": "floor" + }, + { + "x": 12, + "y": 8, + "type": "floor" + } + ], + "par": 22 + }, + { + "level": 24, + "name": "Warp Maze", + "cols": 21, + "rows": 11, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 18, + "y": 9 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 11, + "y": 0, + "type": "floor" + }, + { + "x": 12, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 10, + "y": 1, + "type": "floor" + }, + { + "x": 11, + "y": 1, + "type": "floor" + }, + { + "x": 12, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 5, + "y": 2, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 6, + "y": 2, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 7, + "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 2, + "type": "floor" + }, + { + "x": 11, + "y": 2, + "type": "floor" + }, + { + "x": 12, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "floor" + }, + { + "x": 9, + "y": 3, + "type": "floor" + }, + { + "x": 10, + "y": 3, + "type": "floor" + }, + { + "x": 11, + "y": 3, + "type": "floor" + }, + { + "x": 12, + "y": 3, + "type": "floor" + }, + { + "x": 0, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 2, + "y": 4, + "type": "floor" + }, + { + "x": 3, + "y": 4, + "type": "floor" + }, + { + "x": 4, + "y": 4, + "type": "floor" + }, + { + "x": 7, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 4, + "type": "floor" + }, + { + "x": 9, + "y": 4, + "type": "floor" + }, + { + "x": 10, + "y": 4, + "type": "floor" + }, + { + "x": 11, + "y": 4, + "type": "floor" + }, + { + "x": 12, + "y": 4, + "type": "floor" + }, + { + "x": 7, + "y": 6, + "type": "floor" + }, + { + "x": 8, + "y": 6, + "type": "floor" + }, + { + "x": 9, + "y": 6, + "type": "floor" + }, + { + "x": 10, + "y": 6, + "type": "floor" + }, + { + "x": 11, + "y": 6, + "type": "floor" + }, + { + "x": 12, + "y": 6, + "type": "floor" + }, + { + "x": 16, + "y": 6, + "type": "floor" + }, + { + "x": 17, + "y": 6, + "type": "floor" + }, + { + "x": 18, + "y": 6, + "type": "floor" + }, + { + "x": 19, + "y": 6, + "type": "floor" + }, + { + "x": 20, + "y": 6, + "type": "floor" + }, + { + "x": 7, + "y": 7, + "type": "floor" + }, + { + "x": 8, + "y": 7, + "type": "floor" + }, + { + "x": 9, + "y": 7, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 10, + "y": 7, + "type": "floor" + }, + { + "x": 11, + "y": 7, + "type": "floor" + }, + { + "x": 12, + "y": 7, + "type": "floor" + }, + { + "x": 16, + "y": 7, + "type": "floor" + }, + { + "x": 17, + "y": 7, + "type": "floor" + }, + { + "x": 18, + "y": 7, + "type": "teleport", + "teleportId": "u" + }, + { + "x": 19, + "y": 7, + "type": "floor" + }, + { + "x": 20, + "y": 7, + "type": "floor" + }, + { + "x": 7, + "y": 8, + "type": "floor" + }, + { + "x": 8, + "y": 8, + "type": "floor" + }, + { + "x": 9, + "y": 8, + "type": "floor" + }, + { + "x": 10, + "y": 8, + "type": "floor" + }, + { + "x": 11, + "y": 8, + "type": "floor" + }, + { + "x": 12, + "y": 8, + "type": "floor" + }, + { + "x": 16, + "y": 8, + "type": "floor" + }, + { + "x": 17, + "y": 8, + "type": "floor" + }, + { + "x": 18, + "y": 8, + "type": "floor" + }, + { + "x": 19, + "y": 8, + "type": "floor" + }, + { + "x": 20, + "y": 8, + "type": "floor" + }, + { + "x": 7, + "y": 9, + "type": "floor" + }, + { + "x": 8, + "y": 9, + "type": "floor" + }, + { + "x": 9, + "y": 9, + "type": "floor" + }, + { + "x": 10, + "y": 9, + "type": "teleport", + "teleportId": "u" + }, + { + "x": 11, + "y": 9, + "type": "floor" + }, + { + "x": 12, + "y": 9, + "type": "floor" + }, + { + "x": 16, + "y": 9, + "type": "floor" + }, + { + "x": 17, + "y": 9, + "type": "floor" + }, + { + "x": 18, + "y": 9, + "type": "goal" + }, + { + "x": 19, + "y": 9, + "type": "floor" + }, + { + "x": 20, + "y": 9, + "type": "floor" + }, + { + "x": 7, + "y": 10, + "type": "floor" + }, + { + "x": 8, + "y": 10, + "type": "floor" + }, + { + "x": 9, + "y": 10, + "type": "floor" + }, + { + "x": 10, + "y": 10, + "type": "floor" + }, + { + "x": 11, + "y": 10, + "type": "floor" + }, + { + "x": 12, + "y": 10, + "type": "floor" + }, + { + "x": 16, + "y": 10, + "type": "floor" + }, + { + "x": 17, + "y": 10, + "type": "floor" + }, + { + "x": 18, + "y": 10, + "type": "floor" + }, + { + "x": 19, + "y": 10, + "type": "floor" + }, + { + "x": 20, + "y": 10, + "type": "floor" + } + ], + "par": 27 + }, + { + "level": 25, + "name": "Break Apart", + "cols": 12, + "rows": 6, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 11, + "y": 5 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 5, + "type": "floor" + }, + { + "x": 2, + "y": 5, + "type": "floor" + }, + { + "x": 3, + "y": 5, + "type": "floor" + }, + { + "x": 4, + "y": 5, + "type": "floor" + }, + { + "x": 5, + "y": 5, + "type": "floor", + "split": true + }, + { + "x": 6, + "y": 5, + "type": "floor", + "split": true + }, + { + "x": 7, + "y": 5, + "type": "floor" + }, + { + "x": 8, + "y": 5, + "type": "floor" + }, + { + "x": 9, + "y": 5, + "type": "floor" + }, + { + "x": 10, + "y": 5, + "type": "floor" + }, + { + "x": 11, + "y": 5, "type": "goal" } ], - "par": 6 + "par": 12 + }, + { + "level": 26, + "name": "Split Gate", + "cols": 14, + "rows": 11, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 11, + "y": 9 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 5, + "type": "floor" + }, + { + "x": 2, + "y": 5, + "type": "floor" + }, + { + "x": 3, + "y": 5, + "type": "floor" + }, + { + "x": 4, + "y": 5, + "type": "floor" + }, + { + "x": 5, + "y": 5, + "type": "floor", + "split": true + }, + { + "x": 6, + "y": 5, + "type": "floor", + "split": true + }, + { + "x": 7, + "y": 5, + "type": "floor" + }, + { + "x": 8, + "y": 5, + "type": "floor" + }, + { + "x": 9, + "y": 5, + "type": "floor" + }, + { + "x": 10, + "y": 5, + "type": "floor" + }, + { + "x": 11, + "y": 5, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 11, + "y": 6, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 11, + "y": 7, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 9, + "y": 8, + "type": "floor" + }, + { + "x": 10, + "y": 8, + "type": "floor" + }, + { + "x": 11, + "y": 8, + "type": "floor" + }, + { + "x": 12, + "y": 8, + "type": "floor" + }, + { + "x": 13, + "y": 8, + "type": "floor" + }, + { + "x": 9, + "y": 9, + "type": "floor" + }, + { + "x": 10, + "y": 9, + "type": "floor" + }, + { + "x": 11, + "y": 9, + "type": "goal" + }, + { + "x": 12, + "y": 9, + "type": "floor" + }, + { + "x": 13, + "y": 9, + "type": "floor" + }, + { + "x": 9, + "y": 10, + "type": "floor" + }, + { + "x": 10, + "y": 10, + "type": "floor" + }, + { + "x": 11, + "y": 10, + "type": "floor" + }, + { + "x": 12, + "y": 10, + "type": "floor" + }, + { + "x": 13, + "y": 10, + "type": "floor" + } + ], + "par": 17 + }, + { + "level": 27, + "name": "Split Warp", + "cols": 16, + "rows": 11, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 14, + "y": 9 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 5, + "type": "floor" + }, + { + "x": 2, + "y": 5, + "type": "floor" + }, + { + "x": 3, + "y": 5, + "type": "floor" + }, + { + "x": 4, + "y": 5, + "type": "floor" + }, + { + "x": 5, + "y": 5, + "type": "floor", + "split": true + }, + { + "x": 6, + "y": 5, + "type": "floor", + "split": true + }, + { + "x": 7, + "y": 5, + "type": "floor" + }, + { + "x": 8, + "y": 5, + "type": "floor" + }, + { + "x": 9, + "y": 5, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 10, + "y": 5, + "type": "floor" + }, + { + "x": 11, + "y": 5, + "type": "floor" + }, + { + "x": 10, + "y": 7, + "type": "floor" + }, + { + "x": 11, + "y": 7, + "type": "floor" + }, + { + "x": 12, + "y": 7, + "type": "floor" + }, + { + "x": 13, + "y": 7, + "type": "floor" + }, + { + "x": 14, + "y": 7, + "type": "floor" + }, + { + "x": 15, + "y": 7, + "type": "floor" + }, + { + "x": 10, + "y": 8, + "type": "floor" + }, + { + "x": 11, + "y": 8, + "type": "floor" + }, + { + "x": 12, + "y": 8, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 13, + "y": 8, + "type": "floor" + }, + { + "x": 14, + "y": 8, + "type": "floor" + }, + { + "x": 15, + "y": 8, + "type": "floor" + }, + { + "x": 10, + "y": 9, + "type": "floor" + }, + { + "x": 11, + "y": 9, + "type": "floor" + }, + { + "x": 12, + "y": 9, + "type": "floor" + }, + { + "x": 13, + "y": 9, + "type": "floor" + }, + { + "x": 14, + "y": 9, + "type": "goal" + }, + { + "x": 15, + "y": 9, + "type": "floor" + }, + { + "x": 10, + "y": 10, + "type": "floor" + }, + { + "x": 11, + "y": 10, + "type": "floor" + }, + { + "x": 12, + "y": 10, + "type": "floor" + }, + { + "x": 13, + "y": 10, + "type": "floor" + }, + { + "x": 14, + "y": 10, + "type": "floor" + }, + { + "x": 15, + "y": 10, + "type": "floor" + } + ], + "par": 18 + }, + { + "level": 28, + "name": "Held Back", + "cols": 11, + "rows": 6, + "start": { + "x": 0, + "y": 0, + "orient": "up" + }, + "goal": { + "x": 9, + "y": 5 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor", + "split": true + }, + { + "x": 2, + "y": 0, + "type": "floor", + "split": true + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 5, + "y": 0, + "type": "floor" + }, + { + "x": 6, + "y": 0, + "type": "floor" + }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "wall" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "wall" + }, + { + "x": 5, + "y": 1, + "type": "wall" + }, + { + "x": 6, + "y": 1, + "type": "floor" + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, + "type": "wall" + }, + { + "x": 10, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 5, + "y": 2, + "type": "floor" + }, + { + "x": 6, + "y": 2, + "type": "floor" + }, + { + "x": 7, + "y": 2, + "type": "wall" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 5, + "y": 3, + "type": "floor" + }, + { + "x": 6, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "wall" + }, + { + "x": 9, + "y": 3, + "type": "floor" + }, + { + "x": 10, + "y": 3, + "type": "floor" + }, + { + "x": 0, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 2, + "y": 4, + "type": "floor" + }, + { + "x": 3, + "y": 4, + "type": "floor" + }, + { + "x": 4, + "y": 4, + "type": "floor" + }, + { + "x": 5, + "y": 4, + "type": "floor" + }, + { + "x": 6, + "y": 4, + "type": "floor" + }, + { + "x": 7, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 4, + "type": "floor" + }, + { + "x": 9, + "y": 4, + "type": "floor" + }, + { + "x": 10, + "y": 4, + "type": "floor" + }, + { + "x": 0, + "y": 5, + "type": "floor" + }, + { + "x": 1, + "y": 5, + "type": "floor" + }, + { + "x": 2, + "y": 5, + "type": "floor" + }, + { + "x": 3, + "y": 5, + "type": "floor" + }, + { + "x": 4, + "y": 5, + "type": "floor" + }, + { + "x": 5, + "y": 5, + "type": "floor" + }, + { + "x": 6, + "y": 5, + "type": "floor" + }, + { + "x": 7, + "y": 5, + "type": "floor" + }, + { + "x": 8, + "y": 5, + "type": "wall" + }, + { + "x": 9, + "y": 5, + "type": "goal" + }, + { + "x": 10, + "y": 5, + "type": "floor" + } + ], + "par": 19 + }, + { + "level": 29, + "name": "Split Ice", + "cols": 14, + "rows": 7, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 13, + "y": 6 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "fragile" + }, + { + "x": 2, + "y": 3, + "type": "fragile" + }, + { + "x": 1, + "y": 4, + "type": "fragile" + }, + { + "x": 2, + "y": 4, + "type": "fragile" + }, + { + "x": 1, + "y": 5, + "type": "fragile" + }, + { + "x": 2, + "y": 5, + "type": "fragile" + }, + { + "x": 1, + "y": 6, + "type": "floor" + }, + { + "x": 2, + "y": 6, + "type": "floor" + }, + { + "x": 3, + "y": 6, + "type": "floor" + }, + { + "x": 4, + "y": 6, + "type": "floor" + }, + { + "x": 5, + "y": 6, + "type": "floor" + }, + { + "x": 6, + "y": 6, + "type": "floor" + }, + { + "x": 7, + "y": 6, + "type": "floor", + "split": true + }, + { + "x": 8, + "y": 6, + "type": "floor", + "split": true + }, + { + "x": 9, + "y": 6, + "type": "floor" + }, + { + "x": 10, + "y": 6, + "type": "floor" + }, + { + "x": 11, + "y": 6, + "type": "floor" + }, + { + "x": 12, + "y": 6, + "type": "floor" + }, + { + "x": 13, + "y": 6, + "type": "goal" + } + ], + "par": 19 }, { "level": 30, - "name": "All at Once", - "cols": 19, - "rows": 5, + "name": "Reassembly", + "cols": 11, + "rows": 6, "start": { "x": 0, - "y": 2, + "y": 0, + "orient": "up" + }, + "goal": { + "x": 9, + "y": 5 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor", + "split": true + }, + { + "x": 2, + "y": 0, + "type": "floor", + "split": true + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 5, + "y": 0, + "type": "floor" + }, + { + "x": 6, + "y": 0, + "type": "floor" + }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "wall" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 5, + "y": 1, + "type": "floor" + }, + { + "x": 6, + "y": 1, + "type": "floor" + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "wall" + }, + { + "x": 9, + "y": 1, + "type": "floor" + }, + { + "x": 10, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 5, + "y": 2, + "type": "floor" + }, + { + "x": 6, + "y": 2, + "type": "floor" + }, + { + "x": 7, + "y": 2, + "type": "wall" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 3, + "y": 3, + "type": "wall" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 5, + "y": 3, + "type": "floor" + }, + { + "x": 6, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "wall" + }, + { + "x": 9, + "y": 3, + "type": "floor" + }, + { + "x": 10, + "y": 3, + "type": "floor" + }, + { + "x": 0, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 2, + "y": 4, + "type": "floor" + }, + { + "x": 3, + "y": 4, + "type": "floor" + }, + { + "x": 4, + "y": 4, + "type": "floor" + }, + { + "x": 5, + "y": 4, + "type": "floor" + }, + { + "x": 6, + "y": 4, + "type": "floor" + }, + { + "x": 7, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 4, + "type": "floor" + }, + { + "x": 9, + "y": 4, + "type": "floor" + }, + { + "x": 10, + "y": 4, + "type": "floor" + }, + { + "x": 0, + "y": 5, + "type": "floor" + }, + { + "x": 1, + "y": 5, + "type": "floor" + }, + { + "x": 2, + "y": 5, + "type": "floor" + }, + { + "x": 3, + "y": 5, + "type": "floor" + }, + { + "x": 4, + "y": 5, + "type": "floor" + }, + { + "x": 5, + "y": 5, + "type": "floor" + }, + { + "x": 6, + "y": 5, + "type": "floor" + }, + { + "x": 7, + "y": 5, + "type": "floor" + }, + { + "x": 8, + "y": 5, + "type": "wall" + }, + { + "x": 9, + "y": 5, + "type": "goal" + }, + { + "x": 10, + "y": 5, + "type": "floor" + } + ], + "par": 23 + }, + { + "level": 31, + "name": "Warp and Crack", + "cols": 15, + "rows": 11, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 10, + "y": 9 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "fragile" + }, + { + "x": 10, + "y": 0, + "type": "fragile" + }, + { + "x": 11, + "y": 0, + "type": "fragile" + }, + { + "x": 12, + "y": 0, + "type": "floor" + }, + { + "x": 13, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 5, + "y": 1, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 6, + "y": 1, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, + "type": "fragile" + }, + { + "x": 10, + "y": 1, + "type": "fragile" + }, + { + "x": 11, + "y": 1, + "type": "fragile" + }, + { + "x": 12, + "y": 1, + "type": "floor" + }, + { + "x": 13, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 7, + "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, + "type": "fragile" + }, + { + "x": 10, + "y": 2, + "type": "fragile" + }, + { + "x": 11, + "y": 2, + "type": "fragile" + }, + { + "x": 12, + "y": 2, + "type": "floor" + }, + { + "x": 13, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "floor" + }, + { + "x": 9, + "y": 3, + "type": "fragile" + }, + { + "x": 10, + "y": 3, + "type": "fragile" + }, + { + "x": 11, + "y": 3, + "type": "fragile" + }, + { + "x": 12, + "y": 3, + "type": "floor" + }, + { + "x": 13, + "y": 3, + "type": "floor" + }, + { + "x": 0, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 2, + "y": 4, + "type": "floor" + }, + { + "x": 3, + "y": 4, + "type": "floor" + }, + { + "x": 4, + "y": 4, + "type": "floor" + }, + { + "x": 7, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 4, + "type": "floor" + }, + { + "x": 9, + "y": 4, + "type": "fragile" + }, + { + "x": 10, + "y": 4, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 11, + "y": 4, + "type": "fragile" + }, + { + "x": 12, + "y": 4, + "type": "floor" + }, + { + "x": 13, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 6, + "type": "floor" + }, + { + "x": 9, + "y": 6, + "type": "floor" + }, + { + "x": 10, + "y": 6, + "type": "floor" + }, + { + "x": 11, + "y": 6, + "type": "floor" + }, + { + "x": 12, + "y": 6, + "type": "floor" + }, + { + "x": 13, + "y": 6, + "type": "floor" + }, + { + "x": 14, + "y": 6, + "type": "floor" + }, + { + "x": 8, + "y": 7, + "type": "floor" + }, + { + "x": 9, + "y": 7, + "type": "floor" + }, + { + "x": 10, + "y": 7, + "type": "floor" + }, + { + "x": 11, + "y": 7, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 12, + "y": 7, + "type": "floor" + }, + { + "x": 13, + "y": 7, + "type": "floor" + }, + { + "x": 14, + "y": 7, + "type": "floor" + }, + { + "x": 8, + "y": 8, + "type": "floor" + }, + { + "x": 9, + "y": 8, + "type": "floor" + }, + { + "x": 10, + "y": 8, + "type": "floor" + }, + { + "x": 11, + "y": 8, + "type": "floor" + }, + { + "x": 12, + "y": 8, + "type": "floor" + }, + { + "x": 13, + "y": 8, + "type": "floor" + }, + { + "x": 14, + "y": 8, + "type": "floor" + }, + { + "x": 8, + "y": 9, + "type": "floor" + }, + { + "x": 9, + "y": 9, + "type": "floor" + }, + { + "x": 10, + "y": 9, + "type": "goal" + }, + { + "x": 11, + "y": 9, + "type": "floor" + }, + { + "x": 12, + "y": 9, + "type": "floor" + }, + { + "x": 13, + "y": 9, + "type": "floor" + }, + { + "x": 14, + "y": 9, + "type": "floor" + }, + { + "x": 8, + "y": 10, + "type": "floor" + }, + { + "x": 9, + "y": 10, + "type": "floor" + }, + { + "x": 10, + "y": 10, + "type": "floor" + }, + { + "x": 11, + "y": 10, + "type": "floor" + }, + { + "x": 12, + "y": 10, + "type": "floor" + }, + { + "x": 13, + "y": 10, + "type": "floor" + }, + { + "x": 14, + "y": 10, + "type": "floor" + } + ], + "par": 22 + }, + { + "level": 32, + "name": "Gatehouse", + "cols": 21, + "rows": 5, + "start": { + "x": 1, + "y": 1, "orient": "up" }, "goal": { @@ -4596,537 +9313,619 @@ "tiles": [ { "x": 0, - "y": 2, + "y": 0, "type": "floor" }, { "x": 1, - "y": 2, + "y": 0, "type": "floor" }, { "x": 2, - "y": 2, - "type": "switch", - "switchId": "s1", - "linkedBridgeIds": [ - "b1" - ], - "requireOrientation": "lying", - "mode": "momentary" + "y": 0, + "type": "floor" }, { "x": 3, - "y": 2, - "type": "bridge", - "bridgeId": "b1", - "initiallyOpen": false + "y": 0, + "type": "floor" }, { "x": 4, - "y": 2, + "y": 0, "type": "floor" }, { - "x": 5, - "y": 2, + "x": 7, + "y": 0, "type": "floor" }, { - "x": 6, - "y": 2, + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, "type": "fragile" }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 9, - "y": 2, - "type": "teleport", - "teleportId": "t1" - }, { "x": 10, - "y": 2, - "type": "floor" + "y": 0, + "type": "fragile" }, { "x": 11, - "y": 2, - "type": "floor" + "y": 0, + "type": "fragile" }, { "x": 12, - "y": 2, + "y": 0, "type": "floor" }, { "x": 13, - "y": 2, - "type": "floor" - }, - { - "x": 14, - "y": 2, - "type": "floor" - }, - { - "x": 15, - "y": 2, - "type": "teleport", - "teleportId": "t1" - }, - { - "x": 16, - "y": 2, - "type": "floor" - }, - { - "x": 17, - "y": 2, - "type": "floor" - }, - { - "x": 18, - "y": 2, - "type": "goal" - } - ], - "par": 8 - }, - { - "level": 31, - "name": "Split Decision", - "cols": 11, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 10, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "floor", - "split": true - }, - { - "x": 5, - "y": 2, - "type": "floor", - "split": true - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 9, - "y": 2, - "type": "floor" - }, - { - "x": 10, - "y": 2, - "type": "goal" - } - ], - "par": 7 - }, - { - "level": 32, - "name": "Split and Gate", - "cols": 20, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 19, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "switch", - "switchId": "s1", - "linkedBridgeIds": [ - "b1" - ], - "requireOrientation": "any", - "mode": "toggle" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "bridge", - "bridgeId": "b1", - "initiallyOpen": false - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor", - "split": true - }, - { - "x": 8, - "y": 2, - "type": "floor", - "split": true - }, - { - "x": 9, - "y": 2, - "type": "floor" - }, - { - "x": 10, - "y": 2, - "type": "floor" - }, - { - "x": 11, - "y": 2, - "type": "floor" - }, - { - "x": 12, - "y": 2, - "type": "floor" - }, - { - "x": 13, - "y": 2, - "type": "floor" - }, - { - "x": 14, - "y": 2, - "type": "floor" - }, - { - "x": 15, - "y": 2, + "y": 0, "type": "floor" }, { "x": 16, - "y": 2, + "y": 0, "type": "floor" }, { "x": 17, - "y": 2, + "y": 0, "type": "floor" }, { "x": 18, - "y": 2, + "y": 0, "type": "floor" }, { "x": 19, - "y": 2, - "type": "goal" - } - ], - "par": 13 - }, - { - "level": 33, - "name": "Split and Crack", - "cols": 16, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 15, - "y": 2 - }, - "tiles": [ + "y": 0, + "type": "floor" + }, + { + "x": 20, + "y": 0, + "type": "floor" + }, { "x": 0, - "y": 2, + "y": 1, "type": "floor" }, { "x": 1, - "y": 2, + "y": 1, "type": "floor" }, { "x": 2, - "y": 2, + "y": 1, "type": "floor" }, { "x": 3, - "y": 2, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, "type": "fragile" }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor", - "split": true - }, - { - "x": 7, - "y": 2, - "type": "floor", - "split": true - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 9, - "y": 2, - "type": "floor" - }, { "x": 10, - "y": 2, - "type": "floor" + "y": 1, + "type": "fragile" }, { "x": 11, - "y": 2, - "type": "floor" + "y": 1, + "type": "fragile" }, { "x": 12, - "y": 2, + "y": 1, "type": "floor" }, { "x": 13, - "y": 2, - "type": "floor" - }, - { - "x": 14, - "y": 2, - "type": "floor" - }, - { - "x": 15, - "y": 2, - "type": "goal" - } - ], - "par": 10 - }, - { - "level": 34, - "name": "Split and Hop", - "cols": 17, - "rows": 5, - "start": { - "x": 0, - "y": 2, - "orient": "up" - }, - "goal": { - "x": 16, - "y": 2 - }, - "tiles": [ - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "floor" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "floor", - "split": true - }, - { - "x": 5, - "y": 2, - "type": "floor", - "split": true - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "teleport", - "teleportId": "t1" - }, - { - "x": 8, - "y": 2, - "type": "floor" - }, - { - "x": 9, - "y": 2, - "type": "floor" - }, - { - "x": 10, - "y": 2, - "type": "floor" - }, - { - "x": 11, - "y": 2, - "type": "floor" - }, - { - "x": 12, - "y": 2, - "type": "floor" - }, - { - "x": 13, - "y": 2, - "type": "teleport", - "teleportId": "t1" - }, - { - "x": 14, - "y": 2, - "type": "floor" - }, - { - "x": 15, - "y": 2, + "y": 1, "type": "floor" }, { "x": 16, + "y": 1, + "type": "floor" + }, + { + "x": 17, + "y": 1, + "type": "floor" + }, + { + "x": 18, + "y": 1, + "type": "floor" + }, + { + "x": 19, + "y": 1, + "type": "floor" + }, + { + "x": 20, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 5, + "y": 2, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 6, + "y": 2, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 7, + "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, + "type": "fragile" + }, + { + "x": 10, + "y": 2, + "type": "fragile" + }, + { + "x": 11, + "y": 2, + "type": "fragile" + }, + { + "x": 12, + "y": 2, + "type": "floor" + }, + { + "x": 13, + "y": 2, + "type": "floor" + }, + { + "x": 14, + "y": 2, + "type": "bridge", + "bridgeId": "b2", + "initiallyOpen": false + }, + { + "x": 15, + "y": 2, + "type": "bridge", + "bridgeId": "b2", + "initiallyOpen": false + }, + { + "x": 16, + "y": 2, + "type": "floor" + }, + { + "x": 17, + "y": 2, + "type": "floor" + }, + { + "x": 18, "y": 2, "type": "goal" + }, + { + "x": 19, + "y": 2, + "type": "floor" + }, + { + "x": 20, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "floor" + }, + { + "x": 9, + "y": 3, + "type": "fragile" + }, + { + "x": 10, + "y": 3, + "type": "fragile" + }, + { + "x": 11, + "y": 3, + "type": "fragile" + }, + { + "x": 12, + "y": 3, + "type": "floor" + }, + { + "x": 13, + "y": 3, + "type": "floor" + }, + { + "x": 16, + "y": 3, + "type": "floor" + }, + { + "x": 17, + "y": 3, + "type": "floor" + }, + { + "x": 18, + "y": 3, + "type": "floor" + }, + { + "x": 19, + "y": 3, + "type": "floor" + }, + { + "x": 20, + "y": 3, + "type": "floor" + }, + { + "x": 0, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 2, + "y": 4, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 3, + "y": 4, + "type": "floor" + }, + { + "x": 4, + "y": 4, + "type": "floor" + }, + { + "x": 7, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 4, + "type": "floor" + }, + { + "x": 9, + "y": 4, + "type": "switch", + "switchId": "s2", + "linkedBridgeIds": [ + "b2" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 10, + "y": 4, + "type": "fragile" + }, + { + "x": 11, + "y": 4, + "type": "fragile" + }, + { + "x": 12, + "y": 4, + "type": "floor" + }, + { + "x": 13, + "y": 4, + "type": "floor" + }, + { + "x": 16, + "y": 4, + "type": "floor" + }, + { + "x": 17, + "y": 4, + "type": "floor" + }, + { + "x": 18, + "y": 4, + "type": "floor" + }, + { + "x": 19, + "y": 4, + "type": "floor" + }, + { + "x": 20, + "y": 4, + "type": "floor" } ], - "par": 7 + "par": 23 }, { - "level": 35, - "name": "Around the Pillar", - "cols": 16, - "rows": 7, + "level": 33, + "name": "Pillars", + "cols": 13, + "rows": 8, "start": { - "x": 1, - "y": 3, + "x": 0, + "y": 0, "orient": "up" }, "goal": { - "x": 14, - "y": 3 + "x": 11, + "y": 7 }, "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor", + "split": true + }, + { + "x": 2, + "y": 0, + "type": "floor", + "split": true + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 5, + "y": 0, + "type": "floor" + }, + { + "x": 6, + "y": 0, + "type": "floor" + }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 11, + "y": 0, + "type": "floor" + }, + { + "x": 12, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "wall" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 5, + "y": 1, + "type": "floor" + }, + { + "x": 6, + "y": 1, + "type": "floor" + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, + "type": "floor" + }, + { + "x": 10, + "y": 1, + "type": "wall" + }, + { + "x": 11, + "y": 1, + "type": "floor" + }, + { + "x": 12, + "y": 1, + "type": "floor" + }, { "x": 0, "y": 2, @@ -5135,7 +9934,7 @@ { "x": 1, "y": 2, - "type": "floor" + "type": "wall" }, { "x": 2, @@ -5152,6 +9951,46 @@ "y": 2, "type": "floor" }, + { + "x": 5, + "y": 2, + "type": "floor" + }, + { + "x": 6, + "y": 2, + "type": "floor" + }, + { + "x": 7, + "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 2, + "type": "floor" + }, + { + "x": 11, + "y": 2, + "type": "floor" + }, + { + "x": 12, + "y": 2, + "type": "floor" + }, { "x": 0, "y": 3, @@ -5177,6 +10016,46 @@ "y": 3, "type": "floor" }, + { + "x": 5, + "y": 3, + "type": "floor" + }, + { + "x": 6, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "floor" + }, + { + "x": 9, + "y": 3, + "type": "floor" + }, + { + "x": 10, + "y": 3, + "type": "floor" + }, + { + "x": 11, + "y": 3, + "type": "floor" + }, + { + "x": 12, + "y": 3, + "type": "floor" + }, { "x": 0, "y": 4, @@ -5202,146 +10081,6 @@ "y": 4, "type": "floor" }, - { - "x": 5, - "y": 3, - "type": "floor" - }, - { - "x": 6, - "y": 3, - "type": "floor" - }, - { - "x": 5, - "y": 0, - "type": "floor" - }, - { - "x": 6, - "y": 0, - "type": "floor" - }, - { - "x": 7, - "y": 0, - "type": "floor" - }, - { - "x": 8, - "y": 0, - "type": "floor" - }, - { - "x": 9, - "y": 0, - "type": "floor" - }, - { - "x": 10, - "y": 0, - "type": "floor" - }, - { - "x": 11, - "y": 0, - "type": "floor" - }, - { - "x": 5, - "y": 1, - "type": "floor" - }, - { - "x": 6, - "y": 1, - "type": "floor" - }, - { - "x": 7, - "y": 1, - "type": "floor" - }, - { - "x": 8, - "y": 1, - "type": "floor" - }, - { - "x": 9, - "y": 1, - "type": "floor" - }, - { - "x": 10, - "y": 1, - "type": "floor" - }, - { - "x": 11, - "y": 1, - "type": "floor" - }, - { - "x": 5, - "y": 2, - "type": "floor" - }, - { - "x": 6, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 2, - "type": "floor" - }, - { - "x": 8, - "y": 2, - "type": "wall" - }, - { - "x": 9, - "y": 2, - "type": "floor" - }, - { - "x": 10, - "y": 2, - "type": "floor" - }, - { - "x": 11, - "y": 2, - "type": "floor" - }, - { - "x": 7, - "y": 3, - "type": "floor" - }, - { - "x": 8, - "y": 3, - "type": "wall" - }, - { - "x": 9, - "y": 3, - "type": "floor" - }, - { - "x": 10, - "y": 3, - "type": "floor" - }, - { - "x": 11, - "y": 3, - "type": "floor" - }, { "x": 5, "y": 4, @@ -5360,7 +10099,7 @@ { "x": 8, "y": 4, - "type": "wall" + "type": "floor" }, { "x": 9, @@ -5377,152 +10116,222 @@ "y": 4, "type": "floor" }, - { - "x": 5, - "y": 5, - "type": "floor" - }, - { - "x": 6, - "y": 5, - "type": "floor" - }, - { - "x": 7, - "y": 5, - "type": "floor" - }, - { - "x": 8, - "y": 5, - "type": "floor" - }, - { - "x": 9, - "y": 5, - "type": "floor" - }, - { - "x": 10, - "y": 5, - "type": "floor" - }, - { - "x": 11, - "y": 5, - "type": "floor" - }, - { - "x": 5, - "y": 6, - "type": "floor" - }, - { - "x": 6, - "y": 6, - "type": "floor" - }, - { - "x": 7, - "y": 6, - "type": "floor" - }, - { - "x": 8, - "y": 6, - "type": "floor" - }, - { - "x": 9, - "y": 6, - "type": "floor" - }, - { - "x": 10, - "y": 6, - "type": "floor" - }, - { - "x": 11, - "y": 6, - "type": "floor" - }, { "x": 12, - "y": 2, + "y": 4, "type": "floor" }, { - "x": 13, - "y": 2, + "x": 0, + "y": 5, "type": "floor" }, { - "x": 14, - "y": 2, + "x": 1, + "y": 5, "type": "floor" }, { - "x": 15, - "y": 2, + "x": 2, + "y": 5, + "type": "floor" + }, + { + "x": 3, + "y": 5, + "type": "wall" + }, + { + "x": 4, + "y": 5, + "type": "floor" + }, + { + "x": 5, + "y": 5, + "type": "wall" + }, + { + "x": 6, + "y": 5, + "type": "floor" + }, + { + "x": 7, + "y": 5, + "type": "floor" + }, + { + "x": 8, + "y": 5, + "type": "floor" + }, + { + "x": 9, + "y": 5, + "type": "floor" + }, + { + "x": 10, + "y": 5, + "type": "floor" + }, + { + "x": 11, + "y": 5, "type": "floor" }, { "x": 12, - "y": 3, + "y": 5, "type": "floor" }, { - "x": 13, - "y": 3, + "x": 0, + "y": 6, "type": "floor" }, { - "x": 14, - "y": 3, + "x": 1, + "y": 6, + "type": "floor" + }, + { + "x": 2, + "y": 6, + "type": "floor" + }, + { + "x": 3, + "y": 6, + "type": "floor" + }, + { + "x": 4, + "y": 6, + "type": "floor" + }, + { + "x": 5, + "y": 6, + "type": "floor" + }, + { + "x": 6, + "y": 6, + "type": "floor" + }, + { + "x": 7, + "y": 6, + "type": "floor" + }, + { + "x": 8, + "y": 6, + "type": "floor" + }, + { + "x": 9, + "y": 6, + "type": "wall" + }, + { + "x": 10, + "y": 6, + "type": "floor" + }, + { + "x": 11, + "y": 6, + "type": "wall" + }, + { + "x": 12, + "y": 6, + "type": "floor" + }, + { + "x": 0, + "y": 7, + "type": "floor" + }, + { + "x": 1, + "y": 7, + "type": "floor" + }, + { + "x": 2, + "y": 7, + "type": "floor" + }, + { + "x": 3, + "y": 7, + "type": "floor" + }, + { + "x": 4, + "y": 7, + "type": "floor" + }, + { + "x": 5, + "y": 7, + "type": "floor" + }, + { + "x": 6, + "y": 7, + "type": "floor" + }, + { + "x": 7, + "y": 7, + "type": "floor" + }, + { + "x": 8, + "y": 7, + "type": "wall" + }, + { + "x": 9, + "y": 7, + "type": "floor" + }, + { + "x": 10, + "y": 7, + "type": "floor" + }, + { + "x": 11, + "y": 7, "type": "goal" }, - { - "x": 15, - "y": 3, - "type": "floor" - }, { "x": 12, - "y": 4, - "type": "floor" - }, - { - "x": 13, - "y": 4, - "type": "floor" - }, - { - "x": 14, - "y": 4, - "type": "floor" - }, - { - "x": 15, - "y": 4, + "y": 7, "type": "floor" } ], - "par": 13 + "par": 24 }, { - "level": 36, - "name": "Grand Finale", - "cols": 22, - "rows": 5, + "level": 34, + "name": "The Gauntlet", + "cols": 25, + "rows": 8, "start": { "x": 1, "y": 1, "orient": "up" }, "goal": { - "x": 19, - "y": 2 + "x": 24, + "y": 7 }, "tiles": [ { @@ -5550,6 +10359,46 @@ "y": 0, "type": "floor" }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "fragile" + }, + { + "x": 10, + "y": 0, + "type": "fragile" + }, + { + "x": 11, + "y": 0, + "type": "fragile" + }, + { + "x": 12, + "y": 0, + "type": "fragile" + }, + { + "x": 13, + "y": 0, + "type": "floor" + }, + { + "x": 14, + "y": 0, + "type": "floor" + }, { "x": 0, "y": 1, @@ -5575,62 +10424,6 @@ "y": 1, "type": "floor" }, - { - "x": 0, - "y": 2, - "type": "floor" - }, - { - "x": 1, - "y": 2, - "type": "floor" - }, - { - "x": 2, - "y": 2, - "type": "switch", - "switchId": "s1", - "linkedBridgeIds": [ - "b1" - ], - "requireOrientation": "any", - "mode": "toggle" - }, - { - "x": 3, - "y": 2, - "type": "floor" - }, - { - "x": 4, - "y": 2, - "type": "floor" - }, - { - "x": 0, - "y": 3, - "type": "floor" - }, - { - "x": 1, - "y": 3, - "type": "floor" - }, - { - "x": 2, - "y": 3, - "type": "floor" - }, - { - "x": 3, - "y": 3, - "type": "floor" - }, - { - "x": 4, - "y": 3, - "type": "floor" - }, { "x": 5, "y": 1, @@ -5638,35 +10431,12 @@ "bridgeId": "b1", "initiallyOpen": false }, - { - "x": 6, - "y": 0, - "type": "floor" - }, - { - "x": 7, - "y": 0, - "type": "floor" - }, - { - "x": 8, - "y": 0, - "type": "floor" - }, - { - "x": 9, - "y": 0, - "type": "floor" - }, - { - "x": 10, - "y": 0, - "type": "floor" - }, { "x": 6, "y": 1, - "type": "floor" + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false }, { "x": 7, @@ -5681,42 +10451,402 @@ { "x": 9, "y": 1, - "type": "floor", - "split": true + "type": "fragile" }, { "x": 10, "y": 1, - "type": "floor", - "split": true + "type": "fragile" }, { - "x": 6, + "x": 11, + "y": 1, + "type": "fragile" + }, + { + "x": 12, + "y": 1, + "type": "fragile" + }, + { + "x": 13, + "y": 1, + "type": "floor" + }, + { + "x": 14, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, "y": 2, "type": "floor" }, { "x": 7, "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, "type": "fragile" }, { - "x": 8, + "x": 10, "y": 2, + "type": "fragile" + }, + { + "x": 11, + "y": 2, + "type": "fragile" + }, + { + "x": 12, + "y": 2, + "type": "fragile" + }, + { + "x": 13, + "y": 2, + "type": "floor" + }, + { + "x": 14, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, "type": "floor" }, { "x": 9, - "y": 2, - "type": "floor" + "y": 3, + "type": "fragile" }, { "x": 10, + "y": 3, + "type": "fragile" + }, + { + "x": 11, + "y": 3, + "type": "fragile" + }, + { + "x": 12, + "y": 3, + "type": "fragile" + }, + { + "x": 13, + "y": 3, + "type": "floor" + }, + { + "x": 14, + "y": 3, + "type": "floor" + }, + { + "x": 0, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 2, + "y": 4, + "type": "floor" + }, + { + "x": 3, + "y": 4, + "type": "floor" + }, + { + "x": 4, + "y": 4, + "type": "floor" + }, + { + "x": 7, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 4, + "type": "floor" + }, + { + "x": 9, + "y": 4, + "type": "fragile" + }, + { + "x": 10, + "y": 4, + "type": "fragile" + }, + { + "x": 11, + "y": 4, + "type": "fragile" + }, + { + "x": 12, + "y": 4, + "type": "fragile" + }, + { + "x": 13, + "y": 4, + "type": "floor" + }, + { + "x": 14, + "y": 4, + "type": "floor" + }, + { + "x": 14, + "y": 5, + "type": "floor" + }, + { + "x": 14, + "y": 6, + "type": "floor" + }, + { + "x": 14, + "y": 7, + "type": "floor" + }, + { + "x": 15, + "y": 7, + "type": "floor" + }, + { + "x": 16, + "y": 7, + "type": "floor" + }, + { + "x": 17, + "y": 7, + "type": "floor" + }, + { + "x": 18, + "y": 7, + "type": "floor", + "split": true + }, + { + "x": 19, + "y": 7, + "type": "floor", + "split": true + }, + { + "x": 20, + "y": 7, + "type": "floor" + }, + { + "x": 21, + "y": 7, + "type": "floor" + }, + { + "x": 22, + "y": 7, + "type": "floor" + }, + { + "x": 23, + "y": 7, + "type": "floor" + }, + { + "x": 24, + "y": 7, + "type": "goal" + } + ], + "par": 29 + }, + { + "level": 35, + "name": "Two Wings", + "cols": 24, + "rows": 9, + "start": { + "x": 1, + "y": 4, + "orient": "up" + }, + "goal": { + "x": 21, + "y": 4 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 2, "y": 2, "type": "floor" }, { - "x": 6, + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "floor" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, "y": 3, "type": "floor" }, @@ -5740,111 +10870,6 @@ "y": 3, "type": "floor" }, - { - "x": 11, - "y": 0, - "type": "floor" - }, - { - "x": 12, - "y": 0, - "type": "floor" - }, - { - "x": 13, - "y": 0, - "type": "floor" - }, - { - "x": 14, - "y": 0, - "type": "floor" - }, - { - "x": 15, - "y": 0, - "type": "floor" - }, - { - "x": 16, - "y": 0, - "type": "floor" - }, - { - "x": 17, - "y": 0, - "type": "floor" - }, - { - "x": 11, - "y": 1, - "type": "floor" - }, - { - "x": 12, - "y": 1, - "type": "floor" - }, - { - "x": 13, - "y": 1, - "type": "floor" - }, - { - "x": 14, - "y": 1, - "type": "wall" - }, - { - "x": 15, - "y": 1, - "type": "floor" - }, - { - "x": 16, - "y": 1, - "type": "floor" - }, - { - "x": 17, - "y": 1, - "type": "floor" - }, - { - "x": 11, - "y": 2, - "type": "floor" - }, - { - "x": 12, - "y": 2, - "type": "floor" - }, - { - "x": 13, - "y": 2, - "type": "floor" - }, - { - "x": 14, - "y": 2, - "type": "wall" - }, - { - "x": 15, - "y": 2, - "type": "floor" - }, - { - "x": 16, - "y": 2, - "type": "floor" - }, - { - "x": 17, - "y": 2, - "type": "floor" - }, { "x": 11, "y": 3, @@ -5863,7 +10888,7 @@ { "x": 14, "y": 3, - "type": "wall" + "type": "floor" }, { "x": 15, @@ -5875,128 +10900,869 @@ "y": 3, "type": "floor" }, - { - "x": 17, - "y": 3, - "type": "floor" - }, - { - "x": 11, - "y": 4, - "type": "floor" - }, - { - "x": 12, - "y": 4, - "type": "floor" - }, - { - "x": 13, - "y": 4, - "type": "floor" - }, - { - "x": 14, - "y": 4, - "type": "floor" - }, - { - "x": 15, - "y": 4, - "type": "floor" - }, - { - "x": 16, - "y": 4, - "type": "floor" - }, - { - "x": 17, - "y": 4, - "type": "floor" - }, - { - "x": 18, - "y": 0, - "type": "floor" - }, { "x": 19, - "y": 0, + "y": 3, "type": "floor" }, { "x": 20, - "y": 0, + "y": 3, "type": "floor" }, { "x": 21, - "y": 0, + "y": 3, "type": "floor" }, + { + "x": 22, + "y": 3, + "type": "floor" + }, + { + "x": 23, + "y": 3, + "type": "floor" + }, + { + "x": 0, + "y": 4, + "type": "floor" + }, + { + "x": 1, + "y": 4, + "type": "floor" + }, + { + "x": 2, + "y": 4, + "type": "floor" + }, + { + "x": 3, + "y": 4, + "type": "floor" + }, + { + "x": 4, + "y": 4, + "type": "floor" + }, + { + "x": 5, + "y": 4, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 6, + "y": 4, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 7, + "y": 4, + "type": "floor" + }, + { + "x": 8, + "y": 4, + "type": "floor" + }, + { + "x": 9, + "y": 4, + "type": "floor" + }, + { + "x": 10, + "y": 4, + "type": "floor" + }, + { + "x": 11, + "y": 4, + "type": "floor" + }, + { + "x": 12, + "y": 4, + "type": "floor" + }, + { + "x": 13, + "y": 4, + "type": "floor" + }, + { + "x": 14, + "y": 4, + "type": "floor" + }, + { + "x": 15, + "y": 4, + "type": "floor" + }, + { + "x": 16, + "y": 4, + "type": "floor" + }, + { + "x": 17, + "y": 4, + "type": "bridge", + "bridgeId": "b2", + "initiallyOpen": false + }, { "x": 18, - "y": 1, - "type": "floor" + "y": 4, + "type": "bridge", + "bridgeId": "b2", + "initiallyOpen": false }, { "x": 19, - "y": 1, + "y": 4, "type": "floor" }, { "x": 20, - "y": 1, + "y": 4, "type": "floor" }, { "x": 21, - "y": 1, - "type": "floor" - }, - { - "x": 18, - "y": 2, - "type": "floor" - }, - { - "x": 19, - "y": 2, + "y": 4, "type": "goal" }, { - "x": 20, - "y": 2, + "x": 22, + "y": 4, "type": "floor" }, { - "x": 21, - "y": 2, + "x": 23, + "y": 4, "type": "floor" }, { - "x": 18, - "y": 3, + "x": 0, + "y": 5, + "type": "floor" + }, + { + "x": 1, + "y": 5, + "type": "floor" + }, + { + "x": 2, + "y": 5, + "type": "floor" + }, + { + "x": 3, + "y": 5, + "type": "floor" + }, + { + "x": 4, + "y": 5, + "type": "floor" + }, + { + "x": 7, + "y": 5, + "type": "floor" + }, + { + "x": 8, + "y": 5, + "type": "floor" + }, + { + "x": 9, + "y": 5, + "type": "floor" + }, + { + "x": 10, + "y": 5, + "type": "floor" + }, + { + "x": 11, + "y": 5, + "type": "floor" + }, + { + "x": 12, + "y": 5, + "type": "floor" + }, + { + "x": 13, + "y": 5, + "type": "floor" + }, + { + "x": 14, + "y": 5, + "type": "floor" + }, + { + "x": 15, + "y": 5, + "type": "floor" + }, + { + "x": 16, + "y": 5, "type": "floor" }, { "x": 19, - "y": 3, + "y": 5, "type": "floor" }, { "x": 20, - "y": 3, + "y": 5, "type": "floor" }, { "x": 21, - "y": 3, + "y": 5, + "type": "floor" + }, + { + "x": 22, + "y": 5, + "type": "floor" + }, + { + "x": 23, + "y": 5, + "type": "floor" + }, + { + "x": 2, + "y": 6, + "type": "floor" + }, + { + "x": 0, + "y": 7, + "type": "floor" + }, + { + "x": 1, + "y": 7, + "type": "floor" + }, + { + "x": 2, + "y": 7, + "type": "floor" + }, + { + "x": 3, + "y": 7, + "type": "floor" + }, + { + "x": 4, + "y": 7, + "type": "floor" + }, + { + "x": 0, + "y": 8, + "type": "floor" + }, + { + "x": 1, + "y": 8, + "type": "floor" + }, + { + "x": 2, + "y": 8, + "type": "switch", + "switchId": "s2", + "linkedBridgeIds": [ + "b2" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 3, + "y": 8, + "type": "floor" + }, + { + "x": 4, + "y": 8, "type": "floor" } ], - "par": 17 + "par": 36 + }, + { + "level": 36, + "name": "Grand Finale", + "cols": 24, + "rows": 13, + "start": { + "x": 1, + "y": 1, + "orient": "up" + }, + "goal": { + "x": 23, + "y": 12 + }, + "tiles": [ + { + "x": 0, + "y": 0, + "type": "floor" + }, + { + "x": 1, + "y": 0, + "type": "floor" + }, + { + "x": 2, + "y": 0, + "type": "floor" + }, + { + "x": 3, + "y": 0, + "type": "floor" + }, + { + "x": 4, + "y": 0, + "type": "floor" + }, + { + "x": 7, + "y": 0, + "type": "floor" + }, + { + "x": 8, + "y": 0, + "type": "floor" + }, + { + "x": 9, + "y": 0, + "type": "floor" + }, + { + "x": 10, + "y": 0, + "type": "floor" + }, + { + "x": 11, + "y": 0, + "type": "floor" + }, + { + "x": 12, + "y": 0, + "type": "floor" + }, + { + "x": 13, + "y": 0, + "type": "floor" + }, + { + "x": 0, + "y": 1, + "type": "floor" + }, + { + "x": 1, + "y": 1, + "type": "floor" + }, + { + "x": 2, + "y": 1, + "type": "floor" + }, + { + "x": 3, + "y": 1, + "type": "floor" + }, + { + "x": 4, + "y": 1, + "type": "floor" + }, + { + "x": 5, + "y": 1, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 6, + "y": 1, + "type": "bridge", + "bridgeId": "b1", + "initiallyOpen": false + }, + { + "x": 7, + "y": 1, + "type": "floor" + }, + { + "x": 8, + "y": 1, + "type": "floor" + }, + { + "x": 9, + "y": 1, + "type": "floor" + }, + { + "x": 10, + "y": 1, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 11, + "y": 1, + "type": "floor" + }, + { + "x": 12, + "y": 1, + "type": "floor" + }, + { + "x": 13, + "y": 1, + "type": "floor" + }, + { + "x": 0, + "y": 2, + "type": "floor" + }, + { + "x": 1, + "y": 2, + "type": "floor" + }, + { + "x": 2, + "y": 2, + "type": "floor" + }, + { + "x": 3, + "y": 2, + "type": "floor" + }, + { + "x": 4, + "y": 2, + "type": "floor" + }, + { + "x": 7, + "y": 2, + "type": "floor" + }, + { + "x": 8, + "y": 2, + "type": "floor" + }, + { + "x": 9, + "y": 2, + "type": "floor" + }, + { + "x": 10, + "y": 2, + "type": "floor" + }, + { + "x": 11, + "y": 2, + "type": "floor" + }, + { + "x": 12, + "y": 2, + "type": "floor" + }, + { + "x": 13, + "y": 2, + "type": "floor" + }, + { + "x": 0, + "y": 3, + "type": "floor" + }, + { + "x": 1, + "y": 3, + "type": "floor" + }, + { + "x": 2, + "y": 3, + "type": "switch", + "switchId": "s1", + "linkedBridgeIds": [ + "b1" + ], + "requireOrientation": "standing", + "mode": "toggle" + }, + { + "x": 3, + "y": 3, + "type": "floor" + }, + { + "x": 4, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 3, + "type": "floor" + }, + { + "x": 8, + "y": 3, + "type": "floor" + }, + { + "x": 9, + "y": 3, + "type": "floor" + }, + { + "x": 10, + "y": 3, + "type": "floor" + }, + { + "x": 11, + "y": 3, + "type": "floor" + }, + { + "x": 12, + "y": 3, + "type": "floor" + }, + { + "x": 13, + "y": 3, + "type": "floor" + }, + { + "x": 7, + "y": 5, + "type": "floor" + }, + { + "x": 8, + "y": 5, + "type": "floor" + }, + { + "x": 9, + "y": 5, + "type": "fragile" + }, + { + "x": 10, + "y": 5, + "type": "fragile" + }, + { + "x": 11, + "y": 5, + "type": "fragile" + }, + { + "x": 12, + "y": 5, + "type": "fragile" + }, + { + "x": 13, + "y": 5, + "type": "floor" + }, + { + "x": 14, + "y": 5, + "type": "floor" + }, + { + "x": 7, + "y": 6, + "type": "floor" + }, + { + "x": 8, + "y": 6, + "type": "floor" + }, + { + "x": 9, + "y": 6, + "type": "fragile" + }, + { + "x": 10, + "y": 6, + "type": "fragile" + }, + { + "x": 11, + "y": 6, + "type": "fragile" + }, + { + "x": 12, + "y": 6, + "type": "fragile" + }, + { + "x": 13, + "y": 6, + "type": "floor" + }, + { + "x": 14, + "y": 6, + "type": "floor" + }, + { + "x": 7, + "y": 7, + "type": "teleport", + "teleportId": "t" + }, + { + "x": 8, + "y": 7, + "type": "floor" + }, + { + "x": 9, + "y": 7, + "type": "fragile" + }, + { + "x": 10, + "y": 7, + "type": "fragile" + }, + { + "x": 11, + "y": 7, + "type": "fragile" + }, + { + "x": 12, + "y": 7, + "type": "fragile" + }, + { + "x": 13, + "y": 7, + "type": "floor" + }, + { + "x": 14, + "y": 7, + "type": "floor" + }, + { + "x": 7, + "y": 8, + "type": "floor" + }, + { + "x": 8, + "y": 8, + "type": "floor" + }, + { + "x": 9, + "y": 8, + "type": "fragile" + }, + { + "x": 10, + "y": 8, + "type": "fragile" + }, + { + "x": 11, + "y": 8, + "type": "fragile" + }, + { + "x": 12, + "y": 8, + "type": "fragile" + }, + { + "x": 13, + "y": 8, + "type": "floor" + }, + { + "x": 14, + "y": 8, + "type": "floor" + }, + { + "x": 7, + "y": 9, + "type": "floor" + }, + { + "x": 8, + "y": 9, + "type": "floor" + }, + { + "x": 9, + "y": 9, + "type": "fragile" + }, + { + "x": 10, + "y": 9, + "type": "fragile" + }, + { + "x": 11, + "y": 9, + "type": "fragile" + }, + { + "x": 12, + "y": 9, + "type": "fragile" + }, + { + "x": 13, + "y": 9, + "type": "floor" + }, + { + "x": 14, + "y": 9, + "type": "floor" + }, + { + "x": 13, + "y": 10, + "type": "floor" + }, + { + "x": 13, + "y": 11, + "type": "floor" + }, + { + "x": 13, + "y": 12, + "type": "floor" + }, + { + "x": 14, + "y": 12, + "type": "floor" + }, + { + "x": 15, + "y": 12, + "type": "floor" + }, + { + "x": 16, + "y": 12, + "type": "floor" + }, + { + "x": 17, + "y": 12, + "type": "floor", + "split": true + }, + { + "x": 18, + "y": 12, + "type": "floor", + "split": true + }, + { + "x": 19, + "y": 12, + "type": "floor" + }, + { + "x": 20, + "y": 12, + "type": "floor" + }, + { + "x": 21, + "y": 12, + "type": "floor" + }, + { + "x": 22, + "y": 12, + "type": "floor" + }, + { + "x": 23, + "y": 12, + "type": "goal" + } + ], + "par": 36 } ] } \ No newline at end of file diff --git a/data/totalannihilation-artwork.json b/data/totalannihilation-artwork.json index 5cd5875..4820f55 100644 --- a/data/totalannihilation-artwork.json +++ b/data/totalannihilation-artwork.json @@ -31,8 +31,8 @@ "sheets": { "arm-units": { - "key": "ta-arm-units", "path": null, "kind": "unit", - "frameWidth": 64, "frameHeight": 64, "cols": 8 + "key": "ta-arm-units", "path": "assets/images/ta/arm-units.png", "kind": "unit", + "frameWidth": 128, "frameHeight": 128, "cols": 8 }, "arm-structures": { "key": "ta-arm-structures", "path": "assets/images/ta/arm-structures.png", "kind": "structure", diff --git a/data/totalannihilation-rules.json b/data/totalannihilation-rules.json index bb75825..e7535c3 100644 --- a/data/totalannihilation-rules.json +++ b/data/totalannihilation-rules.json @@ -29,6 +29,7 @@ "separationStiffness": 0.55, "shoveAfterSec": 0.6, "stuckGiveUpSec": 3.0, + "stuckRepathSec": 1.0, "arriveSlackPx": 12, "friendlyFire": false, "buildPowerNominal": 100, @@ -222,7 +223,7 @@ "id": "rifle", "name": "Assault Rifle", "kind": "hitscan", - "damage": 6, + "damage": 9, "reload": 0.5, "burst": 3, "burstDelay": 0.06, @@ -251,7 +252,7 @@ "name": "Marksman Rifle", "kind": "hitscan", "damage": 55, - "reload": 3.0, + "reload": 2.0, "burst": 1, "range": 460, "spread": 0.0, @@ -308,7 +309,7 @@ "id": "rocketpod", "name": "Rocket Pod", "kind": "guided", - "damage": 80, + "damage": 40, "reload": 4.0, "burst": 2, "burstDelay": 0.25, diff --git a/src/games/bloxorz/BloxorzGame.js b/src/games/bloxorz/BloxorzGame.js index bfa9855..ed70bcd 100644 --- a/src/games/bloxorz/BloxorzGame.js +++ b/src/games/bloxorz/BloxorzGame.js @@ -6,17 +6,16 @@ import { playSound, SFX } from '../../ui/Sounds.js'; import { api } from '../../services/api.js'; import { loadLevel, newState, cloneState, applyMove, solve, tileRenderInfo, + blockCells, previewRoll, isWallAt, } from './BloxorzLogic.js'; +import { + EX, EY, TILE_DEPTH, project, depthOf, boardBounds, + boxesForBlock, boxCorners, visibleFaces, faceShade, rollPoints, translatePoints, +} from './BloxorzIso.js'; -// ── Isometric projection ────────────────────────────────────────────────────── -// 2:1 diamond projection. Board size varies a lot across the 36 levels (a 6x5 -// intro vs. a 22x5 capstone), so everything scales per-level to fit the same -// screen area rather than using fixed pixel constants. -const TW = 96; -const TH = 48; -const TILE_DEPTH = 26; -const BLOCK_HEIGHT = 84; -const SPLIT_HEIGHT = 44; +// Board size varies a lot across the 36 levels (a 6x5 intro vs. a 28x5 +// capstone), so everything scales per-level to fit the same screen area rather +// than using fixed pixel constants. The projection itself lives in BloxorzIso. const VOID_BG = 0x05070a; const FLOOR_TOP = 0xd8b98a; @@ -29,9 +28,14 @@ const WALL_TOP = 0x5b6472; const WALL_CLIFF_S = 0x363b44; const WALL_CLIFF_E = 0x454c57; const GOAL_TOP = 0x0a0c10; -const BLOCK_TOP = 0x4a90d9; -const BLOCK_SIDE_S = 0x2f5f8f; -const BLOCK_SIDE_E = 0x3a74ad; +// Block faces are shaded continuously between these two, by their normal, so a +// rolling face darkens as it turns away from the light instead of popping. +const BLOCK_DARK = { r: 0x24, g: 0x48, b: 0x6b }; +const BLOCK_LIT = { r: 0x4a, g: 0x90, b: 0xd9 }; +const BLOCK_EDGE = 0x1d3a57; + +const ROLL_MS = 170; +const FALL_MS = 460; const D = { board: 0, block: 10, ui: 30, overlay: 60, overlayUI: 62 }; @@ -40,26 +44,26 @@ const DIR_KEYS = { KeyA: 'left', KeyD: 'right', KeyW: 'up', KeyS: 'down', }; -function shapeFromBlock(block) { - if (block.mode === 'split') { - return [ - { gx0: block.a.x, gy0: block.a.y, gx1: block.a.x + 1, gy1: block.a.y + 1, height: SPLIT_HEIGHT }, - { gx0: block.b.x, gy0: block.b.y, gx1: block.b.x + 1, gy1: block.b.y + 1, height: SPLIT_HEIGHT }, - ]; - } - if (block.orient === 'up') return [{ gx0: block.x, gy0: block.y, gx1: block.x + 1, gy1: block.y + 1, height: BLOCK_HEIGHT }]; - if (block.orient === 'x') return [{ gx0: block.x, gy0: block.y, gx1: block.x + 2, gy1: block.y + 1, height: BLOCK_HEIGHT }]; - return [{ gx0: block.x, gy0: block.y, gx1: block.x + 1, gy1: block.y + 2, height: BLOCK_HEIGHT }]; +const DIR_STEP = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] }; + +// Screen-space direction each arrow key rolls the block, for the HUD compass +// and for resolving swipes. +const DIR_SCREEN = { + right: { x: EX.x, y: EX.y }, + left: { x: -EX.x, y: -EX.y }, + down: { x: EY.x, y: EY.y }, + up: { x: -EY.x, y: -EY.y }, +}; + +function cellsKey(cells) { + return cells.map(([x, y]) => `${x},${y}`).sort().join('|'); } -function lerpPart(a, b, t) { - return { - gx0: a.gx0 + (b.gx0 - a.gx0) * t, - gy0: a.gy0 + (b.gy0 - a.gy0) * t, - gx1: a.gx1 + (b.gx1 - a.gx1) * t, - gy1: a.gy1 + (b.gy1 - a.gy1) * t, - height: a.height + (b.height - a.height) * t, - }; +function shadeToColor(t) { + const r = Math.round(BLOCK_DARK.r + (BLOCK_LIT.r - BLOCK_DARK.r) * t); + const g = Math.round(BLOCK_DARK.g + (BLOCK_LIT.g - BLOCK_DARK.g) * t); + const b = Math.round(BLOCK_DARK.b + (BLOCK_LIT.b - BLOCK_DARK.b) * t); + return (r << 16) | (g << 8) | b; } export default class BloxorzGame extends Phaser.Scene { @@ -83,8 +87,7 @@ export default class BloxorzGame extends Phaser.Scene { this.overlayUp = false; this.scaleF = 1; - this.originX = 0; - this.originY = 0; + this.proj = { originX: 0, originY: 0, scaleF: 1 }; } async create() { @@ -136,7 +139,14 @@ export default class BloxorzGame extends Phaser.Scene { const dy = p.y - touchStart.y; touchStart = null; if (Math.max(Math.abs(dx), Math.abs(dy)) < 30) return; - const dir = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? 'right' : 'left') : (dy > 0 ? 'down' : 'up'); + // The grid axes aren't screen axes, so pick the direction whose projected + // screen vector the swipe points most closely along. + let dir = 'right'; + let best = -Infinity; + for (const [d, v] of Object.entries(DIR_SCREEN)) { + const dot = (dx * v.x + dy * v.y) / (Math.hypot(v.x, v.y) || 1); + if (dot > best) { best = dot; dir = d; } + } this.tryMove(dir); }); } @@ -279,31 +289,40 @@ export default class BloxorzGame extends Phaser.Scene { this.blockGfx = this.add.graphics().setDepth(D.block); this.layer.add([this.tileGfx, this.blockGfx]); this.redrawTiles(); - this.redrawBlock(shapeFromBlock(this.state.block)); + this.redrawBlockState(this.state.block); } _computeProjection() { const { cols, rows } = this.compiled; - const span = cols + rows; - const boardW = span * (TW / 2); - const boardH = span * (TH / 2); - const availW = 1300; - const availH = 560; - this.scaleF = Math.min(1, availW / boardW, availH / (boardH + 220)); - this.originX = GAME_WIDTH / 2 - (cols - rows) * (TW / 2) * this.scaleF / 2; - this.originY = 300; + // Play area: the HUD column sits left of x=240 and the far right is empty, + // so the board is centred right of screen centre rather than on it. + const availW = 1560; + const availH = 760; + const centreX = 1060; + const centreY = 590; + + const b = boardBounds(cols, rows, 2); + this.scaleF = Math.min(1, availW / b.w, availH / b.h); + // Cached because project() is called per box corner, per frame. + // (`this.proj`, not `this.view` — that one holds 'select' | 'play'.) + this.proj = { + originX: centreX - ((b.minX + b.maxX) / 2) * this.scaleF, + originY: centreY - ((b.minY + b.maxY) / 2) * this.scaleF, + scaleF: this.scaleF, + }; } - isoX(x, y) { return this.originX + (x - y) * (TW / 2) * this.scaleF; } - - isoY(x, y) { return this.originY + (x + y) * (TH / 2) * this.scaleF; } + project3(p) { + const { sx, sy } = project(p, this.proj); + return [sx, sy]; + } footprintCorners(gx0, gy0, gx1, gy1) { return [ - [this.isoX(gx0, gy0), this.isoY(gx0, gy0)], - [this.isoX(gx1, gy0), this.isoY(gx1, gy0)], - [this.isoX(gx1, gy1), this.isoY(gx1, gy1)], - [this.isoX(gx0, gy1), this.isoY(gx0, gy1)], + this.project3({ x: gx0, y: gy0, z: 0 }), + this.project3({ x: gx1, y: gy0, z: 0 }), + this.project3({ x: gx1, y: gy1, z: 0 }), + this.project3({ x: gx0, y: gy1, z: 0 }), ]; } @@ -326,14 +345,13 @@ export default class BloxorzGame extends Phaser.Scene { redrawTiles() { this.tileGfx.clear(); const cells = [...this.compiled.tiles.keys()].map((k) => k.split(',').map(Number)); - cells.sort((a, b) => (a[0] + a[1]) - (b[0] + b[1])); + cells.sort((a, b) => depthOf(a[0], a[1]) - depthOf(b[0], b[1])); for (const [x, y] of cells) this._drawTile(x, y); } _drawTile(x, y) { const info = tileRenderInfo(this.compiled, this.state, x, y); if (!info) return; - if (info.type === 'fragile' && info.broken) return; // broken = void, nothing to draw const c = this.footprintCorners(x, y, x + 1, y + 1); const hasSouth = this.compiled.tiles.has(`${x},${y + 1}`); @@ -364,10 +382,22 @@ export default class BloxorzGame extends Phaser.Scene { const mid = [(c[0][0] + c[2][0]) / 2, (c[0][1] + c[2][1]) / 2]; const r = 10 * this.scaleF; if (info.type === 'switch') { - const isSoft = info.requireOrientation === 'lying'; - this.tileGfx.fillStyle(isSoft ? 0xf2ead8 : 0xd4602a, 0.95); - if (isSoft) this.tileGfx.fillCircle(mid[0], mid[1], r); - else this.tileGfx.fillRect(mid[0] - r, mid[1] - r, r * 2, r * 2); + // Three distinct marks, following the original: a heavy X you must land + // on upright, a round soft switch any contact trips, and a hold pad that + // only counts while a lying block covers it. + if (info.requireOrientation === 'standing') { + this.tileGfx.lineStyle(Math.max(3, 5 * this.scaleF), 0xd4602a, 1); + this.tileGfx.lineBetween(mid[0] - r, mid[1] - r * 0.62, mid[0] + r, mid[1] + r * 0.62); + this.tileGfx.lineBetween(mid[0] - r, mid[1] + r * 0.62, mid[0] + r, mid[1] - r * 0.62); + } else if (info.requireOrientation === 'lying') { + this.tileGfx.fillStyle(0xf2ead8, 0.95); + this.tileGfx.fillRect(mid[0] - r, mid[1] - r * 0.38, r * 2, r * 0.76); + } else { + this.tileGfx.fillStyle(0xd4602a, 0.95); + this.tileGfx.fillCircle(mid[0], mid[1], r * 0.85); + this.tileGfx.lineStyle(Math.max(1, 2 * this.scaleF), 0xf2ead8, 0.9); + this.tileGfx.strokeCircle(mid[0], mid[1], r * 0.85); + } } else if (info.type === 'teleport') { const hue = (hashStr(info.teleportId) % 360) / 360; const color = Phaser.Display.Color.HSVToRGB(hue, 0.65, 0.95).color; @@ -381,18 +411,30 @@ export default class BloxorzGame extends Phaser.Scene { } } - redrawBlock(parts) { + // `pieces` is a list of 8-corner world-space point arrays — the block mid-roll + // is just the same rigid corners rotated, so every draw path shares this. + redrawBlock(pieces) { this.blockGfx.clear(); - for (const part of parts) this._drawBlockPart(part); + for (const pts of pieces) this._drawBox(pts); } - _drawBlockPart({ gx0, gy0, gx1, gy1, height }) { - const h = height * this.scaleF; - const c = this.footprintCorners(gx0, gy0, gx1, gy1); - this.fillFace(this.blockGfx, c[3], c[2], -h, BLOCK_SIDE_S); - this.fillFace(this.blockGfx, c[1], c[2], -h, BLOCK_SIDE_E); - const top = c.map(([px, py]) => [px, py - h]); - this.fillPoly(this.blockGfx, top, BLOCK_TOP, 1); + redrawBlockState(block, zOff = 0) { + this.redrawBlock(boxesForBlock(block) + .map((b) => translatePoints(boxCorners(b), 0, 0, zOff))); + } + + _drawBox(pts) { + const stroke = Math.max(1, 2 * this.scaleF); + for (const face of visibleFaces(pts)) { + const corners = face.pts.map((p) => this.project3(p)); + this.fillPoly(this.blockGfx, corners, shadeToColor(faceShade(face.normal)), 1); + this.blockGfx.lineStyle(stroke, BLOCK_EDGE, 0.9); + this.blockGfx.beginPath(); + this.blockGfx.moveTo(corners[0][0], corners[0][1]); + for (let i = 1; i < corners.length; i++) this.blockGfx.lineTo(corners[i][0], corners[i][1]); + this.blockGfx.closePath(); + this.blockGfx.strokePath(); + } } // ── HUD ────────────────────────────────────────────────────────────────────── @@ -429,9 +471,39 @@ export default class BloxorzGame extends Phaser.Scene { { width: BTN_W, height: BTN_H, fontSize: 22, variant: 'ghost' }); this.undoBtn = undo; this.layer.add([undo, reset, hint, levels]); + this._drawCompass(BTN_X, btnY + 150); this.updateMoves(); } + // The board is yawed, not screen-aligned, so spell the key mapping out: each + // arrow is drawn along the direction that key actually rolls the block. + _drawCompass(cx, cy) { + const g = this.add.graphics().setDepth(D.ui); + const LEN = 34; + g.lineStyle(3, COLORS.gold, 0.75); + for (const v of Object.values(DIR_SCREEN)) { + const m = Math.hypot(v.x, v.y) || 1; + g.lineBetween(cx, cy, cx + (v.x / m) * LEN, cy + (v.y / m) * LEN); + } + g.fillStyle(COLORS.gold, 0.9); + g.fillCircle(cx, cy, 4); + this.layer.add(g); + + const GLYPH = { up: '▲', down: '▼', left: '◀', right: '▶' }; + for (const [dir, v] of Object.entries(DIR_SCREEN)) { + const m = Math.hypot(v.x, v.y) || 1; + const label = this.add.text(cx + (v.x / m) * (LEN + 16), cy + (v.y / m) * (LEN + 16), GLYPH[dir], { + fontFamily: 'Righteous', fontSize: '20px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(D.ui); + this.layer.add(label); + } + + const caption = this.add.text(cx, cy + LEN + 46, 'Arrow keys / WASD', { + fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex, + }).setOrigin(0.5).setDepth(D.ui); + this.layer.add(caption); + } + updateMoves() { if (this.movesText) this.movesText.setText(`Moves: ${this.moves} Par: ${this.par}`); if (this.undoBtn) this.undoBtn.setEnabled(this.undoStack.length > 0); @@ -444,50 +516,136 @@ export default class BloxorzGame extends Phaser.Scene { const prevBlock = this.state.block; const snapshot = cloneState(this.state); const res = applyMove(this.compiled, this.state, dir); - if (!res.moved) { this._shakeBlock(); return; } + if (!res.moved) { this._shakeBlock(dir); return; } this.undoStack.push(snapshot); this.moves++; this.updateMoves(); this.busy = true; playSound(this, SFX.PIECE_CLICK); - this._animateMove(prevBlock, this.state.block, () => { - this.busy = false; + + const rolls = this._buildRolls(prevBlock, dir); + this._playRoll(rolls, () => { + if (this.state.status === 'dead') { this._tumbleOff(rolls); return; } this.redrawTiles(); - if (this.state.status === 'dead') this._onFall(); - else if (this.state.status === 'won') this.onSolved(); + + // A landing that doesn't match where the roll physically put the block + // means the engine relocated it — i.e. a teleport pad. + const teleported = cellsKey(this._rollCells(rolls)) !== cellsKey(blockCells(this.state.block)); + const finish = () => { + if (this.state.status === 'won') this._dropInHole(); + else this.busy = false; + }; + if (teleported) this._teleportIn(finish); + else { this.redrawBlockState(this.state.block); finish(); } }); } - _animateMove(prevBlock, nextBlock, onDone) { - const prevParts = shapeFromBlock(prevBlock); - const nextParts = shapeFromBlock(nextBlock); - if (prevParts.length !== nextParts.length) { - this.redrawBlock(nextParts); - this.blockGfx.setAlpha(0.35); - this.tweens.add({ targets: this.blockGfx, alpha: 1, duration: 160, onComplete: onDone }); - return; + // One entry per rigid piece: its pre-move box, its corners, and the direction + // it tips (null = stayed put, which only happens to a split cube stopped by a + // wall — walls block a single unit without killing it). + _buildRolls(block, dir) { + const [dx, dy] = DIR_STEP[dir]; + return boxesForBlock(block).map((box, i) => { + let dest; + if (block.mode === 'split') { + const u = i === 0 ? block.a : block.b; + dest = isWallAt(this.compiled, u.x + dx, u.y + dy) ? null : [[u.x + dx, u.y + dy]]; + } else { + dest = blockCells(previewRoll(block, dir)); + } + return { box, pts: boxCorners(box), dir: dest ? dir : null, cells: dest ?? this._boxCells(box) }; + }); + } + + _boxCells(box) { + const cells = []; + for (let x = Math.round(box.x0); x < Math.round(box.x1); x++) { + for (let y = Math.round(box.y0); y < Math.round(box.y1); y++) cells.push([x, y]); } + return cells; + } + + _rollCells(rolls) { + return rolls.flatMap((r) => r.cells); + } + + _rolledPoints(rolls, t, dz = 0) { + return rolls.map((r) => { + const pts = r.dir ? rollPoints(r.pts, r.dir, r.box, t) : r.pts; + return dz ? translatePoints(pts, 0, 0, dz) : pts; + }); + } + + // A topple accelerates into the ground, so ease in rather than in-out. + _playRoll(rolls, onDone) { const tw = { t: 0 }; this.tweens.add({ targets: tw, t: 1, - duration: 170, - ease: 'Sine.easeInOut', - onUpdate: () => { - const parts = prevParts.map((p, i) => lerpPart(p, nextParts[i], tw.t)); - this.redrawBlock(parts); - }, + duration: ROLL_MS, + ease: 'Sine.easeIn', + onUpdate: () => this.redrawBlock(this._rolledPoints(rolls, tw.t)), onComplete: onDone, }); } - _shakeBlock() { + _shakeBlock(dir) { if (!this.blockGfx) return; - const x0 = this.blockGfx.x; + const v = DIR_SCREEN[dir] ?? DIR_SCREEN.right; + const m = Math.hypot(v.x, v.y) || 1; + // The graphics object always draws in absolute coordinates, so its + // transform is only ever this nudge — restart it from zero so mashing a + // blocked direction can't accumulate an offset. + this.tweens.killTweensOf(this.blockGfx); + this.blockGfx.setPosition(0, 0); this.tweens.add({ - targets: this.blockGfx, x: x0 + 10, duration: 60, yoyo: true, repeat: 1, - onComplete: () => { this.blockGfx.x = x0; }, + targets: this.blockGfx, + x: (v.x / m) * 10, + y: (v.y / m) * 10, + duration: 60, + yoyo: true, + repeat: 1, + onComplete: () => this.blockGfx.setPosition(0, 0), + }); + } + + _teleportIn(onDone) { + this.tweens.add({ + targets: this.blockGfx, + alpha: 0, + duration: 110, + onComplete: () => { + const tw = { t: 0 }; + this.blockGfx.setAlpha(1); + this.tweens.add({ + targets: tw, + t: 1, + duration: 150, + ease: 'Quad.easeOut', + onUpdate: () => { + this.blockGfx.setAlpha(tw.t); + this.redrawBlockState(this.state.block, (1 - tw.t) * 0.7); + }, + onComplete: () => { this.blockGfx.setAlpha(1); this.redrawBlockState(this.state.block); onDone(); }, + }); + }, + }); + } + + _dropInHole() { + playSound(this, SFX.GEM_BIG_DROP); + const tw = { t: 0 }; + this.tweens.add({ + targets: tw, + t: 1, + duration: 380, + ease: 'Quad.easeIn', + onUpdate: () => { + this.blockGfx.setAlpha(1 - tw.t * 0.9); + this.redrawBlockState(this.state.block, -2.4 * tw.t); + }, + onComplete: () => { this.busy = false; this.onSolved(); }, }); } @@ -497,7 +655,7 @@ export default class BloxorzGame extends Phaser.Scene { this.moves++; this.updateMoves(); this.redrawTiles(); - this.redrawBlock(shapeFromBlock(this.state.block)); + this.redrawBlockState(this.state.block); playSound(this, SFX.PIECE_CLICK); } @@ -509,7 +667,7 @@ export default class BloxorzGame extends Phaser.Scene { this.updateMoves(); this.blockGfx.setAlpha(1); this.redrawTiles(); - this.redrawBlock(shapeFromBlock(this.state.block)); + this.redrawBlockState(this.state.block); playSound(this, SFX.CARD_SHUFFLE); } @@ -530,12 +688,24 @@ export default class BloxorzGame extends Phaser.Scene { // ── Fall / solve flow ───────────────────────────────────────────────────────── - _onFall() { + // Picks up where the roll left off (t = 1, i.e. 90 degrees) and keeps turning + // while the block accelerates into the void. + _tumbleOff(rolls) { + this.busy = false; this.overlayUp = true; playSound(this, SFX.GEM_BIG_DROP); + const tw = { t: 1 }; this.tweens.add({ - targets: this.blockGfx, y: '+=160', alpha: 0, duration: 420, ease: 'Quad.easeIn', - onComplete: () => this._showFallOverlay(), + targets: tw, + t: 2.1, + duration: FALL_MS, + ease: 'Quad.easeIn', + onUpdate: () => { + const fallen = tw.t - 1; + this.blockGfx.setAlpha(Math.max(0, 1 - fallen * 0.9)); + this.redrawBlock(this._rolledPoints(rolls, tw.t, -6 * fallen * fallen)); + }, + onComplete: () => { this.blockGfx.clear(); this._showFallOverlay(); }, }); } diff --git a/src/games/bloxorz/BloxorzIso.js b/src/games/bloxorz/BloxorzIso.js new file mode 100644 index 0000000..c0ad269 --- /dev/null +++ b/src/games/bloxorz/BloxorzIso.js @@ -0,0 +1,178 @@ +// Bloxorz — axonometric projection + rigid box geometry. Pure math, no Phaser, +// so tools/verifyBloxorz.js can unit-test it under Node alongside the engine. +// +// World space: x = grid column (right), y = grid row (into the screen), +// z = height in CELL units (a cell is 1x1x1, the block is 1x1x2). Right-handed. +// +// The projection is a general axonometric built from a yaw and a pitch rather +// than the symmetric 2:1 diamond: at YAW 45 / PITCH 0.5 these basis vectors +// reproduce a classic isometric diamond exactly, but that made the arrow keys +// ambiguous (up moved the block up-LEFT). A shallow yaw keeps a real 3D read of +// the block while making Right read as screen-right and Up as screen-up. + +const UNIT = 96; // world unit -> px at scaleF 1 +export const YAW = 18 * Math.PI / 180; +export const PITCH = 0.58; // sin(camera pitch): ground-plane squash + +export const EX = { x: Math.cos(YAW) * UNIT, y: Math.sin(YAW) * UNIT * PITCH }; +export const EY = { x: -Math.sin(YAW) * UNIT, y: Math.cos(YAW) * UNIT * PITCH }; +export const EZ = Math.sqrt(1 - PITCH * PITCH) * UNIT; + +export const TILE_DEPTH = 26; // px of cliff wall drawn at void-adjacent edges + +// Direction the camera lies in, i.e. the null vector of the projection matrix +// (the ray that projects to a single point), pointing from the scene toward the +// viewer. A face is visible exactly when its outward normal has a positive dot +// product with this. +export const VIEW = normalize({ + x: Math.sin(YAW), + y: Math.cos(YAW), + z: PITCH / Math.sqrt(1 - PITCH * PITCH), +}); + +// Key light: up, in front of, and to the +x side of the board, so the +x face +// stays brighter than the +y face (matching the old hand-picked side colours). +export const LIGHT = normalize({ x: 0.55, y: -0.35, z: 0.76 }); + +function normalize(v) { + const m = Math.hypot(v.x, v.y, v.z) || 1; + return { x: v.x / m, y: v.y / m, z: v.z / m }; +} + +// ── Projection ─────────────────────────────────────────────────────────────── + +// view = { originX, originY, scaleF } +export function project(p, view) { + const z = p.z ?? 0; + return { + sx: view.originX + (p.x * EX.x + p.y * EY.x) * view.scaleF, + sy: view.originY + (p.x * EX.y + p.y * EY.y - z * EZ) * view.scaleF, + }; +} + +// Painter's-algorithm key for ground tiles: both basis vectors point down-screen +// at this yaw, so ascending order draws far cells before near ones. NOT x + y — +// the axes are foreshortened by different amounts once the yaw isn't 45. +export function depthOf(x, y) { + return EX.y * x + EY.y * y; +} + +// Unscaled px bounding box of a cols x rows board, with room above for a block +// `headroomZ` cells tall and below for the cliff faces. +export function boardBounds(cols, rows, headroomZ = 2) { + const corners = [[0, 0], [cols, 0], [0, rows], [cols, rows]] + .map(([x, y]) => ({ sx: x * EX.x + y * EY.x, sy: x * EX.y + y * EY.y })); + const minX = Math.min(...corners.map((c) => c.sx)); + const maxX = Math.max(...corners.map((c) => c.sx)); + const minY = Math.min(...corners.map((c) => c.sy)) - headroomZ * EZ; + const maxY = Math.max(...corners.map((c) => c.sy)) + TILE_DEPTH; + return { minX, minY, maxX, maxY, w: maxX - minX, h: maxY - minY }; +} + +// ── Boxes ──────────────────────────────────────────────────────────────────── + +const SPLIT_INSET = 0.06; + +// Every pose the block can hold, as world-space boxes. The whole point of the +// z-in-cell-units model: a lying block is ONE cell tall, not two, which is what +// makes a roll look like a roll instead of a stretch. +export function boxesForBlock(block) { + if (block.mode === 'split') { + return [block.a, block.b].map((u) => ({ + x0: u.x + SPLIT_INSET, y0: u.y + SPLIT_INSET, z0: 0, + x1: u.x + 1 - SPLIT_INSET, y1: u.y + 1 - SPLIT_INSET, z1: 1, + })); + } + if (block.orient === 'up') return [{ x0: block.x, y0: block.y, z0: 0, x1: block.x + 1, y1: block.y + 1, z1: 2 }]; + if (block.orient === 'x') return [{ x0: block.x, y0: block.y, z0: 0, x1: block.x + 2, y1: block.y + 1, z1: 1 }]; + return [{ x0: block.x, y0: block.y, z0: 0, x1: block.x + 1, y1: block.y + 2, z1: 1 }]; +} + +// Corner order is fixed: bit 1 = x1, bit 2 = y1, bit 4 = z1. +export function boxCorners(b) { + const pts = []; + for (let i = 0; i < 8; i++) { + pts.push({ + x: (i & 1) ? b.x1 : b.x0, + y: (i & 2) ? b.y1 : b.y0, + z: (i & 4) ? b.z1 : b.z0, + }); + } + return pts; +} + +// Faces wound counter-clockwise seen from outside, so the cross product of the +// first two edges is the outward normal even after the box has been rotated. +const FACE_QUADS = [ + [4, 5, 7, 6], // +z (top) + [0, 2, 3, 1], // -z (bottom) + [1, 3, 7, 5], // +x + [0, 4, 6, 2], // -x + [2, 6, 7, 3], // +y + [0, 1, 5, 4], // -y +]; + +function faceNormal(p0, p1, p2) { + const ax = p1.x - p0.x, ay = p1.y - p0.y, az = p1.z - p0.z; + const bx = p2.x - p0.x, by = p2.y - p0.y, bz = p2.z - p0.z; + return normalize({ + x: ay * bz - az * by, + y: az * bx - ax * bz, + z: ax * by - ay * bx, + }); +} + +// Front-facing quads only. A box is convex and the projection is orthographic, +// so the surviving faces tile the silhouette without overlapping each other — +// culling alone is enough, no per-face depth sort needed. +export function visibleFaces(pts) { + const out = []; + for (const quad of FACE_QUADS) { + const [a, b, c] = quad; + const n = faceNormal(pts[a], pts[b], pts[c]); + if (n.x * VIEW.x + n.y * VIEW.y + n.z * VIEW.z <= 1e-9) continue; + out.push({ pts: quad.map((i) => pts[i]), normal: n }); + } + return out; +} + +// 0 (fully shadowed) .. 1 (facing the key light straight on). +export function faceShade(normal) { + const d = normal.x * LIGHT.x + normal.y * LIGHT.y + normal.z * LIGHT.z; + return Math.min(1, Math.max(0, (d + 1) / 1.76)); +} + +// ── Rolling ────────────────────────────────────────────────────────────────── + +// Which ground edge of the box the roll pivots about. +const PIVOTS = { + right: { axis: 'x', sign: 1 }, + left: { axis: 'x', sign: -1 }, + down: { axis: 'y', sign: 1 }, + up: { axis: 'y', sign: -1 }, +}; + +// Rigid rotation of all 8 corners by t * 90 degrees about the leading ground +// edge. t is deliberately unclamped: the fall animation keeps turning past 1. +export function rollPoints(pts, dir, box, t) { + const p = PIVOTS[dir]; + if (!p || t === 0) return pts; + const th = t * Math.PI / 2; + const c = Math.cos(th); + const s = Math.sin(th); + const onX = p.axis === 'x'; + const pv = onX + ? (p.sign > 0 ? box.x1 : box.x0) + : (p.sign > 0 ? box.y1 : box.y0); + + return pts.map((q) => { + const d = (onX ? q.x : q.y) - pv; + const a = pv + d * c + p.sign * q.z * s; + const z = -p.sign * d * s + q.z * c; + return onX ? { x: a, y: q.y, z } : { x: q.x, y: a, z }; + }); +} + +export function translatePoints(pts, dx, dy, dz) { + return pts.map((q) => ({ x: q.x + dx, y: q.y + dy, z: q.z + dz })); +} diff --git a/src/games/bloxorz/BloxorzLogic.js b/src/games/bloxorz/BloxorzLogic.js index 5961aff..00875b6 100644 --- a/src/games/bloxorz/BloxorzLogic.js +++ b/src/games/bloxorz/BloxorzLogic.js @@ -19,14 +19,23 @@ // wall — a permanent, non-lethal obstacle: a move into it is simply // illegal (no-op), never fatal. Used to give split cubes // something to independently route around. +// switch — requireOrientation decides what counts as pressing it: +// 'standing' -> heavy X switch, block must come to rest upright +// 'any' -> soft round switch, any contact incl. a split cube +// 'lying' -> hold pad, only a lying block counts // bridge — impassable unless its linked switch(es) say otherwise: -// mode:'toggle' -> persistent flip on any contact (hard switch) -// mode:'momentary' -> open only while a LYING block currently -// covers the switch cell (soft switch) -// fragile — supports exactly one standing landing, then breaks permanently +// mode:'toggle' -> persistent flip on contact +// mode:'momentary' -> open only while the switch is currently +// covered (a hold pad) +// fragile — orange ground. It takes a lying block's weight fine, but the +// whole block standing upright on one cell goes straight through +// it (as in the original). A lone split cube is half the weight +// and is safe standing on it. // teleport — a standing (single-cell) landing is relocated to its pair -// goal — only a standing joined block ('up') is supported here; any -// other landing shape falls through like void +// goal — the exit hole. A block lying across it bridges it harmlessly, as +// in the original game; only a joined block that ends its move +// STANDING exactly on the hole drops in and wins. A landing here +// is never fatal. export const DIR_LIST = ['up', 'down', 'left', 'right']; const DIRS = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] }; @@ -83,6 +92,9 @@ function isPressed(state, sw) { const onIt = cells.some(([x, y]) => x === sw.x && y === sw.y); if (!onIt) return false; if (sw.requireOrientation === 'any') return true; + // 'standing' is the original game's heavy X switch: the whole block has to + // come to rest upright on the single cell, which is a real routing problem. + if (sw.requireOrientation === 'standing') return state.block.mode === 'joined' && state.block.orient === 'up'; return state.block.mode === 'joined' && state.block.orient !== 'up'; } @@ -97,23 +109,24 @@ function tileAt(level, state, x, y) { const t = level.tiles.get(key(x, y)); if (!t) return null; if (t.type === 'bridge' && !bridgeOpen(level, state, t.bridgeId)) return null; - if (t.type === 'fragile' && state.broken.has(key(x, y))) return null; return t; } -function tileSupports(tile, mode, orient) { - if (!tile) return false; - if (tile.type === 'goal') return mode === 'joined' && orient === 'up'; - return true; -} - // 'ok' (safe to land), 'blocked' (wall — illegal move, not fatal), or 'fatal' -// (void / closed bridge / broken fragile / lying-or-split onto a goal). -function cellStatus(level, state, x, y, mode, orient) { +// (void / closed bridge / upright on fragile ground). `standing` is whether the +// whole block would come to rest upright on this one cell. +// +// The goal is deliberately NOT special-cased here: as in the original game, a +// block lying across the hole simply bridges it. Only a block that ends its +// move standing exactly on the hole drops in, and settle() handles that as the +// win — the landing itself is never fatal. +function cellStatus(level, state, x, y, standing = false) { const raw = level.tiles.get(key(x, y)); if (raw?.type === 'wall') return 'blocked'; const tile = tileAt(level, state, x, y); - return tileSupports(tile, mode, orient) ? 'ok' : 'fatal'; + if (!tile) return 'fatal'; + if (tile.type === 'fragile' && standing) return 'fatal'; + return 'ok'; } // Renderer-facing view of a tile including its current dynamic state. @@ -121,7 +134,6 @@ export function tileRenderInfo(level, state, x, y) { const t = level.tiles.get(key(x, y)); if (!t) return null; if (t.type === 'bridge') return { ...t, open: bridgeOpen(level, state, t.bridgeId) }; - if (t.type === 'fragile') return { ...t, broken: state.broken.has(key(x, y)) }; return t; } @@ -162,27 +174,31 @@ function rollJoined(block, dir) { : { mode: 'joined', orient: 'y', x: block.x + dx, y: block.y }; } +// The pose a joined block WOULD land in, ignoring whether that landing is +// legal. The renderer needs this to animate a fatal move: applyMove leaves +// state.block untouched when the block dies, so the roll it died performing has +// to be reconstructed from the pre-move block. +export function previewRoll(block, dir) { + return rollJoined(block, dir); +} + +// Walls stop a single split cube without killing it (see stepUnit), so the +// renderer asks about them to know which unit stayed put during a move. +export function isWallAt(level, x, y) { + return level.tiles.get(key(x, y))?.type === 'wall'; +} + function tryMerge(a, b) { if (a.x === b.x && Math.abs(a.y - b.y) === 1) return { mode: 'joined', orient: 'y', x: a.x, y: Math.min(a.y, b.y) }; if (a.y === b.y && Math.abs(a.x - b.x) === 1) return { mode: 'joined', orient: 'x', x: Math.min(a.x, b.x), y: a.y }; return null; } -// Shared tail after a successful (non-dead) landing: fragile breakage under a -// standing footprint, goal check (joined+up only), then switch evaluation. +// Shared tail after a successful (non-dead) landing: teleport relocation, goal +// check (joined+up only), then switch evaluation. function settle(level, state) { - const standingCells = state.block.mode === 'split' - ? [state.block.a, state.block.b] - : (state.block.orient === 'up' ? [{ x: state.block.x, y: state.block.y }] : []); - if (state.block.mode === 'joined' && state.block.orient === 'up') { state.block = { ...state.block, ...maybeTeleportPoint(level, { x: state.block.x, y: state.block.y }) }; - standingCells[0] = { x: state.block.x, y: state.block.y }; - } - - for (const { x, y } of standingCells) { - const tile = level.tiles.get(key(x, y)); - if (tile?.type === 'fragile') state.broken.add(key(x, y)); } if (state.block.mode === 'joined' && state.block.orient === 'up') { @@ -199,7 +215,7 @@ function jointStep(level, state, dir) { const cells = blockCells(candidate); for (const [x, y] of cells) { - const status = cellStatus(level, state, x, y, candidate.mode, candidate.orient); + const status = cellStatus(level, state, x, y, candidate.orient === 'up'); if (status === 'blocked') return { moved: false }; if (status === 'fatal') { state.status = 'dead'; return { moved: true, dead: true }; } } @@ -218,7 +234,7 @@ function jointStep(level, state, dir) { function stepUnit(level, state, pt, dx, dy) { const nx = pt.x + dx; const ny = pt.y + dy; - const status = cellStatus(level, state, nx, ny, 'joined', 'up'); + const status = cellStatus(level, state, nx, ny); if (status === 'blocked') return { pt, dead: false }; if (status === 'fatal') return { pt, dead: true }; return { pt: maybeTeleportPoint(level, { x: nx, y: ny }), dead: false }; @@ -230,6 +246,13 @@ function splitStep(level, state, dir) { const rb = stepUnit(level, state, state.block.b, dx, dy); if (ra.dead || rb.dead) { state.status = 'dead'; return { moved: true, dead: true }; } + // Two cubes can never share a cell: if one is held up by a wall, the other + // can't roll into the space it is still filling, so the pair simply doesn't + // move — same no-op a wall gives a joined block. + const samePt = (p, q) => p.x === q.x && p.y === q.y; + if (samePt(ra.pt, rb.pt)) return { moved: false }; + if (samePt(ra.pt, state.block.a) && samePt(rb.pt, state.block.b)) return { moved: false }; + const merged = tryMerge(ra.pt, rb.pt); state.block = merged ?? { mode: 'split', a: ra.pt, b: rb.pt }; return settle(level, state); @@ -254,7 +277,6 @@ export function newState(level) { const state = { block: { mode: 'joined', orient: level.start.orient, x: level.start.x, y: level.start.y }, toggleBridges, - broken: new Set(), status: 'playing', }; evaluateSwitches(level, state); @@ -267,7 +289,6 @@ export function cloneState(state) { ? { ...state.block } : { mode: 'split', a: { ...state.block.a }, b: { ...state.block.b } }, toggleBridges: new Map(state.toggleBridges), - broken: new Set(state.broken), status: state.status, }; } @@ -285,8 +306,7 @@ export function stateKey(state) { .sort((p, q) => (p[0] < q[0] ? -1 : p[0] > q[0] ? 1 : 0)) .map(([id, o]) => `${id}:${o ? 1 : 0}`) .join(','); - const brokenPart = [...state.broken].sort().join(','); - return `${blockPart}#${bridgePart}#${brokenPart}`; + return `${blockPart}#${bridgePart}`; } export function isSolved(state) { diff --git a/src/games/bloxorz/tutorial.md b/src/games/bloxorz/tutorial.md index 6d8e9e0..1ec7f92 100644 --- a/src/games/bloxorz/tutorial.md +++ b/src/games/bloxorz/tutorial.md @@ -9,24 +9,31 @@ goal hole. Fall off the edge and you're back at the start of the level. a time. - Standing upright, it takes up one tile. Tip it over and it lies flat across two tiles — tip it again along its length and it stands back up. -- The goal only accepts the block **standing upright**. Landing on it lying - down just falls through. +- The goal only accepts the block **standing upright**. Rolling over the hole + lying flat is harmless — the block just bridges it. ## Bridges & Switches - **Bridges** are gaps in the platform that only become solid once their linked switch is thrown. -- An **orange square switch** opens its bridge permanently the instant the - block touches it, in any orientation. -- A **pale circle switch** only holds its bridge open while the block is - lying down directly on top of it — time your crossing so you're still - covering the switch as you tip onto the bridge. +- An **orange X switch** is heavy: it only throws when the block comes to rest + **standing upright** on it, which is a routing problem in itself. +- An **orange round switch** is soft — any contact throws it, lying down or + even a single split cube. It toggles, so trip it twice and you're back where + you started. +- A **pale bar pad** only holds its bridge open while the block is lying down + directly on top of it — time your crossing so you're still covering the pad + as you tip onto the bridge. ## Fragile Tiles -- Orange, cracked tiles support the block's weight **once**. Stand on one and - it's fine — but come back to that exact tile a second time and it gives - way. +- Orange, cracked tiles take the block's weight spread over two tiles, but not + concentrated on one: stand **upright** on a cracked tile and you go straight + through it. +- So you cross cracked ground lying down — which means shuffling sideways, + since tipping along your own length stands you back up. You can only change + which way you're lying while on solid ground. +- A single split cube is light enough to stand on cracked ground safely. ## Teleporters diff --git a/src/games/totalannihilation/TALogic.js b/src/games/totalannihilation/TALogic.js index f83512c..eda7a26 100644 --- a/src/games/totalannihilation/TALogic.js +++ b/src/games/totalannihilation/TALogic.js @@ -152,7 +152,7 @@ function baseEntity(state, army, def) { orders: [], path: null, pathIdx: 0, destX: 0, destY: 0, slotX: 0, slotY: 0, wantPath: false, noPath: false, - stuckTicks: 0, blockedTicks: 0, + stuckTicks: 0, blockedTicks: 0, stuckCheckTick: null, stuckCheckX: 0, stuckCheckY: 0, // Far enough in the past that a fresh unit is eligible to regenerate immediately. // A finite sentinel rather than -Infinity, which JSON.stringify turns into null. lastDamagedTick: -1e9, @@ -822,6 +822,7 @@ function stepMovement(state, rules) { for (const e of state.entities) { e.px = e.x; e.py = e.y; e.pheading = e.heading; e.pturretRot = e.turretRot; + e._movingThisTick = false; if (e.dead || e.isBuilding || e.site) { e.vx = 0; e.vy = 0; continue; } const def = defOf(rules, e); @@ -870,8 +871,44 @@ function stepMovement(state, rules) { e.vy = Math.sin(e.heading) * step / dt; e.x += Math.cos(e.heading) * step; e.y += Math.sin(e.heading) * step; + e._movingThisTick = true; + } +} - if (step < speed * dt * 0.2) e.stuckTicks++; else e.stuckTicks = 0; +/** + * Stuck detection, run after stepSeparation so it sees where units actually ended up rather + * than where stepMovement merely aimed them. A unit whose intended step this tick was fine on + * paper can still be shoved straight back by the building-clearance clamp in stepSeparation. + * + * This measures NET displacement over a whole `stuckRepathSec` window rather than per-tick + * displacement. A unit wedged against the corner of a wall doesn't sit still — it gets nudged + * one way by the clearance clamp, drives back toward its blocked waypoint, gets nudged again — + * and every one of those individual ticks moves it plenty. Only the position at the start and + * end of the window tells you it never actually got anywhere. + * + * A path that stops making real progress gets thrown away and re-requested against the + * CURRENT nav grid, which is what makes a unit route around an obstacle it didn't know about + * when the path was first computed (e.g. a building that went up mid-transit) instead of + * grinding against the same blocked waypoint forever. + */ +function finalizeStuck(state, rules) { + const windowTicks = Math.max(1, Math.round((rules.constants.stuckRepathSec ?? 1.0) * rules.constants.tickHz)); + for (const e of state.entities) { + if (!e._movingThisTick) { e.stuckCheckTick = null; continue; } + if (e.stuckCheckTick == null) { + e.stuckCheckTick = state.tick; e.stuckCheckX = e.x; e.stuckCheckY = e.y; + continue; + } + if (state.tick - e.stuckCheckTick < windowTicks) continue; + + const def = defOf(rules, e); + const moved = Math.hypot(e.x - e.stuckCheckX, e.y - e.stuckCheckY); + const expected = def.speed * (windowTicks / rules.constants.tickHz); + e.stuckCheckTick = state.tick; e.stuckCheckX = e.x; e.stuckCheckY = e.y; + + if (moved >= expected * 0.15) { e.stuckTicks = 0; continue; } + e.stuckTicks += windowTicks; + if (e.path) { e.path = null; requestPath(state, e, e.destX, e.destY); } } } @@ -1459,6 +1496,7 @@ export function tick(state, rules) { servicePathQueue(state, rules); stepMovement(state, rules); stepSeparation(state, rules); + finalizeStuck(state, rules); stepCombat(state, rules); stepProjectiles(state, rules); stepRegen(state, rules); diff --git a/src/games/totalannihilation/TARules.js b/src/games/totalannihilation/TARules.js index 5f17e0a..cf779ae 100644 --- a/src/games/totalannihilation/TARules.js +++ b/src/games/totalannihilation/TARules.js @@ -13,6 +13,10 @@ export const UNIT_ROLES = new Set(['builder', 'combat', 'scout', 'artillery']); export const SHEET_SLOTS = new Set(['unitSheet', 'structureSheet']); export const TARGET_DOMAINS = new Set(['ground', 'air']); +// A unit/building must always be able to see a bit past its own guns, or it ends up firing +// into fog it hasn't revealed. Applied as a floor over whatever `sight` the def declares. +const SIGHT_RANGE_MARGIN_TILES = 2; + // Procedural painter shapes. A def may only name a shape TAArt knows how to draw; // this set is duplicated there and the verify script asserts the two agree. export const PROC_SHAPES = new Set([ @@ -204,10 +208,11 @@ export function compileRules(json) { u.radius = u.radius ?? sc.radius; u.massClass = sc.mass; u.footprintTiles = sc.footprint; - u.sightSq = u.sight * u.sight; u.isBuilding = false; u.weaponDefs = (u.weapons ?? []).map((wid) => weaponById[wid]); u.maxRange = autoRangeOf(u.weaponDefs); + if (u.maxRange > 0) u.sight = Math.max(u.sight, u.maxRange + SIGHT_RANGE_MARGIN_TILES * c.tileSize); + u.sightSq = u.sight * u.sight; // Regeneration is per-def data, so any unit can be given it later without code. Rates // are converted to per-tick here so the sim never divides in its hot loop. if (u.selfHeal) { @@ -252,9 +257,10 @@ export function compileRules(json) { b.halfW = (b.footprint.w * c.tileSize) / 2; b.halfH = (b.footprint.h * c.tileSize) / 2; b.sight = b.sight ?? 0; - b.sightSq = b.sight * b.sight; b.weaponDefs = (b.weapons ?? []).map((wid) => weaponById[wid]); b.maxRange = autoRangeOf(b.weaponDefs); + if (b.maxRange > 0) b.sight = Math.max(b.sight, b.maxRange + SIGHT_RANGE_MARGIN_TILES * c.tileSize); + b.sightSq = b.sight * b.sight; buildable[b.id] = b; } const buildingById = indexById(buildings); diff --git a/src/games/totalannihilation/TAWorldView.js b/src/games/totalannihilation/TAWorldView.js index 2f09d0f..e2a2107 100644 --- a/src/games/totalannihilation/TAWorldView.js +++ b/src/games/totalannihilation/TAWorldView.js @@ -324,7 +324,6 @@ export default class TAWorldView { const frameSize = def.sheetSlot === 'unitSheet' ? sheets.unitFrame : sheets.structureFrame; const img = this.scene.add.image(e.x, e.y, key, def.frame); - img.setTint(sheets.color); if (def.isBuilding) { img.setDisplaySize(def.footprint.w * this.ts, def.footprint.h * this.ts); } else { @@ -332,14 +331,24 @@ export default class TAWorldView { } this._addWorld(img); + // Buildings get a second, initially-invisible image showing the FINISHED sprite, + // drawn just behind `img` (which carries the wireframe while under construction) so + // the two can crossfade as the site progresses. See the site/progress block in render(). + let final = null; + if (def.isBuilding) { + final = this.scene.add.image(e.x, e.y, key, def.frame); + final.setDisplaySize(def.footprint.w * this.ts, def.footprint.h * this.ts); + final.setAlpha(0); + this._addWorld(final); + } + let turret = null; if (!def.isBuilding && def.turretFrame != null) { turret = this.scene.add.image(e.x, e.y, key, def.turretFrame); - turret.setTint(sheets.color); turret.setScale((def.spritePx ?? def.radius * 2) / frameSize.w); this._addWorld(turret); } - s = { img, turret, defId: e.defId, site: e.site }; + s = { img, turret, final, defId: e.defId }; this.sprites.set(e.id, s); return s; } @@ -349,6 +358,7 @@ export default class TAWorldView { if (!s) return; s.img.destroy(); s.turret?.destroy(); + s.final?.destroy(); this.sprites.delete(id); } @@ -378,6 +388,7 @@ export default class TAWorldView { const s = this._ensureSprite(e); s.img.setVisible(shown); if (s.turret) s.turret.setVisible(shown); + if (s.final) s.final.setVisible(shown); if (!shown) continue; const x = e.px + (e.x - e.px) * alpha; @@ -389,12 +400,21 @@ export default class TAWorldView { // Y-sorted actor band; buildings sit just under units sharing a row. s.img.setDepth(DEPTHS.actor + (y / state.worldH) * 10 + (def.isBuilding ? 0 : 0.05)); - // Build sites show the wireframe frame and fade in as they complete. - if (e.site !== s.site) { - s.img.setFrame(e.site ? (def.buildFrame ?? def.frame) : def.frame); - s.site = e.site; + // Build sites show the wireframe frame. Queued (progress still 0) sits at 50% + // opacity; once work starts the wireframe fades 100%->0% over construction while + // the finished sprite crossfades in underneath, starting at 20% complete. + s.img.setFrame(e.site ? (def.buildFrame ?? def.frame) : def.frame); + s.img.setAlpha(e.site ? (e.progress <= 0 ? 0.5 : Math.max(0, 1 - e.progress)) : 1); + + if (s.final) { + if (e.site) { + s.final.setPosition(x, y); + s.final.setDepth(DEPTHS.actor + (y / state.worldH) * 10 - 0.001); + s.final.setAlpha(Math.max(0, Math.min(1, (e.progress - 0.2) / 0.8))); + } else { + s.final.setAlpha(0); + } } - s.img.setAlpha(e.site ? 0.35 + e.progress * 0.55 : 1); if (s.turret) { const tr = lerpAngle(e.pturretRot, e.turretRot, alpha); diff --git a/src/games/totalannihilation/sprites.md b/src/games/totalannihilation/sprites.md index fb91c18..367c958 100644 --- a/src/games/totalannihilation/sprites.md +++ b/src/games/totalannihilation/sprites.md @@ -16,12 +16,10 @@ asset manifest resolves every entry in the `sheets` map automatically 1. **Units are drawn once, facing right.** `0 rad` is Phaser's `+x` axis, and the renderer calls `setRotation(heading)` — so a unit needs exactly **one** frame, not eight facings. Draw it pointing **right**, dead centre in its cell. -2. **Units are drawn in neutral grey.** The renderer applies `setTint(armyColour)` per army. - Paint in greys with the detail carried by *value* (light/dark), not hue; any hue you paint - is multiplied by the team colour and will read as muddy. Keep the brightest highlight near - white so the tint stays vivid. +2. **Units are drawn in full colour.** The renderer no longer applies any army tint — paint + each army's sheet in its finished, final colours (ARM blue, CORE red/orange, etc.). -Terrain is exempt from both: tiles are drawn in full colour and are never rotated or tinted. +Terrain follows the same rule: tiles are drawn in full colour and are never rotated or tinted. --- @@ -82,8 +80,10 @@ at 128px and a 3×3 at 192px — draw the 3×3 buildings to fill the cell and ac scaled up 1.5×, or supply a larger cell size in the artwork JSON. Each building has **two** frames: the finished structure, and a **build frame** shown while it -is under construction (the renderer also fades it in by build progress). Draw the build frame -as a skeleton/scaffold version — girders, no panels, no lights. +is under construction. Draw the build frame as a skeleton/scaffold version — girders, no +panels, no lights. The renderer shows it at 50% opacity while a site is queued (placed but no +builder working it yet), then at 100% opacity once work begins, fading it out to 0% as the +build completes; the finished frame crossfades in underneath starting at 20% progress. | Frame | Building | Footprint | Notes | |---|---|---|---| diff --git a/tools/bloxorzMap.js b/tools/bloxorzMap.js new file mode 100644 index 0000000..db4d014 --- /dev/null +++ b/tools/bloxorzMap.js @@ -0,0 +1,157 @@ +// Bloxorz ASCII map parser + level design gates. Shared by genBloxorz.js (which +// builds the bank) and by scratch/tuning scripts, so a level can be audited on +// its own without running the whole generator. Pure Node, no Phaser. +// +// See genBloxorz.js for the map legend. + +import { loadLevel, solve } from '../src/games/bloxorz/BloxorzLogic.js'; + +// ── Map parser ─────────────────────────────────────────────────────────────── + +const BRIDGE_CHARS = '123'; +const HEAVY_CHARS = 'ABC'; +const SOFT_CHARS = 'abc'; +const HOLD_CHARS = 'pqr'; +const TELEPORT_CHARS = 'tuv'; + +export function parseMap(n, name, art, opts = {}) { + const raw = art.replace(/^\n/, '').replace(/\s+$/, '').split('\n'); + const indent = Math.min(...raw.filter((l) => l.trim()).map((l) => l.match(/^ */)[0].length)); + const lines = raw.map((l) => l.slice(indent)); + const rows = lines.length; + const cols = Math.max(...lines.map((l) => l.length)); + + const openGroups = new Set(opts.open ?? []); + const tiles = []; + let start = null; + let goal = null; + let switchSeq = 0; + + const linksFor = (ch, fallback) => (opts.links?.[ch] ?? fallback).map((g) => `b${g}`); + + for (let y = 0; y < rows; y++) { + for (let x = 0; x < lines[y].length; x++) { + const ch = lines[y][x]; + if (ch === ' ') continue; + + if (ch === 'S') { start = { x, y, orient: 'up' }; tiles.push({ x, y, type: 'floor' }); continue; } + if (ch === 'G') { goal = { x, y }; tiles.push({ x, y, type: 'goal' }); continue; } + if (ch === '.') { tiles.push({ x, y, type: 'floor' }); continue; } + if (ch === ':') { tiles.push({ x, y, type: 'floor', split: true }); continue; } + if (ch === '#') { tiles.push({ x, y, type: 'wall' }); continue; } + if (ch === '~') { tiles.push({ x, y, type: 'fragile' }); continue; } + + if (BRIDGE_CHARS.includes(ch)) { + const group = BRIDGE_CHARS.indexOf(ch) + 1; + tiles.push({ x, y, type: 'bridge', bridgeId: `b${group}`, initiallyOpen: openGroups.has(group) }); + continue; + } + if (TELEPORT_CHARS.includes(ch)) { tiles.push({ x, y, type: 'teleport', teleportId: ch }); continue; } + + const asSwitch = (chars, requireOrientation, mode) => { + const group = chars.indexOf(ch) + 1; + switchSeq += 1; + tiles.push({ + x, y, type: 'switch', switchId: `s${switchSeq}`, + linkedBridgeIds: linksFor(ch, [group]), requireOrientation, mode, + }); + }; + if (HEAVY_CHARS.includes(ch)) { asSwitch(HEAVY_CHARS, 'standing', 'toggle'); continue; } + if (SOFT_CHARS.includes(ch)) { asSwitch(SOFT_CHARS, 'any', 'toggle'); continue; } + if (HOLD_CHARS.includes(ch)) { asSwitch(HOLD_CHARS, 'lying', 'momentary'); continue; } + + throw new Error(`L${n} "${name}": unknown map character '${ch}' at ${x},${y}`); + } + } + + if (!start) throw new Error(`L${n} "${name}": no start (S)`); + if (!goal) throw new Error(`L${n} "${name}": no goal (G)`); + + const teleports = new Map(); + for (const t of tiles) { + if (t.type === 'teleport') teleports.set(t.teleportId, (teleports.get(t.teleportId) ?? 0) + 1); + } + for (const [id, count] of teleports) { + if (count !== 2) throw new Error(`L${n} "${name}": teleport '${id}' appears ${count}x, must be exactly 2`); + } + + const splitCount = tiles.filter((t) => t.split).length; + if (splitCount % 2 !== 0) throw new Error(`L${n} "${name}": odd number of split tiles (${splitCount})`); + + const bridgeGroups = new Set(tiles.filter((t) => t.type === 'bridge').map((t) => t.bridgeId)); + const linked = new Map(); + for (const t of tiles) { + if (t.type !== 'switch') continue; + for (const bid of t.linkedBridgeIds) { + if (!linked.has(bid)) linked.set(bid, new Set()); + linked.get(bid).add(t.mode); + } + } + for (const bid of bridgeGroups) { + if (!linked.has(bid)) throw new Error(`L${n} "${name}": bridge ${bid} has no switch`); + if (linked.get(bid).size > 1) throw new Error(`L${n} "${name}": bridge ${bid} mixes toggle and momentary switches`); + } + for (const bid of linked.keys()) { + if (!bridgeGroups.has(bid)) throw new Error(`L${n} "${name}": switch wired to ${bid}, which has no bridge tiles`); + } + + return { level: n, name, cols, rows, start, goal, tiles }; +} + +// ── Design gates ───────────────────────────────────────────────────────────── + +const SOLVE_OPTS = { maxStates: 400000 }; +const clone = (def) => JSON.parse(JSON.stringify(def)); + +export function parOf(def) { + return solve(loadLevel(def), SOLVE_OPTS).moves; +} + +// A mechanic that the level can be finished without is just decoration. Each +// gate strips one mechanic and demands the level become unsolvable. +function withoutTiles(def, pred) { + const out = clone(def); + out.tiles = out.tiles.filter((t) => !pred(t)); + return out; +} + +function mapTiles(def, fn) { + const out = clone(def); + out.tiles = out.tiles.map(fn); + return out; +} + +export function auditLevel(def) { + const problems = []; + const notes = []; + const has = (pred) => def.tiles.some(pred); + + const par = parOf(def); + if (par < 0) { problems.push('unsolvable'); return { par, problems, notes }; } + + if (has((t) => t.type === 'bridge')) { + // Bridges gone: the far side must be genuinely cut off. + if (parOf(withoutTiles(def, (t) => t.type === 'bridge')) >= 0) problems.push('bridges are optional'); + } + if (has((t) => t.type === 'teleport')) { + if (parOf(mapTiles(def, (t) => (t.type === 'teleport' ? { x: t.x, y: t.y, type: 'floor' } : t))) >= 0) { + problems.push('teleporters are optional'); + } + } + if (has((t) => t.split)) { + if (parOf(mapTiles(def, (t) => (t.split ? { ...t, split: false } : t))) >= 0) problems.push('split is optional'); + } + if (has((t) => t.type === 'fragile')) { + // Fragile ground has to be load-bearing (you must cross it) AND has to + // actually constrain the route, or it is just orange-coloured floor. + if (parOf(withoutTiles(def, (t) => t.type === 'fragile')) >= 0) problems.push('fragile ground is optional'); + const solid = parOf(mapTiles(def, (t) => (t.type === 'fragile' ? { x: t.x, y: t.y, type: 'floor' } : t))); + if (solid === par) notes.push('fragile ground does not change the optimal route'); + } + if (has((t) => t.type === 'wall')) { + const noWalls = parOf(withoutTiles(def, (t) => t.type === 'wall')); + if (noWalls === par) notes.push('walls do not change the optimal route'); + } + return { par, problems, notes }; +} + diff --git a/tools/genBloxorz.js b/tools/genBloxorz.js index e91a627..04a5286 100644 --- a/tools/genBloxorz.js +++ b/tools/genBloxorz.js @@ -1,297 +1,477 @@ // Offline generator for the Bloxorz level bank. // -// Levels are hand-authored (switch/bridge/teleporter/split puzzles need -// designed relational intent that random generation doesn't produce — see -// Rush Hour/Dot Link for where randomize-then-filter DOES work, and note this -// is deliberately NOT that). Each level is built from small composable tile -// helpers below, then solved with BloxorzLogic's BFS solver to compute `par` -// and hard-reject the whole build if any level turns out to be unsolvable. +// Levels are hand-authored as ASCII maps (switch/bridge/teleporter/split +// puzzles need designed relational intent that random generation doesn't +// produce — see Rush Hour/Dot Link for where randomize-then-filter DOES work, +// and note this is deliberately NOT that). Every map is then run through +// BloxorzLogic's BFS solver to compute `par`, and through a set of design gates +// that hard-reject the build if a level is unsolvable, if a mechanic on the +// board turns out to be decorative, or if the difficulty curve regresses. +// +// Legend +// (space) void . floor S start (standing) G goal hole +// # wall ~ fragile : split-flagged floor +// 1 2 3 bridge tiles for groups 1-3 +// A B C heavy switch — must come to rest STANDING on it (toggles 1-3) +// a b c soft switch — any contact, incl. a split cube (toggles 1-3) +// p q r hold pad — bridge open only while a LYING block covers it +// t u v teleport pads — each letter must appear exactly twice +// +// Per-level options: { open: [1] } starts those bridge groups open, +// { links: { A: [1, 2] } } wires a switch char to extra bridge groups. // // Usage: node tools/genBloxorz.js [outFile] import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadLevel, solve } from '../src/games/bloxorz/BloxorzLogic.js'; +import { parseMap, auditLevel } from './bloxorzMap.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const OUT_FILE = process.argv[2] ? path.resolve(process.argv[2]) : path.join(__dirname, '../data/bloxorz.json'); -// ── Tile helpers ───────────────────────────────────────────────────────────── +const LEVELS = []; +const lvl = (n, name, art, opts) => { LEVELS.push(parseMap(n, name, art, opts)); }; -const floor = (x, y, extra = {}) => ({ x, y, type: 'floor', ...extra }); -const splitFloor = (x, y) => floor(x, y, { split: true }); -const wallTile = (x, y) => ({ x, y, type: 'wall' }); -const goalTile = (x, y) => ({ x, y, type: 'goal' }); -const fragile = (x, y) => ({ x, y, type: 'fragile' }); -const bridge = (x, y, bridgeId, initiallyOpen = false) => ({ x, y, type: 'bridge', bridgeId, initiallyOpen }); -const hardSwitch = (x, y, switchId, bridgeIds) => ({ - x, y, type: 'switch', switchId, linkedBridgeIds: bridgeIds, requireOrientation: 'any', mode: 'toggle', -}); -const softSwitch = (x, y, switchId, bridgeIds) => ({ - x, y, type: 'switch', switchId, linkedBridgeIds: bridgeIds, requireOrientation: 'lying', mode: 'momentary', -}); -const teleport = (x, y, teleportId) => ({ x, y, type: 'teleport', teleportId }); +// ── Act I (1-6): rolling. Shape, parity and the 1x2 footprint. ─────────────── -function rect(x0, y0, w, h, extra = {}) { - const out = []; - for (let y = y0; y < y0 + h; y++) for (let x = x0; x < x0 + w; x++) out.push(floor(x, y, extra)); - return out; -} +lvl(1, 'First Roll', ` + ....... + .S..... + ....... + .....G. + ....... +`); -// Later entries win on coordinate collision — lets a base rect/fill be -// selectively overridden with bridges/switches/fragile/goal/etc. -function compile(tileList) { - const map = new Map(); - for (const t of tileList) map.set(`${t.x},${t.y}`, t); - return [...map.values()]; -} +lvl(2, 'Mind the Gap', ` + ......... + .S....... + .... ... + .... ... + ......... + .......G. +`); -function level(n, name, cols, rows, start, goal, tileList) { - return { level: n, name, cols, rows, start, goal, tiles: compile(tileList) }; -} +lvl(3, 'Right Angle', ` + ..... + .S... + ..... + .. + .. + ..... + ...G. + ..... +`); -// A single straight horizontal corridor at row `y`, columns 0..length. Rolling -// right from a standing start at column 0 lands STANDING on columns 0,3,6,9… -// and LYING on pairs (1,2),(4,5),(7,8)… — every level below places switches, -// bridges, fragile tiles, teleporters and split-flags using that fixed -// arithmetic, and always ends `length` on a multiple of 3 so the final -// approach lands standing exactly on the goal. `extraRows` widens the board -// so branch tiles (e.g. a switch reached by branching off the row) still fit. -function corridorLevel(n, name, { length, y = 2, rows = 5, overrides = [] }) { - const cols = length + 1; - const tiles = []; - for (let x = 0; x <= length; x++) tiles.push(floor(x, y)); - tiles.push(...overrides); - const hasGoal = overrides.some((o) => o.type === 'goal'); - const goalPos = hasGoal ? overrides.find((o) => o.type === 'goal') : { x: length, y }; - if (!hasGoal) tiles.push(goalTile(length, y)); - return level(n, name, cols, rows, { x: 0, y, orient: 'up' }, { x: goalPos.x, y: goalPos.y }, tiles); -} +lvl(4, 'Narrow Pass', ` + .... ..... + .S.. ..... + ............ + .... ..... + ..G.. +`); -// ── Levels 1-6: plain rolling ──────────────────────────────────────────────── +lvl(5, 'The Ring', ` + ......... + .S....... + ... ... + ... ... + ... ... + .......G. + ......... +`); -const L1 = level(1, 'Warm Up', 6, 5, { x: 1, y: 1, orient: 'up' }, { x: 4, y: 3 }, [ - ...rect(0, 0, 6, 5), goalTile(4, 3), -]); -const L2 = level(2, 'Stretch Out', 7, 5, { x: 1, y: 1, orient: 'up' }, { x: 5, y: 3 }, [ - ...rect(0, 0, 7, 5), goalTile(5, 3), -]); -const L3 = level(3, 'Open Floor', 8, 6, { x: 1, y: 1, orient: 'up' }, { x: 6, y: 4 }, [ - ...rect(0, 0, 8, 6), goalTile(6, 4), -]); -const L4 = level(4, 'The Corner', 9, 7, { x: 1, y: 1, orient: 'up' }, { x: 7, y: 5 }, [ - ...rect(0, 0, 5, 7), ...rect(0, 4, 9, 3), goalTile(7, 5), -]); -const L5 = level(5, 'Crossroads', 9, 9, { x: 4, y: 1, orient: 'up' }, { x: 1, y: 4 }, [ - ...rect(3, 0, 3, 9), ...rect(0, 3, 9, 3), goalTile(1, 4), -]); -const L6 = level(6, 'Staircase', 10, 9, { x: 1, y: 1, orient: 'up' }, { x: 8, y: 7 }, [ - ...rect(0, 0, 4, 3), ...rect(2, 1, 4, 4), ...rect(4, 3, 4, 4), ...rect(6, 5, 4, 4), goalTile(8, 7), -]); +lvl(6, 'Zigzag', ` + ..... + .S... + ..... + ...... + ...... + ...... + ...... + ...... + ....G. + ...... +`); -// ── Levels 7-12: holes, ledges, static causeways ───────────────────────────── +// ── Act II (7-12): switches and bridges. ───────────────────────────────────── -const L7 = level(7, 'Mind the Gap', 8, 6, { x: 1, y: 1, orient: 'up' }, { x: 6, y: 4 }, - compile(rect(0, 0, 8, 6)).filter((t) => !(t.x === 4 && t.y === 2)).concat(goalTile(6, 4))); -const L8 = level(8, 'Pinwheel', 9, 7, { x: 1, y: 1, orient: 'up' }, { x: 7, y: 5 }, - compile(rect(0, 0, 9, 7)) - .filter((t) => !([[4, 2], [4, 3], [4, 4], [3, 3], [5, 3]].some(([hx, hy]) => t.x === hx && t.y === hy))) - .concat(goalTile(7, 5))); -const L9 = level(9, 'Causeway', 9, 3, { x: 1, y: 1, orient: 'up' }, { x: 7, y: 1 }, [ - ...rect(0, 0, 3, 3), ...rect(3, 1, 3, 1), ...rect(6, 0, 3, 3), goalTile(7, 1), -]); -const L10 = level(10, 'Bent Causeway', 8, 8, { x: 1, y: 1, orient: 'up' }, { x: 6, y: 6 }, [ - ...rect(0, 0, 3, 3), ...rect(3, 1, 3, 2), ...rect(4, 1, 2, 5), ...rect(3, 5, 5, 3), goalTile(6, 6), -]); -const L11 = level(11, 'Scattered', 10, 8, { x: 1, y: 1, orient: 'up' }, { x: 8, y: 6 }, - compile(rect(0, 0, 10, 8)) - .filter((t) => !([[3, 3], [6, 2], [4, 6], [7, 5], [2, 6]].some(([hx, hy]) => t.x === hx && t.y === hy))) - .concat(goalTile(8, 6))); -const L12 = level(12, 'Tightrope', 8, 8, { x: 1, y: 1, orient: 'up' }, { x: 6, y: 6 }, [ - ...rect(0, 0, 3, 3), ...rect(1, 1, 3, 3), ...rect(2, 2, 3, 3), - ...rect(3, 3, 3, 3), ...rect(4, 4, 3, 3), ...rect(5, 5, 3, 3), - goalTile(6, 6), -]); +// The heavy X switch only counts if the block comes to rest upright on it. +lvl(7, 'Heavy Switch', ` + ..... ..... + .S... ..... + .....11.G... + ..... ..... + ..A.. ..... +`); -// ── Levels 13-18: switches + bridges ───────────────────────────────────────── +// Soft switches trip on any contact — including in passing. Trip both and you +// have toggled the bridge shut again. +lvl(8, 'Light Touch', ` + ......... + .S....... + ..a...a.. + ......... + 1 + 1 + ......... + ....G.... + ......... +`); -const L13 = corridorLevel(13, 'Flip the Switch', { - length: 9, - overrides: [hardSwitch(2, 2, 's1', ['b1']), bridge(4, 2, 'b1', false)], -}); -const L14 = corridorLevel(14, 'Weight and See', { - length: 6, - overrides: [softSwitch(2, 2, 's1', ['b1']), bridge(3, 2, 'b1', false)], -}); -const L15 = corridorLevel(15, 'Double Gate', { - length: 12, - overrides: [hardSwitch(2, 2, 's1', ['b1', 'b2']), bridge(4, 2, 'b1', false), bridge(8, 2, 'b2', false)], -}); -const L16 = corridorLevel(16, 'Two Keys', { - length: 15, - overrides: [ - hardSwitch(2, 2, 's1', ['b1']), bridge(4, 2, 'b1', false), - hardSwitch(7, 2, 's2', ['b2']), bridge(10, 2, 'b2', false), - ], -}); -const L17 = corridorLevel(17, 'Soft Landing', { - length: 15, - overrides: [ - hardSwitch(2, 2, 's1', ['b1']), bridge(4, 2, 'b1', false), - softSwitch(8, 2, 's2', ['b2']), bridge(9, 2, 'b2', false), - ], -}); -const L18 = corridorLevel(18, 'Three Gates', { - length: 18, - overrides: [ - hardSwitch(2, 2, 's1', ['b1']), bridge(4, 2, 'b1', false), - softSwitch(8, 2, 's2', ['b2']), bridge(9, 2, 'b2', false), - hardSwitch(13, 2, 's3', ['b3']), bridge(16, 2, 'b3', false), - ], -}); +// A hold pad only counts while a LYING block is on it, so the bridge is open +// for exactly the one move that rolls off the pad onto it. +lvl(9, 'Hold the Pad', ` + .... + .S.. + .... + ...p1..... + ..... + ..G.. +`); -// ── Levels 19-24: fragile tiles ─────────────────────────────────────────────── +lvl(10, 'The Long Way', ` + ...... ... + .S.... ... + ...... ... + ... ... + ... ... + ......111... + ...... ... + ..A... .G. + ...... ... +`); -const L19 = corridorLevel(19, 'Cracked Floor', { length: 9, overrides: [fragile(3, 2)] }); -const L20 = corridorLevel(20, 'Eggshells', { length: 12, overrides: [fragile(3, 2), fragile(7, 2)] }); -const L21 = corridorLevel(21, 'No Turning Back', { - length: 15, - overrides: [fragile(3, 2), fragile(6, 2), fragile(10, 2)], -}); -const L22 = corridorLevel(22, 'Fragile Gate', { - length: 12, - overrides: [hardSwitch(2, 2, 's1', ['b1']), bridge(5, 2, 'b1', false), fragile(8, 2)], -}); -const L23 = corridorLevel(23, 'Soft and Thin', { - length: 15, - overrides: [fragile(3, 2), softSwitch(7, 2, 's1', ['b1']), bridge(9, 2, 'b1', false), fragile(11, 2)], -}); -const L24 = corridorLevel(24, 'Every Step Counts', { - length: 18, - overrides: [ - fragile(3, 2), hardSwitch(6, 2, 's1', ['b1']), bridge(8, 2, 'b1', false), - fragile(11, 2), softSwitch(14, 2, 's2', ['b2']), bridge(15, 2, 'b2', false), - ], -}); +lvl(11, 'Two Gates', ` + .... ..... .... + .S.. ..B.. .... + ....11.....22.G.. + ..A. ..... .... +`); -// ── Levels 25-30: teleporters ───────────────────────────────────────────────── +// Bridge 1 starts open and bridge 2 shut; the switch in the middle room swaps +// them, so the way in is also the way you give up. +lvl(12, 'Shuttle', ` + .... .... .... + .S.. .A.. .... + ....11......22..G... + .... .... .... +`, { open: [1], links: { A: [1, 2] } }); -const L25 = corridorLevel(25, 'Shortcut', { length: 13, overrides: [teleport(3, 2, 't1'), teleport(10, 2, 't1')] }); -const L26 = corridorLevel(26, 'Two Hops', { - length: 21, - overrides: [teleport(3, 2, 't1'), teleport(9, 2, 't1'), teleport(12, 2, 't2'), teleport(18, 2, 't2')], -}); -const L27 = corridorLevel(27, 'Triple Hop', { - length: 27, - overrides: [ - teleport(3, 2, 't1'), teleport(9, 2, 't1'), - teleport(12, 2, 't2'), teleport(18, 2, 't2'), - teleport(21, 2, 't3'), teleport(24, 2, 't3'), - ], -}); -const L28 = corridorLevel(28, 'Gate and Go', { - length: 15, - overrides: [ - hardSwitch(2, 2, 's1', ['b1']), bridge(4, 2, 'b1', false), - teleport(6, 2, 't1'), teleport(12, 2, 't1'), - ], -}); -const L29 = corridorLevel(29, 'Brittle Hop', { - length: 12, - overrides: [fragile(3, 2), teleport(6, 2, 't1'), teleport(9, 2, 't1')], -}); -const L30 = corridorLevel(30, 'All at Once', { - length: 18, - overrides: [ - softSwitch(2, 2, 's1', ['b1']), bridge(3, 2, 'b1', false), - fragile(6, 2), teleport(9, 2, 't1'), teleport(15, 2, 't1'), - ], -}); +// ── Act III (13-18): fragile ground. Standing upright on orange puts the block +// straight through it, so these are orientation puzzles: you cross lying, which +// means shuffling sideways, and you can only change axis on solid ground. ──── -// ── Levels 31-36: split tiles ───────────────────────────────────────────────── +lvl(13, 'Two at a Time', ` + .... + .S.. + .... + ~~ + ~~ + .... + ..G. + .... +`); -const L31 = corridorLevel(31, 'Split Decision', { - length: 10, - overrides: [splitFloor(4, 2), splitFloor(5, 2)], -}); -// NOTE: a split-trigger-then-immediate-remerge costs one extra column versus -// a plain tip (the remerge lands one column further right than a same-shape -// non-split roll would have) — lengths below are chosen with that in mind. -const L32 = corridorLevel(32, 'Split and Gate', { - length: 19, - overrides: [ - hardSwitch(2, 2, 's1', ['b1']), bridge(4, 2, 'b1', false), - splitFloor(7, 2), splitFloor(8, 2), - ], -}); -const L33 = corridorLevel(33, 'Split and Crack', { - length: 15, - overrides: [fragile(3, 2), splitFloor(6, 2), splitFloor(7, 2)], -}); -const L34 = corridorLevel(34, 'Split and Hop', { - length: 16, - overrides: [splitFloor(4, 2), splitFloor(5, 2), teleport(7, 2, 't1'), teleport(13, 2, 't1')], -}); +lvl(14, 'Cracked Gate', ` + ..... ..~~~.. + .S... ..~~~.. + .....11..~~~G. + ..... ..~~~.. + ..A.. ..~~~.. +`); -// A wide open room around the split point (not a tight corridor) so the two -// independent units have plenty of room to sidestep the wall pillars and -// reconverge — the BFS solver (not hand-traced choreography) is the actual -// proof this works. -const L35 = level(35, 'Around the Pillar', 16, 7, { x: 1, y: 3, orient: 'up' }, { x: 14, y: 3 }, [ - ...rect(0, 2, 5, 3), // entry platform, rows 2-4 - splitFloor(5, 3), splitFloor(6, 3), // split trigger, row 3 - ...rect(5, 0, 7, 7), // big open room, rows 0-6, cols 5-11 - wallTile(8, 2), wallTile(8, 3), wallTile(8, 4), // pillar splitting the room - ...rect(12, 2, 4, 3), // exit platform, rows 2-4 - goalTile(14, 3), -]); +lvl(15, 'Corner Ice', ` + ....~~~~~~ + .S..~~~~~~ + ....~~~~~~ + ~~~~~~ + ~~~~~~ + ...... + ...G.. +`); -// Open rooms throughout (not a tight single-row corridor) so switch/fragile/ -// split placement doesn't depend on exact tip arithmetic — only reachability -// within each room, which the BFS solver confirms directly. -const L36 = level(36, 'Grand Finale', 22, 5, { x: 1, y: 1, orient: 'up' }, { x: 19, y: 2 }, [ - ...rect(0, 0, 5, 4), // room A: start + switch - hardSwitch(2, 2, 's1', ['b1']), - bridge(5, 1, 'b1', false), // gate into room B - ...rect(6, 0, 5, 4), // room B: fragile + split trigger - fragile(7, 2), - splitFloor(9, 1), splitFloor(10, 1), - ...rect(11, 0, 7, 5), // room C: pillar maze - wallTile(14, 1), wallTile(14, 2), wallTile(14, 3), - ...rect(18, 0, 4, 4), // room D: goal - goalTile(19, 2), -]); +lvl(16, 'Thin Ice', ` + ....~~~~.... + .S..~~~~..G. + ....~~~~.... +`); -const LEVELS = [ - L1, L2, L3, L4, L5, L6, - L7, L8, L9, L10, L11, L12, - L13, L14, L15, L16, L17, L18, - L19, L20, L21, L22, L23, L24, - L25, L26, L27, L28, L29, L30, - L31, L32, L33, L34, L35, L36, -]; +lvl(17, 'Collapse', ` + ..... .~~~~~.. + .S... .~~~~~.. + .....11.~~~~~G. + ..... .~~~~~.. + ..A.. .~~~~~.. +`); -// ── Solve-gate + write ──────────────────────────────────────────────────────── +lvl(18, 'Stepping Stones', ` + ....~~....~~.... + .S..~~....~~..G. + ....~~....~~.... +`); -console.log(`[bloxorz] solving ${LEVELS.length} hand-authored levels…`); +// ── Act IV (19-24): teleport pads. Only a STANDING landing warps. ──────────── + +lvl(19, 'Warp', ` + ....... + .S..... + ...t... + ....... + + ....... + ...t... + ....... + ..G.... +`); + +lvl(20, 'Brittle Warp', ` + ...... + .S.... + ...t.. + ...... + + ~~~~~~~ + ~~t~~~~ + ~~~~~~~ + ~~~~G.. +`); + +lvl(21, 'Crossed Wires', ` + ...... ...... + .S..t. ..u... + ...... ...... + ...t.. + + ...... + ..u... + ...G.. + ...... +`); + +lvl(22, 'Round Trip', ` + ...... ...... + .S.... ..t... + ...t.. ...... + ....u. + + ...... + ..u... + ...... + ...G.. + ...... +`); + +lvl(23, 'Warp Gate', ` + ..... ...... + .S...11...t.. + ..... ...... + ..A.. ...... + + ...... + ..t... + ....G. + ...... +`); + +lvl(24, 'Warp Maze', ` + ..... ...... + .S... ..t... + .....11...... + ..A.. ...... + ..... ...... + + ...... ..... + ..t... ..u.. + ...... ..... + ...u.. ..G.. + ...... ..... +`); + +// ── Act V (25-30): splitting. Two cubes, one input between them. The ground is +// narrow on purpose — an open room lets a joined block fix its parity by +// walking around, which would make the split optional. ────────────────────── + +// The pair rejoins on the very next move, one cell further along than a plain +// tip would have landed: splitting is a parity shift, and this corridor's goal +// sits on the one parity a rolling block can never reach. +lvl(25, 'Break Apart', ` + .... + .S.. + .... + . + . + ....::....G +`); + +lvl(26, 'Split Gate', ` + .... + .S.. + .... + . + . + ....::....A + 1 + 1 + ..... + ..G.. + ..... +`); + +lvl(27, 'Split Warp', ` + .... + .S.. + .... + . + . + ....::..t.. + + ...... + ..t... + ....G. + ...... +`); + +// Here the wall catches one cube while the other rolls on, so the two travel +// the board separately and have to be lined back up to fuse. +lvl(28, 'Held Back', ` + S::........ + .#..##...#. + .......#... + ........#.. + ........... + ........#G. +`); + +lvl(29, 'Split Ice', ` + .... + .S.. + .... + ~~ + ~~ + ~~ + ......::....G +`); + +lvl(30, 'Reassembly', ` + S::........ + ..#.....#.. + .......#... + ...#....#.. + ........... + ........#G. +`); + +// ── Act VI (31-36): everything at once, on bigger boards. ──────────────────── + +lvl(31, 'Warp and Crack', ` + ..... ..~~~.. + .S...11..~~~.. + ..... ..~~~.. + ..A.. ..~~~.. + ..... ..~t~.. + + ....... + ...t... + ....... + ..G.... + ....... +`); + +lvl(32, 'Gatehouse', ` + ..... ..~~~.. ..... + .S... ..~~~.. ..... + .....11..~~~..22..G.. + ..... ..~~~.. ..... + ..A.. ..B~~.. ..... +`); + +lvl(33, 'Pillars', ` + S::.......... + ..#.......#.. + .#........... + ............. + ............. + ...#.#....... + .........#.#. + ........#..G. +`); + +lvl(34, 'The Gauntlet', ` + ..... ..~~~~.. + .S...11..~~~~.. + ..... ..~~~~.. + ..A.. ..~~~~.. + ..... ..~~~~.. + . + . + ....::....G +`); + +lvl(35, 'Two Wings', ` + ..A.. + ..... + . + ..... .......... ..... + .S...11..........22..G.. + ..... .......... ..... + . + ..... + ..B.. +`); + +lvl(36, 'Grand Finale', ` + ..... ....... + .S...11...t... + ..... ....... + ..A.. ....... + + ..~~~~.. + ..~~~~.. + t.~~~~.. + ..~~~~.. + ..~~~~.. + . + . + ....::....G +`); + +// ── Solve-gate + write ─────────────────────────────────────────────────────── + +console.log(`[bloxorz] auditing ${LEVELS.length} hand-authored levels…`); let failed = 0; const levels = LEVELS.map((def) => { - const compiled = loadLevel(def); - const { moves } = solve(compiled, { maxStates: 300000 }); - if (moves < 0) { - console.error(` ✗ level ${def.level} "${def.name}" is UNSOLVABLE`); + const { par, problems, notes } = auditLevel(def); + if (problems.length) { + console.error(` ✗ level ${def.level} "${def.name}" — ${problems.join(', ')}`); failed++; - return { ...def, par: -1 }; + } else { + const tail = notes.length ? ` (note: ${notes.join('; ')})` : ''; + console.log(` ✓ level ${def.level} "${def.name}" — ${def.cols}x${def.rows}, par ${par}${tail}`); } - console.log(` ✓ level ${def.level} "${def.name}" — par ${moves}`); - return { ...def, par: moves }; + return { ...def, par }; }); +// The whole point of the overhaul: difficulty has to actually climb. Compare +// six-level acts rather than adjacent levels, which leaves room for a breather +// after a hard one without letting a whole act flatten out. +const acts = []; +for (let i = 0; i < levels.length; i += 6) { + const slice = levels.slice(i, i + 6); + acts.push(slice.reduce((s, l) => s + l.par, 0) / slice.length); +} +acts.forEach((avg, i) => console.log(`[bloxorz] act ${i + 1} average par ${avg.toFixed(1)}`)); +for (let i = 1; i < acts.length; i++) { + if (acts[i] <= acts[i - 1]) { + console.error(` ✗ act ${i + 1} (avg par ${acts[i].toFixed(1)}) is no harder than act ${i} (${acts[i - 1].toFixed(1)})`); + failed++; + } +} + if (failed > 0) { - console.error(`[bloxorz] ${failed} level(s) unsolvable — refusing to write ${OUT_FILE}`); + console.error(`[bloxorz] ${failed} problem(s) — refusing to write ${OUT_FILE}`); process.exit(1); } diff --git a/tools/verifyBloxorz.js b/tools/verifyBloxorz.js index c59f604..28fdf94 100644 --- a/tools/verifyBloxorz.js +++ b/tools/verifyBloxorz.js @@ -13,8 +13,11 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { - loadLevel, newState, applyMove, solve, + loadLevel, newState, applyMove, solve, previewRoll, DIR_LIST, } from '../src/games/bloxorz/BloxorzLogic.js'; +import { + EX, EY, depthOf, boxesForBlock, boxCorners, rollPoints, visibleFaces, +} from '../src/games/bloxorz/BloxorzIso.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const FILE = path.join(__dirname, '../data/bloxorz.json'); @@ -35,7 +38,7 @@ console.log(`[verify] ${levels.length} levels`); check('bank has 36 levels', levels.length === 36, `found ${levels.length}`); -let prevPar = 0; +const actPars = []; for (const def of levels) { const inBounds = (x, y) => x >= 0 && x < def.cols && y >= 0 && y < def.rows; @@ -54,6 +57,7 @@ for (const def of levels) { switchLinks.get(bid).add(t.mode); } } + if (t.type === 'switch' && !['standing', 'any', 'lying'].includes(t.requireOrientation)) boundsOk = false; if (t.type === 'teleport') teleportCounts.set(t.teleportId, (teleportCounts.get(t.teleportId) ?? 0) + 1); if (t.split) splitCount++; } @@ -79,9 +83,22 @@ for (const def of levels) { check(`L${def.level} "${def.name}": solvable`, moves >= 0, `solve() returned moves=${moves}`); check(`L${def.level}: par matches fresh solve`, moves === def.par, `par=${def.par} solve=${moves}`); - prevPar = def.par; + actPars.push(def.par); } -void prevPar; + +// The bank is built as six six-level acts that have to get harder. Compare act +// averages rather than adjacent levels, so an act can still open with a +// breather without the whole curve flattening out. +const actAvg = []; +for (let i = 0; i < actPars.length; i += 6) { + const slice = actPars.slice(i, i + 6); + actAvg.push(slice.reduce((a, b) => a + b, 0) / slice.length); +} +for (let i = 1; i < actAvg.length; i++) { + check(`act ${i + 1} is harder than act ${i}`, actAvg[i] > actAvg[i - 1], + `${actAvg[i - 1].toFixed(1)} -> ${actAvg[i].toFixed(1)}`); +} +check('no two levels share a name', new Set(levels.map((l) => l.name)).size === levels.length); // ── Engine unit tests ──────────────────────────────────────────────────────── @@ -93,8 +110,9 @@ const wall = (x, y) => ({ x, y, type: 'wall' }); const goal = (x, y) => ({ x, y, type: 'goal' }); const fragileT = (x, y) => ({ x, y, type: 'fragile' }); const bridgeT = (x, y, id, open = false) => ({ x, y, type: 'bridge', bridgeId: id, initiallyOpen: open }); -const hardSw = (x, y, id, ids) => ({ x, y, type: 'switch', switchId: id, linkedBridgeIds: ids, requireOrientation: 'any', mode: 'toggle' }); -const softSw = (x, y, id, ids) => ({ x, y, type: 'switch', switchId: id, linkedBridgeIds: ids, requireOrientation: 'lying', mode: 'momentary' }); +const softSw = (x, y, id, ids) => ({ x, y, type: 'switch', switchId: id, linkedBridgeIds: ids, requireOrientation: 'any', mode: 'toggle' }); +const heavySw = (x, y, id, ids) => ({ x, y, type: 'switch', switchId: id, linkedBridgeIds: ids, requireOrientation: 'standing', mode: 'toggle' }); +const holdPad = (x, y, id, ids) => ({ x, y, type: 'switch', switchId: id, linkedBridgeIds: ids, requireOrientation: 'lying', mode: 'momentary' }); const tp = (x, y, id) => ({ x, y, type: 'teleport', teleportId: id }); // -- Tip-over physics: standing -> lying -- @@ -134,12 +152,18 @@ const tp = (x, y, id) => ({ x, y, type: 'teleport', teleportId: id }); check('rolling off the platform edge is fatal', res.dead === true && s.status === 'dead'); } -// -- Goal only supports a standing landing -- +// -- Only a standing landing drops into the goal; lying just bridges it -- { - const lvl = synthLevel([floor(0, 0), goal(1, 0)], { x: 0, y: 0, orient: 'up' }); + const lvl = synthLevel([floor(0, 0), goal(1, 0), floor(2, 0)], { x: 0, y: 0, orient: 'up' }); const s = newState(lvl); - const res = applyMove(lvl, s, 'right'); // lands lying across [1,2]; 2 is void, 1 is goal (needs 'up') - check('a lying landing on/through the goal falls through (fatal)', res.dead === true); + const res = applyMove(lvl, s, 'right'); // lands lying across the goal [1] and floor [2] + check('a block lying across the goal bridges it instead of falling in', + res.dead !== true && s.status === 'playing', JSON.stringify(s.block)); + + const lvl1b = synthLevel([floor(0, 0), goal(1, 0)], { x: 0, y: 0, orient: 'up' }); + const s1b = newState(lvl1b); + const res1b = applyMove(lvl1b, s1b, 'right'); // lying across the goal [1] and void [2] + check('a lying landing half over the void is still fatal, goal or not', res1b.dead === true); const lvl2 = synthLevel([floor(0, 0), floor(1, 0), floor(2, 0), goal(3, 0)], { x: 0, y: 0, orient: 'up' }); const s2 = newState(lvl2); @@ -148,46 +172,78 @@ const tp = (x, y, id) => ({ x, y, type: 'teleport', teleportId: id }); check('a standing landing exactly on the goal wins', s2.status === 'won'); } -// -- Fragile tiles: safe once, fatal on a second (already-broken) visit -- +// -- Fragile ground: safe lying, straight through it standing -- { - const lvl = synthLevel([floor(0, 0), fragileT(1, 0), floor(2, 0)], { x: 0, y: 0, orient: 'up' }); - const s = { block: { mode: 'joined', orient: 'up', x: 0, y: 0 }, toggleBridges: new Map(), broken: new Set(), status: 'playing' }; - // Simulate having already broken (1,0) on a prior visit, then try to land on it again. - s.broken.add('1,0'); - const res = applyMove(lvl, s, 'right'); // lying[1,2] overlaps the broken cell - check('landing on an already-broken fragile tile is fatal', res.dead === true); + const lvl = synthLevel([floor(0, 0), fragileT(1, 0), fragileT(2, 0), floor(3, 0)], { x: 0, y: 0, orient: 'up' }); + const s = newState(lvl); + const res = applyMove(lvl, s, 'right'); // lying across both fragile cells + check('a lying block rests on fragile ground safely', res.dead !== true && s.status === 'playing'); + const res2 = applyMove(lvl, s, 'right'); // lying[1,2] -> standing3 (solid) — fine + check('rolling off fragile onto solid ground is fine', res2.dead !== true && s.status === 'playing'); } { - const lvl = synthLevel([floor(0, 0), floor(1, 0), floor(2, 0), fragileT(3, 0), floor(4, 0), floor(5, 0)], { x: 0, y: 0, orient: 'up' }); + const lvl = synthLevel([floor(0, 0), floor(1, 0), floor(2, 0), fragileT(3, 0), floor(4, 0)], { x: 0, y: 0, orient: 'up' }); const s = newState(lvl); applyMove(lvl, s, 'right'); // standing0 -> lying[1,2] - applyMove(lvl, s, 'right'); // lying[1,2] -> standing3 (fragile) — first visit, safe - check('first standing visit to a fragile tile survives', s.status === 'playing'); - check('fragile tile is marked broken after the first standing visit', s.broken.has('3,0')); + const res = applyMove(lvl, s, 'right'); // lying[1,2] -> standing3, upright on fragile + check('standing upright on fragile ground drops the block', res.dead === true && s.status === 'dead'); +} +{ + // A lone split cube is half the weight, so it may stand on fragile ground. + const lvl = synthLevel([floor(0, 0), floor(1, 0), floor(1, 1), fragileT(2, 1)], { x: 0, y: 0, orient: 'up' }); + const s = { + block: { mode: 'split', a: { x: 0, y: 0 }, b: { x: 1, y: 1 } }, + toggleBridges: new Map(), status: 'playing', + }; + const res = applyMove(lvl, s, 'right'); // a -> (1,0) solid, b -> (2,1) fragile + check('a split cube is light enough to stand on fragile ground', + res.dead !== true && s.status === 'playing' && s.block.mode === 'split', JSON.stringify(s.block)); } -// -- Hard switch: any orientation, persistent -- +// -- Heavy switch: only counts standing upright on it -- { const lvl = synthLevel([ - floor(0, 0), hardSw(1, 0, 's1', ['b1']), floor(2, 0), bridgeT(3, 0, 'b1', false), floor(4, 0), + floor(0, 0), floor(1, 0), heavySw(2, 0, 's1', ['b1']), bridgeT(3, 0, 'b1', false), floor(4, 0), ], { x: 0, y: 0, orient: 'up' }); const s = newState(lvl); - applyMove(lvl, s, 'right'); // standing0 -> lying[1,2], covers the hard switch - check('hard switch opens its bridge on contact (any orientation)', s.toggleBridges.get('b1') === true); + applyMove(lvl, s, 'right'); // standing0 -> lying[1,2], LYING across the heavy switch + check('a lying block does not press a heavy switch', s.toggleBridges.get('b1') !== true); + const res = applyMove(lvl, s, 'right'); // lying[1,2] -> standing3, the still-closed bridge + check('the heavy-switch bridge is still shut for a lying press', res.dead === true); + + const lvl2 = synthLevel([ + floor(0, 0), floor(1, 0), heavySw(2, 0, 's1', ['b1']), bridgeT(3, 0, 'b1', false), floor(4, 0), + floor(2, 1), floor(2, 2), floor(2, 3), + ], { x: 2, y: 3, orient: 'up' }); + const s2 = newState(lvl2); + applyMove(lvl2, s2, 'up'); // standing (2,3) -> lying (2,1)-(2,2) + applyMove(lvl2, s2, 'up'); // -> standing on (2,0), the heavy switch + check('standing upright on a heavy switch opens its bridge', + s2.block.orient === 'up' && s2.block.y === 0 && s2.toggleBridges.get('b1') === true, JSON.stringify(s2.block)); +} + +// -- Soft switch: any contact, persistent -- +{ + const lvl = synthLevel([ + floor(0, 0), softSw(1, 0, 's1', ['b1']), floor(2, 0), bridgeT(3, 0, 'b1', false), floor(4, 0), + ], { x: 0, y: 0, orient: 'up' }); + const s = newState(lvl); + applyMove(lvl, s, 'right'); // standing0 -> lying[1,2], covers the soft switch + check('soft switch opens its bridge on contact (any orientation)', s.toggleBridges.get('b1') === true); const res = applyMove(lvl, s, 'right'); // lying[1,2] -> standing3, onto the now-open bridge - check('hard-switch bridge stays open for a later crossing', res.dead !== true && s.status === 'playing'); + check('soft-switch bridge stays open for a later crossing', res.dead !== true && s.status === 'playing'); } -// -- Soft (momentary) switch: open only while covered, recloses immediately -- +// -- Hold pad (momentary): open only while covered, recloses immediately -- { const lvl = synthLevel([ - floor(0, 0), floor(1, 0), softSw(2, 0, 's1', ['b1']), bridgeT(3, 0, 'b1', false), floor(4, 0), floor(5, 0), + floor(0, 0), floor(1, 0), holdPad(2, 0, 's1', ['b1']), bridgeT(3, 0, 'b1', false), floor(4, 0), floor(5, 0), ], { x: 0, y: 0, orient: 'up' }); const s = newState(lvl); - applyMove(lvl, s, 'right'); // standing0 -> lying[1,2], covers the soft switch (col2) + applyMove(lvl, s, 'right'); // standing0 -> lying[1,2], covers the hold pad (col2) const res = applyMove(lvl, s, 'right'); // lying[1,2] -> standing3, crossing the bridge in the same instant - check('soft-switch bridge is passable while still covered from the prior landing', res.dead !== true && s.status === 'playing'); - check('soft-switch bridge recloses once the block leaves', s.toggleBridges.get('b1') !== true); + check('hold-pad bridge is passable while still covered from the prior landing', res.dead !== true && s.status === 'playing'); + check('hold-pad bridge recloses once the block leaves', s.toggleBridges.get('b1') !== true); } // -- Teleport relocates a standing landing -- @@ -231,24 +287,99 @@ const tp = (x, y, id) => ({ x, y, type: 'teleport', teleportId: id }); const lvl = synthLevel([floor(5, 5), wall(6, 5), floor(6, 6), floor(7, 5)], { x: 5, y: 5, orient: 'up' }); const s = { block: { mode: 'split', a: { x: 5, y: 5 }, b: { x: 6, y: 5 } }, - toggleBridges: new Map(), broken: new Set(), status: 'playing', + toggleBridges: new Map(), status: 'playing', }; applyMove(lvl, s, 'right'); const { a, b } = s.block; check('a wall-blocked split unit stays put while its partner moves on', a.x === 5 && a.y === 5 && b.x === 7 && b.y === 5, JSON.stringify(s.block)); } +// -- Two cubes can never end up stacked in one cell -- +{ + const lvl = synthLevel([floor(0, 0), floor(1, 0), wall(2, 0)], { x: 0, y: 0, orient: 'up' }); + const s = { + block: { mode: 'split', a: { x: 0, y: 0 }, b: { x: 1, y: 0 } }, + toggleBridges: new Map(), status: 'playing', + }; + const res = applyMove(lvl, s, 'right'); // b is wall-blocked, so a can't take its cell + check('a cube cannot roll into the cell its wall-blocked partner still fills', + res.moved === false && s.block.a.x === 0 && s.block.b.x === 1, JSON.stringify(s.block)); +} + // -- Either split unit falling is fatal for the whole run -- { const lvl = synthLevel([floor(0, 0), floor(1, 0)], { x: 0, y: 0, orient: 'up' }); const s = { block: { mode: 'split', a: { x: 0, y: 0 }, b: { x: 1, y: 0 } }, - toggleBridges: new Map(), broken: new Set(), status: 'playing', + toggleBridges: new Map(), status: 'playing', }; const res = applyMove(lvl, s, 'down'); // both units roll off into the void check('either split unit falling ends the run', res.dead === true && s.status === 'dead'); } +// ── Renderer geometry (BloxorzIso is Phaser-free, so it runs here) ─────────── + +// The rolling animation is only faithful if a 90-degree tip about the leading +// ground edge lands the block exactly where the engine says it goes — this is +// what ties the renderer to the single source of truth. +{ + const canon = (pts) => pts + .map((p) => [p.x, p.y, p.z].map((v) => v.toFixed(6)).join(',')) + .sort() + .join(' | '); + + for (const orient of ['up', 'x', 'y']) { + for (const dir of DIR_LIST) { + const block = { mode: 'joined', orient, x: 5, y: 4 }; + const box = boxesForBlock(block)[0]; + const rolled = canon(rollPoints(boxCorners(box), dir, box, 1)); + const target = canon(boxCorners(boxesForBlock(previewRoll(block, dir))[0])); + check(`rolling a '${orient}' block ${dir} lands on the engine's pose`, rolled === target); + } + } + + // Rigid body: every edge length is preserved through the whole sweep, + // including past 90 degrees where the fall animation keeps turning. + const box = boxesForBlock({ mode: 'joined', orient: 'up', x: 0, y: 0 })[0]; + const start = boxCorners(box); + const EDGES = [[0, 1], [0, 2], [0, 4], [3, 1], [3, 2], [3, 7], [5, 1], [5, 4], [5, 7], [6, 2], [6, 4], [6, 7]]; + const len = (p, q) => Math.hypot(p.x - q.x, p.y - q.y, p.z - q.z); + let rigid = true; + for (const t of [0.13, 0.4, 0.77, 1, 1.6, 2.1]) { + const pts = rollPoints(start, 'right', box, t); + for (const [a, b] of EDGES) { + if (Math.abs(len(start[a], start[b]) - len(pts[a], pts[b])) > 1e-9) rigid = false; + } + } + check('the block stays rigid through the entire roll and tumble', rigid); + + check('a lying block is one cell tall, a standing block two', (() => { + const up = boxesForBlock({ mode: 'joined', orient: 'up', x: 0, y: 0 })[0]; + const lying = boxesForBlock({ mode: 'joined', orient: 'x', x: 0, y: 0 })[0]; + return up.z1 === 2 && lying.z1 === 1 && lying.x1 - lying.x0 === 2; + })()); + + check('split cubes render as two separate inset 1x1x1 pieces', (() => { + const cubes = boxesForBlock({ mode: 'split', a: { x: 1, y: 1 }, b: { x: 4, y: 1 } }); + return cubes.length === 2 && cubes.every((c) => c.z1 === 1 && c.x1 - c.x0 < 1 && c.x1 - c.x0 > 0.8); + })()); + + // Painter's algorithm precondition: both grid axes must recede down-screen, + // otherwise ascending depthOf() no longer draws far tiles first. + check('projection is non-degenerate', Math.abs(EX.x * EY.y - EX.y * EY.x) > 1e-6); + check('both grid axes point down-screen (painter order holds)', EX.y > 0 && EY.y > 0); + check('depthOf increases along both axes', depthOf(1, 0) > depthOf(0, 0) && depthOf(0, 1) > depthOf(0, 0)); + check('the block is not viewed at 45 degrees', Math.abs(EX.y / EX.x) < 0.35); + + // Convex box: culling alone must leave a non-overlapping set of front faces. + let faceCountOk = visibleFaces(boxCorners(box)).length === 3; + for (let t = 0; t <= 2.2; t += 0.05) { + const n = visibleFaces(rollPoints(start, 'right', box, t)).length; + if (n < 2 || n > 3) faceCountOk = false; + } + check('only front faces are drawn, at every roll angle', faceCountOk); +} + // ── Summary ────────────────────────────────────────────────────────────────── console.log(`[verify] ${passes} passed, ${failures} failed`);