186 lines
5.1 KiB
Text
186 lines
5.1 KiB
Text
-- What the tutorial scenes share: the keys a reader presses, pressed the same way, and the two
|
|
-- checks every tutorial ends on. A tutorial in docs/Forge.adoc names keys; its scene presses
|
|
-- them through here, so the text and the test cannot drift apart without the sweep saying so.
|
|
--
|
|
-- A scene sets TUTORIAL (its number and name) before loading this, and calls tutorialBegin to
|
|
-- open a description, tutorialShot to take a named picture, and tutorialEnd to check the result
|
|
-- against the shipped description and run the built game.
|
|
FORGE_LIBRARY = true
|
|
dofile("Forge/Forge.singe")
|
|
|
|
local font = fontLoad("Singe/FreeSansBold.ttf", 15)
|
|
|
|
fontSelect(font)
|
|
fontQuality(FONT_QUALITY_BLENDED)
|
|
overlaySetResolution(720, 480)
|
|
|
|
TUTORIAL_OUT = singeGetDataPath()
|
|
|
|
|
|
-- The engine hands onKeyPressed two integers; so does this.
|
|
function key(code, character)
|
|
forgeKey(character or 0, code.value)
|
|
end
|
|
|
|
|
|
-- Types text as a person would, one character at a time.
|
|
function typeText(text)
|
|
for at = 1, #text do
|
|
forgeKey(string.byte(text, at), 0)
|
|
end
|
|
end
|
|
|
|
|
|
-- ENTER until the field is the one being edited. Answers whether it was reached.
|
|
local function reach(field)
|
|
local guard = 0
|
|
|
|
while (FORGE.lastField ~= field) and (guard < 40) do
|
|
key(SCANCODE.RETURN)
|
|
guard = guard + 1
|
|
end
|
|
if FORGE.lastField ~= field then
|
|
debugPrint(string.format("%s FAIL the form never reached %s", TUTORIAL.tag, field))
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
-- ESC out of the form: off a list first, then off the field it left being typed.
|
|
local function leave()
|
|
if FORGE.picker ~= nil then
|
|
key(SCANCODE.ESCAPE)
|
|
end
|
|
if FORGE.editing ~= nil then
|
|
key(SCANCODE.ESCAPE)
|
|
end
|
|
end
|
|
|
|
|
|
-- Fills in one field of the form ENTER walks: ENTER until the field is the one being edited,
|
|
-- type the value, ENTER to keep it, ESC to leave the form. Exactly the keys the text names.
|
|
-- A field with a list narrows it to the value and takes it, which moves on to the next field.
|
|
function form(field, value)
|
|
if not reach(field) then
|
|
return
|
|
end
|
|
if FORGE.picker ~= nil then
|
|
pick(value)
|
|
else
|
|
typeText(tostring(value))
|
|
key(SCANCODE.RETURN)
|
|
end
|
|
leave()
|
|
end
|
|
|
|
|
|
-- Empties one field of the form: ENTER until it is the one being edited, DELETE, ESC.
|
|
function clear(field)
|
|
if not reach(field) then
|
|
return
|
|
end
|
|
if FORGE.picker ~= nil then
|
|
pick("(none)")
|
|
else
|
|
key(SCANCODE.DELETE)
|
|
end
|
|
leave()
|
|
end
|
|
|
|
|
|
-- Narrows the picker by typing and takes the first match.
|
|
function pick(name)
|
|
typeText(name)
|
|
key(SCANCODE.RETURN)
|
|
end
|
|
|
|
|
|
-- Selects the entity with an id, in the entities panel.
|
|
function selectEntity(id)
|
|
for index, entry in ipairs(forgeRoom().entities) do
|
|
if entry.id == id then
|
|
forgeSelect(index)
|
|
return
|
|
end
|
|
end
|
|
debugPrint(string.format("%s FAIL no entity called %s", TUTORIAL.tag, id))
|
|
end
|
|
|
|
|
|
-- Puts the panel on a mode, by TAB as a reader would.
|
|
function panel(mode)
|
|
local guard = 0
|
|
|
|
while (FORGE.mode ~= mode) and (guard < 6) do
|
|
key(SCANCODE.TAB)
|
|
guard = guard + 1
|
|
end
|
|
end
|
|
|
|
|
|
-- A picture for the book, drawn now and named.
|
|
function tutorialShot(name)
|
|
singeScreenshot(string.format("%02d-%s", TUTORIAL.number, name))
|
|
end
|
|
|
|
|
|
-- Opens the tutorial's working copy: a fresh New game, or the previous tutorial's shipped
|
|
-- description, so each starts where the last left off.
|
|
function tutorialBegin(fromShipped)
|
|
local work = TUTORIAL_OUT .. TUTORIAL.name .. ".game"
|
|
|
|
if fromShipped then
|
|
if not authorCopy("Forge/tutorials/" .. fromShipped .. ".game", work) then
|
|
debugPrint(TUTORIAL.tag .. " FAIL could not copy " .. fromShipped)
|
|
singeQuit()
|
|
end
|
|
else
|
|
authorSave(forgeTemplate(), work)
|
|
end
|
|
if not forgeBegin(work) then
|
|
debugPrint(TUTORIAL.tag .. " FAIL could not open " .. work)
|
|
singeQuit()
|
|
end
|
|
|
|
return work
|
|
end
|
|
|
|
|
|
-- The two checks: what the keys made equals the shipped description, compiled; and the game it
|
|
-- makes runs. A shipped description that is not there yet is written, so a new tutorial's scene
|
|
-- produces the file the book ships, and the next run checks against it.
|
|
function tutorialEnd()
|
|
local shipped = "Forge/tutorials/" .. TUTORIAL.name .. ".game"
|
|
local built
|
|
|
|
forgeSave()
|
|
-- The compiled header names the description's file, which a release has set and a shipped
|
|
-- copy has not; the comparison is of the game, not of where it came from.
|
|
FORGE.game.source = nil
|
|
if lfs.attributes(shipped, "mode") ~= "file" then
|
|
authorCopy(FORGE.path, TUTORIAL_OUT .. TUTORIAL.name .. ".shipped.game")
|
|
debugPrint(TUTORIAL.tag .. " WROTE " .. TUTORIAL_OUT .. TUTORIAL.name .. ".shipped.game -- copy it to " .. shipped)
|
|
else
|
|
local mine = authorCompile(FORGE.game)
|
|
local theirs = authorCompile(authorLoad(shipped))
|
|
|
|
if mine == theirs then
|
|
debugPrint(TUTORIAL.tag .. " the keys made the shipped description")
|
|
else
|
|
local out = assert(io.open(TUTORIAL_OUT .. TUTORIAL.name .. ".mine.singe", "w"))
|
|
|
|
out:write(mine)
|
|
out:close()
|
|
out = assert(io.open(TUTORIAL_OUT .. TUTORIAL.name .. ".theirs.singe", "w"))
|
|
out:write(theirs)
|
|
out:close()
|
|
debugPrint(TUTORIAL.tag .. " FAIL the keys made something else; see " .. TUTORIAL_OUT .. TUTORIAL.name .. ".mine.singe")
|
|
end
|
|
end
|
|
built = forgeBuild(TUTORIAL_OUT .. TUTORIAL.name .. ".singe")
|
|
forgeClose()
|
|
|
|
return built
|
|
end
|