From 8f5cbd65c79c79c394deda06b99d199add214bb0 Mon Sep 17 00:00:00 2001 From: forkimenjeckayang <104195313+forkimenjeckayang@users.noreply.github.com> Date: Tue, 19 Dec 2023 08:07:04 +0100 Subject: [PATCH] Initial commit --- .github/changes/engine.js | 497 ++++++++++++++++++ .github/changes/game-fixed.js | 355 +++++++++++++ .github/changes/game-with-bug.js | 355 +++++++++++++ .github/dependabot.yml | 6 + .github/pull_request_template.md | 3 + .github/script/create-hotfix-pr.sh | 35 ++ .github/script/initialize-repository.sh | 35 ++ .github/steps/-step.txt | 1 + .github/steps/0-welcome.md | 1 + .github/steps/1-create-beta-release.md | 58 ++ .github/steps/2-feature-added-to-release.md | 55 ++ .github/steps/3-release-pr-opened.md | 36 ++ .github/steps/4-release-notes-and-merge.md | 35 ++ .github/steps/5-finalize-release.md | 41 ++ .github/steps/6-commit-hotfix.md | 51 ++ .github/steps/7-create-hotfix-release.md | 29 + .github/steps/X-finish.md | 27 + .github/workflows/0-welcome.yml | 68 +++ .github/workflows/1-create-beta-release.yml | 62 +++ .../workflows/2-feature-added-to-release.yml | 62 +++ .github/workflows/3-release-pr-opened.yml | 62 +++ .../workflows/4-release-notes-and-merge.yml | 67 +++ .github/workflows/5-finalize-release.yml | 69 +++ .github/workflows/6-commit-hotfix.yml | 67 +++ .github/workflows/7-create-hotfix-release.yml | 62 +++ .gitignore | 37 ++ LICENSE | 7 + README.md | 81 +++ base.css | 141 +++++ engine.js | 497 ++++++++++++++++++ game.js | 355 +++++++++++++ game.manifest | 14 + images/sprites.png | Bin 0 -> 62937 bytes index.html | 29 + 34 files changed, 3300 insertions(+) create mode 100644 .github/changes/engine.js create mode 100644 .github/changes/game-fixed.js create mode 100644 .github/changes/game-with-bug.js create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100755 .github/script/create-hotfix-pr.sh create mode 100755 .github/script/initialize-repository.sh create mode 100644 .github/steps/-step.txt create mode 100644 .github/steps/0-welcome.md create mode 100644 .github/steps/1-create-beta-release.md create mode 100644 .github/steps/2-feature-added-to-release.md create mode 100644 .github/steps/3-release-pr-opened.md create mode 100644 .github/steps/4-release-notes-and-merge.md create mode 100644 .github/steps/5-finalize-release.md create mode 100644 .github/steps/6-commit-hotfix.md create mode 100644 .github/steps/7-create-hotfix-release.md create mode 100644 .github/steps/X-finish.md create mode 100644 .github/workflows/0-welcome.yml create mode 100644 .github/workflows/1-create-beta-release.yml create mode 100644 .github/workflows/2-feature-added-to-release.yml create mode 100644 .github/workflows/3-release-pr-opened.yml create mode 100644 .github/workflows/4-release-notes-and-merge.yml create mode 100644 .github/workflows/5-finalize-release.yml create mode 100644 .github/workflows/6-commit-hotfix.yml create mode 100644 .github/workflows/7-create-hotfix-release.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 base.css create mode 100644 engine.js create mode 100644 game.js create mode 100644 game.manifest create mode 100644 images/sprites.png create mode 100644 index.html diff --git a/.github/changes/engine.js b/.github/changes/engine.js new file mode 100644 index 0000000..113e89b --- /dev/null +++ b/.github/changes/engine.js @@ -0,0 +1,497 @@ +(function () { + var lastTime = 0; + var vendors = ["ms", "moz", "webkit", "o"]; + for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x] + "RequestAnimationFrame"]; + window.cancelAnimationFrame = + window[vendors[x] + "CancelAnimationFrame"] || + window[vendors[x] + "CancelRequestAnimationFrame"]; + } + + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function (callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function () { + callback(currTime + timeToCall); + }, timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function (id) { + clearTimeout(id); + }; +})(); + +var Game = new (function () { + var boards = []; + + // Game Initialization + this.initialize = function (canvasElementId, sprite_data, callback) { + this.canvas = document.getElementById(canvasElementId); + + this.playerOffset = 10; + this.canvasMultiplier = 1; + this.setupMobile(); + + this.width = this.canvas.width; + this.height = this.canvas.height; + + this.ctx = this.canvas.getContext && this.canvas.getContext("2d"); + if (!this.ctx) { + return alert("Please upgrade your browser to play"); + } + + this.setupInput(); + + this.loop(); + + if (this.mobile) { + this.setBoard(4, new TouchControls()); + } + + SpriteSheet.load(sprite_data, callback); + }; + + // Handle Input + var KEY_CODES = { 37: "left", 39: "right", 32: "fire" }; + this.keys = {}; + + this.setupInput = function () { + window.addEventListener( + "keydown", + function (e) { + if (KEY_CODES[e.keyCode]) { + Game.keys[KEY_CODES[e.keyCode]] = true; + e.preventDefault(); + } + }, + false + ); + + window.addEventListener( + "keyup", + function (e) { + if (KEY_CODES[e.keyCode]) { + Game.keys[KEY_CODES[e.keyCode]] = false; + e.preventDefault(); + } + }, + false + ); + }; + + var lastTime = new Date().getTime(); + var maxTime = 1 / 30; + // Game Loop + this.loop = function () { + var curTime = new Date().getTime(); + requestAnimationFrame(Game.loop); + var dt = (curTime - lastTime) / 1000; + if (dt > maxTime) { + dt = maxTime; + } + + for (var i = 0, len = boards.length; i < len; i++) { + if (boards[i]) { + boards[i].step(dt); + boards[i].draw(Game.ctx); + } + } + lastTime = curTime; + }; + + // Change an active game board + this.setBoard = function (num, board) { + boards[num] = board; + }; + + this.setupMobile = function () { + var container = document.getElementById("container"), + hasTouch = !!("ontouchstart" in window), + w = window.innerWidth, + h = window.innerHeight; + + if (hasTouch) { + this.mobile = true; + } + + if (screen.width >= 1280 || !hasTouch) { + return false; + } + + if (w > h) { + alert("Please rotate the device and then click OK"); + w = window.innerWidth; + h = window.innerHeight; + } + + container.style.height = h * 2 + "px"; + window.scrollTo(0, 1); + + h = window.innerHeight + 2; + container.style.height = h + "px"; + container.style.width = w + "px"; + container.style.padding = 0; + + if (h >= this.canvas.height * 1.75 || w >= this.canvas.height * 1.75) { + this.canvasMultiplier = 2; + this.canvas.width = w / 2; + this.canvas.height = h / 2; + this.canvas.style.width = w + "px"; + this.canvas.style.height = h + "px"; + } else { + this.canvas.width = w; + this.canvas.height = h; + } + + this.canvas.style.position = "absolute"; + this.canvas.style.left = "0px"; + this.canvas.style.top = "0px"; + }; +})(); + +var SpriteSheet = new (function () { + this.map = {}; + + this.load = function (spriteData, callback) { + this.map = spriteData; + this.image = new Image(); + this.image.onload = callback; + this.image.src = "images/sprites.png"; + }; + + this.draw = function (ctx, sprite, x, y, frame) { + var s = this.map[sprite]; + if (!frame) frame = 0; + ctx.drawImage( + this.image, + s.sx + frame * s.w, + s.sy, + s.w, + s.h, + Math.floor(x), + Math.floor(y), + s.w, + s.h + ); + }; + + return this; +})(); + +var TitleScreen = function TitleScreen(title, subtitle, callback) { + var up = false; + this.step = function (dt) { + if (!Game.keys["fire"]) up = true; + if (up && Game.keys["fire"] && callback) callback(); + }; + + this.draw = function (ctx) { + ctx.fillStyle = "#00FF00"; + + ctx.font = "bold 40px bangers"; + var measure = ctx.measureText(title); + ctx.fillText(title, Game.width / 2 - measure.width / 2, Game.height / 2); + + ctx.font = "bold 20px bangers"; + var measure2 = ctx.measureText(subtitle); + ctx.fillText( + subtitle, + Game.width / 2 - measure2.width / 2, + Game.height / 2 + 40 + ); + }; +}; + +var GameBoard = function () { + var board = this; + + // The current list of objects + this.objects = []; + this.cnt = {}; + + // Add a new object to the object list + this.add = function (obj) { + obj.board = this; + this.objects.push(obj); + this.cnt[obj.type] = (this.cnt[obj.type] || 0) + 1; + return obj; + }; + + // Mark an object for removal + this.remove = function (obj) { + var idx = this.removed.indexOf(obj); + if (idx == -1) { + this.removed.push(obj); + return true; + } else { + return false; + } + }; + + // Reset the list of removed objects + this.resetRemoved = function () { + this.removed = []; + }; + + // Removed an objects marked for removal from the list + this.finalizeRemoved = function () { + for (var i = 0, len = this.removed.length; i < len; i++) { + var idx = this.objects.indexOf(this.removed[i]); + if (idx != -1) { + this.cnt[this.removed[i].type]--; + this.objects.splice(idx, 1); + } + } + }; + + // Call the same method on all current objects + this.iterate = function (funcName) { + var args = Array.prototype.slice.call(arguments, 1); + for (var i = 0, len = this.objects.length; i < len; i++) { + var obj = this.objects[i]; + obj[funcName].apply(obj, args); + } + }; + + // Find the first object for which func is true + this.detect = function (func) { + for (var i = 0, val = null, len = this.objects.length; i < len; i++) { + if (func.call(this.objects[i])) return this.objects[i]; + } + return false; + }; + + // Call step on all objects and them delete + // any object that have been marked for removal + this.step = function (dt) { + this.resetRemoved(); + this.iterate("step", dt); + this.finalizeRemoved(); + }; + + // Draw all the objects + this.draw = function (ctx) { + this.iterate("draw", ctx); + }; + + // Check for a collision between the + // bounding rects of two objects + this.overlap = function (o1, o2) { + return !( + o1.y + o1.h - 1 < o2.y || + o1.y > o2.y + o2.h - 1 || + o1.x + o1.w - 1 < o2.x || + o1.x > o2.x + o2.w - 1 + ); + }; + + // Find the first object that collides with obj + // match against an optional type + this.collide = function (obj, type) { + return this.detect(function () { + if (obj != this) { + var col = (!type || this.type & type) && board.overlap(obj, this); + return col ? this : false; + } + }); + }; +}; + +var Sprite = function () {}; + +Sprite.prototype.setup = function (sprite, props) { + this.sprite = sprite; + this.merge(props); + this.frame = this.frame || 0; + this.w = SpriteSheet.map[sprite].w; + this.h = SpriteSheet.map[sprite].h; +}; + +Sprite.prototype.merge = function (props) { + if (props) { + for (var prop in props) { + this[prop] = props[prop]; + } + } +}; + +Sprite.prototype.draw = function (ctx) { + SpriteSheet.draw(ctx, this.sprite, this.x, this.y, this.frame); +}; + +Sprite.prototype.hit = function (damage) { + this.board.remove(this); +}; + +var Level = function (levelData, callback) { + this.levelData = []; + for (var i = 0; i < levelData.length; i++) { + this.levelData.push(Object.create(levelData[i])); + } + this.t = 0; + this.callback = callback; +}; + +Level.prototype.step = function (dt) { + var idx = 0, + remove = [], + curShip = null; + + // Update the current time offset + this.t += dt * 1000; + + // Start, End, Gap, Type, Override + // [ 0, 4000, 500, 'step', { x: 100 } ] + while ((curShip = this.levelData[idx]) && curShip[0] < this.t + 2000) { + // Check if we've passed the end time + if (this.t > curShip[1]) { + remove.push(curShip); + } else if (curShip[0] < this.t) { + // Get the enemy definition blueprint + var enemy = enemies[curShip[3]], + override = curShip[4]; + + // Add a new enemy with the blueprint and override + this.board.add(new Enemy(enemy, override)); + + // Increment the start time by the gap + curShip[0] += curShip[2]; + } + idx++; + } + + // Remove any objects from the levelData that have passed + for (var i = 0, len = remove.length; i < len; i++) { + var remIdx = this.levelData.indexOf(remove[i]); + if (remIdx != -1) this.levelData.splice(remIdx, 1); + } + + // If there are no more enemies on the board or in + // levelData, this level is done + if (this.levelData.length === 0 && this.board.cnt[OBJECT_ENEMY] === 0) { + if (this.callback) this.callback(); + } +}; + +Level.prototype.draw = function (ctx) {}; + +var TouchControls = function () { + var gutterWidth = 10; + var unitWidth = Game.width / 5; + var blockWidth = unitWidth - gutterWidth; + + this.drawSquare = function (ctx, x, y, txt, on) { + ctx.globalAlpha = on ? 0.9 : 0.6; + ctx.fillStyle = "#CCC"; + ctx.fillRect(x, y, blockWidth, blockWidth); + + ctx.fillStyle = "#FFF"; + ctx.globalAlpha = 1.0; + ctx.font = "bold " + (3 * unitWidth) / 4 + "px arial"; + + var txtSize = ctx.measureText(txt); + + ctx.fillText( + txt, + x + blockWidth / 2 - txtSize.width / 2, + y + (3 * blockWidth) / 4 + 5 + ); + }; + + this.draw = function (ctx) { + ctx.save(); + + var yLoc = Game.height - unitWidth; + this.drawSquare(ctx, gutterWidth, yLoc, "\u25C0", Game.keys["left"]); + this.drawSquare( + ctx, + unitWidth + gutterWidth, + yLoc, + "\u25B6", + Game.keys["right"] + ); + this.drawSquare(ctx, 4 * unitWidth, yLoc, "A", Game.keys["fire"]); + + ctx.restore(); + }; + + this.step = function (dt) {}; + + this.trackTouch = function (e) { + var touch, x; + + e.preventDefault(); + Game.keys["left"] = false; + Game.keys["right"] = false; + for (var i = 0; i < e.targetTouches.length; i++) { + touch = e.targetTouches[i]; + x = touch.pageX / Game.canvasMultiplier - Game.canvas.offsetLeft; + if (x < unitWidth) { + Game.keys["left"] = true; + } + if (x > unitWidth && x < 2 * unitWidth) { + Game.keys["right"] = true; + } + } + + if (e.type == "touchstart" || e.type == "touchend") { + for (i = 0; i < e.changedTouches.length; i++) { + touch = e.changedTouches[i]; + x = touch.pageX / Game.canvasMultiplier - Game.canvas.offsetLeft; + if (x > 4 * unitWidth) { + Game.keys["fire"] = e.type == "touchstart"; + } + } + } + }; + + Game.canvas.addEventListener("touchstart", this.trackTouch, true); + Game.canvas.addEventListener("touchmove", this.trackTouch, true); + Game.canvas.addEventListener("touchend", this.trackTouch, true); + + // For Android + Game.canvas.addEventListener( + "dblclick", + function (e) { + e.preventDefault(); + }, + true + ); + Game.canvas.addEventListener( + "click", + function (e) { + e.preventDefault(); + }, + true + ); + + Game.playerOffset = unitWidth + 20; +}; + +var GamePoints = function () { + Game.points = 0; + + var pointsLength = 8; + + this.draw = function (ctx) { + ctx.save(); + ctx.font = "bold 18px arial"; + ctx.fillStyle = "#00FF00"; + + var txt = "" + Game.points; + var i = pointsLength - txt.length, + zeros = ""; + while (i-- > 0) { + zeros += "0"; + } + + ctx.fillText(zeros + txt, 10, 20); + ctx.restore(); + }; + + this.step = function (dt) {}; +}; diff --git a/.github/changes/game-fixed.js b/.github/changes/game-fixed.js new file mode 100644 index 0000000..4c059ee --- /dev/null +++ b/.github/changes/game-fixed.js @@ -0,0 +1,355 @@ +var sprites = { + ship: { sx: 0, sy: 0, w: 37, h: 42, frames: 1 }, + missile: { sx: 0, sy: 30, w: 2, h: 10, frames: 1 }, + enemy_purple: { sx: 37, sy: 0, w: 42, h: 43, frames: 1 }, + enemy_bee: { sx: 79, sy: 0, w: 37, h: 43, frames: 1 }, + enemy_ship: { sx: 116, sy: 0, w: 42, h: 43, frames: 1 }, + enemy_circle: { sx: 158, sy: 0, w: 32, h: 33, frames: 1 }, + explosion: { sx: 0, sy: 64, w: 64, h: 64, frames: 12 }, + enemy_missile: { sx: 9, sy: 42, w: 3, h: 20, frame: 1 }, +}; + +var enemies = { + straight: { x: 0, y: -50, sprite: "enemy_ship", health: 10, E: 100 }, + ltr: { + x: 0, + y: -100, + sprite: "enemy_purple", + health: 10, + B: 75, + C: 1, + E: 100, + missiles: 2, + }, + circle: { + x: 250, + y: -50, + sprite: "enemy_circle", + health: 10, + A: 0, + B: -100, + C: 1, + E: 20, + F: 100, + G: 1, + H: Math.PI / 2, + }, + wiggle: { + x: 100, + y: -50, + sprite: "enemy_bee", + health: 20, + B: 50, + C: 4, + E: 100, + firePercentage: 0.001, + missiles: 2, + }, + step: { + x: 0, + y: -50, + sprite: "enemy_circle", + health: 10, + B: 150, + C: 1.2, + E: 75, + }, +}; + +var OBJECT_PLAYER = 1, + OBJECT_PLAYER_PROJECTILE = 2, + OBJECT_ENEMY = 4, + OBJECT_ENEMY_PROJECTILE = 8, + OBJECT_POWERUP = 16; + +var startGame = function () { + var ua = navigator.userAgent.toLowerCase(); + + // Only 1 row of stars + if (ua.match(/android/)) { + Game.setBoard(0, new Starfield(50, 0.6, 100, true)); + } else { + Game.setBoard(0, new Starfield(20, 0.4, 100, true)); + Game.setBoard(1, new Starfield(50, 0.6, 100)); + Game.setBoard(2, new Starfield(100, 1.0, 50)); + } + Game.setBoard( + 3, + new TitleScreen("Alien Invasion", "Press fire to start playing", playGame) + ); +}; + +var level1 = [ + // Start, End, Gap, Type, Override + [0, 4000, 500, "step"], + [6000, 13000, 800, "ltr"], + [10000, 16000, 400, "circle"], + [17800, 20000, 500, "straight", { x: 50 }], + [18200, 20000, 500, "straight", { x: 90 }], + [18200, 20000, 500, "straight", { x: 10 }], + [22000, 25000, 400, "wiggle", { x: 150 }], + [22000, 25000, 400, "wiggle", { x: 100 }], +]; + +var playGame = function () { + var board = new GameBoard(); + board.add(new PlayerShip()); + board.add(new Level(level1, winGame)); + Game.setBoard(3, board); + Game.setBoard(5, new GamePoints(0)); +}; + +var winGame = function () { + Game.setBoard( + 3, + new TitleScreen("You win!", "Press fire to play again", playGame) + ); +}; + +var loseGame = function () { + Game.setBoard( + 3, + new TitleScreen("You lose!", "Press fire to play again", playGame) + ); +}; + +var Starfield = function (speed, opacity, numStars, clear) { + // Set up the offscreen canvas + var stars = document.createElement("canvas"); + stars.width = Game.width; + stars.height = Game.height; + var starCtx = stars.getContext("2d"); + + var offset = 0; + + // If the clear option is set, + // make the background black instead of transparent + if (clear) { + starCtx.fillStyle = "#000"; + starCtx.fillRect(0, 0, stars.width, stars.height); + } + + // Now draw a bunch of random 2 pixel + // rectangles onto the offscreen canvas + starCtx.fillStyle = "#FFF"; + starCtx.globalAlpha = opacity; + for (var i = 0; i < numStars; i++) { + starCtx.fillRect( + Math.floor(Math.random() * stars.width), + Math.floor(Math.random() * stars.height), + 2, + 2 + ); + } + + // This method is called every frame + // to draw the starfield onto the canvas + this.draw = function (ctx) { + var intOffset = Math.floor(offset); + var remaining = stars.height - intOffset; + + // Draw the top half of the starfield + if (intOffset > 0) { + ctx.drawImage( + stars, + 0, + remaining, + stars.width, + intOffset, + 0, + 0, + stars.width, + intOffset + ); + } + + // Draw the bottom half of the starfield + if (remaining > 0) { + ctx.drawImage( + stars, + 0, + 0, + stars.width, + remaining, + 0, + intOffset, + stars.width, + remaining + ); + } + }; + + // This method is called to update + // the starfield + this.step = function (dt) { + offset += dt * speed; + offset = offset % stars.height; + }; +}; + +var PlayerShip = function () { + this.setup("ship", { vx: 0, reloadTime: 0.25, maxVel: 200 }); + + this.reload = this.reloadTime; + this.x = Game.width / 2 - this.w / 2; + this.y = Game.height - Game.playerOffset - this.h; + + this.step = function (dt) { + if (Game.keys["left"]) { + this.vx = -this.maxVel; + } else if (Game.keys["right"]) { + this.vx = this.maxVel; + } else { + this.vx = 0; + } + + this.x += this.vx * dt; + + if (this.x < 0) { + this.x = 0; + } else if (this.x > Game.width - this.w) { + this.x = Game.width - this.w; + } + + this.reload -= dt; + if (Game.keys["fire"] && this.reload < 0) { + Game.keys["fire"] = false; + this.reload = this.reloadTime; + + this.board.add(new PlayerMissile(this.x, this.y + this.h / 2)); + this.board.add(new PlayerMissile(this.x + this.w, this.y + this.h / 2)); + } + }; +}; + +PlayerShip.prototype = new Sprite(); +PlayerShip.prototype.type = OBJECT_PLAYER; + +PlayerShip.prototype.hit = function (damage) { + if (this.board.remove(this)) { + loseGame(); + } +}; + +var PlayerMissile = function (x, y) { + this.setup("missile", { vy: -700, damage: 10 }); + this.x = x - this.w / 2; + this.y = y - this.h; +}; + +PlayerMissile.prototype = new Sprite(); +PlayerMissile.prototype.type = OBJECT_PLAYER_PROJECTILE; + +PlayerMissile.prototype.step = function (dt) { + this.y += this.vy * dt; + var collision = this.board.collide(this, OBJECT_ENEMY); + if (collision) { + collision.hit(this.damage); + this.board.remove(this); + } else if (this.y < -this.h) { + this.board.remove(this); + } +}; + +var Enemy = function (blueprint, override) { + this.merge(this.baseParameters); + this.setup(blueprint.sprite, blueprint); + this.merge(override); +}; + +Enemy.prototype = new Sprite(); +Enemy.prototype.type = OBJECT_ENEMY; + +Enemy.prototype.baseParameters = { + A: 0, + B: 0, + C: 0, + D: 0, + E: 0, + F: 0, + G: 0, + H: 0, + t: 0, + reloadTime: 0.75, + reload: 0, +}; + +Enemy.prototype.step = function (dt) { + this.t += dt; + + this.vx = this.A + this.B * Math.sin(this.C * this.t + this.D); + this.vy = this.E + this.F * Math.sin(this.G * this.t + this.H); + + this.x += this.vx * dt; + this.y += this.vy * dt; + + var collision = this.board.collide(this, OBJECT_PLAYER); + if (collision) { + collision.hit(this.damage); + this.board.remove(this); + } + + if (Math.random() < 0.01 && this.reload <= 0) { + this.reload = this.reloadTime; + if (this.missiles == 2) { + this.board.add(new EnemyMissile(this.x + this.w - 2, this.y + this.h)); + this.board.add(new EnemyMissile(this.x + 2, this.y + this.h)); + } else { + this.board.add(new EnemyMissile(this.x + this.w / 2, this.y + this.h)); + } + } + this.reload -= dt; + + if (this.y > Game.height || this.x < -this.w || this.x > Game.width) { + this.board.remove(this); + } +}; + +Enemy.prototype.hit = function (damage) { + this.health -= damage; + if (this.health <= 0) { + if (this.board.remove(this)) { + Game.points += this.points || 100; + this.board.add(new Explosion(this.x + this.w / 2, this.y + this.h / 2)); + } + } +}; + +var EnemyMissile = function (x, y) { + this.setup("enemy_missile", { vy: 200, damage: 10 }); + this.x = x - this.w / 2; + this.y = y; +}; + +EnemyMissile.prototype = new Sprite(); +EnemyMissile.prototype.type = OBJECT_ENEMY_PROJECTILE; + +EnemyMissile.prototype.step = function (dt) { + this.y += this.vy * dt; + var collision = this.board.collide(this, OBJECT_PLAYER); + if (collision) { + collision.hit(this.damage); + this.board.remove(this); + } else if (this.y > Game.height) { + this.board.remove(this); + } +}; + +var Explosion = function (centerX, centerY) { + this.setup("explosion", { frame: 0 }); + this.x = centerX - this.w / 2; + this.y = centerY - this.h / 2; +}; + +Explosion.prototype = new Sprite(); + +Explosion.prototype.step = function (dt) { + this.frame++; + if (this.frame >= 12) { + this.board.remove(this); + } +}; + +window.addEventListener("load", function () { + Game.initialize("game", sprites, startGame); +}); diff --git a/.github/changes/game-with-bug.js b/.github/changes/game-with-bug.js new file mode 100644 index 0000000..6c2becd --- /dev/null +++ b/.github/changes/game-with-bug.js @@ -0,0 +1,355 @@ +var sprites = { + ship: { sx: 0, sy: 0, w: 37, h: 42, frames: 1 }, + missile: { sx: 0, sy: 30, w: 2, h: 10, frames: 1 }, + enemy_purple: { sx: 37, sy: 0, w: 42, h: 43, frames: 1 }, + enemy_bee: { sx: 79, sy: 0, w: 37, h: 43, frames: 1 }, + enemy_ship: { sx: 116, sy: 0, w: 42, h: 43, frames: 1 }, + enemy_circle: { sx: 158, sy: 0, w: 32, h: 33, frames: 1 }, + explosion: { sx: 0, sy: 64, w: 64, h: 64, frames: 12 }, + enemy_missile: { sx: 9, sy: 42, w: 3, h: 20, frame: 1 }, +}; + +var enemies = { + straight: { x: 0, y: -50, sprite: "enemy_ship", health: 10, E: 100 }, + ltr: { + x: 0, + y: -100, + sprite: "enemy_purple", + health: 10, + B: 75, + C: 1, + E: 100, + missiles: 2, + }, + circle: { + x: 250, + y: -50, + sprite: "enemy_circle", + health: 10, + A: 0, + B: -100, + C: 1, + E: 20, + F: 100, + G: 1, + H: Math.PI / 2, + }, + wiggle: { + x: 100, + y: -50, + sprite: "enemy_bee", + health: 20, + B: 50, + C: 4, + E: 100, + firePercentage: 0.001, + missiles: 2, + }, + step: { + x: 0, + y: -50, + sprite: "enemy_circle", + health: 10, + B: 150, + C: 1.2, + E: 75, + }, +}; + +var OBJECT_PLAYER = 1, + OBJECT_PLAYER_PROJECTILE = 2, + OBJECT_ENEMY = 4, + OBJECT_ENEMY_PROJECTILE = 8, + OBJECT_POWERUP = 16; + +var startGame = function () { + var ua = navigator.userAgent.toLowerCase(); + + // Only 1 row of stars + if (ua.match(/android/)) { + Game.setBoard(0, new Starfield(50, 0.6, 100, true)); + } else { + Game.setBoard(0, new Starfield(20, 0.4, 100, true)); + Game.setBoard(1, new Starfield(50, 0.6, 100)); + Game.setBoard(2, new Starfield(100, 1.0, 50)); + } + Game.setBoard( + 3, + new TitleScreen("Alien Invasion", "Press fire to start playing", playGame) + ); +}; + +var level1 = [ + // Start, End, Gap, Type, Override + [0, 4000, 500, "step"], + [6000, 13000, 800, "ltr"], + [10000, 16000, 400, "circle"], + [17800, 20000, 500, "straight", { x: 50 }], + [18200, 20000, 500, "straight", { x: 90 }], + [18200, 20000, 500, "straight", { x: 10 }], + [22000, 25000, 400, "wiggle", { x: 150 }], + [22000, 25000, 400, "wiggle", { x: 100 }], +]; + +var playGame = function () { + var board = new GameBoard(); + board.add(new PlayerShip()); + board.add(new Level(level1, winGame)); + Game.setBoard(3, board); + Game.setBoard(5, new GamePoints(0)); +}; + +var winGame = function () { + Game.setBoard( + 3, + new TitleScreen("You win!", "Press fire to play again", playGame) + ); +}; + +var loseGame = function () { + Game.setBoard( + 3, + new TitleScreen("You lose!", "Press fire to play again", playGame) + ); +}; + +var Starfield = function (speed, opacity, numStars, clear) { + // Set up the offscreen canvas + var stars = document.createElement("canvas"); + stars.width = Game.width; + stars.height = Game.height; + var starCtx = stars.getContext("2d"); + + var offset = 0; + + // If the clear option is set, + // make the background black instead of transparent + if (clear) { + starCtx.fillStyle = "#0F0"; + starCtx.fillRect(0, 0, stars.width, stars.height); + } + + // Now draw a bunch of random 2 pixel + // rectangles onto the offscreen canvas + starCtx.fillStyle = "#FFF"; + starCtx.globalAlpha = opacity; + for (var i = 0; i < numStars; i++) { + starCtx.fillRect( + Math.floor(Math.random() * stars.width), + Math.floor(Math.random() * stars.height), + 2, + 2 + ); + } + + // This method is called every frame + // to draw the starfield onto the canvas + this.draw = function (ctx) { + var intOffset = Math.floor(offset); + var remaining = stars.height - intOffset; + + // Draw the top half of the starfield + if (intOffset > 0) { + ctx.drawImage( + stars, + 0, + remaining, + stars.width, + intOffset, + 0, + 0, + stars.width, + intOffset + ); + } + + // Draw the bottom half of the starfield + if (remaining > 0) { + ctx.drawImage( + stars, + 0, + 0, + stars.width, + remaining, + 0, + intOffset, + stars.width, + remaining + ); + } + }; + + // This method is called to update + // the starfield + this.step = function (dt) { + offset += dt * speed; + offset = offset % stars.height; + }; +}; + +var PlayerShip = function () { + this.setup("ship", { vx: 0, reloadTime: 0.25, maxVel: 200 }); + + this.reload = this.reloadTime; + this.x = Game.width / 2 - this.w / 2; + this.y = Game.height - Game.playerOffset - this.h; + + this.step = function (dt) { + if (Game.keys["left"]) { + this.vx = -this.maxVel; + } else if (Game.keys["right"]) { + this.vx = this.maxVel; + } else { + this.vx = 0; + } + + this.x += this.vx * dt; + + if (this.x < 0) { + this.x = 0; + } else if (this.x > Game.width - this.w) { + this.x = Game.width - this.w; + } + + this.reload -= dt; + if (Game.keys["fire"] && this.reload < 0) { + Game.keys["fire"] = false; + this.reload = this.reloadTime; + + this.board.add(new PlayerMissile(this.x, this.y + this.h / 2)); + this.board.add(new PlayerMissile(this.x + this.w, this.y + this.h / 2)); + } + }; +}; + +PlayerShip.prototype = new Sprite(); +PlayerShip.prototype.type = OBJECT_PLAYER; + +PlayerShip.prototype.hit = function (damage) { + if (this.board.remove(this)) { + loseGame(); + } +}; + +var PlayerMissile = function (x, y) { + this.setup("missile", { vy: -700, damage: 10 }); + this.x = x - this.w / 2; + this.y = y - this.h; +}; + +PlayerMissile.prototype = new Sprite(); +PlayerMissile.prototype.type = OBJECT_PLAYER_PROJECTILE; + +PlayerMissile.prototype.step = function (dt) { + this.y += this.vy * dt; + var collision = this.board.collide(this, OBJECT_ENEMY); + if (collision) { + collision.hit(this.damage); + this.board.remove(this); + } else if (this.y < -this.h) { + this.board.remove(this); + } +}; + +var Enemy = function (blueprint, override) { + this.merge(this.baseParameters); + this.setup(blueprint.sprite, blueprint); + this.merge(override); +}; + +Enemy.prototype = new Sprite(); +Enemy.prototype.type = OBJECT_ENEMY; + +Enemy.prototype.baseParameters = { + A: 0, + B: 0, + C: 0, + D: 0, + E: 0, + F: 0, + G: 0, + H: 0, + t: 0, + reloadTime: 0.75, + reload: 0, +}; + +Enemy.prototype.step = function (dt) { + this.t += dt; + + this.vx = this.A + this.B * Math.sin(this.C * this.t + this.D); + this.vy = this.E + this.F * Math.sin(this.G * this.t + this.H); + + this.x += this.vx * dt; + this.y += this.vy * dt; + + var collision = this.board.collide(this, OBJECT_PLAYER); + if (collision) { + collision.hit(this.damage); + this.board.remove(this); + } + + if (Math.random() < 0.01 && this.reload <= 0) { + this.reload = this.reloadTime; + if (this.missiles == 2) { + this.board.add(new EnemyMissile(this.x + this.w - 2, this.y + this.h)); + this.board.add(new EnemyMissile(this.x + 2, this.y + this.h)); + } else { + this.board.add(new EnemyMissile(this.x + this.w / 2, this.y + this.h)); + } + } + this.reload -= dt; + + if (this.y > Game.height || this.x < -this.w || this.x > Game.width) { + this.board.remove(this); + } +}; + +Enemy.prototype.hit = function (damage) { + this.health -= damage; + if (this.health <= 0) { + if (this.board.remove(this)) { + Game.points += this.points || 100; + this.board.add(new Explosion(this.x + this.w / 2, this.y + this.h / 2)); + } + } +}; + +var EnemyMissile = function (x, y) { + this.setup("enemy_missile", { vy: 200, damage: 10 }); + this.x = x - this.w / 2; + this.y = y; +}; + +EnemyMissile.prototype = new Sprite(); +EnemyMissile.prototype.type = OBJECT_ENEMY_PROJECTILE; + +EnemyMissile.prototype.step = function (dt) { + this.y += this.vy * dt; + var collision = this.board.collide(this, OBJECT_PLAYER); + if (collision) { + collision.hit(this.damage); + this.board.remove(this); + } else if (this.y > Game.height) { + this.board.remove(this); + } +}; + +var Explosion = function (centerX, centerY) { + this.setup("explosion", { frame: 0 }); + this.x = centerX - this.w / 2; + this.y = centerY - this.h / 2; +}; + +Explosion.prototype = new Sprite(); + +Explosion.prototype.step = function (dt) { + this.frame++; + if (this.frame >= 12) { + this.board.remove(this); + } +}; + +window.addEventListener("load", function () { + Game.initialize("game", sprites, startGame); +}); diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8ac6b8c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..bbbf96f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,3 @@ +## Description: + +_Description of changes made and why._ diff --git a/.github/script/create-hotfix-pr.sh b/.github/script/create-hotfix-pr.sh new file mode 100755 index 0000000..2ddc53f --- /dev/null +++ b/.github/script/create-hotfix-pr.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Make sure this file is executable +# chmod a+x .github/script/create-hotfix-pr.sh + +echo "Set committer details" +git config user.name github-actions +git config user.email github-actions@github.com + +echo "Create hotfix branch" +RELEASE_BRANCH=hotfix-v1.0.1 +git checkout main +git pull origin main +git checkout -b $RELEASE_BRANCH + +echo "Push release branch" +git commit --allow-empty --message="Empty commit to initialize branch" +git push --set-upstream origin $RELEASE_BRANCH + +echo "Create feature branch" +git checkout main +FEATURE_BRANCH=fix-game-background +git checkout -b $FEATURE_BRANCH + +echo "Make changes to files" +cp .github/changes/game-fixed.js game.js + +echo "Commit file changes" +git add game.js +git commit -m "Set game background back to black" + +echo "Push feature branch" +git push --set-upstream origin $FEATURE_BRANCH + +echo "Restore main" +git checkout main \ No newline at end of file diff --git a/.github/script/initialize-repository.sh b/.github/script/initialize-repository.sh new file mode 100755 index 0000000..8fee8cf --- /dev/null +++ b/.github/script/initialize-repository.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Make sure this file is executable +# chmod a+x .github/script/initialize-repository.sh + +echo "Set committer details" +git config user.name github-actions +git config user.email github-actions@github.com + +echo "Create release branch" +RELEASE_BRANCH=release-v1.0 +git checkout main +git checkout -b $RELEASE_BRANCH + +echo "Push release branch" +git commit --allow-empty --message="Empty commit to initialize branch" +git push --set-upstream origin $RELEASE_BRANCH + +echo "Create feature branch" +git checkout main +FEATURE_BRANCH=update-text-colors +git checkout -b $FEATURE_BRANCH + +echo "Make changes to files" +cp .github/changes/engine.js engine.js +cp .github/changes/game-with-bug.js game.js + +echo "Commit file changes" +git add engine.js game.js +git commit -m "Changed game text colors to green" + +echo "Push feature branch" +git push --set-upstream origin $FEATURE_BRANCH + +echo "Restore main" +git checkout main \ No newline at end of file diff --git a/.github/steps/-step.txt b/.github/steps/-step.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/.github/steps/-step.txt @@ -0,0 +1 @@ +0 diff --git a/.github/steps/0-welcome.md b/.github/steps/0-welcome.md new file mode 100644 index 0000000..9ff13a5 --- /dev/null +++ b/.github/steps/0-welcome.md @@ -0,0 +1 @@ + diff --git a/.github/steps/1-create-beta-release.md b/.github/steps/1-create-beta-release.md new file mode 100644 index 0000000..ef14f64 --- /dev/null +++ b/.github/steps/1-create-beta-release.md @@ -0,0 +1,58 @@ + + +## Step 1: Create a beta release + +_Welcome to "Release-based workflow" :sparkle:_ + +### The GitHub flow + +The [GitHub flow](https://guides.github.com/introduction/flow/) is a lightweight, branch-based workflow for projects with regular deployments. + +![github-flow](https://user-images.githubusercontent.com/6351798/48032310-63842400-e114-11e8-8db0-06dc0504dcb5.png) + +Some projects may deploy more often, with continuous deployment. There might be a "release" every time there's a new commit on main. + +But, some projects rely on a different structure for versions and releases. + +### Versions + +Versions are different iterations of updated software like operating systems, apps, or dependencies. Common examples are "Windows 8.1" to "Windows 10", or "macOS High Sierra" to "macOS Mojave". + +Developers update code and then run tests on the project for bugs. During that time, the developers might set up certain securities to protect from new code or bugs. Then, the tested code is ready for production. Teams version the code and release it for installation by end users. + +### :keyboard: Activity: Create a release for the current codebase + +In this step, you will create a release for this repository on GitHub. + +GitHub Releases point to a specific commit. Releases can include release notes in Markdown files, and attached binaries. + +Before using a release based workflow for a larger release, let's create a tag and a release. + +1. Open a new browser tab, and work on the steps in your second tab while you read the instructions in this tab. +1. Go to the **Releases** page for this repository. + - _Tip: To reach this page, click the **Code** tab at the top of your repository. Then, find the navigation bar below the repository description, and click the **Releases** heading link._ +1. Click **Create a new release**. +1. In the field for _Tag version_, specify a number. In this case, use **v0.9**. Keep the _Target_ as **main**. +1. Give the release a title, like "First beta release". If you'd like, you could also give the release a short description. +1. Select the checkbox next to **Set as a pre-release**, since it is representing a beta version. +1. Click **Publish release**. + +### :keyboard: Activity: Introduce a bug to be fixed later + +To set the stage for later, let's also add a bug that we'll fix as part of the release workflow in later steps. We've already created a `update-text-colors` branch for you so let's create and merge a pull request with this branch. + +1. Open a **new pull request** with `base: release-v1.0` and `compare: update-text-colors`. +1. Set the pull request title to "Updated game text style". You can include a detailed pull request body, an example is below: + ``` + ## Description: + - Updated game text color to green + ``` +1. Click **Create pull request**. +1. We'll merge this pull request now. Click **Merge pull request** and delete your branch. +1. Wait about 20 seconds then refresh this page (the one you're following instructions from). [GitHub Actions](https://docs.github.com/en/actions) will automatically update to the next step. diff --git a/.github/steps/2-feature-added-to-release.md b/.github/steps/2-feature-added-to-release.md new file mode 100644 index 0000000..bdebafc --- /dev/null +++ b/.github/steps/2-feature-added-to-release.md @@ -0,0 +1,55 @@ + + +## Step 2: Add a new feature to the release branch + +_Great job creating a beta release :heart:_ + +### Release management + +As you prepare for a future release, you'll need to organize more than the tasks and features. It's important to create a clear workflow for your team, and to make sure that the work remains organized. + +There are several strategies for managing releases. Some teams might use long-lived branches, like `production`, `dev`, and `main`. Some teams use simple feature branches, releasing from the main branch. + +No one strategy is better than another. We always recommend being intentional about branches and reducing long-lived branches whenever possible. + +In this exercise, you'll use the `release-v1.0` branch to be your one long-lived branch per release version. + +### Protected branches + +Like the `main` branch, you can protect release branches. This means you can protect branches from force pushes or accidental deletion. This is already configured in this repository. + +### Add a feature + +Releases are usually made of many smaller changes. Let's pretend we don't know about the bug we added earlier and we'll focus on a few features to update our game before the version update. + +- You should update the page background color to black. +- I'll help you change the text colors to green. + +### :keyboard: Activity: Update `base.css` + +1. Create a new branch off of the `main` branch and change the `body` CSS declaration in `base.css` to match what is below. This will set the page background to black. + +``` +body { + background-color: black; +} +``` + +1. Open a pull request with `release-v1.0` as the `base` branch, and your new branch as the `compare` branch. +1. Fill in the pull request template to describe your changes. +1. Click **Create pull request**. + +### Merge the new feature to the release branch + +Even with releases, the GitHub flow is still an important strategy for working with your team. It's a good idea to use short-lived branches for quick feature additions and bug fixes. + +Merge this feature pull request so that you can open the release pull request as early as possible. + +### :keyboard: Activity: Merge the pull request + +1. Click **Merge pull request**, and delete your branch. +1. Wait about 20 seconds then refresh this page (the one you're following instructions from). [GitHub Actions](https://docs.github.com/en/actions) will automatically update to the next step. diff --git a/.github/steps/3-release-pr-opened.md b/.github/steps/3-release-pr-opened.md new file mode 100644 index 0000000..e43202f --- /dev/null +++ b/.github/steps/3-release-pr-opened.md @@ -0,0 +1,36 @@ + + +## Step 3: Open a release pull request + +_Nice work adding a new feature :smile:_ + +### Release branches and `main` + +You should open a pull request between your release branch and main as early as possible. It might be open for a long time, and that's okay. + +In general, the pull request description can include: + +- A [reference to an issue](https://docs.github.com/en/articles/basic-writing-and-formatting-syntax/#mentioning-people-and-teams) that the pull request addresses. +- A description of the changes proposed in the pull request. +- [@mentions](https://docs.github.com/en/articles/basic-writing-and-formatting-syntax/#mentioning-people-and-teams) of the person or team responsible for reviewing proposed changes. + +To expedite the creation of this pull request, I've added a pull request template to the repository. When you create a pull request, default text will automatically be displayed. This should help you identify and fill out all the necessary information. If you don't want to use the template content, just remove the text from the pull request and replace it with your pull request message. + +### :keyboard: Activity: Open a release pull request + +Let's make a new pull request comparing the `release-v1.0` branch to the `main` branch. + +1. Open a **new pull request** with `base: main` and `compare: release-v1.0`. +1. Ensure the title of your pull request is "Release v1.0". +1. Include a detailed pull request body, an example is below: + ``` + ## Description: + - Changed page background color to black. + - Changed game text color to green. + ``` +1. Click **Create pull request**. +1. Wait about 20 seconds then refresh this page (the one you're following instructions from). [GitHub Actions](https://docs.github.com/en/actions) will automatically update to the next step. diff --git a/.github/steps/4-release-notes-and-merge.md b/.github/steps/4-release-notes-and-merge.md new file mode 100644 index 0000000..0f8f516 --- /dev/null +++ b/.github/steps/4-release-notes-and-merge.md @@ -0,0 +1,35 @@ + + +## Step 4: Generate release notes and merge + +_Thanks for opening that pull request :dancer:_ + +### Automatically generated release notes + +[Automatically generated release notes](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) provide an automated alternative to manually writing release notes for your GitHub releases. With automatically generated release notes, you can quickly generate an overview of the contents of a release. Automatically generated release notes include a list of merged pull requests, a list of contributors to the release, and a link to a full changelog. You can also customize your release notes once they are generated. + +### :keyboard: Activity: Generate release notes + +1. In a separate tab, go to the **Releases** page for this repository. + - _Tip: To reach this page, click the **Code** tab at the top of your repository. Then, find the navigation bar below the repository description, and click the **Releases** heading link._ +1. Click the **Draft a new release** button. +1. In the field for _Tag version_, specify `v1.0.0`. +1. To the right of the tag dropdown, click the _Target_ dropddown and select the `release-v1.0` branch. + - _Tip: This is temporary in order to generate release notes based on the changes in this branch._ +1. To the top right of the description text box, click **Generate release notes**. +1. Review the release notes in the text box and customize the content if desired. +1. Set the _Target_ branch back to the `main`, as this is the branch you want to create your tag on once the release branch is merged. +1. Click **Save draft**, as you will publish this release in the next step. + +You can now [merge](https://docs.github.com/en/get-started/quickstart/github-glossary#merge) your pull request! + +### :keyboard: Activity: Merge into main + +1. In a separate tab, go to the **Pull requests** page for this repository. +1. Open your **Release v1.0** pull request. +1. Click **Merge pull request**. +1. Wait about 20 seconds then refresh this page (the one you're following instructions from). [GitHub Actions](https://docs.github.com/en/actions) will automatically update to the next step. diff --git a/.github/steps/5-finalize-release.md b/.github/steps/5-finalize-release.md new file mode 100644 index 0000000..62fdb39 --- /dev/null +++ b/.github/steps/5-finalize-release.md @@ -0,0 +1,41 @@ + + +## Step 5: Finalize the release + +_Awesome work on the release notes :+1:_ + +### Finalizing releases + +It's important to be aware of the information what will be visible in that release. In the pre-release, the version and commit messages are visible. + +![image](https://user-images.githubusercontent.com/13326548/47883578-bdba7780-ddea-11e8-84b8-563e12f02ca6.png) + +### Semantic versioning + +Semantic versioning is a formal convention for specifying compatibility. It uses a three-part version number: **major version**; **minor version**; and **patch**. Version numbers convey meaning about the underlying code and what has been modified. For example, versioning could be handled as follows: + +| Code status | Stage | Rule | Example version | +| ------------------------------- | ------------- | ---------------------------------------------------------------------- | --------------- | +| First release | New product | Start with 1.0.0 | 1.0.0 | +| Backward compatible fix | Patch release | Increment the third digit | 1.0.1 | +| Backward compatible new feature | Minor release | Increment the middle digit and reset the last digit to zero | 1.1.0 | +| Breaking updates | Major release | Increment the first digit and reset the middle and last digits to zero | 2.0.0 | + +Check out this article on [Semantic versioning](https://semver.org/) to learn more. + +### Finalize the release + +Now let's change our recently automated release from _draft_ to _latest release_. + +### :keyboard: Activity: Finalize release + +1. In a separate tab, go to the **Releases** page for this repository. + - _Tip: To reach this page, click the **Code** tab at the top of your repository. Then, find the navigation bar below the repository description, and click the **Releases** heading link._ +1. Click the **Edit** button next to your draft release. +1. Ensure the _Target_ branch is set to `main`. +1. Click **Publish release**. +1. Wait about 20 seconds then refresh this page (the one you're following instructions from). [GitHub Actions](https://docs.github.com/en/actions) will automatically update to the next step. diff --git a/.github/steps/6-commit-hotfix.md b/.github/steps/6-commit-hotfix.md new file mode 100644 index 0000000..fae0005 --- /dev/null +++ b/.github/steps/6-commit-hotfix.md @@ -0,0 +1,51 @@ + + +## Step 6: Commit a hotfix to the release + +_Almost there :heart:_ + +Notice that I didn't delete the branch? That's intentional. + +Sometimes mistakes can happen with releases, and we'll want to be able to correct them on the same branch. + +Now that your release is finalized, we have a confession to make. Somewhere in our recent update, I made a mistake and introduced a bug. Instead of changing the text colors to green, we changed the whole game background. + +_Tip: Sometimes GitHub Pages takes a few minutes to update. Your page might not immediately show the recent updates you've made._ + +![image](https://user-images.githubusercontent.com/13326548/48045461-487dd800-e145-11e8-843c-b91a82213eb8.png) + +"Hotfixes", or a quick fix to address a bug in software, are a normal part of development. Oftentimes you'll see application updates whose only description is "bug fixes". + +When bugs come up after you release a version, you'll need to address them. We've already created a `hotfix-v1.0.1` and `fix-game-background` branches for you to start. + +We'll submit a hotfix by creating and merging the pull request. + +### :keyboard: Activity: Create and merge the hotfix pull request + +1. Open a pull request with `hotfix-v1.0.1` as the `base` branch, and `fix-game-background` as the `compare` branch. +1. Fill in the pull request template to describe your changes. You can set the pull request title to "Hotfix for broken game style". You can include a detailed pull request body, an example is below: + ``` + ## Description: + - Fixed bug, set game background back to black + ``` +1. Review the changes and click **Create pull request**. +1. We want to merge this into our hotfix branch now so click **Merge pull request**. + +Now we want these changes merged into `main` as well so let's create and merge a pull request with our hotfix to `main`. + +### :keyboard: Activity: Create the release pull request + +1. Open a pull request with `main` as the `base` branch, and `hotfix-v1.0.1` as the `compare` branch. +1. Ensure the title of your pull request is "Hotfix v1.0.1". +1. Include a detailed pull request body, an example is below: + ``` + ## Description: + - Fixed bug introduced in last production release - set game background back to black + ``` +1. Review the changes and click **Create pull request**. +1. Click **Merge pull request**. +1. Wait about 20 seconds then refresh this page (the one you're following instructions from). [GitHub Actions](https://docs.github.com/en/actions) will automatically update to the next step. diff --git a/.github/steps/7-create-hotfix-release.md b/.github/steps/7-create-hotfix-release.md new file mode 100644 index 0000000..6c1adfc --- /dev/null +++ b/.github/steps/7-create-hotfix-release.md @@ -0,0 +1,29 @@ + + +## Step 7: Create release v1.0.1 + +_One last step to go!_ + +### A final release + +You updated the source code, but users can't readily access your most recent changes. Prepare a new release, and distribute that release to the necessary channels. + +### Create release v1.0.1 + +With descriptive pull requests and auto generated release notes, you don't have to spend a lot of time working on your release draft. Follow the steps below to create your new release, generate the release notes, and publish. + +### :keyboard: Activity: Complete release + +1. In a separate tab, go to to the **Releases** page for this repository. + - _Tip: To reach this page, click the **Code** tab at the top of your repository. Then, find the navigation bar below the repository description, and click the **Releases** heading link._ +1. Click the **Draft a new release** button. +1. Set the _Target_ branch to `main`. + - _Tip: Practice your semantic version syntax. What should the tag and title for this release be?_ +1. To the top right of the description text box, click **Generate release notes**. +1. Review the release notes in the text box and customize the content if desired. +1. Click **Publish release**. +1. Wait about 20 seconds then refresh this page (the one you're following instructions from). [GitHub Actions](https://docs.github.com/en/actions) will automatically update to the next step. diff --git a/.github/steps/X-finish.md b/.github/steps/X-finish.md new file mode 100644 index 0000000..ac53c60 --- /dev/null +++ b/.github/steps/X-finish.md @@ -0,0 +1,27 @@ + + +## Finish + +celebrate + +### Congratulations friend, you've completed this course! + +Here's a recap of all the tasks you've accomplished in your repository: + +- Create a beta release. +- Add a new feature to the release branch. +- Open a release pull request +- Automate release notes. +- Merge and finalize the release branch. +- Commit a hotfix to the release. +- Create release v1.0.1. + +### What's next? + +- [We'd love to hear what you thought of this course](https://github.com/orgs/skills/discussions/categories/release-based-workflow). +- [Take another GitHub Skills course](https://github.com/skills). +- [Read the GitHub Getting Started docs](https://docs.github.com/en/get-started). +- To find projects to contribute to, check out [GitHub Explore](https://github.com/explore). diff --git a/.github/workflows/0-welcome.yml b/.github/workflows/0-welcome.yml new file mode 100644 index 0000000..487bbd8 --- /dev/null +++ b/.github/workflows/0-welcome.yml @@ -0,0 +1,68 @@ +name: Step 0, Welcome + +# This step triggers after the learner creates a new repository from the template. +# This workflow updates from step 0 to step 1. + +# This will run every time we create push a commit to `main`. +# Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows +on: + workflow_dispatch: + push: + branches: + - main + +# Reference: https://docs.github.com/en/actions/security-guides/automatic-token-authentication +permissions: + contents: write + pull-requests: write + +jobs: + # Get the current step to only run the main job when the learner is on the same step. + get_current_step: + name: Check current step number + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - id: get_step + run: | + echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT + outputs: + current_step: ${{ steps.get_step.outputs.current_step }} + + on_start: + name: On start + needs: get_current_step + + # We will only run this action when: + # 1. This repository isn't the template repository. + # 2. The step is currently 0. + # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts + # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions + if: >- + ${{ !github.event.repository.is_template + && needs.get_current_step.outputs.current_step == 0 }} + + # We'll run Ubuntu for performance instead of Mac or Windows. + runs-on: ubuntu-latest + + steps: + # We'll need to check out the repository so that we can edit the README. + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Create a release-v1.0 branch. + - name: Initialize repository + run: ./.github/script/initialize-repository.sh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Update README from step 0 to step 1. + - name: Update to step 1 + uses: skills/action-update-step@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + from_step: 0 + to_step: 1 diff --git a/.github/workflows/1-create-beta-release.yml b/.github/workflows/1-create-beta-release.yml new file mode 100644 index 0000000..7a0c1a7 --- /dev/null +++ b/.github/workflows/1-create-beta-release.yml @@ -0,0 +1,62 @@ +name: Step 1, Create a beta release + +# This step triggers after 0-start.yml. +# This workflow updates from step 1 to step 2. + +# This will run when a release is published. +# Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows +on: + workflow_dispatch: + release: + types: [published] + +# Reference: https://docs.github.com/en/actions/security-guides/automatic-token-authentication +permissions: + contents: write + +jobs: + # Get the current step to only run the main job when the learner is on the same step. + get_current_step: + name: Check current step number + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - id: get_step + run: | + echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT + outputs: + current_step: ${{ steps.get_step.outputs.current_step }} + + on_beta_release_created: + name: On beta release created + needs: get_current_step + + # We will only run this action when: + # 1. This repository isn't the template repository. + # 2. The step is currently 1. + # 3. The tag for the published release is v0.9. + # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts + # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions + if: >- + ${{ !github.event.repository.is_template + && needs.get_current_step.outputs.current_step == 1 + && github.ref_name == 'v0.9' }} + + # We'll run Ubuntu for performance instead of Mac or Windows. + runs-on: ubuntu-latest + + steps: + # We'll need to check out the repository so that we can edit the README. + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Update README to from step 1 to step 2. + - name: Update to step 2 + uses: skills/action-update-step@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + from_step: 1 + to_step: 2 diff --git a/.github/workflows/2-feature-added-to-release.yml b/.github/workflows/2-feature-added-to-release.yml new file mode 100644 index 0000000..ed996cb --- /dev/null +++ b/.github/workflows/2-feature-added-to-release.yml @@ -0,0 +1,62 @@ +name: Step 2, Add feature to release branch + +# This step triggers after 1-create-beta-release.yml. +# This workflow updates from step 2 to step 3. + +# This will run when a change to base.css is pushed to the main branch. +# Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows +on: + workflow_dispatch: + push: + branches: + - release-v1.0 + paths: + - "base.css" + +permissions: + contents: write + +jobs: + # Get the current step to only run the main job when the learner is on the same step. + get_current_step: + name: Check current step number + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - id: get_step + run: | + echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT + outputs: + current_step: ${{ steps.get_step.outputs.current_step }} + + on_feature_added: + name: On feature added + needs: get_current_step + + # We will only run this action when: + # 1. This repository isn't the template repository. + # 2. The step is currently 2. + # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts + # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions + if: >- + ${{ !github.event.repository.is_template + && needs.get_current_step.outputs.current_step == 2 }} + + # We'll run Ubuntu for performance instead of Mac or Windows. + runs-on: ubuntu-latest + + steps: + # We'll need to check out the repository so that we can edit the README. + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Update README from step 2 to step 3. + - name: Update to step 3 + uses: skills/action-update-step@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + from_step: 2 + to_step: 3 diff --git a/.github/workflows/3-release-pr-opened.yml b/.github/workflows/3-release-pr-opened.yml new file mode 100644 index 0000000..9c31de9 --- /dev/null +++ b/.github/workflows/3-release-pr-opened.yml @@ -0,0 +1,62 @@ +name: Step 3, Release pull request opened + +# This step triggers after 2-feature-added-to-release.yml. +# This workflow updates from step 3 to step 4. + +# This will run every time a pull request is opened to main. +# Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows +on: + workflow_dispatch: + pull_request: + types: [opened] + branches: [main] + +permissions: + contents: write + +jobs: + # Get the current step to only run the main job when the learner is on the same step. + get_current_step: + name: Check current step number + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - id: get_step + run: | + echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT + outputs: + current_step: ${{ steps.get_step.outputs.current_step }} + + on_release_pr_opened: + name: On release pull request opened + needs: get_current_step + + # We will only run this action when: + # 1. This repository isn't the template repository. + # 2. The step is currently 3. + # 3. The pull request head branch is 'release-v1.0' + # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts + # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions + if: >- + ${{ !github.event.repository.is_template + && needs.get_current_step.outputs.current_step == 3 + && github.head_ref == 'release-v1.0' }} + + # We'll run Ubuntu for performance instead of Mac or Windows. + runs-on: ubuntu-latest + + steps: + # We'll need to check out the repository so that we can edit the README. + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Update README from step 3 to step 4. + - name: Update to step 4 + uses: skills/action-update-step@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + from_step: 3 + to_step: 4 diff --git a/.github/workflows/4-release-notes-and-merge.yml b/.github/workflows/4-release-notes-and-merge.yml new file mode 100644 index 0000000..1a083f9 --- /dev/null +++ b/.github/workflows/4-release-notes-and-merge.yml @@ -0,0 +1,67 @@ +name: Step 4, Generate release notes and merge + +# This step triggers after 3-release-pr-opened.yml. +# This workflow updates from step 4 to step 5. + +# This will run when a pull request to main is closed (merged). +# Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows +on: + workflow_dispatch: + pull_request: + types: + - closed + branches: + - main + +# Reference: https://docs.github.com/en/actions/security-guides/automatic-token-authentication +permissions: + contents: write + +jobs: + # Get the current step to only run the main job when the learner is on the same step. + get_current_step: + name: Check current step number + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - id: get_step + run: | + echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT + outputs: + current_step: ${{ steps.get_step.outputs.current_step }} + + on_release_merged: + name: On release-v1.0 merged + needs: get_current_step + + # We will only run this action when: + # 1. This repository isn't the template repository. + # 2. The step is currently 4. + # 3. The pull request was closed through a merge. + # 4. The pull request head branch is release-v1.0. + # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts + # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions + if: >- + ${{ !github.event.repository.is_template + && needs.get_current_step.outputs.current_step == 4 + && github.event.pull_request.merged == true + && github.head_ref == 'release-v1.0' }} + + # We'll run Ubuntu for performance instead of Mac or Windows. + runs-on: ubuntu-latest + + steps: + # We'll need to check out the repository so that we can edit the README. + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Update README from step 4 to step 5. + - name: Update to step 5 + uses: skills/action-update-step@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + from_step: 4 + to_step: 5 diff --git a/.github/workflows/5-finalize-release.yml b/.github/workflows/5-finalize-release.yml new file mode 100644 index 0000000..745e000 --- /dev/null +++ b/.github/workflows/5-finalize-release.yml @@ -0,0 +1,69 @@ +name: Step 5, Commit hotfix + +# This step triggers after 4-release-notes-and-merge.yml. +# This workflow updates from step 1 to step 2. + +# This will run when a release is published. +# Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows +on: + workflow_dispatch: + release: + types: [published] + +# Reference: https://docs.github.com/en/actions/security-guides/automatic-token-authentication +permissions: + contents: write + pull-requests: write + +jobs: + # Get the current step to only run the main job when the learner is on the same step. + get_current_step: + name: Check current step number + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - id: get_step + run: | + echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT + outputs: + current_step: ${{ steps.get_step.outputs.current_step }} + + on_release_published: + name: On release v1.0 published + needs: get_current_step + + # We will only run this action when: + # 1. This repository isn't the template repository. + # 2. The step is currently 5. + # 3. The tag for the published release is v1.0.0. + # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts + # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions + if: >- + ${{ !github.event.repository.is_template + && needs.get_current_step.outputs.current_step == 5 + && github.ref_name == 'v1.0.0' }} + + # We'll run Ubuntu for performance instead of Mac or Windows. + runs-on: ubuntu-latest + + steps: + # We'll need to check out the repository so that we can edit the README. + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Create a release-v1.0.1 hotfix branch. + - name: Create hotfix branch + run: ./.github/script/create-hotfix-pr.sh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Update README from step 5 to step 6. + - name: Update to step 6 + uses: skills/action-update-step@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + from_step: 5 + to_step: 6 diff --git a/.github/workflows/6-commit-hotfix.yml b/.github/workflows/6-commit-hotfix.yml new file mode 100644 index 0000000..d8a6643 --- /dev/null +++ b/.github/workflows/6-commit-hotfix.yml @@ -0,0 +1,67 @@ +name: Step 6, Commit hotfix + +# This step triggers after 5-finalize-release.yml. +# This workflow updates from step 6 to step 7. + +# This will run when a pull request to main is closed (merged). +# Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows +on: + workflow_dispatch: + pull_request: + types: + - closed + branches: + - main + +# Reference: https://docs.github.com/en/actions/security-guides/automatic-token-authentication +permissions: + contents: write + +jobs: + # Get the current step to only run the main job when the learner is on the same step. + get_current_step: + name: Check current step number + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - id: get_step + run: | + echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT + outputs: + current_step: ${{ steps.get_step.outputs.current_step }} + + on_hotfix_merged: + name: On hotfix-v1.0.1 merged + needs: get_current_step + + # We will only run this action when: + # 1. This repository isn't the template repository. + # 2. The step is currently 6. + # 3. The pull request was closed through a merge. + # 4. The pull request head branch is hotfix-v1.0.1. + # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts + # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions + if: >- + ${{ !github.event.repository.is_template + && needs.get_current_step.outputs.current_step == 6 + && github.event.pull_request.merged == true + && github.head_ref == 'hotfix-v1.0.1' }} + + # We'll run Ubuntu for performance instead of Mac or Windows. + runs-on: ubuntu-latest + + steps: + # We'll need to check out the repository so that we can edit the README. + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Update README from step 6 to step 7. + - name: Update to step 7 + uses: skills/action-update-step@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + from_step: 6 + to_step: 7 diff --git a/.github/workflows/7-create-hotfix-release.yml b/.github/workflows/7-create-hotfix-release.yml new file mode 100644 index 0000000..e9e583d --- /dev/null +++ b/.github/workflows/7-create-hotfix-release.yml @@ -0,0 +1,62 @@ +name: Step 7, Create release v1.0.1 + +# This step triggers after 6-commit-hotfix.yml. +# This workflow updates from step 7 to step X. + +# This will run when a release is published. +# Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows +on: + workflow_dispatch: + release: + types: [published] + +# Reference: https://docs.github.com/en/actions/security-guides/automatic-token-authentication +permissions: + contents: write + +jobs: + # Get the current step to only run the main job when the learner is on the same step. + get_current_step: + name: Check current step number + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - id: get_step + run: | + echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT + outputs: + current_step: ${{ steps.get_step.outputs.current_step }} + + on_hotfix_release_published: + name: On hotfix release v1.0.1 published + needs: get_current_step + + # We will only run this action when: + # 1. This repository isn't the template repository. + # 2. The step is currently 7. + # 3. The tag for the published release is v1.0.1 + # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts + # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions + if: >- + ${{ !github.event.repository.is_template + && needs.get_current_step.outputs.current_step == 7 + && github.ref_name == 'v1.0.1' }} + + # We'll run Ubuntu for performance instead of Mac or Windows. + runs-on: ubuntu-latest + + steps: + # We'll need to check out the repository so that we can edit the README. + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Update README from step 7 to step X. + - name: Update to finish + uses: skills/action-update-step@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + from_step: 7 + to_step: X diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..773bfd6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log +*.sql +*.sqlite + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6c5bc3d --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) GitHub, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5df74cc --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +
+ + + +# Create a release based workflow + +_Create a release based workflow that is built on the foundations of the GitHub flow._ + +
+ + + +## Welcome + +Create a release based workflow that is built on the foundations of the [GitHub flow](https://guides.github.com/introduction/flow/). When your team uses a release-based workflow, GitHub makes it easy to collaborate with deployable iterations of your project that you can package and make available for a wider audience to download and use. + +GitHub releases allow your team to package and provide software to your users based on a specific point in the history of your project. + +- **Who is this for**: Developers, DevOps Engineers, IT Operations, managers, and teams. +- **What you'll learn**: How to follow a release-based workflow. +- **What you'll build**: You will create tags, releases, and release notes. +- **Prerequisites**: If you need to learn about branches, commits, and pull requests, take [Introduction to GitHub](https://github.com/skills/introduction-to-github) first. +- **How long**: This course takes less than 1 hour to complete. + +In this course, you will: + +1. Create a beta release +2. Add a feature to a release +3. Open a release pull request +4. Add release notes and merge +5. Finalize a release +6. Commit a hotfix +7. Create a hotfix release + +### How to start this course + + + +[![start-course](https://user-images.githubusercontent.com/1221423/235727646-4a590299-ffe5-480d-8cd5-8194ea184546.svg)](https://github.com/new?template_owner=skills&template_name=release-based-workflow&owner=%40me&name=skills-release-based-workflow&description=My+clone+repository&visibility=public) + +1. Right-click **Start course** and open the link in a new tab. +2. In the new tab, most of the prompts will automatically fill in for you. + - For owner, choose your personal account or an organization to host the repository. + - We recommend creating a public repository, as private repositories will [use Actions minutes](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions). + - Scroll down and click the **Create repository** button at the bottom of the form. +3. After your new repository is created, wait about 20 seconds, then refresh the page. Follow the step-by-step instructions in the new repository's README. + + diff --git a/base.css b/base.css new file mode 100644 index 0000000..d51128e --- /dev/null +++ b/base.css @@ -0,0 +1,141 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} +body { + /* Add background-color declaration here */ +} +ol, +ul { + list-style: none; +} +blockquote, +q { + quotes: none; +} +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ""; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +/* Center the container */ +#container { + padding-top: 50px; + margin: 0 auto; + width: 480px; +} + +/* Give canvas a background */ +canvas { + background-color: black; +} diff --git a/engine.js b/engine.js new file mode 100644 index 0000000..8c59877 --- /dev/null +++ b/engine.js @@ -0,0 +1,497 @@ +(function () { + var lastTime = 0; + var vendors = ["ms", "moz", "webkit", "o"]; + for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x] + "RequestAnimationFrame"]; + window.cancelAnimationFrame = + window[vendors[x] + "CancelAnimationFrame"] || + window[vendors[x] + "CancelRequestAnimationFrame"]; + } + + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function (callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function () { + callback(currTime + timeToCall); + }, timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function (id) { + clearTimeout(id); + }; +})(); + +var Game = new (function () { + var boards = []; + + // Game Initialization + this.initialize = function (canvasElementId, sprite_data, callback) { + this.canvas = document.getElementById(canvasElementId); + + this.playerOffset = 10; + this.canvasMultiplier = 1; + this.setupMobile(); + + this.width = this.canvas.width; + this.height = this.canvas.height; + + this.ctx = this.canvas.getContext && this.canvas.getContext("2d"); + if (!this.ctx) { + return alert("Please upgrade your browser to play"); + } + + this.setupInput(); + + this.loop(); + + if (this.mobile) { + this.setBoard(4, new TouchControls()); + } + + SpriteSheet.load(sprite_data, callback); + }; + + // Handle Input + var KEY_CODES = { 37: "left", 39: "right", 32: "fire" }; + this.keys = {}; + + this.setupInput = function () { + window.addEventListener( + "keydown", + function (e) { + if (KEY_CODES[e.keyCode]) { + Game.keys[KEY_CODES[e.keyCode]] = true; + e.preventDefault(); + } + }, + false + ); + + window.addEventListener( + "keyup", + function (e) { + if (KEY_CODES[e.keyCode]) { + Game.keys[KEY_CODES[e.keyCode]] = false; + e.preventDefault(); + } + }, + false + ); + }; + + var lastTime = new Date().getTime(); + var maxTime = 1 / 30; + // Game Loop + this.loop = function () { + var curTime = new Date().getTime(); + requestAnimationFrame(Game.loop); + var dt = (curTime - lastTime) / 1000; + if (dt > maxTime) { + dt = maxTime; + } + + for (var i = 0, len = boards.length; i < len; i++) { + if (boards[i]) { + boards[i].step(dt); + boards[i].draw(Game.ctx); + } + } + lastTime = curTime; + }; + + // Change an active game board + this.setBoard = function (num, board) { + boards[num] = board; + }; + + this.setupMobile = function () { + var container = document.getElementById("container"), + hasTouch = !!("ontouchstart" in window), + w = window.innerWidth, + h = window.innerHeight; + + if (hasTouch) { + this.mobile = true; + } + + if (screen.width >= 1280 || !hasTouch) { + return false; + } + + if (w > h) { + alert("Please rotate the device and then click OK"); + w = window.innerWidth; + h = window.innerHeight; + } + + container.style.height = h * 2 + "px"; + window.scrollTo(0, 1); + + h = window.innerHeight + 2; + container.style.height = h + "px"; + container.style.width = w + "px"; + container.style.padding = 0; + + if (h >= this.canvas.height * 1.75 || w >= this.canvas.height * 1.75) { + this.canvasMultiplier = 2; + this.canvas.width = w / 2; + this.canvas.height = h / 2; + this.canvas.style.width = w + "px"; + this.canvas.style.height = h + "px"; + } else { + this.canvas.width = w; + this.canvas.height = h; + } + + this.canvas.style.position = "absolute"; + this.canvas.style.left = "0px"; + this.canvas.style.top = "0px"; + }; +})(); + +var SpriteSheet = new (function () { + this.map = {}; + + this.load = function (spriteData, callback) { + this.map = spriteData; + this.image = new Image(); + this.image.onload = callback; + this.image.src = "images/sprites.png"; + }; + + this.draw = function (ctx, sprite, x, y, frame) { + var s = this.map[sprite]; + if (!frame) frame = 0; + ctx.drawImage( + this.image, + s.sx + frame * s.w, + s.sy, + s.w, + s.h, + Math.floor(x), + Math.floor(y), + s.w, + s.h + ); + }; + + return this; +})(); + +var TitleScreen = function TitleScreen(title, subtitle, callback) { + var up = false; + this.step = function (dt) { + if (!Game.keys["fire"]) up = true; + if (up && Game.keys["fire"] && callback) callback(); + }; + + this.draw = function (ctx) { + ctx.fillStyle = "#FFFFFF"; + + ctx.font = "bold 40px bangers"; + var measure = ctx.measureText(title); + ctx.fillText(title, Game.width / 2 - measure.width / 2, Game.height / 2); + + ctx.font = "bold 20px bangers"; + var measure2 = ctx.measureText(subtitle); + ctx.fillText( + subtitle, + Game.width / 2 - measure2.width / 2, + Game.height / 2 + 40 + ); + }; +}; + +var GameBoard = function () { + var board = this; + + // The current list of objects + this.objects = []; + this.cnt = {}; + + // Add a new object to the object list + this.add = function (obj) { + obj.board = this; + this.objects.push(obj); + this.cnt[obj.type] = (this.cnt[obj.type] || 0) + 1; + return obj; + }; + + // Mark an object for removal + this.remove = function (obj) { + var idx = this.removed.indexOf(obj); + if (idx == -1) { + this.removed.push(obj); + return true; + } else { + return false; + } + }; + + // Reset the list of removed objects + this.resetRemoved = function () { + this.removed = []; + }; + + // Removed an objects marked for removal from the list + this.finalizeRemoved = function () { + for (var i = 0, len = this.removed.length; i < len; i++) { + var idx = this.objects.indexOf(this.removed[i]); + if (idx != -1) { + this.cnt[this.removed[i].type]--; + this.objects.splice(idx, 1); + } + } + }; + + // Call the same method on all current objects + this.iterate = function (funcName) { + var args = Array.prototype.slice.call(arguments, 1); + for (var i = 0, len = this.objects.length; i < len; i++) { + var obj = this.objects[i]; + obj[funcName].apply(obj, args); + } + }; + + // Find the first object for which func is true + this.detect = function (func) { + for (var i = 0, val = null, len = this.objects.length; i < len; i++) { + if (func.call(this.objects[i])) return this.objects[i]; + } + return false; + }; + + // Call step on all objects and them delete + // any object that have been marked for removal + this.step = function (dt) { + this.resetRemoved(); + this.iterate("step", dt); + this.finalizeRemoved(); + }; + + // Draw all the objects + this.draw = function (ctx) { + this.iterate("draw", ctx); + }; + + // Check for a collision between the + // bounding rects of two objects + this.overlap = function (o1, o2) { + return !( + o1.y + o1.h - 1 < o2.y || + o1.y > o2.y + o2.h - 1 || + o1.x + o1.w - 1 < o2.x || + o1.x > o2.x + o2.w - 1 + ); + }; + + // Find the first object that collides with obj + // match against an optional type + this.collide = function (obj, type) { + return this.detect(function () { + if (obj != this) { + var col = (!type || this.type & type) && board.overlap(obj, this); + return col ? this : false; + } + }); + }; +}; + +var Sprite = function () {}; + +Sprite.prototype.setup = function (sprite, props) { + this.sprite = sprite; + this.merge(props); + this.frame = this.frame || 0; + this.w = SpriteSheet.map[sprite].w; + this.h = SpriteSheet.map[sprite].h; +}; + +Sprite.prototype.merge = function (props) { + if (props) { + for (var prop in props) { + this[prop] = props[prop]; + } + } +}; + +Sprite.prototype.draw = function (ctx) { + SpriteSheet.draw(ctx, this.sprite, this.x, this.y, this.frame); +}; + +Sprite.prototype.hit = function (damage) { + this.board.remove(this); +}; + +var Level = function (levelData, callback) { + this.levelData = []; + for (var i = 0; i < levelData.length; i++) { + this.levelData.push(Object.create(levelData[i])); + } + this.t = 0; + this.callback = callback; +}; + +Level.prototype.step = function (dt) { + var idx = 0, + remove = [], + curShip = null; + + // Update the current time offset + this.t += dt * 1000; + + // Start, End, Gap, Type, Override + // [ 0, 4000, 500, 'step', { x: 100 } ] + while ((curShip = this.levelData[idx]) && curShip[0] < this.t + 2000) { + // Check if we've passed the end time + if (this.t > curShip[1]) { + remove.push(curShip); + } else if (curShip[0] < this.t) { + // Get the enemy definition blueprint + var enemy = enemies[curShip[3]], + override = curShip[4]; + + // Add a new enemy with the blueprint and override + this.board.add(new Enemy(enemy, override)); + + // Increment the start time by the gap + curShip[0] += curShip[2]; + } + idx++; + } + + // Remove any objects from the levelData that have passed + for (var i = 0, len = remove.length; i < len; i++) { + var remIdx = this.levelData.indexOf(remove[i]); + if (remIdx != -1) this.levelData.splice(remIdx, 1); + } + + // If there are no more enemies on the board or in + // levelData, this level is done + if (this.levelData.length === 0 && this.board.cnt[OBJECT_ENEMY] === 0) { + if (this.callback) this.callback(); + } +}; + +Level.prototype.draw = function (ctx) {}; + +var TouchControls = function () { + var gutterWidth = 10; + var unitWidth = Game.width / 5; + var blockWidth = unitWidth - gutterWidth; + + this.drawSquare = function (ctx, x, y, txt, on) { + ctx.globalAlpha = on ? 0.9 : 0.6; + ctx.fillStyle = "#CCC"; + ctx.fillRect(x, y, blockWidth, blockWidth); + + ctx.fillStyle = "#FFF"; + ctx.globalAlpha = 1.0; + ctx.font = "bold " + (3 * unitWidth) / 4 + "px arial"; + + var txtSize = ctx.measureText(txt); + + ctx.fillText( + txt, + x + blockWidth / 2 - txtSize.width / 2, + y + (3 * blockWidth) / 4 + 5 + ); + }; + + this.draw = function (ctx) { + ctx.save(); + + var yLoc = Game.height - unitWidth; + this.drawSquare(ctx, gutterWidth, yLoc, "\u25C0", Game.keys["left"]); + this.drawSquare( + ctx, + unitWidth + gutterWidth, + yLoc, + "\u25B6", + Game.keys["right"] + ); + this.drawSquare(ctx, 4 * unitWidth, yLoc, "A", Game.keys["fire"]); + + ctx.restore(); + }; + + this.step = function (dt) {}; + + this.trackTouch = function (e) { + var touch, x; + + e.preventDefault(); + Game.keys["left"] = false; + Game.keys["right"] = false; + for (var i = 0; i < e.targetTouches.length; i++) { + touch = e.targetTouches[i]; + x = touch.pageX / Game.canvasMultiplier - Game.canvas.offsetLeft; + if (x < unitWidth) { + Game.keys["left"] = true; + } + if (x > unitWidth && x < 2 * unitWidth) { + Game.keys["right"] = true; + } + } + + if (e.type == "touchstart" || e.type == "touchend") { + for (i = 0; i < e.changedTouches.length; i++) { + touch = e.changedTouches[i]; + x = touch.pageX / Game.canvasMultiplier - Game.canvas.offsetLeft; + if (x > 4 * unitWidth) { + Game.keys["fire"] = e.type == "touchstart"; + } + } + } + }; + + Game.canvas.addEventListener("touchstart", this.trackTouch, true); + Game.canvas.addEventListener("touchmove", this.trackTouch, true); + Game.canvas.addEventListener("touchend", this.trackTouch, true); + + // For Android + Game.canvas.addEventListener( + "dblclick", + function (e) { + e.preventDefault(); + }, + true + ); + Game.canvas.addEventListener( + "click", + function (e) { + e.preventDefault(); + }, + true + ); + + Game.playerOffset = unitWidth + 20; +}; + +var GamePoints = function () { + Game.points = 0; + + var pointsLength = 8; + + this.draw = function (ctx) { + ctx.save(); + ctx.font = "bold 18px arial"; + ctx.fillStyle = "#FFFFFF"; + + var txt = "" + Game.points; + var i = pointsLength - txt.length, + zeros = ""; + while (i-- > 0) { + zeros += "0"; + } + + ctx.fillText(zeros + txt, 10, 20); + ctx.restore(); + }; + + this.step = function (dt) {}; +}; diff --git a/game.js b/game.js new file mode 100644 index 0000000..4c059ee --- /dev/null +++ b/game.js @@ -0,0 +1,355 @@ +var sprites = { + ship: { sx: 0, sy: 0, w: 37, h: 42, frames: 1 }, + missile: { sx: 0, sy: 30, w: 2, h: 10, frames: 1 }, + enemy_purple: { sx: 37, sy: 0, w: 42, h: 43, frames: 1 }, + enemy_bee: { sx: 79, sy: 0, w: 37, h: 43, frames: 1 }, + enemy_ship: { sx: 116, sy: 0, w: 42, h: 43, frames: 1 }, + enemy_circle: { sx: 158, sy: 0, w: 32, h: 33, frames: 1 }, + explosion: { sx: 0, sy: 64, w: 64, h: 64, frames: 12 }, + enemy_missile: { sx: 9, sy: 42, w: 3, h: 20, frame: 1 }, +}; + +var enemies = { + straight: { x: 0, y: -50, sprite: "enemy_ship", health: 10, E: 100 }, + ltr: { + x: 0, + y: -100, + sprite: "enemy_purple", + health: 10, + B: 75, + C: 1, + E: 100, + missiles: 2, + }, + circle: { + x: 250, + y: -50, + sprite: "enemy_circle", + health: 10, + A: 0, + B: -100, + C: 1, + E: 20, + F: 100, + G: 1, + H: Math.PI / 2, + }, + wiggle: { + x: 100, + y: -50, + sprite: "enemy_bee", + health: 20, + B: 50, + C: 4, + E: 100, + firePercentage: 0.001, + missiles: 2, + }, + step: { + x: 0, + y: -50, + sprite: "enemy_circle", + health: 10, + B: 150, + C: 1.2, + E: 75, + }, +}; + +var OBJECT_PLAYER = 1, + OBJECT_PLAYER_PROJECTILE = 2, + OBJECT_ENEMY = 4, + OBJECT_ENEMY_PROJECTILE = 8, + OBJECT_POWERUP = 16; + +var startGame = function () { + var ua = navigator.userAgent.toLowerCase(); + + // Only 1 row of stars + if (ua.match(/android/)) { + Game.setBoard(0, new Starfield(50, 0.6, 100, true)); + } else { + Game.setBoard(0, new Starfield(20, 0.4, 100, true)); + Game.setBoard(1, new Starfield(50, 0.6, 100)); + Game.setBoard(2, new Starfield(100, 1.0, 50)); + } + Game.setBoard( + 3, + new TitleScreen("Alien Invasion", "Press fire to start playing", playGame) + ); +}; + +var level1 = [ + // Start, End, Gap, Type, Override + [0, 4000, 500, "step"], + [6000, 13000, 800, "ltr"], + [10000, 16000, 400, "circle"], + [17800, 20000, 500, "straight", { x: 50 }], + [18200, 20000, 500, "straight", { x: 90 }], + [18200, 20000, 500, "straight", { x: 10 }], + [22000, 25000, 400, "wiggle", { x: 150 }], + [22000, 25000, 400, "wiggle", { x: 100 }], +]; + +var playGame = function () { + var board = new GameBoard(); + board.add(new PlayerShip()); + board.add(new Level(level1, winGame)); + Game.setBoard(3, board); + Game.setBoard(5, new GamePoints(0)); +}; + +var winGame = function () { + Game.setBoard( + 3, + new TitleScreen("You win!", "Press fire to play again", playGame) + ); +}; + +var loseGame = function () { + Game.setBoard( + 3, + new TitleScreen("You lose!", "Press fire to play again", playGame) + ); +}; + +var Starfield = function (speed, opacity, numStars, clear) { + // Set up the offscreen canvas + var stars = document.createElement("canvas"); + stars.width = Game.width; + stars.height = Game.height; + var starCtx = stars.getContext("2d"); + + var offset = 0; + + // If the clear option is set, + // make the background black instead of transparent + if (clear) { + starCtx.fillStyle = "#000"; + starCtx.fillRect(0, 0, stars.width, stars.height); + } + + // Now draw a bunch of random 2 pixel + // rectangles onto the offscreen canvas + starCtx.fillStyle = "#FFF"; + starCtx.globalAlpha = opacity; + for (var i = 0; i < numStars; i++) { + starCtx.fillRect( + Math.floor(Math.random() * stars.width), + Math.floor(Math.random() * stars.height), + 2, + 2 + ); + } + + // This method is called every frame + // to draw the starfield onto the canvas + this.draw = function (ctx) { + var intOffset = Math.floor(offset); + var remaining = stars.height - intOffset; + + // Draw the top half of the starfield + if (intOffset > 0) { + ctx.drawImage( + stars, + 0, + remaining, + stars.width, + intOffset, + 0, + 0, + stars.width, + intOffset + ); + } + + // Draw the bottom half of the starfield + if (remaining > 0) { + ctx.drawImage( + stars, + 0, + 0, + stars.width, + remaining, + 0, + intOffset, + stars.width, + remaining + ); + } + }; + + // This method is called to update + // the starfield + this.step = function (dt) { + offset += dt * speed; + offset = offset % stars.height; + }; +}; + +var PlayerShip = function () { + this.setup("ship", { vx: 0, reloadTime: 0.25, maxVel: 200 }); + + this.reload = this.reloadTime; + this.x = Game.width / 2 - this.w / 2; + this.y = Game.height - Game.playerOffset - this.h; + + this.step = function (dt) { + if (Game.keys["left"]) { + this.vx = -this.maxVel; + } else if (Game.keys["right"]) { + this.vx = this.maxVel; + } else { + this.vx = 0; + } + + this.x += this.vx * dt; + + if (this.x < 0) { + this.x = 0; + } else if (this.x > Game.width - this.w) { + this.x = Game.width - this.w; + } + + this.reload -= dt; + if (Game.keys["fire"] && this.reload < 0) { + Game.keys["fire"] = false; + this.reload = this.reloadTime; + + this.board.add(new PlayerMissile(this.x, this.y + this.h / 2)); + this.board.add(new PlayerMissile(this.x + this.w, this.y + this.h / 2)); + } + }; +}; + +PlayerShip.prototype = new Sprite(); +PlayerShip.prototype.type = OBJECT_PLAYER; + +PlayerShip.prototype.hit = function (damage) { + if (this.board.remove(this)) { + loseGame(); + } +}; + +var PlayerMissile = function (x, y) { + this.setup("missile", { vy: -700, damage: 10 }); + this.x = x - this.w / 2; + this.y = y - this.h; +}; + +PlayerMissile.prototype = new Sprite(); +PlayerMissile.prototype.type = OBJECT_PLAYER_PROJECTILE; + +PlayerMissile.prototype.step = function (dt) { + this.y += this.vy * dt; + var collision = this.board.collide(this, OBJECT_ENEMY); + if (collision) { + collision.hit(this.damage); + this.board.remove(this); + } else if (this.y < -this.h) { + this.board.remove(this); + } +}; + +var Enemy = function (blueprint, override) { + this.merge(this.baseParameters); + this.setup(blueprint.sprite, blueprint); + this.merge(override); +}; + +Enemy.prototype = new Sprite(); +Enemy.prototype.type = OBJECT_ENEMY; + +Enemy.prototype.baseParameters = { + A: 0, + B: 0, + C: 0, + D: 0, + E: 0, + F: 0, + G: 0, + H: 0, + t: 0, + reloadTime: 0.75, + reload: 0, +}; + +Enemy.prototype.step = function (dt) { + this.t += dt; + + this.vx = this.A + this.B * Math.sin(this.C * this.t + this.D); + this.vy = this.E + this.F * Math.sin(this.G * this.t + this.H); + + this.x += this.vx * dt; + this.y += this.vy * dt; + + var collision = this.board.collide(this, OBJECT_PLAYER); + if (collision) { + collision.hit(this.damage); + this.board.remove(this); + } + + if (Math.random() < 0.01 && this.reload <= 0) { + this.reload = this.reloadTime; + if (this.missiles == 2) { + this.board.add(new EnemyMissile(this.x + this.w - 2, this.y + this.h)); + this.board.add(new EnemyMissile(this.x + 2, this.y + this.h)); + } else { + this.board.add(new EnemyMissile(this.x + this.w / 2, this.y + this.h)); + } + } + this.reload -= dt; + + if (this.y > Game.height || this.x < -this.w || this.x > Game.width) { + this.board.remove(this); + } +}; + +Enemy.prototype.hit = function (damage) { + this.health -= damage; + if (this.health <= 0) { + if (this.board.remove(this)) { + Game.points += this.points || 100; + this.board.add(new Explosion(this.x + this.w / 2, this.y + this.h / 2)); + } + } +}; + +var EnemyMissile = function (x, y) { + this.setup("enemy_missile", { vy: 200, damage: 10 }); + this.x = x - this.w / 2; + this.y = y; +}; + +EnemyMissile.prototype = new Sprite(); +EnemyMissile.prototype.type = OBJECT_ENEMY_PROJECTILE; + +EnemyMissile.prototype.step = function (dt) { + this.y += this.vy * dt; + var collision = this.board.collide(this, OBJECT_PLAYER); + if (collision) { + collision.hit(this.damage); + this.board.remove(this); + } else if (this.y > Game.height) { + this.board.remove(this); + } +}; + +var Explosion = function (centerX, centerY) { + this.setup("explosion", { frame: 0 }); + this.x = centerX - this.w / 2; + this.y = centerY - this.h / 2; +}; + +Explosion.prototype = new Sprite(); + +Explosion.prototype.step = function (dt) { + this.frame++; + if (this.frame >= 12) { + this.board.remove(this); + } +}; + +window.addEventListener("load", function () { + Game.initialize("game", sprites, startGame); +}); diff --git a/game.manifest b/game.manifest new file mode 100644 index 0000000..78aa44c --- /dev/null +++ b/game.manifest @@ -0,0 +1,14 @@ +CACHE MANIFEST + +# v1.02 +CACHE: +index.html +engine.js +game.js +images/sprites.png +base.css + +NETWORK: +http://fonts.googleapis.com/ +http://www.google-analytics.com/ +http://themes.googleusercontent.com/ diff --git a/images/sprites.png b/images/sprites.png new file mode 100644 index 0000000000000000000000000000000000000000..78c91758e8f5d7473ad452c07d2d50627ebca722 GIT binary patch literal 62937 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�SW1Ungn~;hx#INMj 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 literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 0000000..b583ba2 --- /dev/null +++ b/index.html @@ -0,0 +1,29 @@ + + + + + Alien Invasion + + + + + + + +
+ +
+ + + +