hopkit

Find your next move

Search the guide, API, and recipes.

    Pocket Trail / Chapter 07 of 08

    Another trail

    Pause, save, and set off somewhere new.

    Download checkpoint
    src/chapters/07.ts
    import {
      makeGame,
      drawTrail,
      movePlayer,
      playerAtStart,
      addCoins,
      platforms,
      trailWidth,
    } from "../trail.js"
    import { animateHero, playSound, coinFeedback, mushroomBump } from "../feedback.js"
    import { readLevel, saveLevel, clearSave } from "../save.js"
    import type { LessonOptions } from "../types.js"
    
    export async function mount(canvas: HTMLCanvasElement, options: LessonOptions) {
      const game = await makeGame(canvas, options)
      function enterLevel(level: 0 | 1): void {
        game.scene((scene) => {
          const terrain = platforms.map((box, index) => ({
            ...box,
            y: box.y - (level === 1 && index > 0 ? 20 : 0),
          }))
          drawTrail(scene, terrain)
          const player = playerAtStart()
          const hero = scene.sprite("hero", {
            bind: player,
            anchor: "center",
            width: 64,
            height: 64,
          })
          const coins = addCoins(scene, terrain)
          let elapsed = 0
          let hurtUntil = 0
          let won = false
          scene.camera.follow(player, {
            bounds: { x: 0, y: 0, width: trailWidth, height: 360 },
            speed: options.cameraSpeed,
          })
          scene.input.onPress("pause", () => {
            game.pause()
            options.status("Paused. Choose Resume to keep walking.")
          })
          scene.update((dt) => {
            if (won) return
            elapsed += dt
            const direction = scene.input.axis("left", "right")
            if (movePlayer(player, direction, scene.input.pressed("jump"), dt, options, terrain))
              playSound(scene, options, "jump")
            animateHero(scene, hero, player, direction, elapsed, options)
            hurtUntil = mushroomBump(scene, hero, player, options, elapsed, hurtUntil)
            if (coins.collect(player)) {
              playSound(scene, options, "gem")
              coinFeedback(scene, player, options)
            }
            if (!coins.finished(player)) return
            won = true
            if (level === 0) {
              const saved = saveLevel(1)
              options.status(
                saved
                  ? "First trail complete. Progress saved. Next trail in a moment…"
                  : "First trail complete. Storage unavailable; next trail still works.",
              )
              scene.wait(1.2, () => enterLevel(1))
            } else
              options.status(
                "Both trails complete. You made a whole game. Start a new save to play again.",
              )
          })
          options.status(
            `Trail ${level + 1} of 2. Collect four coins and reach the flag. Escape pauses.`,
          )
          options.ready(scene)
        })
      }
      enterLevel(readLevel())
      return {
        game,
        restart: () => game.restart(),
        newGame: () => {
          clearSave()
          enterLevel(0)
        },
      }
    }
    Live gameOpen game ↗
    Play, change a setting, and play again.How it works ↓

    The game now has two trails. Finish the first to unlock the second. The platforms on the second trail sit a little higher.

    Try it

    Press Escape or Pause during a jump. Choose Resume inside the canvas. Your position and timers resume together. Finish the first trail, then reload. If local storage is available, the next session starts on trail two.

    New save clears the tutorial’s saved level and starts again. It does not touch other storage entries.

    Save a small fact

    SaveStore and SnapshotCodec, imported from @hopkit/game, store a versioned level number. It never serializes sprites, callbacks or a whole engine instance. readLevel accepts only the known version and a valid level; missing, malformed or blocked storage starts trail one.

    enterLevel replaces the scene. Hopkit removes the previous scene’s sprites, input subscriptions, timers and sound voices. The second scene owns new state. The outer game keeps its canvas and loaded artwork.

    The canvas Resume button calls game.resume(). Canvas UI keeps responding while simulation is paused. A paused scene cannot depend on a gameplay update callback to resume.

    Read the shared save source
    src/save.ts
    import { SaveStore, SnapshotCodec } from "@hopkit/game"
    
    const progress = new SaveStore(
      "hopkit-pocket-trail-v2",
      new SnapshotCodec<0 | 1>({
        format: "pocket-trail",
        version: 1,
        validate(value) {
          if (value !== 0 && value !== 1) throw new Error("A saved trail must be 0 or 1")
          return value
        },
      }),
    )
    
    export function readLevel(): 0 | 1 {
      const result = progress.load()
      return result.ok ? result.value : 0
    }
    export function saveLevel(level: 0 | 1): boolean {
      return progress.save(level).ok
    }
    export function clearSave(): void {
      progress.remove()
    }

    Make one change

    Add a third level in your copy. Extend both the level selection and save validation before trusting the stored value.