-- Forge tutorial 10, "Releasing your game": the platformer with the arcade's plumbing -- lives, -- a coin slot, a continue, the bezel, the score sent to the board -- released to a folder of its -- own with everything it needs. Continues from tutorial 3. TUTORIAL = { number = 10, name = "10-release", tag = "TUTORIAL10" } dofile("testScripts/tutorialDrive.singe") local frames = 0 local stage = 0 local game = nil local folder = nil tutorialBegin("03-sprites") 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 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 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 -- The game's form: a title of its own, lives and credits, and the bezel around the picture. key(SCANCODE.MAIN_1) forgeSelect(nil) form("title", "Coin Run") form("vars", "score=0, lives=3, credits=0") form("layers", "world2d, bezel") tutorialShot("bezel") elseif stage == 2 then -- Spikes cost a life now, and a coin buys a continue. key(SCANCODE.MAIN_3) ruleGo(6, 0) key(SCANCODE.DOWN) key(SCANCODE.DOWN) key(SCANCODE.DOWN) part(SCANCODE.T, "addVar", { { "name", "lives" }, { "amount", -1 } }) clear("entity") rule("Out of lives", "frame", {}) part(SCANCODE.C, "test", { { "expr", "lives <= 0" } }) part(SCANCODE.C, "once", { { "tag", "over" }, { "scope", "game" } }) part(SCANCODE.T, "submitScore", { { "board", "default" } }) part(SCANCODE.T, "say", { { "text", '"GAME OVER"' }, { "seconds", 2 } }) part(SCANCODE.T, "gameOver", {}) rule("A coin", "pressed", { { "switch", "SWITCH_COIN1" } }) part(SCANCODE.T, "credit", {}) rule("Continue", "pressed", { { "switch", "SWITCH_START1" } }) part(SCANCODE.C, "test", { { "expr", "credits > 0 and lives <= 0" } }) part(SCANCODE.T, "addVar", { { "name", "credits" }, { "amount", -1 } }) clear("entity") part(SCANCODE.T, "setVar", { { "name", "lives" }, { "value", "3" } }) clear("entity") part(SCANCODE.T, "restart", {}) ruleGo(4, 1) form("text", '"score " .. score .. " lives " .. lives') tutorialShot("arcadeRules") elseif stage == 3 then key(SCANCODE.S) -- X releases: the compiled game, the runtime, the description, a games.dat, and every -- file the description names, in a folder of the game's own. key(SCANCODE.X) folder = forgeReleaseFolder() tutorialShot("released") local files = {} for name in lfs.dir(folder) do if name:sub(1, 1) ~= "." then files[#files + 1] = name end end table.sort(files) debugPrint(TUTORIAL.tag .. " released to " .. folder .. ": " .. table.concat(files, ", ")) for _, want in ipairs({ "CoinRun.singe", "CoinRun.game", "Author.singe", "games.dat", "Forge" }) do if lfs.attributes(folder .. "/" .. want) == nil then debugPrint(TUTORIAL.tag .. " FAIL the release lacks " .. want) end end if lfs.attributes(folder .. "/Forge/kit/hero.png", "mode") ~= "file" then debugPrint(TUTORIAL.tag .. " FAIL the release should carry the kit files it names") end elseif stage == 4 then -- Packed, as the tutorial's last step says, when the harness says where the engine is: a -- scene cannot know its own executable, so SINGE_BIN carries it, and without it the step -- is reported as not tried rather than failed. local bin = os.getenv("SINGE_BIN") if bin then local packed = TUTORIAL_OUT .. "CoinRun.game" os.remove(packed) os.execute(string.format('"%s" --pack "%s" "%s" > "%s" 2>&1', bin, folder, packed, TUTORIAL_OUT .. "pack.log")) -- What the packer said is the measure: a .game is a database, and the engine's own -- file lookups treat one as a game rather than as bytes, so its size is not readable -- from inside a running script. local log = io.open(TUTORIAL_OUT .. "pack.log", "r") local size = 0 if log then for line in log:lines() do debugPrint(TUTORIAL.tag .. " pack: " .. line) size = tonumber(line:match("Packed %d+ files %((%d+) bytes%)")) or size end log:close() end debugPrint(string.format("%s packed the release into %s: %d bytes", TUTORIAL.tag, packed, size)) if size < 1000 then debugPrint(TUTORIAL.tag .. " FAIL --pack should have written a .game") end else debugPrint(TUTORIAL.tag .. " --pack not tried: SINGE_BIN is not set") end tutorialEnd() -- The release runs from its own folder, finding its runtime and its files beside itself. local mine = onOverlayUpdate dofile(folder .. "/CoinRun.singe") game = onOverlayUpdate onOverlayUpdate = mine authorKeyDown(0, SCANCODE.RIGHT.value) elseif stage == 8 then tutorialShot("playing") debugPrint(string.format("%s the release plays from %s: score %s, lives %s, runtime at %s", TUTORIAL.tag, folder, tostring(AUTHOR_VARS.score), tostring(AUTHOR_VARS.lives), tostring(AUTHOR_DIR))) if AUTHOR_DIR ~= folder .. "/" then debugPrint(TUTORIAL.tag .. " FAIL the release should know its own folder") end elseif stage == 9 then debugPrint(TUTORIAL.tag .. " RESULT done") singeQuit() end end function onOverlayUpdate() frames = frames + 1 if frames % 12 == 0 and stage < 9 then stage = stage + 1 step() end if game ~= nil then return game() end forgeDraw(nil, nil) return OVERLAY_UPDATED end