308 lines
8.8 KiB
Text
308 lines
8.8 KiB
Text
-- 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
|