Skip to content

Tutorial 08 Quest

bryanthaboi edited this page Jul 19, 2026 · 1 revision

Tutorial 08 — Quest

Goal. Build a multi-map fetch quest: an NPC asks for an item, private state tracks progress across saves, and completion plays a scripted cutscene.

Prerequisites. Tutorials 06–07; Concepts: Save Model; Reference: Script Commands.

Files created.

mods/tutorial_08_quest/
├── manifest.json
└── main.lua

Steps

  1. Quest state in mod.save, not global flags. Flags are a shared namespace every mod and the story can see; mod.save is yours alone and survives saves. Script rows reach it through set_field with the mod: prefix:

    -- stage 0 = not started, 1 = asked, 2 = done
  2. The quest giver. A talk script in Pallet Town that branches on stage — reading state via a check the script sets up:

    return function(mod)
      mod.content.map_scripts:register("PALLET_TOWN", {
        talk = {
          TEXT_TUT8_GIVER = {
            { "check_flag", "TUT8_DONE" },
            { "jump_if_true", "thanks" },
            { "check_item", "NUGGET" },
            { "jump_if_true", "complete" },
            { "show_text", "Fetch me a NUGGET\nfrom MODROUTE!" },
            { "jump", "end" },
    
            { "label", "complete" },
            { "take_item", "NUGGET", 1 },
            { "set_flag", "TUT8_DONE" },
            { "jump", "cutscene" },
    
            { "label", "thanks" },
            { "show_text", "Thanks again!" },
            { "jump", "end" },
    
            { "label", "cutscene" },
            { "play_music", "Music_Surfing", { keep = false } },
            { "emote", "player", "happy", 40 },
            { "walk_npc", "player", { "down", "down" } },
            { "fade", "out", 30 },
            { "warp", "PALLET_TOWN", 10, 8, "down" },
            { "fade", "in", 30 },
            { "show_text", "You did it!\nQuest complete!" },
          },
        },
      })
      mod.content.maps:patch("PALLET_TOWN", {
        objects = { __append = {
          { index = 92, x = 12, y = 8, sprite = "SPRITE_SCIENTIST",
            movement = "STAY", range = "NONE", text = "TEXT_TUT8_GIVER" },
        } },
      })
    end

    Outcome: the giver asks, acknowledges the fetched item, runs a cutscene in pure command rows — no raw Lua — and thanks you ever after. (The quest flag here is a story flag so the talk rows can branch on it; keep bulk private state in mod.save.)

  3. The item on the far map. Hide a Nugget on tutorial 07's MODROUTE through the field deep registry:

    mod.content.field:patch("hiddenItems", {
      MODROUTE = { { x = 4, y = 2, item = "NUGGET" } },
    })

    Outcome: pressing A on that cell finds the Nugget.

  4. An onStep nudge. A compose contribution can add a step trigger beside the map's own scripts:

    mod.content.map_scripts:register("MODROUTE", {
      onStep = function(game, ow, x, y)
        if x == 4 and y == 2 and not game.save.flags.TUT8_HINTED then
          game.save.flags.TUT8_HINTED = true
        end
        -- return true to consume the step (stop later handlers)
      end,
    })

    The handler is onStep(game, overworld, cellX, cellY); contributions chain with first-truthy-return-consumes semantics (src/script/MapScripts.lua). Outcome: stepping on the spot marks a hint flag once — and the map's own onStep (if any) still runs, because compose contributions stack instead of replacing.

  5. A migration for the future. When v1.1 renames its state, old saves upgrade themselves:

    mod.migrations:add("1.1.0", function(save)
      local bucket = save.modData[mod.id]
      if bucket and bucket.stage == nil then bucket.stage = 0 end
    end)

    Outcome: a save written by mod 1.0.0 loads cleanly under 1.1.0 (Save Model).

Checkpoint

Fresh game: giver asks → find the Nugget on MODROUTE → return → cutscene plays → subsequent talks thank you. Save and reload at each stage; the quest resumes where it stood. Disable the mod: Pallet Town and the flags' effects are gone, and the save loads clean.

Common pitfalls

  • Global flags for private state. Another mod's TUT8_DONE collides with yours; prefix flags with your mod id, or better, keep state in mod.save and use flags only where script rows must branch.
  • Blocking verbs in parallel scripts. run_parallel scripts may not use foreground commands (show_text, ...) or move the player — the runner errors, attributed to your mod.
  • Two cutscenes at once. Queued scripts are FIFO through mod.world:queueScript; a second queueScript while one runs returns an error rather than interleaving.

Next: Tutorial 09 — Custom Music.

Clone this wiki locally