PAU
make the squirrle have a gun
PAU
make the squirrel have a gun
BUDDY
Whoa, that app got too big for me to finish in one go! Try asking for something a little simpler, or build it up in smaller steps. 🙂
BUDDY
Whoa, that app got too big for me to finish in one go! Try asking for something a little simpler, or build it up in smaller steps. 🙂
CODE
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Squeaks</title> <style> body { margin: 0; background: #101820; display: flex; align-items: center; justify-content: center; height: 100vh; overflow: hidden; } /* This is what keeps the pixels sharp instead of blurry. */ canvas { image-rendering: pixelated; image-rendering: crisp-edges; } </style> </head> <body> <canvas id="screen"></canvas> <!-- These are all the pieces of the game. The order matters! Each file can only use things that were loaded before it. tune.js is first because everything else uses its numbers. --> <script> // ============================================================ // TUNE.JS — every number in the game lives here. // This is your file, Tree. Change a number, save, refresh // the game, and see what happens. You cannot break anything // that a quick undo won't fix. Go wild. // ============================================================ // The version number, shown in the corner of the screen. // Bump it whenever you change something. var VERSION = 'v1.5.0'; // --- The screen --------------------------------------------- // The game is drawn tiny and then blown up big, which is what // makes the pixels look chunky. 320 x 180 is the tiny size. var SCREEN_WIDTH = 320; var SCREEN_HEIGHT = 180; // The color of the sky. var SKY_COLOR = '#8fd3f4'; // The sky isn't one flat color anymore — it fades from a deeper blue // up high (SKY_TOP_COLOR) down to a paler blue near the ground // (SKY_BOTTOM_COLOR). Change either and the whole sky changes. var SKY_TOP_COLOR = '#5bb0e8'; var SKY_BOTTOM_COLOR = '#bfe8fb'; // The pale blue trees far, far behind the forest — just scenery you // can't touch. Make this closer to the sky color and they almost // disappear; make it darker and they feel closer. var FAR_TREE_COLOR = '#7fbfe0'; // The grassy top of the ground. The dirt beneath it is the 'd' color // in the sprite palette (js/sprites.js). var GRASS_COLOR = '#4a7c2f'; // --- The world ---------------------------------------------- // The world is 640 across and 1080 tall. That's six screens // stacked up. The top is y = 0 and the bottom is y = 1080, // which feels backwards but that's how computers do it. var WORLD_WIDTH = 4000; var WORLD_HEIGHT = 2760; // How high up the ground is. var GROUND_Y = 2720; // Unused now — acorns, birds and the door are placed from the tree // math (treeX/nthBranchOfTree) instead of one hardcoded "big tree". // Left here in case something still wants a nominal starting-tree x. var BIG_TREE_X = 60; var TRUNK_WIDTH = 28; // --- The forest --------------------------------------------- // The world is a big wide-and-tall forest you wind your way up. // Change these and the whole forest changes shape. var TREE_COUNT = 30; // how many trees. Try 50 for an even bigger forest! var FIRST_TREE_X = 120; // where the first (leftmost) tree stands var TREE_SPACING = 130; // how far apart the trees are, left to right var BRANCH_STEP = 110; // how far apart a tree's branches are, top to bottom var BRANCH_LENGTH = 40; // how long each branch sticks out var FIRST_TREE_TOP = 2220; // the top (highest point) of the first, shortest tree var TREE_TOP_RISE = 71; // each tree's top is this many pixels higher than the last var GOAL_TREE_TOP = 150; // the tallest trees stop growing at this height // The goal tree is the last, tallest one — it holds the owl and the door home. var GOAL_TREE_X = FIRST_TREE_X + (TREE_COUNT - 1) * TREE_SPACING; // How high the goal tree actually reaches (same formula the trees use). var GOAL_TREE_TOP_ACTUAL = Math.max(GOAL_TREE_TOP, FIRST_TREE_TOP - (TREE_COUNT - 1) * TREE_TOP_RISE); // The wide branch at the top of the goal tree where you fight the owl. var OWL_ARENA_TOP = GOAL_TREE_TOP_ACTUAL + 100; // How thick branches are. Fatter branches are easier to land on. var BRANCH_THICKNESS = 6; // --- Squeaks ------------------------------------------------ // These are the numbers that decide how the game FEELS. // This is the most fun part of the file. Try them all. // How hard gravity pulls him down. Bigger = heavier. var GRAVITY = 0.25; // How fast he runs. Try 3 for a very speedy squirrel. var RUN_SPEED = 1.2; // The fastest he can ever fall, so he doesn't turn into a bullet. var MAX_FALL_SPEED = 5.0; // How many acorns of health he starts with. var START_HEALTH = 3; // How many acorns he starts with to throw. var START_ACORNS = 3; // Where he starts: on the ground, near the porcupine. var SQUIRREL_START_X = 200; // How fast he climbs up a trunk. var CLIMB_SPEED = 0.8; // --- Jumping and gliding ------------------------------------ // This is the heart of the game. Squeaks is a FLYING squirrel, // but he can't actually fly — he glides. That means he is // always going down. You climb up to get height, and then you // spend that height gliding across to the next tree. // // That one rule is what makes the game a game. If you make // GLIDE_FALL_SPEED a negative number he'd fly upward forever // and there'd be no reason to climb anything. (Try it once. // It's funny. Then put it back.) // How high he jumps. Bigger = higher. var JUMP_POWER = 4.0; // How hard the rabbit's DOUBLE JUMP boosts you the second time, in // mid-air. Same strength as a normal jump (see JUMP_POWER). var DOUBLE_JUMP_POWER = 4.0; // How fast he sinks while gliding. Smaller = floatier. // Try 0.1 for a feather. Try 2 for a rock with a cape. var GLIDE_FALL_SPEED = 0.4; // How fast he steers left and right while gliding. // This is faster than running, because gliding should feel // swoopy and free. var GLIDE_STEER_SPEED = 1.0; // How fast he drops when you hold DOWN to dive. var DIVE_FALL_SPEED = 3.0; // --- Acorns ------------------------------------------------- // Acorns are your ammo. You throw them at things. // (Your health is ALSO drawn as acorns, but that's a separate // pile — throwing acorns never costs you health.) // How fast a thrown acorn flies forwards. var THROW_SPEED = 3.0; // How much a thrown acorn is tossed upward at the start. // This is what makes it arc instead of going in a straight line. var THROW_LIFT = 1.2; // How hard gravity pulls on a flying acorn. var ACORN_GRAVITY = 0.12; // Nudges a dropped acorn sideways so it falls from the middle of // Squeaks rather than his left edge. He's 8 wide and an acorn is // 5 wide, so the middle is 1.5 — we use 1 because whole pixels // look tidier. var ACORN_DROP_OFFSET_X = 1; // --- Birds -------------------------------------------------- // Birds fly back and forth. If you get close, they dive at you. // Getting hit costs a health acorn AND knocks you out of your // glide, which means you lose all the height you climbed for. // That's why birds are scary even when they miss. // How fast birds patrol back and forth. var BIRD_SPEED = 0.7; // How fast they dive at you. Bigger = scarier. var BIRD_SWOOP_SPEED = 2.2; // How close you have to be before a bird notices you. // Make this smaller if the game is too hard. var BIRD_SEE_DISTANCE = 70; // How far from its own home a swooping bird will chase you — in ANY // direction, not just downward — before giving up and flying back. // Too small and birds are barely a threat; too big and they'll // chase you almost anywhere, including all the way up a tree. var BIRD_GIVE_UP_DISTANCE = 120; // How long you blink for after getting hit, in thinks. // 60 thinks = 1 second. You can't be hit again while blinking. var HURT_BLINK_TIME = 90; // --- Cats --------------------------------------------------- // Cats prowl the forest floor. The porcupine wants them gone. // You can't fight them on the ground — you have to glide over // and bomb them. That's what teaches you to glide. var CAT_SPEED = 0.4; // --- The porcupine ------------------------------------------ // He stands at the bottom of the big tree and gives you the // cats quest. Later he gives you one of his spikes to use as // a sword against the owl. var PORCUPINE_X = 210; // How close you have to stand to talk to him. var TALK_DISTANCE = 14; // How long his messages stay on screen, in thinks (60 = 1 sec). var MESSAGE_TIME = 240; // --- Doors -------------------------------------------------- // Doors in the trunks. Going in one: // 1. fills your health back up // 2. saves your spot, so dying isn't a disaster // 3. pops you out higher up — which is how you skip past a // sky full of birds when it gets too dangerous out there var DOOR_WIDTH = 4; var DOOR_HEIGHT = 5; // --- Rooms (inside the trees) ------------------------------- // Each door opens into a little room. A room is SMALLER than the // screen on purpose: the camera then just centers it, which is why // a cozy room feels cozy. Make them bigger and they'd start to // scroll like the forest does. var ROOM_WIDTH = 240; var ROOM_HEIGHT = 150; // How thick the room's wooden floor is. var ROOM_FLOOR_THICKNESS = 10; // The most hearts you can ever have. You start with 3 and each of // the 3 bedrooms gives one more, which is exactly 6. The heart row // has to fit across the top of the screen, so don't crank this up. var MAX_HEARTS = 6; // The dark frame around a cozy room (the room is smaller than the // screen, so this shows in the margins on the sides and top/bottom). var ROOM_SURROUND_COLOR = '#241812'; // --- The spike (your close-range weapon) -------------------- // The porcupine gives you a spike after the cats quest. Press C // to jab: anything right in front of you takes a hit (a bird or // cat is gone; the owl loses one of his six). // How far the spike pokes out in front of you, in pixels. var STAB_REACH = 6; // How long the spike stays out after a jab, in thinks (60 = 1 // second). You can't jab again until it tucks back in — so this // is also the cooldown. The hit only counts on the FIRST think. var STAB_SHOW_TIME = 8; // --- The owl (the boss!) ------------------------------------ // He sleeps on the top branch until you get close. Then he // flies back and forth and dives at you. Hit him with acorns. // // In round 2 he gets more attacks and you get the spike sword. // How many acorns it takes to beat him. var OWL_HEALTH = 6; // How fast he flies back and forth. var OWL_SPEED = 1.0; // How fast he dives at you. He's much scarier than a bird. var OWL_DIVE_SPEED = 2.6; // How close you have to get before he wakes up. var OWL_WAKE_DISTANCE = 60; // How long he flies around between dives, in thinks. var OWL_WAIT_BETWEEN_DIVES = 100; // Where the boss fight happens — the owl now lives at the top of // the goal tree (the last, tallest one). // Everything here is worked out FROM the arena branch, never guessed — // move the goal tree and these follow it. var OWL_START_X = GOAL_TREE_X + 10; var OWL_START_Y = OWL_ARENA_TOP - 80; // he flies 80px above the arena var OWL_ARENA_LEFT = GOAL_TREE_X - 30; // the arena branch's left edge var OWL_ARENA_RIGHT = GOAL_TREE_X + 70; // its right edge (branch is 100 wide) var OWL_FIGHT_START_X = GOAL_TREE_X - 20; // you stand on the left of the arena branch var OWL_FIGHT_START_Y = OWL_ARENA_TOP - 7; // your 7-tall body, feet on the branch top // How far below his flying height he's allowed to dive before he // gives up and flies back up, even if he never quite reached you. var OWL_DIVE_DEPTH = 90; // How close counts as "arrived" when he's diving at you. Once // he's this close he pulls back up instead of trying to land // exactly on top of you. var OWL_DIVE_ARRIVE_DISTANCE = 4; // --- The nest (home!) --------------------------------------- // Beat the owl, then land here. That's the whole game. // // The nest-that-becomes-a-door sits just past the goal trunk, close // enough above the arena that a jump reaches it. var NEST_X = GOAL_TREE_X + TRUNK_WIDTH + 4; var NEST_Y = OWL_ARENA_TOP - 30; // --- The HUD (the stuff drawn on top of the game) ----------- // Your health, your acorns, the version number, and messages, // all stuck to the corners of the screen. // How far the health acorns and the "xN" acorn count sit from // the top-left corner of the screen. var HUD_MARGIN = 4; // How far apart each health acorn is drawn, so they line up in // a neat row instead of overlapping. var HEALTH_ICON_SPACING = 7; // Where the "xN" acorn count text sits, and where the little // acorn icon next to it sits. var ACORN_COUNT_TEXT_Y = 14; var ACORN_COUNT_ICON_X = 20; var ACORN_COUNT_ICON_Y = 12; // Where the "Friends N/5" team counter sits (top-left, just below // the acorn count). var FRIENDS_TEXT_Y = 24; // How far the version number sits from the bottom-right corner. var VERSION_MARGIN_RIGHT = 34; var VERSION_MARGIN_BOTTOM = 8; // Where the porcupine/owl message banner sits, above the bottom // edge of the screen. // // The porcupine has a LOT to say, and the screen is only 320 // pixels wide, so his messages get chopped onto more than one // line (see splitIntoLines in hud.js). These numbers control that: // MESSAGE_MARGIN — gap kept clear on the left AND right // edges, so no line is allowed to touch // the sides of the screen. // MESSAGE_BOTTOM — how far the LAST line sits above the // bottom edge (same job the old // MESSAGE_MARGIN_BOTTOM did for the one // line we used to draw). // MESSAGE_LINE_HEIGHT — how tall one line is, so extra lines // stack neatly instead of overlapping. // MESSAGE_MOST_LINES — the most lines we ever expect a message // to need. Tested against in tests.js, so // that if someone writes a message so long // it needs a 4th line, the test catches it // instead of the player finding it. var MESSAGE_MARGIN = 6; var MESSAGE_BOTTOM = 20; var MESSAGE_LINE_HEIGHT = 9; var MESSAGE_MOST_LINES = 3; // Where "YOU MADE IT HOME!" is written when you win. Roughly the // middle of the 320x180 screen. var WIN_TEXT_X = 100; var WIN_TEXT_Y = 80; // --- The five helpers (your team) --------------------------- // Four friends scattered through the world (plus the porcupine). // Each gives a little task, then a gift when you finish it. // How many acorns the rabbit wants you to bring him. var HELPER_FETCH_COST = 3; // The acorn gifts for joining. (The roly-poly's gift is the curl // shield, not acorns, so his acorn gift is 0.) var RABBIT_GIFT = 6; var MOUSE_GIFT = 4; var FROG_GIFT = 4; var ROLYPOLY_GIFT = 0; </script> <script> // ============================================================ // SPRITES.JS — the pictures. // Every picture is just letters. Each letter is a color from // the PALETTE below. A dot means "see-through". // Want to redraw Squeaks? Go ahead. Change the letters. // Just keep every row the same length — the tests check! // ============================================================ var PALETTE = { 'k': '#1a1410', // k = almost black 'b': '#4a2f1a', // b = dark brown 'B': '#8b5a2b', // B = brown 'L': '#c08a4a', // L = light brown 'W': '#f4efe4', // W = white 'g': '#2d5016', // g = dark green 'G': '#4a7c2f', // G = green 'y': '#e8c547', // y = yellow 'r': '#c1443c', // r = red 'e': '#5c5751', // e = grey 'E': '#9a938a', // E = light grey 'o': '#d98b3a', // o = orange 'n': '#2b1a0e', // n = darkest bark / deep-brown shadow 'H': '#e8c99a', // H = warm sunny highlight 't': '#20430f', // t = leaf shadow (deepest green) 'm': '#3f6b22', // m = mid leaf green 'M': '#79b23e', // M = bright leaf green (sun on leaves) 'D': '#3a3733', // D = deep grey shadow 'd': '#6b4a2a' // d = dirt brown }; // Squeaks! Facing right. Dark outline, sunny back, an eye, a curly tail. var SQUIRREL_SPRITE = [ '.kk...k.', 'kHHk.kBk', 'kHBBBBWk', 'kHBBBBBk', '.kBbbBk.', '..kbbk..', '..b..b..' ]; var ACORN_SPRITE = [ '.kkk.', 'kLHLk', 'kBHBk', '.kBk.', '..k..' ]; // A golden acorn — a checkpoint. It glows when you've reached it. var GOLD_ACORN = [ '.kkk.', 'kyoyk', 'koyok', '.kok.', '..k..' ]; // A grey bird, wings out, facing right. White eye, orange beak. var BIRD_SPRITE = [ '.k...k..', 'kEk.kEk.', '.EDDDDDk', '.kDDWkko', '..kkk...' ]; // A prowling cat with yellow eyes, facing right. var CAT_SPRITE = [ '.k...k..', 'kek.kek.', '.keyeyek', 'keeeeeeD', '.k....k.' ]; // A porcupine bristling with quills. Eye on the right. var PORCUPINE_SPRITE = [ '.k.k.k.k..', 'kEkEkEkEk.', 'kBBBBBBBkk', '.BBBBBBWkk', '.b.b..b.b.' ]; // The owl. Bigger, because he is the boss. Big eyes, orange beak. var OWL_SPRITE = [ '.kEE..EEk.', 'kEEEEEEEEk', 'kEyWEEWyEk', 'kEEEooEEEk', 'kEDEEEEDEk', '.kEEEEEEk.', '..kE..Ek..' ]; // A little wooden door with a knob. (The BIG tree-door is a later batch.) var DOOR_SPRITE = [ 'kkkk', 'kBLk', 'kBLk', 'kByk', 'kkkk' ]; var NEST_SPRITE = [ '.kbbbbk.', 'kBLBLBLk', 'kLBLBLBk', '.knbbnk.' ]; // A rounded clump of leaves. These grow on the branches and crown // the trees. Redraw it like any other sprite — just keep it 9 wide. var LEAF_CLUMP = [ '...kkk...', '..kMMMk..', '.kMMmMMk.', 'kMmMMMmMk', '.kmMmMmk.', '..ktmtk..', '...kkk...' ]; // The four new friends, and Squeaks curled into a ball. var RABBIT_SPRITE = [ '.kk..kk.', '.kEk.kEk', '.kEEEEk.', 'kEEEEEEk', 'kEWWWWEk', '.kk..kk.' ]; var MOUSE_SPRITE = [ '.kk...k.', 'kEEk.kE.', 'kEEEEEkk', 'kEEEEEk.', '.kkkkk..' ]; var FROG_SPRITE = [ 'kWk..kWk', 'kGGGGGGk', 'GGGGGGGG', 'kGGGGGGk', 'k.kkkk.k' ]; var ROLYPOLY_SPRITE = [ '.kkkkk..', 'kDeDeDk.', 'keDeDeDk', 'kDeDeDk.', '.kk..k..' ]; var SQUIRREL_CURLED = [ '..kkkk..', '.kBBBBk.', 'kBBBBBBk', 'kBBbbBBk', 'kBBBBBBk', '.kBBBBk.', '..kkkk..' ]; // A full heart — one point of health. var HEART_SPRITE = [ '.r.r.', 'rrrrr', 'rrrrr', '.rrr.', '..r..' ]; // A faint empty heart — a slot you've earned (from a bed) but lost. var HEART_EMPTY_SPRITE = [ '.e.e.', 'e.e.e', 'e...e', '.e.e.', '..e..' ]; // A cozy bed: white pillow on the left, a warm blanket, wooden legs. var BED_SPRITE = [ 'WW.......', 'WWrrrrrrr', 'bbbbbbbbb', 'b.......b' ]; // Every sprite in the game, all in one list. // The tests use this to check every single sprite — that each row // is the same length, and that every letter is a real color. // // If you draw a NEW sprite, add it to this list too. Then the tests // will look after it for you, just like the others. var ALL_SPRITES = [ { name: 'squirrel', grid: SQUIRREL_SPRITE }, { name: 'acorn', grid: ACORN_SPRITE }, { name: 'bird', grid: BIRD_SPRITE }, { name: 'cat', grid: CAT_SPRITE }, { name: 'porcupine', grid: PORCUPINE_SPRITE }, { name: 'owl', grid: OWL_SPRITE }, { name: 'door', grid: DOOR_SPRITE }, { name: 'nest', grid: NEST_SPRITE }, { name: 'leaf', grid: LEAF_CLUMP }, { name: 'gold_acorn', grid: GOLD_ACORN }, { name: 'heart', grid: HEART_SPRITE }, { name: 'heart_empty', grid: HEART_EMPTY_SPRITE } ,{ name: 'bed', grid: BED_SPRITE } ,{ name: 'rabbit', grid: RABBIT_SPRITE } ,{ name: 'mouse', grid: MOUSE_SPRITE } ,{ name: 'frog', grid: FROG_SPRITE } ,{ name: 'rolypoly', grid: ROLYPOLY_SPRITE } ,{ name: 'squirrel_curled', grid: SQUIRREL_CURLED } ]; function spriteWidth(grid) { return grid[0].length; } function spriteHeight(grid) { return grid.length; } </script> <script> // ============================================================ // DRAW.JS — puts pixels on the screen. // This is one of only two files that talk to the browser. // ============================================================ var canvas; var ctx; var screenScale = 1; // The camera is where the screen is looking in the big world. // When Squeaks climbs, the camera follows him up. var cameraX = 0; var cameraY = 0; function setupScreen() { canvas = document.getElementById('screen'); canvas.width = SCREEN_WIDTH; canvas.height = SCREEN_HEIGHT; ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = false; resizeScreen(); window.addEventListener('resize', resizeScreen); } // We only ever blow the picture up by a whole number (2x, 3x, 4x). // If we used 2.5x, some pixels would be fat and some would be thin // and the whole thing would look mushy. function resizeScreen() { var scaleX = Math.floor(window.innerWidth / SCREEN_WIDTH); var scaleY = Math.floor(window.innerHeight / SCREEN_HEIGHT); screenScale = Math.min(scaleX, scaleY); if (screenScale < 1) { screenScale = 1; } canvas.style.width = (SCREEN_WIDTH * screenScale) + 'px'; canvas.style.height = (SCREEN_HEIGHT * screenScale) + 'px'; } function clearScreen(color) { ctx.fillStyle = color; ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); } // Paints the sky as a top-to-bottom fade, then a few faraway tree // shapes for depth. Called at the very start of every frame. function drawSky() { var gradient = ctx.createLinearGradient(0, 0, 0, SCREEN_HEIGHT); gradient.addColorStop(0, SKY_TOP_COLOR); gradient.addColorStop(1, SKY_BOTTOM_COLOR); ctx.fillStyle = gradient; ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); drawFarTrees(); } // Simple darker-blue tree shapes far behind the real forest. They // don't move and you can't touch them — they're only scenery. function drawFarTrees() { ctx.fillStyle = FAR_TREE_COLOR; drawFarTree(40, 120); drawFarTree(150, 135); drawFarTree(250, 115); } function drawFarTree(baseX, baseY) { ctx.fillRect(baseX - 2, baseY, 4, SCREEN_HEIGHT - baseY); ctx.beginPath(); ctx.arc(baseX, baseY, 18, 0, Math.PI * 2); ctx.fill(); } // Draws a box at a spot in the world (not on the screen). // The camera math is what turns world spots into screen spots. function drawBox(x, y, width, height, color) { ctx.fillStyle = color; ctx.fillRect(Math.round(x - cameraX), Math.round(y - cameraY), width, height); } // Is a world-space box anywhere on the screen right now? Used to skip // drawing the far-away parts of a big forest, so 30 trees stay smooth. function onScreen(x, y, width, height) { return x + width >= cameraX && x <= cameraX + SCREEN_WIDTH && y + height >= cameraY && y <= cameraY + SCREEN_HEIGHT; } // Turns a grid of letters into a real picture the browser can draw. // We only do this once per sprite when the game starts, because // doing it every frame would be slow. function makeSprite(grid) { var width = grid[0].length; var height = grid.length; var made = document.createElement('canvas'); made.width = width; made.height = height; var g = made.getContext('2d'); var x; var y; for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { var letter = grid[y].charAt(x); var color = PALETTE[letter]; if (color) { g.fillStyle = color; g.fillRect(x, y, 1, 1); } } } return made; } // Draws a sprite at a spot in the world. // Set flip to true to make it face the other way. function drawSprite(sprite, x, y, flip) { var screenX = Math.round(x - cameraX); var screenY = Math.round(y - cameraY); if (flip) { ctx.save(); ctx.translate(screenX + sprite.width, screenY); ctx.scale(-1, 1); ctx.drawImage(sprite, 0, 0); ctx.restore(); } else { ctx.drawImage(sprite, screenX, screenY); } } // Point the camera at something, but never look past the edge // of the world — nobody wants to see the empty grey nothing. function moveCameraTo(targetX, targetY, worldWidth, worldHeight) { cameraX = keepCameraInside(targetX - SCREEN_WIDTH / 2, worldWidth, SCREEN_WIDTH); cameraY = keepCameraInside(targetY - SCREEN_HEIGHT / 2, worldHeight, SCREEN_HEIGHT); } // Keeps the camera inside the world, in one direction. // We do the same job sideways and up-and-down, so it's written // once and used twice. function keepCameraInside(where, worldSize, screenSize) { // If the world is SMALLER than the screen — like a little room // inside a tree — there's nothing to scroll to. So we just put // the room in the middle of the screen and leave it there. if (worldSize < screenSize) { return -(screenSize - worldSize) / 2; } if (where < 0) { return 0; } if (where > worldSize - screenSize) { return worldSize - screenSize; } return where; } // Fills the current room with warm wood walls and a darker floor // strip. The camera has already been pointed at the room, so we draw // straight onto the room's own space (0,0 is the room's top-left). // Plank lines give it a little cabin feel. function drawRoomBackground(room) { clearScreen(ROOM_SURROUND_COLOR); // dark frame in the margins around the centered room drawBox(0, 0, room.width, room.height, PALETTE['B']); // warm wood wall var y; for (y = 8; y < room.height; y = y + 12) { drawBox(0, y, room.width, 1, PALETTE['b']); // horizontal planks } var x; for (x = 0; x < room.width; x = x + 40) { drawBox(x, 0, 1, room.height, PALETTE['b']); // plank seams } // A darker floor strip along the bottom, with a sunny top edge. drawBox(0, room.height - ROOM_FLOOR_THICKNESS, room.width, ROOM_FLOOR_THICKNESS, PALETTE['b']); drawBox(0, room.height - ROOM_FLOOR_THICKNESS, room.width, 2, PALETTE['L']); } </script> <script> // ============================================================ // INPUT.JS — the keyboard. // This is the other file that talks to the browser. // // Want different keys? Change KEY_NAMES below. // ============================================================ // Which real keyboard key means which game action. // The left side is what the browser calls the key. var KEY_NAMES = { 'ArrowLeft': 'left', 'ArrowRight': 'right', 'ArrowUp': 'up', 'ArrowDown': 'down', 'a': 'left', 'd': 'right', 'w': 'up', 's': 'down', ' ': 'jump', 'z': 'throw', 'x': 'drop', 'c': 'stab', 'v': 'curl' }; // Keys being held down right now. var keysDown = {}; // Keys that were pushed down since the last time we looked. // This is the difference between "holding" and "tapping". var keysPressed = {}; // Turns a real keyboard key into a game action, like 'left'. // // Arrow keys are spelled out ('ArrowLeft'), but letter keys are // just the letter ('a'). The catch: if CapsLock is on, or you're // holding shift, the browser says 'A' instead of 'a'. So we look // for the key as-is first, and then try it in lowercase. // // Without this, WASD would mysteriously stop working whenever // CapsLock was on, and nothing would tell you why. function actionForKey(key) { if (KEY_NAMES[key]) { return KEY_NAMES[key]; } return KEY_NAMES[key.toLowerCase()]; } function setupInput() { document.addEventListener('keydown', function (event) { var name = actionForKey(event.key); if (name) { if (!keysDown[name]) { keysPressed[name] = true; } keysDown[name] = true; event.preventDefault(); } }); document.addEventListener('keyup', function (event) { var name = actionForKey(event.key); if (name) { keysDown[name] = false; event.preventDefault(); } }); } // Is the player holding this key right now? // Use this for running and gliding. function isKeyDown(name) { return keysDown[name] === true; } // Did the player just tap this key? // Use this for throwing, so one tap throws one acorn. function wasKeyPressed(name) { return keysPressed[name] === true; } // Called at the end of every think, to forget the taps. function clearPressed() { keysPressed = {}; } </script> <script> // ============================================================ // COLLIDE.JS — figuring out when things bump into things. // In Scratch you would say "if touching Bird". This is that, // and it is called the same thing. // ============================================================ function makeBox(x, y, width, height) { var box = {}; box.x = x; box.y = y; box.width = width; box.height = height; return box; } // Are these two things overlapping? // Picture two rectangles. They are NOT touching if one is // completely to the left of the other, or completely above it. // If none of those are true, they must be overlapping. function isTouching(a, b) { if (a.x + a.width <= b.x) { return false; } if (b.x + b.width <= a.x) { return false; } if (a.y + a.height <= b.y) { return false; } if (b.y + b.height <= a.y) { return false; } return true; } // How far apart are two things, in a straight line? // This is the triangle rule you might know as // a-squared plus b-squared equals c-squared. // Birds use it to spot you, the porcupine uses it to know // you're close enough to talk to, and the owl uses it to wake up. function howFarApart(a, b) { var acrossX = a.x - b.x; var acrossY = a.y - b.y; return Math.sqrt(acrossX * acrossX + acrossY * acrossY); } // Move a thing straight towards another thing. // Birds use this to swoop at you, and the owl uses it to dive. // It also turns them to face the way they're going. // It hands back how far away the target was, because the owl // wants to know when he's arrived. function moveToward(thing, target, speed) { var towardX = target.x - thing.x; var towardY = target.y - thing.y; var distance = howFarApart(thing, target); // If we're already exactly on top of it, don't move — dividing // by zero makes the computer produce nonsense. if (distance > 0) { thing.x = thing.x + (towardX / distance) * speed; thing.y = thing.y + (towardY / distance) * speed; thing.facingRight = towardX > 0; } return distance; } </script> <script> // ============================================================ // WORLD.JS — the forest. // Trunks are the tall bits you climb. Branches are the flat // bits you stand on. Together they are the "solids" — the // stuff Squeaks can't walk through. // // Want to add a tree? Add a trunk and some branches below. // ============================================================ var trunks = []; var branches = []; var leafSprite; // How big the current "world" is. Normally the whole forest — but // when you step inside a tree it becomes that little room's size, so // the camera and the keep-inside-the-world fence both shrink to fit. // The door code swaps these; setupWorld puts them back every new game. var worldW = WORLD_WIDTH; var worldH = WORLD_HEIGHT; function setupWorld() { worldW = WORLD_WIDTH; worldH = WORLD_HEIGHT; trunks = []; branches = []; var i; for (i = 0; i < TREE_COUNT; i++) { addTree(i); } // The goal tree (last, tallest) gets a wide arena branch at its top // for the owl fight. addBranch(GOAL_TREE_X - 30, OWL_ARENA_TOP, 100); // The ground is one very wide, very flat branch. addBranch(0, GROUND_Y, WORLD_WIDTH); } // Where tree number i stands, left to right. function treeX(i) { return FIRST_TREE_X + i * TREE_SPACING; } // The top (highest point) of tree number i. Trees get taller toward // the goal, but never grow past GOAL_TREE_TOP. function treeTopY(i) { var top = FIRST_TREE_TOP - i * TREE_TOP_RISE; if (top < GOAL_TREE_TOP) { top = GOAL_TREE_TOP; } return top; } // The j-th branch of tree i (j = 0 is the highest one). Branches // alternate left, right, left, right down the trunk. Returns a plain // {x, y, width}, or null once the branches would reach the ground. function nthBranchOfTree(i, j) { var y = treeTopY(i) + (j + 1) * BRANCH_STEP; if (y > GROUND_Y - 30) { return null; } var x; if (j % 2 === 0) { x = treeX(i) - BRANCH_LENGTH; // left side } else { x = treeX(i) + TRUNK_WIDTH; // right side } return { x: x, y: y, width: BRANCH_LENGTH }; } // Build one whole tree: a trunk from its top down to the ground, plus // its ladder of branches. function addTree(i) { var topY = treeTopY(i); addTrunk(treeX(i), topY, TRUNK_WIDTH, GROUND_Y - topY); var j = 0; var b = nthBranchOfTree(i, j); while (b) { addBranch(b.x, b.y, b.width); j = j + 1; b = nthBranchOfTree(i, j); } } function addTrunk(x, y, width, height) { trunks.push(makeBox(x, y, width, height)); } // Branches are always 6 pixels thick. That number is in tune.js // if you want fatter ones. function addBranch(x, y, width) { branches.push(makeBox(x, y, width, BRANCH_THICKNESS)); } // Is this thing standing on something? // // We don't just look at its feet — we look at the whole space it // moved through since the last think. Here's why: if something is // falling really fast, it can jump right over a thin branch in one // go. Looking only at its feet afterwards would miss the branch // completely and it would drop through the tree like it wasn't there. // // Because we check the whole path, landing works at ANY speed. // Go on — set MAX_FALL_SPEED to 50 in tune.js. It still works. function findSolidUnder(thing, howFarItFell) { // Standing still (or we weren't told) — just check under the feet. if (!howFarItFell || howFarItFell < 2) { howFarItFell = 2; } var feetNow = thing.y + thing.height; var path = makeBox(thing.x, feetNow - howFarItFell, thing.width, howFarItFell + 2); var i; for (i = 0; i < branches.length; i++) { if (isTouching(path, branches[i])) { return branches[i]; } } return null; } function isOnSolid(thing) { return findSolidUnder(thing) !== null; } // Is this thing up against a trunk it could climb? function findTrunkTouching(thing) { var i; for (i = 0; i < trunks.length; i++) { if (isTouching(thing, trunks[i])) { return trunks[i]; } } return null; } function drawWorld() { drawEarth(); var i; for (i = 0; i < trunks.length; i++) { if (!onScreen(trunks[i].x, trunks[i].y, trunks[i].width, trunks[i].height)) { continue; } drawTrunk(trunks[i]); } for (i = 0; i < branches.length; i++) { if (!onScreen(branches[i].x, branches[i].y, branches[i].width, branches[i].height)) { continue; } drawBranch(branches[i]); } drawLeaves(); } // A trunk with bark: dark edges, a rounded lighter core, and a few // darker streaks down its length. Only drawing — the trunk box you // actually climb never changes. function drawTrunk(trunk) { drawBox(trunk.x, trunk.y, trunk.width, trunk.height, PALETTE['b']); drawBox(trunk.x, trunk.y, 2, trunk.height, PALETTE['n']); drawBox(trunk.x + trunk.width - 2, trunk.y, 2, trunk.height, PALETTE['n']); drawBox(trunk.x + 5, trunk.y, 6, trunk.height, PALETTE['B']); drawBox(trunk.x + 7, trunk.y, 2, trunk.height, PALETTE['L']); var y; for (y = trunk.y + 12; y < trunk.y + trunk.height; y = y + 26) { drawBox(trunk.x + 3, y, 2, 8, PALETTE['n']); drawBox(trunk.x + trunk.width - 6, y + 13, 2, 6, PALETTE['n']); } } // A branch with a sunny top edge and a shadow underneath. function drawBranch(branch) { drawBox(branch.x, branch.y, branch.width, branch.height, PALETTE['b']); drawBox(branch.x, branch.y, branch.width, 2, PALETTE['L']); drawBox(branch.x, branch.y + branch.height - 2, branch.width, 2, PALETTE['n']); } // The ground: dirt below, a grassy top, and a few specks so it isn't // a flat slab. Without this you'd see sky under the forest floor. function drawEarth() { drawBox(0, GROUND_Y, WORLD_WIDTH, WORLD_HEIGHT - GROUND_Y, PALETTE['d']); drawBox(0, GROUND_Y, WORLD_WIDTH, 4, GRASS_COLOR); drawBox(0, GROUND_Y + 4, WORLD_WIDTH, 1, PALETTE['g']); var x; for (x = 8; x < WORLD_WIDTH; x = x + 24) { drawBox(x, GROUND_Y + 10, 2, 2, PALETTE['n']); drawBox(x + 12, GROUND_Y + 20, 2, 2, PALETTE['b']); } } // Leafy clumps growing on the branches, plus a little crown on top of // each tree. Drawn after the wood (so leaves sit on the branches) but // before the animals (so Squeaks is always drawn on top of them). function drawLeaves() { var i; for (i = 0; i < branches.length; i++) { if (branches[i].width >= WORLD_WIDTH) { continue; // the wide ground branch gets no leaves } if (!onScreen(branches[i].x, branches[i].y - leafSprite.height, branches[i].width, leafSprite.height + branches[i].height)) { continue; } drawLeafClumpOnBranch(branches[i]); } for (i = 0; i < trunks.length; i++) { if (!onScreen(trunks[i].x - leafSprite.width, trunks[i].y - leafSprite.height, trunks[i].width + leafSprite.width * 2, leafSprite.height * 2)) { continue; } drawCrown(trunks[i]); } } // Puts a leaf clump at the branch's OUTER tip — the end farther from // the trunk it grows out of. function drawLeafClumpOnBranch(branch) { var trunk = nearestTrunk(branch); var branchCenter = branch.x + branch.width / 2; var clumpX; if (trunk && branchCenter < trunk.x) { clumpX = branch.x - 3; } else { clumpX = branch.x + branch.width - leafSprite.width + 3; } var clumpY = branch.y - leafSprite.height + 3; drawSprite(leafSprite, clumpX, clumpY, false); } // A small crown of three leaf clumps at the very top of a trunk. function drawCrown(trunk) { var midX = trunk.x + trunk.width / 2 - leafSprite.width / 2; var topY = trunk.y - leafSprite.height + 4; drawSprite(leafSprite, midX, topY, false); drawSprite(leafSprite, midX - 6, topY + 4, false); drawSprite(leafSprite, midX + 6, topY + 4, false); } // Which trunk is this branch growing from? The closest one by center. function nearestTrunk(branch) { var branchCenter = branch.x + branch.width / 2; var best = null; var bestDist = 999999; var i; for (i = 0; i < trunks.length; i++) { var trunkCenter = trunks[i].x + trunks[i].width / 2; var dist = Math.abs(trunkCenter - branchCenter); if (dist < bestDist) { bestDist = dist; best = trunks[i]; } } return best; } </script> <script> // ============================================================ // SQUIRREL.JS — Squeaks himself. // // Squeaks is always doing exactly ONE of these things: // GROUNDED — standing or running on something // CLIMBING — holding onto a tree trunk // AIRBORNE — in the air, falling normally // GLIDING — in the air with his flaps out // HURT — just got hit, blinking, can't be hit again // // That's called a "state machine" and it is the whole trick // to making a character that never does two things at once. // ============================================================ var squirrel; var squirrelSprite; var squirrelCurledSprite; function setupSquirrel() { squirrel = makeBox(SQUIRREL_START_X, GROUND_Y - 7, 8, 7); squirrel.state = 'GROUNDED'; squirrel.fallSpeed = 0; squirrel.facingRight = true; squirrel.health = START_HEALTH; squirrel.maxHealth = START_HEALTH; // beds can grow this, forever squirrel.acorns = START_ACORNS; squirrel.hurtTimer = 0; squirrel.stabTimer = 0; // counts down a jab (and its cooldown) squirrel.canDoubleJump = false; // the rabbit's gift turns this on squirrel.usedDoubleJump = false; // one double jump per airtime squirrel.hasShield = false; // the roly-poly's gift turns this on squirrel.curled = false; // true while holding V (a shield ball) } function updateSquirrel() { // Curl into a ball (once the roly-poly gave you the shield): hold V // while on the ground. You can't move, but nothing can hurt you. squirrel.curled = squirrel.hasShield && isKeyDown('curl') && squirrel.state === 'GROUNDED'; if (!squirrel.curled) { if (squirrel.state === 'GROUNDED') { updateGrounded(); } else if (squirrel.state === 'CLIMBING') { updateClimbing(); } else if (squirrel.state === 'AIRBORNE') { updateAirborne(); } else if (squirrel.state === 'GLIDING') { updateGliding(); } } if (squirrel.hurtTimer > 0) { squirrel.hurtTimer = squirrel.hurtTimer - 1; } keepInsideWorld(); } function updateGrounded() { if (tryJump()) { return; } if (tryGrabTrunk()) { return; } runSideways(); // Walked off the end of a branch? Start falling. if (!isOnSolid(squirrel)) { squirrel.state = 'AIRBORNE'; } } function updateAirborne() { if (tryGrabTrunk()) { return; } // Double jump (once the rabbit taught you): a fresh tap of jump in // the air gives ONE more boost up, once per airtime. A tap does // this; HOLDING jump still opens the glide below — different keys // reads (wasKeyPressed vs isKeyDown), so they never fight. if (wasKeyPressed('jump') && squirrel.canDoubleJump && !squirrel.usedDoubleJump) { squirrel.fallSpeed = -DOUBLE_JUMP_POWER; squirrel.usedDoubleJump = true; return; } // Holding jump while going down opens the flaps. // We only allow it once he's actually falling, so that a normal // jump goes all the way up before the glide starts. if (isKeyDown('jump') && squirrel.fallSpeed > 0) { squirrel.state = 'GLIDING'; return; } runSideways(); fall(MAX_FALL_SPEED); } // A tap of the jump key. Works from the ground AND off a trunk, // which is how you launch yourself into a glide. function tryJump() { if (!wasKeyPressed('jump')) { return false; } squirrel.fallSpeed = -JUMP_POWER; squirrel.state = 'AIRBORNE'; return true; } // Gliding! Flaps out, sinking slowly, steering hard. function updateGliding() { // Let go of jump and the flaps close. if (!isKeyDown('jump')) { squirrel.state = 'AIRBORNE'; return; } // Flaps out! If he was still shooting upward from a jump, the // flaps kill that the instant they open — BEFORE he moves. // // This is the most important line in the game. Gliding always // goes down, never up. That's the whole point: you climb to get // height, and you spend that height gliding somewhere. If a // squirrel could glide upwards there'd be no reason to ever // climb anything, and the game would fall apart. // // It's written before fall() on purpose. If you zero the speed // AFTER moving, he drifts up for one frame first — and the rule // only holds because of a check in a different function. Here, // it can't break no matter what. if (squirrel.fallSpeed < 0) { squirrel.fallSpeed = 0; } // Steer. Faster than running, because swooping is the fun part. if (isKeyDown('left')) { squirrel.x = squirrel.x - GLIDE_STEER_SPEED; squirrel.facingRight = false; } if (isKeyDown('right')) { squirrel.x = squirrel.x + GLIDE_STEER_SPEED; squirrel.facingRight = true; } // Hold DOWN to tuck in and dive. You go down faster, but you // also go sideways further before you land. Good for crossing // a really big gap, and for dodging birds. var sinkSpeed = GLIDE_FALL_SPEED; if (isKeyDown('down')) { sinkSpeed = DIVE_FALL_SPEED; } fall(sinkSpeed); } // If he's touching a trunk and pressing up, he grabs on. function tryGrabTrunk() { if (!isKeyDown('up')) { return false; } var trunk = findTrunkTouching(squirrel); if (!trunk) { return false; } squirrel.state = 'CLIMBING'; squirrel.fallSpeed = 0; squirrel.climbingTrunk = trunk; squirrel.usedDoubleJump = false; // grabbed on — double jump ready again return true; } // While climbing there is NO gravity. He's holding on with claws. // If you let go of the keys he just stays put. function updateClimbing() { var trunk = squirrel.climbingTrunk; // Jump off the tree. This is how most glides start. if (tryJump()) { return; } if (isKeyDown('up')) { squirrel.y = squirrel.y - CLIMB_SPEED; } if (isKeyDown('down')) { squirrel.y = squirrel.y + CLIMB_SPEED; } // Climbed off the top? Let go — this is how you get launched // out of the top of a tree and into a glide. if (squirrel.y + squirrel.height < trunk.y) { squirrel.state = 'AIRBORNE'; return; } // Climbed down to the bottom? Stand up. // // Careful: this checks the GROUND, not the bottom of the tree. // That works because every tree in the forest grows all the way // down to the ground. If you ever add a tree that stops partway // up — a stump, say — you'd need to check the trunk's own bottom // instead, or Squeaks would climb down through it into thin air. if (squirrel.y + squirrel.height >= GROUND_Y) { squirrel.y = GROUND_Y - squirrel.height; squirrel.state = 'GROUNDED'; } } // Left and right. Used on the ground AND in the air, because a // squirrel who can't steer while falling is a sad squirrel. function runSideways() { if (isKeyDown('left')) { squirrel.x = squirrel.x - RUN_SPEED; squirrel.facingRight = false; } if (isKeyDown('right')) { squirrel.x = squirrel.x + RUN_SPEED; squirrel.facingRight = true; } } // Gravity pulls him down until he hits his top speed. // Then we check if he landed on anything. function fall(topSpeed) { squirrel.fallSpeed = squirrel.fallSpeed + GRAVITY; if (squirrel.fallSpeed > topSpeed) { squirrel.fallSpeed = topSpeed; } squirrel.y = squirrel.y + squirrel.fallSpeed; // Only land if we're falling DOWN. Otherwise he'd snap onto // branches while jumping up through them, which feels awful. if (squirrel.fallSpeed > 0) { // We hand over how far he just fell, so landing works even if // he's going very fast. See findSolidUnder in world.js. var ground = findSolidUnder(squirrel, squirrel.fallSpeed); if (ground) { land(ground); } } } function land(ground) { // Put his feet exactly on top, not halfway through. squirrel.y = ground.y - squirrel.height; squirrel.fallSpeed = 0; squirrel.state = 'GROUNDED'; squirrel.usedDoubleJump = false; // back on solid ground — double jump ready again // Notice there is NO damage here. Falling never hurts. // Squeaks is a flying squirrel. Falling is his whole thing. } function keepInsideWorld() { if (squirrel.x < 0) { squirrel.x = 0; } if (squirrel.x > worldW - squirrel.width) { squirrel.x = worldW - squirrel.width; } } // Ouch. Called by anything that hurts Squeaks. function hurtSquirrel() { // Curled into a ball? Nothing gets through. if (squirrel.curled) { return; } // Already blinking? Then you're safe for now. if (squirrel.hurtTimer > 0) { return; } squirrel.health = squirrel.health - 1; squirrel.hurtTimer = HURT_BLINK_TIME; // Getting hit closes your flaps. This is the real punishment — // you drop out of your glide and lose all that lovely height. if (squirrel.state === 'GLIDING' || squirrel.state === 'CLIMBING') { squirrel.state = 'AIRBORNE'; } } function drawSquirrel() { // When he's hurt he blinks, so you can tell. if (squirrel.hurtTimer > 0 && Math.floor(squirrel.hurtTimer / 4) % 2 === 0) { return; } if (squirrel.curled) { drawSprite(squirrelCurledSprite, squirrel.x, squirrel.y, !squirrel.facingRight); return; } drawSprite(squirrelSprite, squirrel.x, squirrel.y, !squirrel.facingRight); } </script> <script> // ============================================================ // SPIKE.JS — your close-range weapon. // // The porcupine gives you a spike after the cats quest // (squirrel.hasSpike). Press C to jab: a short spike pokes out // the way you're facing, and anything touching it takes a hit. // // It works just like the acorns do: enemies ask "wasStabbed(me)?" // the same way they ask "wasHitByAcorn(me)?". The difference is // range — the spike only reaches right next to you, so you have // to get close, which is risky. Acorns stay your safe far-away // option; the spike is unlimited but up-close. // ============================================================ // Ticks the jab timer, and starts a new jab on a tap of C. // Called every think in the forest, BEFORE the enemies update, so // that wasStabbed() is true while they check themselves. function updateSpike() { // A jab already out? Count it down toward tucking back in. if (squirrel.stabTimer > 0) { squirrel.stabTimer = squirrel.stabTimer - 1; } // Start a new jab? You need the spike, a fresh tap of C, and no // jab already out (that's the cooldown). if (wasKeyPressed('stab') && squirrel.hasSpike && squirrel.stabTimer <= 0 && !squirrel.curled) { squirrel.stabTimer = STAB_SHOW_TIME; } } // The spike's reach, as a box just in front of the squirrel on the // side he's facing. null when no jab is out. function spikeBox() { if (squirrel.stabTimer <= 0) { return null; } var x; if (squirrel.facingRight) { x = squirrel.x + squirrel.width; // poking off his right side } else { x = squirrel.x - STAB_REACH; // poking off his left side } return makeBox(x, squirrel.y, STAB_REACH, squirrel.height); } // Did this jab hit `thing`? Only TRUE on the very think the jab // starts (stabTimer at full), so a jab that's drawn for several // thinks still only deals its damage once — this is what stops one // press from draining the owl's whole health bar in a few thinks. function wasStabbed(thing) { if (squirrel.stabTimer !== STAB_SHOW_TIME) { return false; } var box = spikeBox(); if (!box) { return false; } return isTouching(box, thing); } // Draw the spike while it's out: a few pale quill-colored pixels // poking out on the facing side. drawBox is world-space (camera // relative), like the door icons. function drawSpike() { var box = spikeBox(); if (!box) { return; } drawBox(box.x, box.y + 1, box.width, 2, PALETTE['E']); // light-grey quill drawBox(box.x, box.y + 3, box.width, 1, PALETTE['W']); // a white glint } </script> <script> // ============================================================ // ACORNS.JS — collecting and throwing acorns. // // There are two kinds of acorn in here: // acornPickups — sitting still, waiting to be collected // flyingAcorns — in the air, thrown or dropped // ============================================================ var acornPickups = []; var flyingAcorns = []; var acornSprite; function setupAcorns() { acornPickups = []; flyingAcorns = []; // A couple on the ground by the start. addAcornPickup(140, GROUND_Y - 5); addAcornPickup(260, GROUND_Y - 5); // A low one and a mid one on every 2nd tree, all the way up. Each // rests on its branch (branch top minus 5), so it's reachable. var i; for (i = 2; i < TREE_COUNT; i = i + 2) { var b = nthBranchOfTree(i, 1); if (b) { addAcornPickup(b.x + b.width / 2 - 2, b.y - 5); } var b2 = nthBranchOfTree(i, 4); if (b2) { addAcornPickup(b2.x + b2.width / 2 - 2, b2.y - 5); } } } function addAcornPickup(x, y) { acornPickups.push(makeBox(x, y, 5, 5)); } function updateAcorns() { collectAcorns(); moveFlyingAcorns(); } // If Squeaks touches an acorn, it goes in his pockets. function collectAcorns() { var i; // We count BACKWARDS because we're removing things from the // list as we go. Counting forwards would skip one every time. for (i = acornPickups.length - 1; i >= 0; i--) { if (isTouching(squirrel, acornPickups[i])) { acornPickups.splice(i, 1); squirrel.acorns = squirrel.acorns + 1; } } } // Throw one, in the direction Squeaks is facing. // It arcs, because you're throwing it, not shooting it. function throwAcorn() { if (squirrel.acorns <= 0) { return; } squirrel.acorns = squirrel.acorns - 1; var acorn = makeBox(squirrel.x, squirrel.y, 5, 5); if (squirrel.facingRight) { acorn.speedX = THROW_SPEED; } else { acorn.speedX = -THROW_SPEED; } acorn.speedY = -THROW_LIFT; flyingAcorns.push(acorn); } // Drop an acorn straight down like a bomb. // You can only do this while GLIDING — that's why the cats // quest makes you learn to glide first. You have to soar over // the cats and bomb them. // // Note this is the X key, not DOWN. DOWN is for diving, and // you're often diving and bombing at the same moment. function dropAcorn() { if (squirrel.state !== 'GLIDING') { return; } if (squirrel.acorns <= 0) { return; } squirrel.acorns = squirrel.acorns - 1; // The acorn appears at Squeaks' body, not down at his feet. // // This matters more than it looks. An acorn is 5 tall and checks // another 2 below itself for something to hit — so an acorn // spawned at his feet reaches about 7 pixels below him. But he // doesn't actually land until his feet are within 2 pixels of a // branch. That leaves a gap where he's still gliding, but the // acorn appears already touching the branch and vanishes // instantly — eating one of your acorns and dropping nothing. // // Spawning at his body keeps the acorn's reach inside him, so // that can't happen. var acorn = makeBox(squirrel.x + ACORN_DROP_OFFSET_X, squirrel.y, 5, 5); acorn.speedX = 0; // straight down. no forward throw. acorn.speedY = 0; // gravity takes it from here acorn.isBomb = true; // bombs hit harder than thrown acorns flyingAcorns.push(acorn); } function moveFlyingAcorns() { var i; for (i = flyingAcorns.length - 1; i >= 0; i--) { var acorn = flyingAcorns[i]; acorn.speedY = acorn.speedY + ACORN_GRAVITY; acorn.x = acorn.x + acorn.speedX; acorn.y = acorn.y + acorn.speedY; // Gone off the edge of the world, or hit something solid? // Then it's done and we forget about it. if (acornIsFinished(acorn)) { flyingAcorns.splice(i, 1); } } } function acornIsFinished(acorn) { if (acorn.y > WORLD_HEIGHT) { return true; } if (acorn.x < 0 || acorn.x > WORLD_WIDTH) { return true; } if (findSolidUnder(acorn)) { return true; } return false; } // Did a flying acorn hit this thing? If it did, the acorn is // used up and we say yes. // // Birds, cats and the owl all use this same function, because an // acorn doesn't care what it hits. // // Pass true for onlyBombsCount if a thrown acorn shouldn't work — // the cats need that. See cats.js for why. function wasHitByAcorn(thing, onlyBombsCount) { var i; // Backwards, because we remove the acorn as we go. for (i = flyingAcorns.length - 1; i >= 0; i--) { var acorn = flyingAcorns[i]; if (onlyBombsCount && !acorn.isBomb) { // A thrown acorn — this thing shrugs it off. Leave it flying. continue; } if (isTouching(acorn, thing)) { flyingAcorns.splice(i, 1); return true; } } return false; } function drawAcorns() { var i; for (i = 0; i < acornPickups.length; i++) { drawSprite(acornSprite, acornPickups[i].x, acornPickups[i].y, false); } for (i = 0; i < flyingAcorns.length; i++) { drawSprite(acornSprite, flyingAcorns[i].x, flyingAcorns[i].y, false); } } </script> <script> // ============================================================ // BIRDS.JS — the birds that want to grab you. // // A bird is always doing one of these: // PATROL — flying back and forth, looking // SWOOP — diving at Squeaks // RETURN — flying back up to where it started // ============================================================ var birds = []; var birdSprite; function setupBirds() { birds = []; // addBird(x, y, howWideItPatrols) — one near every 4th tree, in the // open sky just off the trunk at a mid height. var i; for (i = 3; i < TREE_COUNT; i = i + 4) { addBird(treeX(i) + TRUNK_WIDTH + 30, treeTopY(i) + 60, 120); } } function addBird(x, y, patrolWidth) { var bird = makeBox(x, y, 8, 5); bird.state = 'PATROL'; bird.speedX = BIRD_SPEED; bird.speedY = 0; // Where this bird lives. If it chases Squeaks too far from here, // it gives up and flies home. Birds have nests to think about. bird.homeX = x; bird.homeY = y; bird.patrolLeft = x - patrolWidth / 2; bird.patrolRight = x + patrolWidth / 2; birds.push(bird); } function updateBirds() { var i; for (i = birds.length - 1; i >= 0; i--) { var bird = birds[i]; if (bird.state === 'PATROL') { birdPatrol(bird); } else if (bird.state === 'SWOOP') { birdSwoop(bird); } else if (bird.state === 'RETURN') { birdReturn(bird); } // Did it catch Squeaks? if (isTouching(bird, squirrel)) { hurtSquirrel(); } // Did an acorn hit it, or a spike jab? if (wasHitByAcorn(bird) || wasStabbed(bird)) { birds.splice(i, 1); } } } function birdPatrol(bird) { bird.x = bird.x + bird.speedX; // Bounce off the ends of its patrol. if (bird.x < bird.patrolLeft) { bird.speedX = BIRD_SPEED; } if (bird.x > bird.patrolRight) { bird.speedX = -BIRD_SPEED; } // Spotted him! if (howFarApart(bird, squirrel) < BIRD_SEE_DISTANCE) { bird.state = 'SWOOP'; } } // Where this bird calls home, as a box we can measure against. function birdsHome(bird) { return makeBox(bird.homeX, bird.homeY, bird.width, bird.height); } // Dive straight at wherever Squeaks is. function birdSwoop(bird) { moveToward(bird, squirrel, BIRD_SWOOP_SPEED); // Chased too far from home? Give up and fly back. // // We measure the distance in EVERY direction, not just downwards. // The first version only checked "have I dived too far below my // nest", which meant a bird would happily follow Squeaks all the // way UP a tree and never come back — you'd end up towing a whole // flock behind you and the forest would empty out. if (howFarApart(bird, birdsHome(bird)) > BIRD_GIVE_UP_DISTANCE) { bird.state = 'RETURN'; } } // Fly all the way back home — sideways as well as up. // (The first version only flew back UP, so a bird that chased // sideways ended up patrolling somewhere it had never lived.) function birdReturn(bird) { var distanceFromHome = moveToward(bird, birdsHome(bird), BIRD_SPEED); if (distanceFromHome <= BIRD_SPEED) { bird.x = bird.homeX; bird.y = bird.homeY; bird.state = 'PATROL'; } } function drawBirds() { var i; for (i = 0; i < birds.length; i++) { drawSprite(birdSprite, birds[i].x, birds[i].y, !birds[i].facingRight); } } </script> <script> // ============================================================ // CATS.JS — the cats on the forest floor. // They just pace. They're not clever. But you can't beat them // on the ground — you have to bomb them from the air. // ============================================================ var cats = []; var catSprite; function setupCats() { cats = []; // The cats prowling the forest floor. // addCat(where it stands, how far it paces) // // Want more cats? Copy one of these lines and change the numbers. // Want a really lazy cat? Give it a tiny pacing distance. addCat(300, 60); addCat(520, 80); addCat(740, 50); } function addCat(x, pacingWidth) { var cat = makeBox(x, GROUND_Y - 5, 8, 5); cat.speedX = CAT_SPEED; cat.pacingLeft = x - pacingWidth / 2; cat.pacingRight = x + pacingWidth / 2; cat.facingRight = true; cats.push(cat); } function updateCats() { var i; for (i = cats.length - 1; i >= 0; i--) { var cat = cats[i]; cat.x = cat.x + cat.speedX; if (cat.x < cat.pacingLeft) { cat.speedX = CAT_SPEED; cat.facingRight = true; } if (cat.x > cat.pacingRight) { cat.speedX = -CAT_SPEED; cat.facingRight = false; } if (isTouching(cat, squirrel)) { hurtSquirrel(); } // Cats only go down to an acorn DROPPED on them from above. // Throw one at a cat and it just bounces off — they're too // quick for that. // // This is the whole point of the cats. To get rid of them you // have to climb the big tree, jump off, glide over them and // press X. Which means that by the time the porcupine's job is // done, you've taught yourself to climb, glide AND bomb without // anyone sitting you down and explaining it. // A bomb from above, or a spike jab up close. if (wasHitByAcorn(cat, true) || wasStabbed(cat)) { cats.splice(i, 1); } } } function howManyCatsLeft() { return cats.length; } function drawCats() { var i; for (i = 0; i < cats.length; i++) { drawSprite(catSprite, cats[i].x, cats[i].y, !cats[i].facingRight); } } </script> <script> // ============================================================ // PORCUPINE.JS — your grumpy friend at the bottom. // // The cats quest is secretly the tutorial. To get rid of the // cats you HAVE to climb the big tree, glide off it, and drop // an acorn. So by the time the cats are gone, you've learned // the whole game without anyone telling you anything. // ============================================================ var porcupine; var porcupineSprite; // Where we are in the story: // NOT_MET — haven't talked to him yet // CATS_TODO — he's asked you to deal with the cats // CATS_DONE — the cats are gone, go tell him // HAS_SPIKE — he gave you his spike. Ready for the owl. var questState = 'NOT_MET'; var messageText = ''; var messageTimer = 0; function setupPorcupine() { porcupine = makeBox(PORCUPINE_X, GROUND_Y - 5, 10, 5); questState = 'NOT_MET'; messageText = ''; messageTimer = 0; } function updatePorcupine() { // The cats quest finishes by itself the moment the last cat is // gone — you don't have to walk all the way back down. if (questState === 'CATS_TODO' && howManyCatsLeft() === 0) { questState = 'CATS_DONE'; say('The cats are gone! Come see me, Squeaks!'); // Stop here for this think. // // If you happened to be standing right next to him pressing UP // at the exact moment your last acorn landed on the last cat, // he'd hear you talking in the same breath, hand over the spike // straight away, and you'd never even see the message above. // One thing at a time. return; } if (isNearPorcupine() && wasKeyPressed('up')) { talkToPorcupine(); } if (messageTimer > 0) { messageTimer = messageTimer - 1; } } function isNearPorcupine() { return howFarApart(squirrel, porcupine) < TALK_DISTANCE; } function talkToPorcupine() { if (questState === 'NOT_MET') { questState = 'CATS_TODO'; say('Cats! Prowling my forest! Climb the big tree and drop acorns on them!'); } else if (questState === 'CATS_TODO') { say('Still cats out there. Climb up, jump off, hold SPACE to glide, press X to drop!'); } else if (questState === 'CATS_DONE') { questState = 'HAS_SPIKE'; squirrel.hasSpike = true; say('You did it! Take one of my spikes. The owl at the top is next. Good luck.'); } else if (questState === 'HAS_SPIKE') { say('The owl lives at the very top. Use my spike. I believe in you.'); } } function say(words) { messageText = words; messageTimer = MESSAGE_TIME; } function currentMessage() { if (messageTimer > 0) { return messageText; } return ''; } function drawPorcupine() { drawSprite(porcupineSprite, porcupine.x, porcupine.y, false); } </script> <script> // ============================================================ // HELPERS.JS — your team of forest friends. // // Four critters are scattered through the world: a rabbit, a // mouse, a frog and a roly-poly. Walk up and press UP to talk, // just like the porcupine. Each gives you a little task; finish // it and they give you a gift and join your team. The porcupine // is the fifth friend (he joins when he hands over the spike). // // Two gifts are powers, not acorns: the rabbit teaches the // double jump, and the roly-poly teaches the curl shield. // ============================================================ var helpers = []; var rabbitSprite; var mouseSprite; var frogSprite; var rolyPolySprite; function setupHelpers() { helpers = []; // Rabbit — on the start ground. Bring him 3 acorns. helpers.push(makeHelper('rabbit', 'fetch', makeBox(280, GROUND_Y - 6, 8, 6), RABBIT_GIFT, 'Ooh, acorns! Bring me 3 and I will teach you a SUPER hop!', 'Still hungry... come back with 3 acorns!', 'Yum! Now tap jump AGAIN in the air to double-jump! Acorns for you too.', 'Boing! We are a team now.')); // Mouse — by the start-area cats. Get rid of the cat chasing him. helpers.push(makeHelper('mouse', 'cat', makeBox(500, GROUND_Y - 6, 8, 6), MOUSE_GIFT, 'Eek! A cat keeps chasing me. Get rid of it, please!', 'That cat is still out there! Bomb it, or poke it with your spike.', 'My hero! Take some acorns. I am with you!', 'Squeak! Ready when you are.')); // Frog — up on a branch. Reaching him is the task. var frogBox = branchTipBox(4, 1, 8, 6); if (frogBox) { helpers.push(makeHelper('frog', 'reach', frogBox, FROG_GIFT, 'Bet you cannot hop all the way up to me!', 'Climb on up here!', 'You made it up here? Amazing! Take these acorns, friend!', 'Ribbit! Team frog, ready!')); } // Roly-poly — across a gap higher up. Reaching him is the task. var rolyBox = branchTipBox(8, 2, 8, 5); if (rolyBox) { helpers.push(makeHelper('rolypoly', 'reach', rolyBox, ROLYPOLY_GIFT, 'Glide across the gap to me!', 'Come glide over here!', 'You glided all this way! Here — hold V to curl into a shield!', 'All rolled up and ready!')); } // Tell the mouse which cat is bugging him (a start-area cat). var i; for (i = 0; i < cats.length; i++) { cats[i].isMouseTarget = false; } if (cats.length > 1) { cats[1].isMouseTarget = true; } } function makeHelper(name, kind, box, giftAcorns, askText, hintText, joinText, joinedText) { var h = {}; h.name = name; h.kind = kind; h.box = box; h.giftAcorns = giftAcorns; h.met = false; h.joined = false; h.facingRight = true; h.askText = askText; h.hintText = hintText; h.joinText = joinText; h.joinedText = joinedText; return h; } // A box sitting on the OUTER tip of a branch (the end away from the // trunk), so pressing UP there talks to the helper instead of // grabbing the trunk. null if that branch doesn't exist. function branchTipBox(treeIndex, branchIndex, w, h) { var b = nthBranchOfTree(treeIndex, branchIndex); if (!b) { return null; } var trunkCenter = treeX(treeIndex) + TRUNK_WIDTH / 2; var branchCenter = b.x + b.width / 2; var x; if (branchCenter < trunkCenter) { x = b.x; // left branch: outer tip is the left end } else { x = b.x + b.width - w; // right branch: outer tip is the right end } return makeBox(x, b.y - h, w, h); } function updateHelpers() { var i; for (i = 0; i < helpers.length; i++) { if (isNearHelper(helpers[i]) && wasKeyPressed('up')) { talkToHelper(helpers[i]); return; // one helper per press } } } function isNearHelper(helper) { return howFarApart(squirrel, helper.box) < TALK_DISTANCE; } function talkToHelper(helper) { if (helper.joined) { say(helper.joinedText); return; } if (!helper.met) { helper.met = true; if (helperTaskDone(helper)) { joinHelper(helper); } else { say(helper.askText); } return; } if (helperTaskDone(helper)) { joinHelper(helper); } else { say(helper.hintText); } } // Is this helper's task finished? function helperTaskDone(helper) { if (helper.kind === 'fetch') { return squirrel.acorns >= HELPER_FETCH_COST; } if (helper.kind === 'cat') { return !anyMouseCatLeft(); } return true; // 'reach' — being close enough to talk IS the task } function anyMouseCatLeft() { var i; for (i = 0; i < cats.length; i++) { if (cats[i].isMouseTarget) { return true; } } return false; } function joinHelper(helper) { helper.joined = true; if (helper.kind === 'fetch') { squirrel.acorns = squirrel.acorns - HELPER_FETCH_COST; } squirrel.acorns = squirrel.acorns + helper.giftAcorns; if (helper.name === 'rabbit') { squirrel.canDoubleJump = true; } if (helper.name === 'rolypoly') { squirrel.hasShield = true; } say(helper.joinText); } // How many friends are on your team: joined helpers plus the // porcupine once he has given you the spike. Never more than five. function friendsOnTeam() { var n = 0; var i; for (i = 0; i < helpers.length; i++) { if (helpers[i].joined) { n = n + 1; } } if (questState === 'HAS_SPIKE') { n = n + 1; } return n; } function helperByName(name) { var i; for (i = 0; i < helpers.length; i++) { if (helpers[i].name === name) { return helpers[i]; } } return null; } function drawHelpers() { var i; for (i = 0; i < helpers.length; i++) { var h = helpers[i]; if (!onScreen(h.box.x, h.box.y, h.box.width, h.box.height)) { continue; } drawSprite(helperSprite(h.name), h.box.x, h.box.y, !h.facingRight); } } function helperSprite(name) { if (name === 'rabbit') { return rabbitSprite; } if (name === 'mouse') { return mouseSprite; } if (name === 'frog') { return frogSprite; } return rolyPolySprite; } </script> <script> // ============================================================ // ROOMS.JS — the cozy rooms inside the trees. // // A door opens into one of these. A room is its own tiny // "world": it has its own floor and platforms (branches), and // while you're inside, the game swaps to using those instead of // the forest's. That's the whole trick — every running, jumping // and falling rule works in a room without changing one line of // the squirrel's code. // // Rooms have no climbable walls (their trunks list is empty), so // pressing Up in a room always means "leave through the door", // never "grab a wall" — no confusing the two. // ============================================================ var bedSprite; // The shell every room shares: a wooden floor across the bottom, // a spot to appear standing on it, and a way out. The door code // fills in the rest (a bed, platforms). function makeRoom(type, width, height) { var room = {}; room.type = type; room.width = width; room.height = height; room.trunks = []; room.branches = []; // The floor: one solid branch all the way across the bottom. var floorTop = height - ROOM_FLOOR_THICKNESS; room.branches.push(makeBox(0, floorTop, width, ROOM_FLOOR_THICKNESS)); // You appear standing on the floor near the left wall (7 tall). room.entryX = 12; room.entryY = floorTop - 7; room.bed = null; room.bedUsed = false; // The way out: a door box on the floor near the right wall. room.exit = makeBox(width - 12 - DOOR_WIDTH, floorTop - DOOR_HEIGHT, DOOR_WIDTH, DOOR_HEIGHT); return room; } // A bedroom: a floor, a bed in the middle, and the way out. function makeBedRoom() { var room = makeRoom('bed', ROOM_WIDTH, ROOM_HEIGHT); var bedW = spriteWidth(BED_SPRITE); var bedH = spriteHeight(BED_SPRITE); var floorTop = room.height - ROOM_FLOOR_THICKNESS; room.bed = makeBox(room.width / 2 - bedW / 2, floorTop - bedH, bedW, bedH); return room; } // A course room: a floor plus a few platforms to jump around on. // `hard` makes narrow ledges with bigger jumps; otherwise wide, // gentle steps. Every step is under 28px so a plain jump reaches it. function makeCourseRoom(hard) { var room = makeRoom('course', ROOM_WIDTH, ROOM_HEIGHT); var floorTop = room.height - ROOM_FLOOR_THICKNESS; if (hard) { room.branches.push(makeBox(44, floorTop - 26, 16, BRANCH_THICKNESS)); room.branches.push(makeBox(104, floorTop - 50, 16, BRANCH_THICKNESS)); room.branches.push(makeBox(164, floorTop - 26, 16, BRANCH_THICKNESS)); } else { room.branches.push(makeBox(36, floorTop - 24, 40, BRANCH_THICKNESS)); room.branches.push(makeBox(96, floorTop - 46, 40, BRANCH_THICKNESS)); room.branches.push(makeBox(150, floorTop - 68, 40, BRANCH_THICKNESS)); } return room; } // Sleep in a bedroom's bed. The FIRST time you sleep in a given bed // it gives you one more heart forever (your max goes up, up to // MAX_HEARTS). Every time, it fills you all the way up. function sleepInBed(room) { if (!room.bedUsed) { if (squirrel.maxHealth < MAX_HEARTS) { squirrel.maxHealth = squirrel.maxHealth + 1; } room.bedUsed = true; } squirrel.health = squirrel.maxHealth; say('Zzz... a good nap! You feel stronger.'); } </script> <script> // ============================================================ // DOORS.JS — the doors into the trees. // // Press UP at a door to step inside. Inside is a real room you // walk around in — a cozy bedroom (sleep in the bed for a heart // you keep forever) or a fun platforming course. Press UP at the // room's own door to come back out. // // The whole trick: a room is its own little world with its own // floor and platforms. Going in swaps the game over to the room's // world; coming out swaps the forest back. So the exact same // running and jumping code works in both places. // ============================================================ var doors = []; var doorSprite; // The scene we're in. null = out in the forest. When you step into a // door this becomes that door's room, and the game swaps to the // room's little world until you leave. var insideRoom = null; var currentDoor = null; // While inside a room, the forest's world is stashed here so we can // put it back exactly as it was when you leave. var savedTrunks = null; var savedBranches = null; var savedWorldW = 0; var savedWorldH = 0; // Where you come back if you run out of health. var checkpointX = 0; var checkpointY = 0; function setupDoors() { doors = []; insideRoom = null; currentDoor = null; // Six doors up the climb: three bedrooms (a bed = +1 heart forever) // and three course rooms (just for fun). Some are come-back doors // (you step out where you went in); some are shortcuts that pop you // out higher up the tree. The last argument, when given, is the // [tree, branch] you get shortcutted up to. addDoorOnTree('bed', makeBedRoom(), 6, 1, null); // come-back addDoorOnTree('course', makeCourseRoom(false), 9, 2, null); // come-back, easy addDoorOnTree('bed', makeBedRoom(), 13, 3, [13, 0]); // shortcut up addDoorOnTree('course', makeCourseRoom(true), 17, 4, null); // come-back, hard addDoorOnTree('bed', makeBedRoom(), 21, 5, [21, 1]); // shortcut up addDoorOnTree('course', makeCourseRoom(false), 25, 6, null); // come-back, easy // You start out respawning where you started the game. checkpointX = SQUIRREL_START_X; checkpointY = GROUND_Y - 7; } // Put a door on the middle of a branch. shortcutTo (or null) is the // [tree, branch] you come out on; null means come back to this branch. function addDoorOnTree(type, room, treeIndex, branchIndex, shortcutTo) { var b = nthBranchOfTree(treeIndex, branchIndex); if (!b) { return; // that tree isn't tall enough for that branch — skip it } var doorX = b.x + b.width / 2 - DOOR_WIDTH / 2; var doorY = b.y - DOOR_HEIGHT; var backX; var backY; if (shortcutTo) { var up = nthBranchOfTree(shortcutTo[0], shortcutTo[1]); if (up) { backX = up.x + up.width / 2 - 4; // stand on the higher branch backY = up.y - 7; } else { backX = doorX; // no higher branch — come back backY = b.y - 7; } } else { backX = doorX; // come-back: step out right here backY = b.y - 7; } addDoor(type, room, doorX, doorY, backX, backY); } function addDoor(type, room, x, y, backX, backY) { var door = makeBox(x, y, DOOR_WIDTH, DOOR_HEIGHT); door.type = type; door.room = room; door.backX = backX; door.backY = backY; doors.push(door); } function updateDoors() { var i; for (i = 0; i < doors.length; i++) { if (isTouching(squirrel, doors[i]) && wasKeyPressed('up')) { enterRoom(doors[i]); return; } } } function enterRoom(door) { // Dead this very frame (an enemy's last hit landed as you tapped Up)? // Don't step inside — the health check right after will respawn you in // the forest. Entering while dead would strand you in the room world at // a forest coordinate with no floor: an unrecoverable fall. if (squirrel.health <= 0) { return; } // Remember where you'll come out, so dying isn't a disaster. checkpointX = door.backX; checkpointY = door.backY; // Stash the forest, switch to the room's little world. savedTrunks = trunks; savedBranches = branches; savedWorldW = worldW; savedWorldH = worldH; trunks = door.room.trunks; branches = door.room.branches; worldW = door.room.width; worldH = door.room.height; insideRoom = door.room; currentDoor = door; // Pop you inside, standing just in from the door. squirrel.x = door.room.entryX; squirrel.y = door.room.entryY; squirrel.fallSpeed = 0; squirrel.state = 'GROUNDED'; moveCameraTo(squirrel.x, squirrel.y, worldW, worldH); } function leaveRoom() { // Put the forest back exactly as it was. trunks = savedTrunks; branches = savedBranches; worldW = savedWorldW; worldH = savedWorldH; insideRoom = null; // Step out into the forest at this door's come-out spot. squirrel.x = currentDoor.backX; squirrel.y = currentDoor.backY; squirrel.fallSpeed = 0; squirrel.state = 'GROUNDED'; moveCameraTo(squirrel.x, squirrel.y, worldW, worldH); currentDoor = null; } // Ran out of health? Back to your last checkpoint, good as new. function respawnSquirrel() { squirrel.x = checkpointX; squirrel.y = checkpointY; squirrel.health = squirrel.maxHealth; squirrel.fallSpeed = 0; squirrel.hurtTimer = 0; squirrel.state = 'GROUNDED'; squirrel.usedDoubleJump = false; } function drawDoors() { var i; for (i = 0; i < doors.length; i++) { var door = doors[i]; if (!onScreen(door.x - 3, door.y - 8, DOOR_WIDTH + 6, DOOR_HEIGHT + 8)) { continue; } drawSprite(doorSprite, door.x, door.y, false); drawDoorIcon(door); } } // A little heart (bedroom) or star (course room) just above each // door, so you can tell what's inside before you go in. function drawDoorIcon(door) { var cx = door.x + DOOR_WIDTH / 2; var y = door.y - 6; if (door.type === 'bed') { drawBox(cx - 2, y, 1, 1, PALETTE['r']); drawBox(cx + 1, y, 1, 1, PALETTE['r']); drawBox(cx - 2, y + 1, 4, 1, PALETTE['r']); drawBox(cx - 1, y + 2, 2, 1, PALETTE['r']); } else { drawBox(cx, y, 1, 3, PALETTE['y']); drawBox(cx - 2, y + 1, 5, 1, PALETTE['y']); } } </script> <script> // ============================================================ // CHECKPOINTS.JS — the glowing golden acorns. // // Touch one and it lights up gold — that becomes your save // spot. Run out of health and you come back at the last golden // acorn you lit, instead of all the way at the bottom. // // The respawn point itself (checkpointX / checkpointY) lives in // doors.js — both doors and checkpoints set it. // ============================================================ var checkpoints = []; var goldAcornSprite; function setupCheckpoints() { checkpoints = []; // addCheckpointOnTree(which tree, which branch down from the top). // Spread up the climb, on trees that are tall enough to have that // branch (short early trees are skipped automatically). addCheckpointOnTree(4, 2); addCheckpointOnTree(8, 3); addCheckpointOnTree(12, 4); addCheckpointOnTree(16, 5); addCheckpointOnTree(20, 6); addCheckpointOnTree(24, 7); addCheckpointOnTree(28, 8); } function addCheckpointOnTree(treeIndex, branchIndex) { var b = nthBranchOfTree(treeIndex, branchIndex); if (!b) { return; // that tree isn't tall enough for that branch — skip it } // The golden acorn rests on top of the branch, near its middle. var cp = makeBox(b.x + b.width / 2 - 2, b.y - 5, 5, 5); cp.lit = false; // Where you respawn: standing on that branch (8 wide, 7 tall squirrel). cp.standX = b.x + b.width / 2 - 4; cp.standY = b.y - 7; checkpoints.push(cp); } function updateCheckpoints() { var i; for (i = 0; i < checkpoints.length; i++) { var cp = checkpoints[i]; if (!cp.lit && isTouching(squirrel, cp)) { cp.lit = true; checkpointX = cp.standX; checkpointY = cp.standY; } } } function drawCheckpoints() { var i; for (i = 0; i < checkpoints.length; i++) { var cp = checkpoints[i]; if (!onScreen(cp.x - 2, cp.y - 2, 9, 9)) { continue; } if (cp.lit) { // A bright halo behind it, so a lit checkpoint clearly glows. drawBox(cp.x - 2, cp.y - 2, 9, 9, PALETTE['y']); } drawSprite(goldAcornSprite, cp.x, cp.y, false); } } </script> <script> // ============================================================ // OWL.JS — the boss. // // He is asleep on the top branch. Get close and he wakes up. // Then he flies back and forth, and every so often he dives // at you. Throw acorns at him. Six hits and he's done. // // If he beats you, you restart the FIGHT — not the whole // climb. Losing a boss fight should never cost you an hour. // ============================================================ var owl; var owlSprite; function setupOwl() { owl = makeBox(OWL_START_X, OWL_START_Y, 10, 7); owl.state = 'ASLEEP'; owl.health = OWL_HEALTH; owl.speedX = OWL_SPEED; owl.homeY = OWL_START_Y; owl.diveTimer = OWL_WAIT_BETWEEN_DIVES; owl.facingRight = false; } function updateOwl() { if (owl.state === 'ASLEEP') { owlSleep(); return; } if (owlIsBeaten()) { return; } if (owl.state === 'FLYING') { owlFly(); } else if (owl.state === 'DIVING') { owlDive(); } if (isTouching(owl, squirrel)) { hurtSquirrel(); } if (wasHitByAcorn(owl) || wasStabbed(owl)) { owl.health = owl.health - 1; } } function owlSleep() { if (howFarApart(owl, squirrel) < OWL_WAKE_DISTANCE) { owl.state = 'FLYING'; say('WHOO dares climb MY tree?'); } } function owlFly() { owl.x = owl.x + owl.speedX; // Turn around at the edges of his arena. if (owl.x < OWL_ARENA_LEFT) { owl.speedX = OWL_SPEED; owl.facingRight = true; } if (owl.x > OWL_ARENA_RIGHT) { owl.speedX = -OWL_SPEED; owl.facingRight = false; } // Drift back up to his flying height. if (owl.y > owl.homeY) { owl.y = owl.y - OWL_SPEED; } // Time to dive? owl.diveTimer = owl.diveTimer - 1; if (owl.diveTimer <= 0) { owl.state = 'DIVING'; } } function owlDive() { var distance = moveToward(owl, squirrel, OWL_DIVE_SPEED); // Dived far enough, or arrived — go back to flying and wait a bit. if (owl.y > owl.homeY + OWL_DIVE_DEPTH || distance < OWL_DIVE_ARRIVE_DISTANCE) { owl.state = 'FLYING'; owl.diveTimer = OWL_WAIT_BETWEEN_DIVES; } } function owlIsAwake() { return owl.state !== 'ASLEEP'; } function owlIsBeaten() { return owl.health <= 0; } // He got you. Try again — but only the fight, not the climb. function restartOwlFight() { setupOwl(); owl.state = 'FLYING'; squirrel.x = OWL_FIGHT_START_X; squirrel.y = OWL_FIGHT_START_Y; squirrel.health = squirrel.maxHealth; // come back with every heart you earned from beds squirrel.acorns = START_ACORNS + 2; squirrel.fallSpeed = 0; squirrel.hurtTimer = 0; squirrel.state = 'GROUNDED'; squirrel.usedDoubleJump = false; } function drawOwl() { if (owlIsBeaten()) { return; } drawSprite(owlSprite, owl.x, owl.y, owl.facingRight); } </script> <script> // ============================================================ // HUD.JS — the stuff drawn on top of the game. // Your health, your acorns, the version number, and messages. // // The HUD does NOT move with the camera — it's stuck to the // screen, always in the same corner no matter where Squeaks is // in the world. drawSprite() and drawBox() (in draw.js) both // subtract the camera position, which is exactly what we do NOT // want here. So the HUD talks to ctx directly instead — this // file is one of the few allowed to, right alongside draw.js. // ============================================================ var nest; var nestSprite; var heartSprite; var heartEmptySprite; var playerHasWon = false; // The one and only font the HUD ever draws text in. It lives here, // not in tune.js, because it's not a "feel" number you'd tune for // fun (like GRAVITY or RUN_SPEED) — it's plumbing. splitIntoLines() // has to measure text in the EXACT SAME font that drawTinyText() // draws it in, or the wrapping math lies: it might say a line fits // when the drawn letters are actually a different width. One // constant, used by both, means they can never disagree. var HUD_FONT = '8px monospace'; function setupHud() { nest = makeBox(NEST_X, NEST_Y, 8, 4); playerHasWon = false; } // You win by beating the owl AND getting home. // Beating the owl alone isn't enough — you still have to // actually get to your nest. function checkForWin() { if (!owlIsBeaten()) { return; } if (isTouching(squirrel, nest)) { playerHasWon = true; } } function hasWon() { return playerHasWon; } function drawHud() { var i; // Health, drawn as hearts along the top left. A filled heart for // each point you have now, and a faint empty heart for each slot // you've earned but lost — so the hearts a bed gave you still show. for (i = 0; i < squirrel.maxHealth; i++) { var heartPic = i < squirrel.health ? heartSprite : heartEmptySprite; ctx.drawImage(heartPic, HUD_MARGIN + i * HEALTH_ICON_SPACING, HUD_MARGIN); } // How many acorns you're carrying. drawTinyText('x' + squirrel.acorns, HUD_MARGIN, ACORN_COUNT_TEXT_Y, '#ffffff'); ctx.drawImage(acornSprite, ACORN_COUNT_ICON_X, ACORN_COUNT_ICON_Y); // Your team of friends, growing as you recruit them. drawTinyText('Friends ' + friendsOnTeam() + '/5', HUD_MARGIN, FRIENDS_TEXT_Y, '#ffffff'); // The version number, bottom right. Bump it in tune.js // whenever you change something! drawTinyText(VERSION, SCREEN_WIDTH - VERSION_MARGIN_RIGHT, SCREEN_HEIGHT - VERSION_MARGIN_BOTTOM, '#ffffff88'); // Whatever the porcupine or the owl just said. measureText only // gives right answers once ctx.font is set to what we're about to // draw with — set it here, before splitIntoLines does any // measuring, so measuring and drawing can never disagree. ctx.font = HUD_FONT; var words = currentMessage(); if (words.length > 0) { var lines = splitIntoLines(words, SCREEN_WIDTH - MESSAGE_MARGIN * 2); var i; for (i = 0; i < lines.length; i++) { // Stacked from the bottom up, so the message grows upward and // never covers the version number in the corner. var lineY = SCREEN_HEIGHT - MESSAGE_BOTTOM - ((lines.length - 1 - i) * MESSAGE_LINE_HEIGHT); drawTinyText(lines[i], MESSAGE_MARGIN, lineY, '#ffffff'); } } if (playerHasWon) { drawTinyText('YOU MADE IT HOME!', WIN_TEXT_X, WIN_TEXT_Y, '#ffffff'); } } // The canvas can draw text for us, so we don't have to make a // letter sprite for every letter in the alphabet. function drawTinyText(words, x, y, color) { ctx.fillStyle = color; ctx.font = HUD_FONT; ctx.fillText(words, x, y); } // Chops a long message into lines that actually fit on screen. // // The screen is only 320 pixels across, and the porcupine has a lot // to say — his first line ran right off the edge and you couldn't // read the end of it. // // This means you can give him ANY new thing to say and it just // works. You never have to count the letters. function splitIntoLines(words, howWideCanItBe) { var lines = []; var pieces = words.split(' '); var line = ''; var i; for (i = 0; i < pieces.length; i++) { var tryThis = line; if (tryThis.length > 0) { tryThis = tryThis + ' '; } tryThis = tryThis + pieces[i]; // Too wide? Then the line we already had is finished, and this // word starts the next one. if (ctx.measureText(tryThis).width > howWideCanItBe && line.length > 0) { lines.push(line); line = pieces[i]; } else { line = tryThis; } } if (line.length > 0) { lines.push(line); } return lines; } function drawNest() { drawSprite(nestSprite, nest.x, nest.y, false); } </script> <script> // ============================================================ // GAME.JS — the main loop. // In Scratch this would be the "when green flag clicked" // block with a "forever" loop inside it. // ============================================================ var lastTime = 0; var timeBank = 0; // --- Engine plumbing -------------------------------------- // These two are NOT in tune.js on purpose. Everything in tune.js // is safe to play with. These two are not: changing them makes // the game behave differently on fast and slow computers, which // is a horrible thing to try to work out. Leave them alone. // We think 60 times per second, always. var STEP = 1000 / 60; // If the game freezes (you dragged the window, or your computer // got busy), don't try to catch up on more than a quarter of a // second of thinking all at once — it would look like a teleport. var LONGEST_CATCHUP = 250; // Scratch's "when green flag clicked" function start() { setupScreen(); setupInput(); setupWorld(); setupSquirrel(); setupAcorns(); setupBirds(); setupCats(); setupHelpers(); setupPorcupine(); setupDoors(); setupCheckpoints(); setupOwl(); setupHud(); squirrelSprite = makeSprite(SQUIRREL_SPRITE); acornSprite = makeSprite(ACORN_SPRITE); birdSprite = makeSprite(BIRD_SPRITE); catSprite = makeSprite(CAT_SPRITE); porcupineSprite = makeSprite(PORCUPINE_SPRITE); doorSprite = makeSprite(DOOR_SPRITE); goldAcornSprite = makeSprite(GOLD_ACORN); owlSprite = makeSprite(OWL_SPRITE); nestSprite = makeSprite(NEST_SPRITE); leafSprite = makeSprite(LEAF_CLUMP); heartSprite = makeSprite(HEART_SPRITE); heartEmptySprite = makeSprite(HEART_EMPTY_SPRITE); bedSprite = makeSprite(BED_SPRITE); squirrelCurledSprite = makeSprite(SQUIRREL_CURLED); rabbitSprite = makeSprite(RABBIT_SPRITE); mouseSprite = makeSprite(MOUSE_SPRITE); frogSprite = makeSprite(FROG_SPRITE); rolyPolySprite = makeSprite(ROLYPOLY_SPRITE); requestAnimationFrame(frame); } // Scratch's "forever" function frame(now) { if (lastTime === 0) { lastTime = now; } var elapsed = now - lastTime; lastTime = now; if (elapsed > LONGEST_CATCHUP) { elapsed = LONGEST_CATCHUP; } timeBank = timeBank + elapsed; // We always think in exactly the same size steps. That way the // game feels the same on a fast computer and a slow one. while (timeBank >= STEP) { update(); timeBank = timeBank - STEP; } draw(); requestAnimationFrame(frame); } function update() { if (insideRoom) { updateInsideRoom(); } else { updateForest(); } // This must stay LAST in update(): it forgets this step's taps // right before the next step starts listening for new ones. clearPressed(); } function updateForest() { updateSquirrel(); moveCameraTo(squirrel.x, squirrel.y, worldW, worldH); updateAcorns(); updateSpike(); // start/tick a jab before the enemies check wasStabbed() updateBirds(); updateCats(); updatePorcupine(); updateHelpers(); updateOwl(); checkForWin(); if (!squirrel.curled && wasKeyPressed('throw')) { throwAcorn(); } if (!squirrel.curled && wasKeyPressed('drop')) { dropAcorn(); } // Enemies above may have just dropped you to 0 health; the health // check below handles that. Entering a door no longer heals — the // enterRoom guard refuses to enter while dead, so the order here is // safe either way. updateDoors(); updateCheckpoints(); // Ran out of health? If the owl is awake and unbeaten, restart just // the fight. Otherwise go back to your last checkpoint. if (squirrel.health <= 0) { if (owlIsAwake() && !owlIsBeaten()) { restartOwlFight(); } else { respawnSquirrel(); } } } function updateInsideRoom() { updateSquirrel(); moveCameraTo(squirrel.x, squirrel.y, worldW, worldH); // Sleep in the bed (bedrooms only): a heart the first time, and a // full heal every time. Touch the bed and press Down. if (insideRoom.bed && isTouching(squirrel, insideRoom.bed) && wasKeyPressed('down')) { sleepInBed(insideRoom); } // Leave through the room's own door — the same "press Up at a door" // gesture as coming in. if (isTouching(squirrel, insideRoom.exit) && wasKeyPressed('up')) { leaveRoom(); return; } // Ran out of health in here? Step outside first, then the normal // respawn sends you to your last checkpoint. (The owl isn't active // in a room, so the boss-restart path never applies.) if (squirrel.health <= 0) { leaveRoom(); respawnSquirrel(); } } function draw() { if (insideRoom) { drawInsideRoom(); } else { drawForest(); } // The HUD is stuck to the screen, not the world, so it must be // drawn LAST and in BOTH scenes — everything else draws in world // space and gets moved by the camera; the HUD sits on top. drawHud(); } function drawForest() { drawSky(); drawWorld(); drawNest(); drawDoors(); drawCheckpoints(); drawAcorns(); drawBirds(); drawCats(); drawPorcupine(); drawHelpers(); drawOwl(); drawSquirrel(); drawSpike(); } function drawInsideRoom() { drawRoomBackground(insideRoom); var i; for (i = 0; i < insideRoom.branches.length; i++) { drawBranch(insideRoom.branches[i]); } for (i = 0; i < insideRoom.trunks.length; i++) { drawTrunk(insideRoom.trunks[i]); } if (insideRoom.bed) { drawSprite(bedSprite, insideRoom.bed.x, insideRoom.bed.y, false); } drawSprite(doorSprite, insideRoom.exit.x, insideRoom.exit.y, false); drawSquirrel(); } </script> <script>start();</script> </body> </html>
PREVIEW
PICTURES0/50
No pictures yet. Add one, then ask your buddy to put it in your app!