-- Forge tutorial 9, "A rail shooter": a camera on a rail with stops, a painted backdrop and an -- occluder pillar that is only in the picture, zombies that spawn at each stop and walk to the -- camera, a gun on the pointer, ragdolls, and the rail moving on. Starts from New game. TUTORIAL = { number = 9, name = "09-rail", tag = "TUTORIAL9" } dofile("testScripts/tutorialDrive.singe") local frames = 0 local stage = 0 local game = nil local fired = 0 local shot = false 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 meshType(name, w, h, d, r, g, b) key(SCANCODE.MAIN_2) key(SCANCODE.A) form("name", name) form("look", "mesh") form("look.shape", "box") form("look.w", w) form("look.h", h) form("look.d", d) if r then form("look.r", r) form("look.g", g) form("look.b", b) end end local function placeAt(id, x, y, z) key(SCANCODE.MAIN_1) key(SCANCODE.A) form("id", id) form("x", x) form("y", y) form("z", z) 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 -- A rail point: K puts one where the editor camera stands, and the form moves it. local function railPoint(at, x, y, z, lookX, lookY, lookZ, stop) key(SCANCODE.K) form("at", at) form("x", x) form("y", y) form("z", z) form("lookX", lookX) form("lookY", lookY) form("lookZ", lookZ) if stop then form("stop", stop) end end local function step() if stage == 1 then 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) key(SCANCODE.MAIN_1) forgeSelect(nil) form("title", "The Dead Yard") form("vars", "score=0, health=3") form("layers", "scene3d, overlay") form("scene3d.fov", 60) elseif stage == 2 then -- The room: a floor, the yard as a painting at the back, and the pillar in it as an -- occluder, so a zombie walks behind a pillar that is only paint. meshType("floor", 40, 0.2, 60, 70, 75, 90) form("behaviours", "solid") meshType("painting", 24, 16, 0.2, 255, 255, 255) form("look.texture", "yard.png") form("look.unlit", "true") meshType("pillar", 1.2, 8, 1.2) form("look.occluder", "true") tutorialShot("room") elseif stage == 3 then key(SCANCODE.A) form("name", "camera") form("look", "none") form("behaviours", "camera") form("camera.mode", "rail") form("camera.track", "rail") key(SCANCODE.A) form("name", "gun") form("look", "none") form("vars", "ammo=6") form("behaviours", "gun") form("gun.player", 1) form("gun.ammo", 6) form("gun.reload", "offscreen") key(SCANCODE.A) form("name", "zombie") form("look", "model") form("look.file", "fox.glb") form("look.scale", 0.01) form("look.w", 0.8) form("look.h", 0.9) form("look.d", 1.4) form("vars", "health=2") form("behaviours", "health, seek, animator, target, timer") form("health.max", 2) form("health.ragdoll", "true") form("health.linger", 2) form("seek.target", "camera") form("seek.speed", 1.6) form("seek.stopAt", 2.2) form("animator.idle", "Survey") form("animator.walk", "Walk") form("animator.attack", "Run") form("timer.name", "bite") form("timer.after", 1.5) form("timer.start", "false") tutorialShot("zombie") elseif stage == 4 then lightType("floor") placeAt("floor", 0, -0.1, -20) key(SCANCODE.MAIN_2) lightType("painting") placeAt("painting", 0, 6, -30) key(SCANCODE.MAIN_2) lightType("pillar") placeAt("pillar", 0, 4, -22) key(SCANCODE.MAIN_2) lightType("camera") placeAt("camera", 0, 1.6, 0) key(SCANCODE.MAIN_2) lightType("gun") placeAt("gun", 0, 0, 0) selectEntity("pillar") tutorialShot("placed") elseif stage == 5 then -- The rail: a track of points in time, two of them stops. key(SCANCODE.MAIN_4) key(SCANCODE.N) form("name", "rail") railPoint(0, 0, 1.6, 0, 0, 1.2, -10) railPoint(4, 0, 1.6, -6, 0, 1.2, -16, "gate") railPoint(10, 2, 1.6, -14, 2, 1.2, -24, "pillar") railPoint(14, 2, 1.6, -18, 2, 1.2, -28) tutorialShot("rail") elseif stage == 6 then key(SCANCODE.MAIN_3) rule("The gate: two come out of the dark", "stopped", { { "name", "gate" } }) part(SCANCODE.T, "spawn", { { "type", "zombie" }, { "x", -2 }, { "y", 0 }, { "z", -14 } }) part(SCANCODE.T, "spawn", { { "type", "zombie" }, { "x", 2 }, { "y", 0 }, { "z", -15 } }) rule("The pillar: three more, one from behind it", "stopped", { { "name", "pillar" } }) part(SCANCODE.T, "spawn", { { "type", "zombie" }, { "x", -1 }, { "y", 0 }, { "z", -23 } }) part(SCANCODE.T, "spawn", { { "type", "zombie" }, { "x", 0 }, { "y", 0 }, { "z", -26 } }) part(SCANCODE.T, "spawn", { { "type", "zombie" }, { "x", 4 }, { "y", 0 }, { "z", -24 } }) rule("A zombie walks the moment it is made", "spawn", { { "type", "zombie" } }) part(SCANCODE.T, "setState", { { "state", "walk" } }) rule("A shot lands", "hit", { { "type", "zombie" } }) part(SCANCODE.T, "damage", { { "amount", 1 } }) part(SCANCODE.T, "addScore", { { "amount", 100 } }) part(SCANCODE.T, "playSound", { { "file", "hit.wav" } }) tutorialShot("hitRule") elseif stage == 7 then rule("It reaches the camera and rears up", "arrived", { { "type", "zombie" } }) part(SCANCODE.T, "setState", { { "state", "attack" } }) part(SCANCODE.T, "timerStart", { { "name", "bite" }, { "after", 1.5 } }) rule("The bite lands unless it was killed in time", "timer", { { "type", "zombie" }, { "name", "bite" } }) part(SCANCODE.C, "inState", { { "state", "attack" } }) part(SCANCODE.T, "addVar", { { "name", "health" }, { "amount", -1 } }) clear("entity") part(SCANCODE.T, "flash", { { "r", 255 }, { "g", 40 }, { "b", 40 }, { "seconds", 0.3 } }) part(SCANCODE.T, "shake", { { "amount", 0.3 }, { "seconds", 0.4 } }) part(SCANCODE.T, "timerStart", { { "name", "bite" }, { "after", 1.5 } }) rule("The stop is clear: move on", "frame", {}) part(SCANCODE.C, "waiting", { { "entity", "camera" } }) part(SCANCODE.C, "test", { { "expr", 'count("zombie") == 0' } }) part(SCANCODE.T, "pathNext", { { "entity", "camera" } }) rule("Out of health", "frame", {}) part(SCANCODE.C, "test", { { "expr", "health <= 0" } }) part(SCANCODE.C, "once", { { "tag", "over" }, { "scope", "game" } }) part(SCANCODE.T, "say", { { "text", '"YOU ARE DEAD"' }, { "seconds", 2 } }) part(SCANCODE.T, "gameOver", {}) rule("The end of the rail", "railEnd", {}) part(SCANCODE.T, "say", { { "text", '"STAGE CLEAR"' }, { "seconds", 2 } }) ruleGo(1, 1) form("text", '"score " .. score .. " health " .. health .. " ammo " .. gun.ammo') tutorialShot("rules") elseif stage == 8 then key(SCANCODE.S) local built = tutorialEnd() local mine = onOverlayUpdate dofile(built) game = onOverlayUpdate onOverlayUpdate = mine end end function onOverlayUpdate() frames = frames + 1 if frames % 12 == 0 and stage < 8 then stage = stage + 1 step() end if game ~= nil then local t = authorTime() -- At the gate stop, shoot the first zombie through the middle of the picture. if (t > 5) and (fired < 2) and (authorCount("zombie") > 0) then local zombie = authorEntity and nil local sx, sy = sceneProject(-2, 0.6, -14) fired = fired + 1 authorSetPointer(1, sx, sy) authorSwitchDown(SWITCH_BUTTON3) authorSwitchUp(SWITCH_BUTTON3) end if (t > 6) and not shot then shot = true tutorialShot("playing") end if t > 8 then debugPrint(string.format("%s after 8 seconds: score %s, health %s, %d zombies, camera %s", TUTORIAL.tag, tostring(AUTHOR_VARS.score), tostring(AUTHOR_VARS.health), authorCount("zombie"), authorWaiting(authorEntity("camera")) and "waiting" or "riding")) if authorCount("zombie") == 0 then debugPrint(TUTORIAL.tag .. " FAIL the gate stop should have spawned zombies") end debugPrint(TUTORIAL.tag .. " RESULT done") singeQuit() end return game() end forgeDraw(nil, nil) return OVERLAY_UPDATED end