-- Forge tutorial 4, "A shoot-em-up": a new game with no gravity, a ship on the keys, shots, -- rocks from spawners, health and death, lives, and game over. Starts from New game. TUTORIAL = { number = 4, name = "04-shmup", tag = "TUTORIAL4" } dofile("testScripts/tutorialDrive.singe") local frames = 0 local stage = 0 local game = nil tutorialBegin(nil) local function lightType(name) local names = {} local from, to = 1, 1 for typeName in pairs(FORGE.game.types) do names[#names + 1] = typeName end table.sort(names) for at, typeName in ipairs(names) do if typeName == FORGE.typeName then from = at end if typeName == name then to = at end end for _ = 1, math.abs(to - from) do key((to > from) and SCANCODE.DOWN or SCANCODE.UP) end end local function ruleGo(index, part) local guard = 0 while ((FORGE.rule ~= index) or (FORGE.part ~= (part or 0))) and (guard < 60) do local past = (FORGE.rule > index) or ((FORGE.rule == index) and (FORGE.part > (part or 0))) key(past and SCANCODE.UP or SCANCODE.DOWN) guard = guard + 1 end end local function kitType(name, file) key(SCANCODE.MAIN_2) key(SCANCODE.A) form("name", name) form("look", "sprite") form("look.file", file) end local function place(x, y) key(SCANCODE.MAIN_1) key(SCANCODE.A) local entry = forgeRoom().entities[FORGE.selected] forgePress(entry.x, entry.y) forgeDrag(x, y) forgeRelease() end -- A rule from scratch: N, its note, E and its event, then the filter fields. local function rule(note, on, filters) key(SCANCODE.N) form("note", note) key(SCANCODE.E) pick(on) for _, pair in ipairs(filters or {}) do form(pair[1], pair[2]) end end -- A part: C or T, its name, and its fields. local function part(partKey, name, fields) key(partKey) pick(name) for _, pair in ipairs(fields or {}) do form(pair[1], pair[2]) end end local function step() if stage == 1 then -- Clear the starter: the hero and the ground go, types and all, and so do their rules. key(SCANCODE.MAIN_1) selectEntity("hero") key(SCANCODE.DELETE) selectEntity("ground") key(SCANCODE.DELETE) key(SCANCODE.MAIN_2) lightType("hero") key(SCANCODE.DELETE) lightType("ground") key(SCANCODE.DELETE) key(SCANCODE.MAIN_3) ruleGo(1, 0) key(SCANCODE.DELETE) key(SCANCODE.DELETE) key(SCANCODE.DELETE) -- The game itself: no world, so no gravity; lives to lose. key(SCANCODE.MAIN_1) forgeSelect(nil) form("title", "Rocks") form("vars", "score=0, lives=3") form("layers", "overlay") tutorialShot("cleared") elseif stage == 2 then kitType("ship", "ship.png") form("vars", "health=1") form("behaviours", "keys, mover, shooter, health, sound") form("keys.left", "LEFT") form("keys.right", "RIGHT") form("keys.up", "UP") form("keys.down", "DOWN") form("keys.fire", "SPACE") form("mover.speed", 260) form("mover.clamp", "true") form("shooter.spawns", "shot") form("shooter.rate", 0.15) form("shooter.offsetY", -14) form("health.max", 1) form("health.keep", "true") form("sound.hit", "hit.wav") tutorialShot("ship") elseif stage == 3 then kitType("shot", "shot.png") form("behaviours", "projectile, sound") form("projectile.vy", -520) form("projectile.life", 3) form("sound.spawn", "shot.wav") kitType("rock", "rock.png") form("vars", "health=2") form("behaviours", "drift, health, sound") form("drift.vy", 90) form("health.max", 2) form("sound.death", "hit.wav") key(SCANCODE.A) form("name", "hive") form("look", "none") form("behaviours", "spawner") form("spawner.spawns", "rock") form("spawner.every", 0.6) form("spawner.max", 6) form("spawner.total", 12) tutorialShot("types") elseif stage == 4 then lightType("ship") place(360, 420) key(SCANCODE.MAIN_2) lightType("hive") place(360, 20) key(SCANCODE.D) forgePress(380, 40) forgeDrag(560, 20) forgeRelease() tutorialShot("placed") elseif stage == 5 then key(SCANCODE.MAIN_3) rule("A shot lands on a rock", "collision", { { "a", "shot" }, { "b", "rock" } }) part(SCANCODE.T, "destroy", {}) part(SCANCODE.T, "damage", { { "entity", "other" }, { "amount", 1 } }) part(SCANCODE.T, "addScore", { { "amount", 10 } }) rule("A rock dies", "death", { { "type", "rock" } }) part(SCANCODE.T, "emit", { { "count", 24 }, { "r", 255 }, { "g", 120 }, { "b", 60 } }) part(SCANCODE.T, "addScore", { { "amount", 50 } }) tutorialShot("hitRule") elseif stage == 6 then rule("A rock rams the ship", "collision", { { "a", "rock" }, { "b", "ship" } }) part(SCANCODE.T, "destroy", {}) part(SCANCODE.T, "damage", { { "entity", "other" }, { "amount", 1 } }) rule("The ship is lost", "death", { { "type", "ship" } }) part(SCANCODE.T, "addVar", { { "name", "lives" }, { "amount", -1 } }) clear("entity") part(SCANCODE.T, "flash", { { "r", 255 }, { "g", 60 }, { "b", 60 }, { "seconds", 0.3 } }) part(SCANCODE.T, "setVar", { { "name", "health" }, { "value", "1" } }) part(SCANCODE.T, "moveTo", { { "x", 360 }, { "y", 420 } }) rule("Out of lives", "frame", {}) part(SCANCODE.C, "test", { { "expr", "lives <= 0" } }) part(SCANCODE.T, "gameOver", {}) rule("A rock slips past the bottom", "frame", { { "each", "rock" } }) part(SCANCODE.C, "test", { { "expr", "self.y > 500" } }) part(SCANCODE.T, "destroy", {}) -- The readout, kept from the starter, says the lives too. ruleGo(1, 1) form("text", '"score " .. score .. " lives " .. lives') tutorialShot("rules") elseif stage == 7 then key(SCANCODE.S) local built = tutorialEnd() local mine = onOverlayUpdate dofile(built) game = onOverlayUpdate onOverlayUpdate = mine authorKeyDown(0, SCANCODE.SPACE.value) elseif stage == 19 then tutorialShot("playing") debugPrint(string.format("%s after firing a while: score %s, lives %s, %d rocks, %d shots", TUTORIAL.tag, tostring(AUTHOR_VARS.score), tostring(AUTHOR_VARS.lives), authorCount("rock"), authorCount("shot"))) elseif stage == 20 then debugPrint(TUTORIAL.tag .. " RESULT done") singeQuit() end end function onOverlayUpdate() frames = frames + 1 if frames % 12 == 0 and stage < 20 then stage = stage + 1 step() end if game ~= nil then return game() end forgeDraw(nil, nil) return OVERLAY_UPDATED end