From ed33d95de89ba7cfb9c3d364eb91037a5a6f213a Mon Sep 17 00:00:00 2001 From: zichen Date: Tue, 1 Nov 2022 19:58:29 +0800 Subject: [PATCH] Initial commit --- LICENSE | 201 +++++++++++++++++++++ code/chapter/15_game.js | 368 ++++++++++++++++++++++++++++++++++++++ code/chapter/16_canvas.js | 146 +++++++++++++++ code/game_levels.js | 144 +++++++++++++++ img/player.png | Bin 0 -> 8989 bytes img/sprites.png | Bin 0 -> 5711 bytes index.html | 10 ++ 7 files changed, 869 insertions(+) create mode 100644 LICENSE create mode 100644 code/chapter/15_game.js create mode 100644 code/chapter/16_canvas.js create mode 100644 code/game_levels.js create mode 100644 img/player.png create mode 100644 img/sprites.png create mode 100644 index.html diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/code/chapter/15_game.js b/code/chapter/15_game.js new file mode 100644 index 0000000..08e994d --- /dev/null +++ b/code/chapter/15_game.js @@ -0,0 +1,368 @@ +var simpleLevelPlan = [ + " ", + " ", + " x = x ", + " x o o x ", + " x @ xxxxx x ", + " xxxxx x ", + " x!!!!!!!!!!!!x ", + " xxxxxxxxxxxxxx ", + " " +]; + +function Level(plan) { + this.width = plan[0].length; + this.height = plan.length; + this.grid = []; + this.actors = []; + + for (var y = 0; y < this.height; y++) { + var line = plan[y], + gridLine = []; + for (var x = 0; x < this.width; x++) { + var ch = line[x], + fieldType = null; + var Actor = actorChars[ch]; + if (Actor) + this.actors.push(new Actor(new Vector(x, y), ch)); + else if (ch == "x") + fieldType = "wall"; + else if (ch == "!") + fieldType = "lava"; + gridLine.push(fieldType); + } + this.grid.push(gridLine); + } + + this.player = this.actors.filter(function(actor) { + return actor.type == "player"; + })[0]; + this.status = this.finishDelay = null; +} + +Level.prototype.isFinished = function() { + return this.status != null && this.finishDelay < 0; +}; + +function Vector(x, y) { + this.x = x; + this.y = y; +} +Vector.prototype.plus = function(other) { + return new Vector(this.x + other.x, this.y + other.y); +}; +Vector.prototype.times = function(factor) { + return new Vector(this.x * factor, this.y * factor); +}; + +var actorChars = { + "@": Player, + "o": Coin, + "=": Lava, + "|": Lava, + "v": Lava +}; + +function Player(pos) { + this.pos = pos.plus(new Vector(0, -0.5)); + this.size = new Vector(0.8, 1.5); + this.speed = new Vector(0, 0); +} +Player.prototype.type = "player"; + +function Lava(pos, ch) { + this.pos = pos; + this.size = new Vector(1, 1); + if (ch == "=") { + this.speed = new Vector(2, 0); + } else if (ch == "|") { + this.speed = new Vector(0, 2); + } else if (ch == "v") { + this.speed = new Vector(0, 3); + this.repeatPos = pos; + } +} +Lava.prototype.type = "lava"; + +function Coin(pos) { + this.basePos = this.pos = pos.plus(new Vector(0.2, 0.1)); + this.size = new Vector(0.6, 0.6); + this.wobble = Math.random() * Math.PI * 2; +} +Coin.prototype.type = "coin"; + +var simpleLevel = new Level(simpleLevelPlan); + +function elt(name, className) { + var elt = document.createElement(name); + if (className) elt.className = className; + return elt; +} + +function DOMDisplay(parent, level) { + this.wrap = parent.appendChild(elt("div", "game")); + this.level = level; + + this.wrap.appendChild(this.drawBackground()); + this.actorLayer = null; + this.drawFrame(); +} + +var scale = 20; + +DOMDisplay.prototype.drawBackground = function() { + var table = elt("table", "background"); + table.style.width = this.level.width * scale + "px"; + this.level.grid.forEach(function(row) { + var rowElt = table.appendChild(elt("tr")); + rowElt.style.height = scale + "px"; + row.forEach(function(type) { + rowElt.appendChild(elt("td", type)); + }); + }); + return table; +}; + +DOMDisplay.prototype.drawActors = function() { + var wrap = elt("div"); + this.level.actors.forEach(function(actor) { + var rect = wrap.appendChild(elt("div", + "actor " + actor.type)); + rect.style.width = actor.size.x * scale + "px"; + rect.style.height = actor.size.y * scale + "px"; + rect.style.left = actor.pos.x * scale + "px"; + rect.style.top = actor.pos.y * scale + "px"; + }); + return wrap; +}; + +DOMDisplay.prototype.drawFrame = function() { + if (this.actorLayer) + this.wrap.removeChild(this.actorLayer); + this.actorLayer = this.wrap.appendChild(this.drawActors()); + this.wrap.className = "game " + (this.level.status || ""); + this.scrollPlayerIntoView(); +}; + +DOMDisplay.prototype.scrollPlayerIntoView = function() { + var width = this.wrap.clientWidth; + var height = this.wrap.clientHeight; + var margin = width / 3; + + // The viewport + var left = this.wrap.scrollLeft, + right = left + width; + var top = this.wrap.scrollTop, + bottom = top + height; + + var player = this.level.player; + var center = player.pos.plus(player.size.times(0.5)) + .times(scale); + + if (center.x < left + margin) + this.wrap.scrollLeft = center.x - margin; + else if (center.x > right - margin) + this.wrap.scrollLeft = center.x + margin - width; + if (center.y < top + margin) + this.wrap.scrollTop = center.y - margin; + else if (center.y > bottom - margin) + this.wrap.scrollTop = center.y + margin - height; +}; + +DOMDisplay.prototype.clear = function() { + this.wrap.parentNode.removeChild(this.wrap); +}; + +Level.prototype.obstacleAt = function(pos, size) { + var xStart = Math.floor(pos.x); + var xEnd = Math.ceil(pos.x + size.x); + var yStart = Math.floor(pos.y); + var yEnd = Math.ceil(pos.y + size.y); + + if (xStart < 0 || xEnd > this.width || yStart < 0) + return "wall"; + if (yEnd > this.height) + return "lava"; + for (var y = yStart; y < yEnd; y++) { + for (var x = xStart; x < xEnd; x++) { + var fieldType = this.grid[y][x]; + if (fieldType) return fieldType; + } + } +}; + +Level.prototype.actorAt = function(actor) { + for (var i = 0; i < this.actors.length; i++) { + var other = this.actors[i]; + if (other != actor && + actor.pos.x + actor.size.x > other.pos.x && + actor.pos.x < other.pos.x + other.size.x && + actor.pos.y + actor.size.y > other.pos.y && + actor.pos.y < other.pos.y + other.size.y) + return other; + } +}; + +var maxStep = 0.05; + +Level.prototype.animate = function(step, keys) { + if (this.status != null) + this.finishDelay -= step; + + while (step > 0) { + var thisStep = Math.min(step, maxStep); + this.actors.forEach(function(actor) { + actor.act(thisStep, this, keys); + }, this); + step -= thisStep; + } +}; + +Lava.prototype.act = function(step, level) { + var newPos = this.pos.plus(this.speed.times(step)); + if (!level.obstacleAt(newPos, this.size)) + this.pos = newPos; + else if (this.repeatPos) + this.pos = this.repeatPos; + else + this.speed = this.speed.times(-1); +}; + +var wobbleSpeed = 8, + wobbleDist = 0.07; + +Coin.prototype.act = function(step) { + this.wobble += step * wobbleSpeed; + var wobblePos = Math.sin(this.wobble) * wobbleDist; + this.pos = this.basePos.plus(new Vector(0, wobblePos)); +}; + +var playerXSpeed = 7; + +Player.prototype.moveX = function(step, level, keys) { + this.speed.x = 0; + if (keys.left) this.speed.x -= playerXSpeed; + if (keys.right) this.speed.x += playerXSpeed; + + var motion = new Vector(this.speed.x * step, 0); + var newPos = this.pos.plus(motion); + var obstacle = level.obstacleAt(newPos, this.size); + if (obstacle) + level.playerTouched(obstacle); + else + this.pos = newPos; +}; + +var gravity = 30; +var jumpSpeed = 17; + +Player.prototype.moveY = function(step, level, keys) { + this.speed.y += step * gravity; + var motion = new Vector(0, this.speed.y * step); + var newPos = this.pos.plus(motion); + var obstacle = level.obstacleAt(newPos, this.size); + if (obstacle) { + level.playerTouched(obstacle); + if (keys.up && this.speed.y > 0) + this.speed.y = -jumpSpeed; + else + this.speed.y = 0; + } else { + this.pos = newPos; + } +}; + +Player.prototype.act = function(step, level, keys) { + this.moveX(step, level, keys); + this.moveY(step, level, keys); + + var otherActor = level.actorAt(this); + if (otherActor) + level.playerTouched(otherActor.type, otherActor); + + // Losing animation + if (level.status == "lost") { + this.pos.y += step; + this.size.y -= step; + } +}; + +Level.prototype.playerTouched = function(type, actor) { + if (type == "lava" && this.status == null) { + this.status = "lost"; + this.finishDelay = 1; + } else if (type == "coin") { + this.actors = this.actors.filter(function(other) { + return other != actor; + }); + if (!this.actors.some(function(actor) { + return actor.type == "coin"; + })) { + this.status = "won"; + this.finishDelay = 1; + } + } +}; + +var arrowCodes = { 37: "left", 38: "up", 39: "right" }; + +function trackKeys(codes) { + var pressed = Object.create(null); + + function handler(event) { + if (codes.hasOwnProperty(event.keyCode)) { + var down = event.type == "keydown"; + pressed[codes[event.keyCode]] = down; + event.preventDefault(); + } + } + addEventListener("keydown", handler); + addEventListener("keyup", handler); + return pressed; +} + +function runAnimation(frameFunc) { + var lastTime = null; + + function frame(time) { + var stop = false; + if (lastTime != null) { + var timeStep = Math.min(time - lastTime, 100) / 1000; + stop = frameFunc(timeStep) === false; + } + lastTime = time; + if (!stop) + requestAnimationFrame(frame); + } + requestAnimationFrame(frame); +} + +var arrows = trackKeys(arrowCodes); + +function runLevel(level, Display, andThen) { + var display = new Display(document.body, level); + runAnimation(function(step) { + level.animate(step, arrows); + display.drawFrame(step); + if (level.isFinished()) { + display.clear(); + if (andThen) + andThen(level.status); + return false; + } + }); +} + +function runGame(plans, Display) { + function startLevel(n) { + runLevel(new Level(plans[n]), Display, function(status) { + if (status == "lost") + startLevel(n); + else if (n < plans.length - 1) + startLevel(n + 1); + else + console.log("You win!"); + }); + } + startLevel(0); +} \ No newline at end of file diff --git a/code/chapter/16_canvas.js b/code/chapter/16_canvas.js new file mode 100644 index 0000000..49bdb32 --- /dev/null +++ b/code/chapter/16_canvas.js @@ -0,0 +1,146 @@ +var results = [ + { name: "Satisfied", count: 1043, color: "lightblue" }, + { name: "Neutral", count: 563, color: "lightgreen" }, + { name: "Unsatisfied", count: 510, color: "pink" }, + { name: "No comment", count: 175, color: "silver" } +]; + +function flipHorizontally(context, around) { + context.translate(around, 0); + context.scale(-1, 1); + context.translate(-around, 0); +} + +function CanvasDisplay(parent, level) { + this.canvas = document.createElement("canvas"); + this.canvas.width = Math.min(600, level.width * scale); + this.canvas.height = Math.min(450, level.height * scale); + parent.appendChild(this.canvas); + this.cx = this.canvas.getContext("2d"); + + this.level = level; + this.animationTime = 0; + this.flipPlayer = false; + + this.viewport = { + left: 0, + top: 0, + width: this.canvas.width / scale, + height: this.canvas.height / scale + }; + + this.drawFrame(0); +} + +CanvasDisplay.prototype.clear = function() { + this.canvas.parentNode.removeChild(this.canvas); +}; + +CanvasDisplay.prototype.drawFrame = function(step) { + this.animationTime += step; + + this.updateViewport(); + this.clearDisplay(); + this.drawBackground(); + this.drawActors(); +}; + +CanvasDisplay.prototype.updateViewport = function() { + var view = this.viewport, + margin = view.width / 3; + var player = this.level.player; + var center = player.pos.plus(player.size.times(0.5)); + + if (center.x < view.left + margin) + view.left = Math.max(center.x - margin, 0); + else if (center.x > view.left + view.width - margin) + view.left = Math.min(center.x + margin - view.width, + this.level.width - view.width); + if (center.y < view.top + margin) + view.top = Math.max(center.y - margin, 0); + else if (center.y > view.top + view.height - margin) + view.top = Math.min(center.y + margin - view.height, + this.level.height - view.height); +}; + +CanvasDisplay.prototype.clearDisplay = function() { + //this.cx.fillStyle = "rgb(0, 0, 0)"; + if (this.level.status == "won") + this.cx.fillStyle = "rgb(245, 245, 228)"; + else if (this.level.status == "lost") + this.cx.fillStyle = "rgb(220, 20, 60)"; + else + this.cx.fillStyle = "rgb(0, 0, 0)"; + this.cx.fillRect(0, 0, + this.canvas.width, this.canvas.height); +}; + +var otherSprites = document.createElement("img"); +otherSprites.src = "img/sprites.png"; + +CanvasDisplay.prototype.drawBackground = function() { + var view = this.viewport; + var xStart = Math.floor(view.left); + var xEnd = Math.ceil(view.left + view.width); + var yStart = Math.floor(view.top); + var yEnd = Math.ceil(view.top + view.height); + + for (var y = yStart; y < yEnd; y++) { + for (var x = xStart; x < xEnd; x++) { + var tile = this.level.grid[y][x]; + if (tile == null) continue; + var screenX = (x - view.left) * scale; + var screenY = (y - view.top) * scale; + var tileX = tile == "lava" ? scale : 0; + this.cx.drawImage(otherSprites, + tileX, 0, scale, scale, + screenX, screenY, scale, scale); + } + } +}; + +var playerSprites = document.createElement("img"); +playerSprites.src = "img/player.png"; +var playerXOverlap = 4; + +CanvasDisplay.prototype.drawPlayer = function(x, y, width, + height) { + var sprite = 8, + player = this.level.player; + width += playerXOverlap * 2; + x -= playerXOverlap; + if (player.speed.x != 0) + this.flipPlayer = player.speed.x < 0; + + if (player.speed.y != 0) + sprite = 9; + else if (player.speed.x != 0) + sprite = Math.floor(this.animationTime * 12) % 8; + + this.cx.save(); + if (this.flipPlayer) + flipHorizontally(this.cx, x + width / 2); + + this.cx.drawImage(playerSprites, + sprite * width, 0, width, height, + x, y, width, height); + + this.cx.restore(); +}; + +CanvasDisplay.prototype.drawActors = function() { + this.level.actors.forEach(function(actor) { + var width = actor.size.x * scale; + var height = actor.size.y * scale; + var x = (actor.pos.x - this.viewport.left) * scale; + var y = (actor.pos.y - this.viewport.top) * scale; + if (actor.type == "player") { + this.drawPlayer(x, y, width, height); + } else { + var tileX = (actor.type == "coin" ? 2 : 1) * scale; + this.cx.drawImage(otherSprites, + tileX, 0, width, height, + x, y, width, height); + } + }, this); +}; \ No newline at end of file diff --git a/code/game_levels.js b/code/game_levels.js new file mode 100644 index 0000000..af48802 --- /dev/null +++ b/code/game_levels.js @@ -0,0 +1,144 @@ +var GAME_LEVELS = [ + [" ", + " ", + " @ ", + " ", + " ", + " ", + " xxx ", + " xx xx xx!xx ", + " o o xx x!!!x ", + " xx!xx ", + " xxxxx xvx ", + " xx ", + " xx o o x ", + " x o x ", + " x xxxxx o x ", + " x xxxx o x ", + " x x x xxxxx x ", + " xxxxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxx xxxxxxx xxxxxxxxx ", + " x x x x ", + " x!!!x x!!!!!x ", + " x!!!x x!!!!!x ", + " xxxxx xxxxxxx ", + " ", + " " + ], + [" x!!x xxxxxxx x!x ", + " @ x!!x xxxx xxxx x!x ", + " x!!xxxxxxxxxx xx xx x!x ", + " xx!!!!!!!!!!xx xx xx x!x ", + " xxxxxxxxxx!!x x o o o x!x ", + " xx!x x o o xx!x ", + " x!x x xxxxxxxxxxxxxxx!!x ", + " xvx x x x !!!!!!!!!!!!!!xx ", + " xx | | | xx xxxxxxxxxxxxxxxxxxxxx ", + " xx!!!!!!!!!!!xx v ", + " xxxx!!!!!xxxx ", + " x x xxxxxxx xxx xxx ", + " x x x x x x ", + " x x x x ", + " x x xx x ", + " xx x x x ", + " x x o o x x x x ", + " xxxxxxx xxx xxx x x x x x x ", + " xx xx x x x x xxxxxx x x xxxxxxxxx x ", + " xx xx x o x x xx x x x x ", + " x x x x x x x x x x ", + " xxx x x x x x x x xxxxx xxxxxx x ", + " x x x x xx o xx x x x o x x x ", + "!!!!x x!!!!!!x x!!!!!!xx xx!!!!!!!!xx x!!!!!!!!!! x = x x x ", + "!!!!x x!!!!!!x x!!!!!xx xxxxxxxxxx x!!!!!!!xx! xxxxxxxxxxxxx xx o o xx ", + "!!!!x x!!!!!!x x!!!!!x o xx!!!!!!xx ! xx xx ", + "!!!!x x!!!!!!x x!!!!!x xx!!!!!!xx ! xxxxxxx ", + "!!!!x x!!!!!!x x!!!!!xx xxxxxxxxxxxxxx!!!!!!xx ! ", + "!!!!x x!!!!!!x x!!!!!!xxxxxxxxx!!!!!!!!!!!!!!!!!!xx ! ", + "!!!!x x!!!!!!x x!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!xx ! " + ], + [" ", + " ", + " ", + " @ ", + " ", + " o ", + " ", + " x ", + " x ", + " x ", + " x ", + " xxx ", + " x x !!! !!! xxx ", + " x x !x! !x! ", + " xxx xxx x x ", + " x x x oooo x xxx ", + " x x x x x!!!x ", + " x x xxxxxxxxxxxx xxx ", + " xx xx x x x ", + " x xxxxxxxxx xxxxxxxx x x ", + " x x x x!!!x ", + " x x x xxx ", + " xx xx x ", + " x x= = = = x xxx ", + " x x x x!!!x ", + " x x = = = =x o xxx xxx ", + " xx xx x x!!!x ", + " o o x x x x xxv xxx ", + " x x x x x!!!x ", + " xxx xxx xxx xxx o o x!!!!!!!!!!!!!!x vx ", + " x xxx x x xxx x x!!!!!!!!!!!!!!x ", + " x x xxxxxxxxxxxxxxxxxxxxxxx ", + " xx xx xxx ", + " xxx x x x x!!!x xxx ", + " x x x xxx x xxx x x ", + " x x xxx xxxxxxx xxxxx x ", + " x x x x x x ", + " x xx x x x x x ", + " x x |xxxx| |xxxx| xxx xxx x ", + " x xxx o o x x xxx x ", + " x xxxxx xx x xxx x!!!x x x ", + " x oxxxo x xxx x x x xxx xxx x ", + " x xxx xxxxxxxxxxxxx x oo x x oo x x oo xx xx xxx x ", + " x x x x!!x x!!!!x x!!!!x xx xx x x ", + " xxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ", + " ", + " " + ], + [" xxx x ", + " x ", + " xxxxx ", + " @ x ", + " x xxx ", + " o x x x ", + " o o oxxx x ", + " xxx x ", + " ! o ! xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx ", + " x x x x x x x x x x x x x x x ", + " x= o x x xxx x xxx x xxx x xxx x xxx x xxx x xxxxx ", + " x x x x x x x x x x x x x x x ", + " ! o ! o xxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxxxx ", + " ", + " o xxx xx ", + " ", + " ", + " xx ", + " xxx xxx ", + " ", + " o x x ", + " xx xx ", + " xxx xxx xxx x x ", + " ", + " || ", + " x xxxx ", + " x x o xxxxxxxxx o xxxxxxxxx o xx x ", + " x x x x x x x || x x ", + " x xxxxx o xxxxx o xxxxx ", + " xxxxxxx xxxxx xx xx xxx ", + " x= = =x x xxx ", + " xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx x!!!!!!!!!!!!!!!!!!!!!xxx!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", + " xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + " " + ] +]; + +if (typeof module != "undefined" && module.exports) + module.exports = GAME_LEVELS; \ No newline at end of file diff --git a/img/player.png b/img/player.png new file mode 100644 index 0000000000000000000000000000000000000000..ea5a9fad7211413475b6797f3f1fb986c4225940 GIT binary patch literal 8989 zcmbVy1zc0_+xJjfK?I~@Ae|dMxAt4}0HxkmR z#4~^O{-5W4>V41Wb9U}??(cP7->dHXvz1xSBQ1pweC*+UK84K>uoVF+hFOKXG`oX^|Y6@vx&uOkd%%`77gl=K$I zC~$_mTLQhEom^1j-crne=oQC|e?R7D2L1tYca&n5`)v?tsG$W^Kp^2jVLlOF7+6RE zC<@^Ni+~^kLOehL5LlESB+3sK!o7V;P@NW%@a1;z_@9J)kZ~^|- zXlaG;aF=4nF#Xd7XV-t!x}g3F6DDB%-j=TXU_Q`qm;L}+!~Vg!dLW(tFm4UwhdaTY z;V$kd3>N$k*3}N-jzHNV{tMOrO#eFrn9yoy{A1%k<>Ku8j|r5!k|)NEzXI}~qES#E zS2({89EI>e!r)4t7@7}%d*dpufP`DRBal!8!s+isY5h$ZsGtDkFtB&AMtGq({{;?L zv~-6{F$;hMK)fIkUa&9}1Q8boi3>ouL1N+{(4SBZgtfhm&;JDl|Hg?63;sJOreLfs z-7Wt|ur*BF27z?8#JFqkY-tPUcXhF42L2O7aRr1E0*Nt8kX^9oyw z+3><4LIS*2AWIuwQ6U%zEFdHxYz2e;Ew6}xdHgPbzvZnl@*-B&U{Q#`Zvh)?Ua*jm z7%#*Mj2Q_F2#8q;fW&OTe})TV{|V9_(}0#v|KEBrjq-m&@wXN^j7KO;hEadDk-Vkt zUn3`bX?Y=G0f>kwSV%++EGHPMn&bcPdHqrE;;Qz)dHVciOBe3; zk5%9wB`$6W``x-y%&^}z3%6$e>#_ZRlDGd|-2YVTWe3M7{co!IPZ$be&?T$v+dq{~vSp z$K1cHtbfBX%k}T!Kl=pc=AWGg?t)Q`#Oxz(CW|Tn03Aq8Q4Z?;dIIfZVggU!MQLVE zpT$aP;pE~cgg&E*P46ryp!WkiON`0r zmY9C;yg~s}Hz7@;3RVL~YGD!JbZN>x&>9ceLLcGLaYX5A>5&V1%?EJJBdDb0|m$GWJlXfF4=8JkU4VJ7XMsdgvX?OG6YUtN0Ac z*0tXjT@kpZO3^Y)n;U=;apqwnN__V~5hk)(JZZ;1M2z!8!HPaV7{BuT7d_FreleZx zbR$I<<>S14ye5w8Ww8|^o&B=pX9dK z5YL-KSgm8#+de{loudnT-O*njh^;f6;!S0gv5sot*A7eP?+&FznbD-Gn12ZC3%7yL z5%&b2TNRRjxp%Kn<#w(&X_16|*x}b5x}1_P@3uL{H&1_FE_LtpS(@ki0TzbXmiJ6k zQH`e zQ6n|!29Iz#OGOp^#G>``#)_*^oL<@}UGosLM<--j*^BEL_s}CA3J1a$FT1cTNnXzwPX$Sf^WRu-E(F*Cl`QA+<)wAVs#Ue zfAhoj9spD*(u&Dwd-AIneVa5P%AT?6%&9TH3hXXstcV)b#WON+@^ilDOE1V22_Ar> zgaR$u8j4M?cwxK&Cy$0-nOli$70l?{RrKff4;0s$KK&#@N$J~tGc@|8F{iQ6ZNX>8 zfdX3y|NGZdP5pucvZW{I?>>BcdoZW{mL<6XLac#dl zHc9gOqjJY1r58#Eb1x43b6AdxP6sI$7`> zJ!8=|Hys0KxN&H2=}&GCD-oYkeT}%@3%#KT?OQ(2!J2%UbNcwDsbb40^zGc?iBr8> zo+eknnXbH%{)WlfLEe00JpGP$Im^uXKvir;dLZO0qaL@?ySdDELLm9g%ca-Ia!v84idZ zmGT)nY7gkstQO5~S*dBS;chKbaYV5b7#P!C&&tB7pyG zLgOckH!5!9ze_0sRz54(u`mX{4hMV((0^8@R&Yu2<} zR9)8bvvCbE4Uy5{tDj1dlk7GW>Cezyu*ejm*QQ5lCRZx*=*dzX^Ph&8W9jJAbw(N7s*QWbNff)gHiiYPoqb$aq3NoGd1tgVCUqxxSP}m zM}qL0JgFx2(|XF^{XCT}YrE?~NW5=F=%bQN0cT@9EOO>B%3z0#etCKrVGwsMNTAH8 zRByLp#nV%*b^PVR$>+qB>L@lxD@Qp+lAq~Lq=K(~*_V$RoG)@@Mye$QAme8tL^f@= zazn}$YP%go=1HOh57T6xk_7%ygwQI6tO(BW0ut-P;7aGUN<9a z7_HLft9YOv<#(y5Kb{B9z5vHjaN^u)U5S@aX=W%=Cz9be|GG}saNaWe!p&u6`}3V; zJhesUyqxmq_C-8Km$N(xx4Ka}>{hw#LnenCHnuqkSMt``@CW)qpWV1XdC9xiW=kJUNh{^RaJFrbz?(kLLEplGF+qT`?$u@Bhp_!pxQUq18OG-tIcqdMzLF2pJzGj zqVBAEdhPdm5!|j9!30rfA@m%TLX@mj7VsRn* zIo$~@>;jUI>@gpH{*uoAX(yx-==UOpR(E+avn$K?wmK{5&TlKOa(HALXK1LAgfhMo1>us(=?{THb z1J7b9%iX5@*NR*?4Y)rov{)%`78ijt6Jy6ET`O)XHCE3%M|~iaMddleoS#Q+*w^=H(V#0ATADr`J&PQosDQa4^%_L)Y&p8)zL&hW$dN$8n_vop~-A2 zLSkP~mUS5fgT+{ke%E^2D0N@dVZAnIj_Fm-8hj9++RNwui?I8w*evaY%lR+-YGJ*+ z;+&ptd3^jrY)gLXp=+{_sht{XaCzK0mu4a)!~DfllWU1W^b6M4(1j-x1u0anSwHeQ zw{((8gaK}nwcQ0q`H385aUgPzsN2kR%%k{<%rvINctXa!gEoSa4ELzQ<(!>)Yn^Ap z>*js$>@lLVNVmUUh9*K}a4Q&9L9LrytF4O`t5yap{2cZprAD%CP_0$lQcfj(IcXhh zrhdETb*A`!NTqT4{KO{m&MaQ04TSk-1ZwfGQ%JO&$H><^b0B(4GKL9 zlP6)xrxM)XBNrr8dekgaDO|EBpBYr(k}G5vFA!7;)H~*7XUi$h(w7_NMa5~yWg6U; z77dq766%^tb3&ag_}tR;tkJ9+DAZcV=tKEr`FuOWzhCau=pLcX@HWXp+{;) zf~lT?MHro?)N_j^~(pi@R1WjSggS0NC@;1POc8; zv9EvUK%DR+c(zUS*zHC~vva_+{?Ec0$KpSl5(I$cc%*A2$2f0w3$wKg+p2gznem(O zQF27dV^YQ7y@(asVQFH&_cpoc(`Vy{FfX%BJA2yn;qjCVM-h8yQN;7m?9^a$`122! zV`rjmz&lIoJ?AFquw?{hVyrbn$F^{shFSs*;}wOLhN7mV@MLOnStsBlw5X)=AkFCP zgJx9I^rD}AB;KH2%jr5f2gTh z8cY+UVqgfD&u$$V0R><8^`*=QWw}ggd4E{lY6P%oEwXat4i=gJ(g7`g&`&NR<3MM+jlhVU`*Pnl$q*h0gcCMgqn~ki$+mq$V z7l>uldBngN={>v+^pj^zHBRixXAZE7;G;LY$=n{2zP?n6G~ubig}$S?v|VK*JC8(? zMGtS?*|l6v`;j^IGY(hsCAsB#oo}g#4-L4t z{seixWR}|AEgeeb!d*e$+_&ovECcdJiCX@7o_u#$+ZkC$WyZ|KX1L2fO3RF$lWX`y zDjx5_umV-p!D`iZ6f-@w@$P#%w9WNEX-ujmv2Koh!AWip40mTQZI>cm7J5B1N^uve zxzVp|=CWs$=YmjDC4^J=hTLV*QvUj~^g0Yy@k{izI>A7HN`vJbQ0H{-lF|cpu4a&2z`25v>(>+JPACu;PJg8-aT@@6w;E zigd~MEz(Zeaf^y1?2WDQ-3d@7M<)^eG^ZFKKjRH=adC!Fd6_@fl+!qx$JxK22~AGy z`C-eq%fVk4Jer$-E3H5Fv(0TnZmm9?x z659}TiC@b`qbq9Eqfj~5F)NSX#=O4RU-dUIg@tiS@MA@bEFIPhCd7YGNAK%-XZq#3 zS)%8OG<-BBQ*=u6r`6!^p+L4%VNryiF4O%9)m5yOZ!S;TXYZoF#f-&i3iWWv3e+2+ zj%r0QJRAixf2Cga5CH6;ge9YbQ|V2B>&PQJJ5?<6x6P;A9aDyOt)QGo-&c;v@_`S) zv=m2f-!}~a0Dj3U{5Nw)xDctUpA6MFb@K_afpfMI4Mle+iyID%ZUCU3pPtwIhMV5rwLfP&v2d4QIHy2frN{ z#S<5}cz*r!AYd&KtaSwp)$<6oDV=B2F%@`17NO*|Xg5 zTg=AV1rZ;T2J_F*j`(`slAFXgY+*cp^>F7fR*F8ZAgMh|HzWwG>f3PN=mq2f5GX5j zxsKvzES=1#J~$?t930SinR8F-4T&iztzh%3UD+!PSHV=j;)zE) zzn)mce+zzp>Spm;KE=|m;lrca5J^OP#oHi}r#X2--a?+V-} z>aEd+rTT|EZ$vA4oYIbdsyq={Udj{}JKe&M+bDGXrtnZ*4f8rjzKqxyj9%~)ybOePywV-;Rx*kYFE0pMsv1HR#Lf34EgbGsVP|PvqV*( zslA+I8ZN7e=$EcK<5Fxut>1CMq@PW|F8Ha|##Vdug~-lO;!@$Sx;+25DH=sN3mMN~ zwCw=bvEZW^b&CKZ&DSbp8|)7sjYSbfPb`MzGhzWG8#iLpX!h$X1L|&_qVioG2&~>@*Pd0O=@k2Mh>iilD0J_US&`|lAH`rQ$Fr8I!!$XX2{2k%o43g8@ z3A|-uDi{=aZ2b_hXnXU8*Y`N0E|i_dn4|vdc~R&)pe2VKuJPbGRtPn42^7rx<6O6e zd~^9}seaYm!%HI2e8BW%oWh<-E4<0D zh`hPD&ex4C!DA?EjPUgwre$fatvolFn$76Yjxx!K)Y|N1eC!HvJuW=*KJ7y+e%IYN zh}k1$xLV@1G*`F-7rdxdDg1;p+?Ey8lg5<2slPmo1+XxR@td7`jHUJDs%5TDD$Vl?4#{HmFpF2&%n*G^k=aZ+p zzr;?b-b1~&Cmj1y%ojow82!U~=wEv~+p9g=5Ia8RMp0)x6gy~XX@yBRYF*dutz@2= z$xv;&Tu;lcFpc&FVR@Sjk!9PRXQ>SD^@NucI_6pPNUPkp<)3%)TH$(ny}h|A&5$vS zxA6FJTj1*1Q>~U2048wkOXf0*2ltmcZ;&X0PIA|q7n;bi34N}V^NnK?o5=5&pkb+> zjC2e)uo!$h49x0>5HmV;(_=W)tpMnV@w)EF*Jw!WH4#UI+D~t$I{!~Mn`Ue$d(=_Ir@f*Ci*E@ zH2}at+Lc*HzLM>ge$fAveJC%uhqBgeFOB+{xB=y?coS%8kJ%2q z4A=pVXm&$ToV`wp0{Uuao4@5iZHqW4O*)p6MufEOV^8ctQvlx@-Ge9h5Ic?U#)OBB`xM8Q)HGsUc_zS~~q+X=8|&W8Mwm2g;x; zo=B&t#W?!j;1%`h>J^|`;qIgmxi^fJGrad6bjj+{7Kh=H@LUaB|!}MSx-cRaAFBIH_7R^QfYpao(J>ak8Ltp$7mC` z=w8^JiPHAUC0pCx#j#2TK3G99C3kKd!U_V6m&TB!wvIO@_vj7=>y5g(;TuN_Z)n0> z0v78YpHMbR?Z^7|Oip5ry(HM2HjBm)Fw}}?bKjk{E{iv>IqJ%+nDGy{1gxt^It>wiF6XFh^!ao^m{vwFJry&w(GrXC zbo~JUu(;KJr6MjUf~^}PBlT%4mH6RN{UY_GLk($KEsbBzGmoGCr|XlzG5wBo7H;+` z!@T%%xgCuZb)s+cAg$(I-*M3vks7JodPwIqG)~rf+M)Nugl>Ch@va-SZwMV|cDWMA?n`S%=8^*DCZ%Ie|YYH`3%S22uc0M1_pwf6alcK09yasVpb&9f`Rfyaz+3Vb ztGsnKPpVU6o}+LvvWpG>{ez?0@$>ByhF*)d*`-V!*mZg5q3xnfQdx^PmZ}PqZsoUX zLxMfW$|)SihXj=;7d?LW`*H0-Gv!-ab`NxAwcshFWK7B_(}au8(hDI{j3WHH|uxT&^4aM$|7I z!IP-hyLv@)J0e+c=EIc4Ret*UKo3G?TWdap7@4_ z)Z4z_cs-LITR-m0#4IKh4fSay`}o%)I?T~G^ERe5=El9)%2nrS-Ph$GFmmKLZ*U+5Lu!Sk^o_Z5E4Meg@_7P6crJiNL9pw)e1;Xm069{HJUZAPk55R%$-RIA z6-eL&AQ0xu!e<4=008gy@A0LT~suv4>S3ILP<0Bm`DLLvaF4FK%)Nj?Pt*r}7;7Xa9z9H|HZjR63e zC`Tj$K)V27Re@400>HumpsYY5E(E}?0f1SyGDiY{y#)Yvj#!WnKwtoXnL;eg03bL5 z07D)V%>y7z1E4U{zu>7~aD})?0RX_umCct+(lZpemCzb@^6=o|A>zVpu|i=NDG+7} zl4`aK{0#b-!z=TL9Wt0BGO&T{GJWpjryhdijfaIQ&2!o}p04JRKYg3k&Tf zVxhe-O!X z{f;To;xw^bEES6JSc$k$B2CA6xl)ltA<32E66t?3@gJ7`36pmX0IY^jz)rRYwaaY4 ze(nJRiw;=Qb^t(r^DT@T3y}a2XEZW-_W%Hszxj_qD**t_m!#tW0KDiJT&R>6OvVTR z07RgHDzHHZ48atvzz&?j9lXF70$~P3Knx_nJP<+#`N z#-MZ2bTkiLfR>_b(HgWKJ%F~Nr_oF3b#wrIijHG|(J>BYjM-sajE6;FiC7vY#};Gd zST$CUHDeuEH+B^pz@B062qXfFfD`NpUW5?BY=V%GM_5c)L#QR}BeW8_2v-S%gfYS= zB9o|3v?Y2H`NVi)In3rTB8+ej^> zQ=~r95NVuDChL%G$=>7$vVg20myx%S50Foi`^m%Pw-h?Xh~i8Mq9jtJloCocWk2Nv zrJpiFnV_ms&8eQ$2&#xWpIS+6pmtC%Q-`S&GF4Q#^mhymh7E(qNMa}%YZ-ePrx>>xFPTiH1=E+A$W$=bG8>s^ zm=Bn5Rah$aDtr}@$`X}2l~$F0mFKEdRdZE8)p@E5RI61Ft6o-prbbn>P~)iy)E2AN zsU20jsWz_8Qg>31P|s0cqrPALg8E|(vWA65poU1JRAaZs8I2(p#xiB`SVGovRs-uS zYnV-9TeA7=Om+qP8+I>yOjAR1s%ETak!GFdam@h^# z)@rS0t$wXH+Irf)+G6c;?H29p+V6F6oj{!|o%K3xI`?%6x;DB|x`n#ibhIR?(H}Q3Gzd138Ei2)WAMz7W9Vy`X}HnwgyEn!VS)>mv$8&{hQn>w4zwy3R}t;BYlZQm5)6pty=DfLrs+A-|>>;~;Q z_F?uV_HFjh9n2gO9o9Q^JA86v({H5aB!kjoO6 zc9$1ZZKsN-Zl8L~mE{`ly3)1N^`o1+o7}D0ZPeY&J;i;i`%NyJ8_8Y6J?}yE@b_5a zam?eLr<8@mESk|3$_SkmS{wQ>%qC18))9_|&j{ZT zes8AvOzF(F2#DZEY>2oYX&IRp`F#{ADl)1r>QS^)ba8a|EY_^#S^HO&t^Rgqwv=MZThqqEWH8 zxJo>d=ABlR_Bh=;eM9Tw|Ih34~oTE|= zX_mAr*D$vzw@+p(E0Yc6dFE}(8oqt`+R{gE3x4zjX+Sb3_cYE^= zgB=w+-tUy`ytONMS8KgRef4hA?t0j zufM;t32jm~jUGrkaOInTZ`zyfns>EuS}G30LFK_G-==(f<51|K&cocp&EJ`SxAh3? zNO>#LI=^+SEu(FqJ)ynt=!~PC9bO$rzPJB=?=j6w@a-(u02P7 zaQ)#(uUl{HW%tYNS3ItC^iAtK(eKlL`f9+{bJzISE?u8_z3;~C8@FyI-5j_jy7l;W z_U#vU3hqqYU3!mrul&B+{ptt$59)uk{;_4iZQ%G|z+lhASr6|H35TBkl>gI*;nGLU zN7W-nBaM%pA0HbH8olyl&XeJ%vZoWz%6?Y=dFykl=imL}`%BMQ{Mhgd`HRoLu6e2R za__6DuR6yg#~-}Tc|Gx_{H@O0eebyMy5GmWADJlpK>kqk(fVV@r_fLLKIeS?{4e)} z^ZO;zpECde03c&XQcVB=dL;k=fP(-4`Tqa_faw4Lbua(`>RI+y?e7jKeZ#YO-C z3w%jLK~#9!l-Ap?rS)CM@%QidoYs2Qde&+0z4mEl_RO?QXG+;PQ&K2IrGP}x3nvk2 zQ=6D-6HiUNX$;1w2_`khyBe;H#G6LFqiH-)3WA{JFqRgW8D?M(`@GJl=lnds-_Hxs z=p~=O;QPw=<}JSc`eXass}BVy-N$*a_X3}8pP}L0!Or|m-tNBwP7g=a=$Q{GGmG=h zi&Whv!miUW%Y5(VQ~cbCuTU*N$Md(JS=|1scQ=jEpb8wv#&RUa7^IXKz!*uECNgo> zpR(P~KMM5q*2)^4N{K5|5+e|R5R%c+E~iecU#ZoWKG?eJ-mjHb?KjVz{mj#+PoJ&| zA^6ejuV0Gd|G@ z&EVGb!yt`K;F((R_VD{bFWC#OkADzcD=r05xEy@{oxce>twx~I`G%-A5w#A@%{y=^ zHH@$^P7@;>Fo2L$mXB%U)bDW|d#w>v2uhgtf4*7w4`;m(uU3oE^TMh8k==0Wi@os2K}|-e(qk;&rX@d1 zSDt54UZE)G*hv=^x`h6Kzzz5ye2ysUvsq|7C*jTg_jqOJSyGiQj8bR;jpcQ`^)2>p zUPBwu+Ci!W+xEyrg+->eEJr&{+ZDjFKo6vp7zG&6#-O$0*M8&opMG(3u!_{g2L zdgFh&QcBiVS5axW_Es6Mt=@U>`;$d-)((|oH~I<3S~*8iO3lvc=@(e^6g#tjVt06n zPIeI?3#R2|X2Qofa~!M9batJBUAi@gm#+LB9sBdtn;o)Je=1JQx+%(6jS)hi3{nzj zHkL6B$Hp#|78qShWBY^t-UCIYe9jnY+@O7YXLmoylH{@wf`5DYr6=#X|Iu@{l=N?2 zMHdOevJl2l?QXHT^(nsk&2Mw{{rCJJHm)tBQ(WuaV7~bp6?Kfk-h%GwH%P_{;)Q`? zneJurwZNb9FbSw2#+ z_*bO_^TImeiE`bMGP8tOPg3(}F68l=2;v_E>Fg zv)nqx&HWD<52mzR1=X6NTv&+GCM#pIu!7DdB~xPUql@yjG|RkMA)ayTEmIPlLy47^ z@6l;BQ?10hl(NVRvA)RjyYnJHr;YVsyXofhWO~ME6Wg|plmf3Jv??>|BBR@B(P}oS zRBJ3Pb!dr%aPJEK(i$sw|AMIm%~)o|jApe724?da<=iG5)$yDvJ4e@<%nq51#_aDc z==Bu`dsTe9iXV)L=LtsDY1TI}d4)xo(+H|yIR`?=-y2L8_gT_;*tNMa$(>&eOZOZw z%8FuBYhm)T=oTh!WSRceI4hRM(Y&06@z1C-+fzzWt9G$1j7A`ImBpguxtHGp0k(3$ z*q9>20Iur-M#wl;SeC|h6j5mC^@ofP!4oS~ZH;5<+`fH{{%8+JEF%`jINH@L(gjB5 z9PY=o8^<`kdKZ)N6p|aZuMY4n^S5yvGfl+PsPcw3oSmh`cZ{%qPY42~m7ka88ukE-pI17KvpHbZ?m!7Cd*4KPZMi{v%G}>u3x`4c=OF4 zy(Q=46lD|?0;i}^v6l#wn5aCU(Wsyb*x7#{69p_)PqKToLmHRJw1X*KihRc5;T0$? zTzqEw#Dl_a68O#wLd937VR5fp2`-dnE{i;mQd9d=E#zYYni_9ADY&SFQI-f_(1N>l zsU0a@R|waaQu?>e*V~u=>!lZuVwEeS*;sGkxfYfb#KOjFt#Nn8+mtNi<=cV8Y3hQI#hC!YAh zh4bf;^?Fsfj*sOklu58%jc2v+>o%t}?r1tAjSMQ6SjMI_lKyZ^8X4xJgvn%tZ?*8sE`!O4B+D@v1Xbqq5zV?y-B~4# zgPu@>Tv7iY%!^kOZP%)8dpsH;3X9U! z)N5n1NO0r5l)4o%k2RJpXjhksqXGLz13IlWlr$I;jI9|=ZV`x2>cbESsMMr$MN6-sV(dT&+CAS8T2L;OKtSrLm3~>ZPIKO zsM2EhgFY*34emI8hQa8FTCfJ5L6uXoL=iH_wOwxY245Ax`r1ia?KXq{A;V!0ui~=S z?XtIf3#|=N6j2sAouySurO5M~C=6L$-JmQ|q;MKaiH};xtN-C^S8nZ2uTB1C>)7$H zEF0@rj&={8JDOd2=HkUCFMSNk0qp5)Ag}_gvdwa}O0801M2v5(aQf~xJ2wx|EO8^c zO;dTSdn?Sm9)(V^6Cdd~rp z%7|90i|s3{wF-yRK3QJFRZR|OeLAZRJm2T7*DkYis*Ss8C?*qp*(S zJfdDNi1U38d$&0_=+kU9X!#HxrUcF^i(!maNDL{JS_K_CsK}7!b42bUrN*`$szHq; z%@D%Ev0S1sB1tp4-8DeaY_@1NTTCV+M#Da~ZP92n$kG(0G{y+#^9io&GMmk*xb7zh zNnL^+A&i4b3z~sT9*4NXW0dbQob9vHK2DY>n%*+&-7d3HpKLluIu3;rc)Ce=2yO+c zj(RmMOKiuXR0aD7`v~dMZm+Piv`#H(ql$tkj&R&6s?-#!01F2FBjRX5p2ci#t`oF3 zKRKvw1>)f56w7L|dFlj}x?tElVDDNVw^#<1Qt?w(*Va&(B2*)`&NeVffiIg>N{MAE zEaHUuDB^+hr~kmVOca*|zF(u_c^n?@GMx@7%bZ%Z%Erb9$Bu7N + + + + + + + -- GitLab