A walk through the forest
(Or whatever this game will end up being called)
I’ve started a Narrat Jam game. It’s an interactive forest walk with branching and pictures to click on.
I’m thinking of having pictures of plants and info about them. maybe a fun mode where there’s also a test where you have to identify plants I don’t know.
Video of the basic system working (I can’t upload video here so here’s a link to a mastodon post):
How it works
I’ve made a system to read a yaml data file and use that as the base to dynamically do everything. So in this case, the file defining all the paths etc looks like this:
Source code
I will probably add a basic setup for making this type of point and click game to the narrat examples repo at some point, but for now here’s the important source code (as of today) if anyone wants to use it as a base for something:
Code Details
Utilities
utilities.narrat which contains general useful functions:
add_paths content:
  if $content.paths:
    log "adding paths"
    for_of $content.paths add_path
  elseif (< $current.index (- $current.path.contents.length 1)):
    log "Adding default forward path"
    var path (new Object)
    set path.id "next"
    set path.x 960
    set path.y 540
    set path.arrow "forward"
    run add_path $path
  if (!= $content.backAllowed false):
    var backPath (new Object)
    set backPath.direction "back"
    set backPath.id "previous"
    if $content.back:
      set backPath.id $content.back
    set backPath.x 960
    set backPath.y 900
    set backPath.arrow "back"
    run add_path $backPath
add_path path:
  var arrowType $path.arrow
  log "adding path " $path.id " with arrow " $arrowType
  var arrowConfig $data.SPRITES[$arrowType]
  var spriteData (new Object)
  set spriteData.background $arrowConfig.background
  set spriteData.x $path.x
  set spriteData.y $path.y
  set spriteData.width $arrowConfig.width
  set spriteData.height $arrowConfig.height
  set spriteData.anchor.x $arrowConfig.anchor.x
  set spriteData.anchor.y $arrowConfig.anchor.y
  var pathCallback (concat "load_path " $path.id )
  if (== $path.direction "back"):
    set pathCallback (concat "go_back " $path.id)
  set spriteData.onClick $pathCallback
  run load_sprite $spriteData
load_sprite spriteData:
  var sprite (create_sprite $spriteData.background $spriteData.x $spriteData.y)
  run setup_sprite $spriteData $sprite
  return $sprite
load_object spriteData:
  var object (create_object $spriteData.x $spriteData.y)
  run setup_sprite $spriteData $object
  return $object
setup_sprite spriteData sprite:
  if $spriteData.anchor:
    set sprite.anchor.x $spriteData.anchor.x
    set sprite.anchor.y $spriteData.anchor.y
  if $spriteData.width:
    set sprite.width $spriteData.width
  if $spriteData.height:
    set sprite.height $spriteData.height
  if $spriteData.onClick:
    set sprite.onClick $spriteData.onClick
  if $spriteData.layer:
    set sprite.layer $spriteData.layer
  if $spriteData.cssClass:
    set sprite.cssClass $spriteData.cssClass
  return $sprite
load_path pathId:
  log "Moving to path %{$pathId}"
  var previousId $previousPathId
  var previous $PATHS[$previousId]
  set current.lastIndex $current.index
  set previousPathId $current.id
  if (== $pathId "next"):
    set current.index (+ $current.index 1)
  else:
    set current.id $pathId
    set current.path $PATHS[$pathId]
    set current.index 0
  run process_current_path
go_back backId:
  if (!= $backId "previous"):
    set current.id $backId
    set current.path $PATHS[$backId]
    set current.index (- $current.path.contents.length 1)
  else:
    set current.index (- $current.index 1)
  if (>= $current.index 0):
    run process_current_path
process_current_path:
  empty_sprites
  set current.content $current.path.contents[$current.index]
  if $current.content.bg:
    log "Setting background to %{$current.content.bg}"
    set_screen $current.content.bg 0 fade 1000 0
  run add_paths $current.content
  run add_pics $current.content
Pics display
pics.narrat which takes care of reading the info about pics and adding them to the screens so they appear and are clickable:
add_pics content:
  if $content.pics:
    log "adding pics"
    for_of $content.pics add_pic
add_pic pic:
  var imagePath (concat "img/pics/" $pic.img)
  var width 160
  var height 90
  var spriteData (new Object)
  set spriteData.background $imagePath
  set spriteData.cssClass pic-preview
  set spriteData.x $pic.x
  set spriteData.y $pic.y
  set spriteData.width $width
  set spriteData.height $height
  set spriteData.onClick (concat "show_pic " $pic.label " " $imagePath)
  run load_sprite $spriteData
show_pic label img:
  log "Show pic %{$label} %{$img}"
  set data.overlay (run load_object $data.SPRITES.big_overlay)
  var width 1760
  var height 990
  var spriteData (new Object)
  set spriteData.background $img
  set spriteData.x 20
  set spriteData.y 20
  set spriteData.anchor.x 0
  set spriteData.anchor.y 0
  set spriteData.width $width
  set spriteData.height $height
  set spriteData.cssClass "full-pic"
  set spriteData.onClick "close_pic"
  set spriteData.layer 2
  set data.currentPic (run load_sprite $spriteData)
  if $label:
    run $label
close_pic:
  log "Closing pic"
  delete_sprite $data.currentPic
  delete_sprite $data.overlay
Main game script
Basic game.narrat which mostly uses the above:
main:
  set data.PATHS (load_data data/forest.yaml)
  set data.SPRITES (load_data data/common_sprites.yaml)
  set data.previousPathId forest
  set current (new Object)
  set current.id forest
  set current.path $PATHS.forest
  set current.index 0
  set_screen @empty 1
  set_screen @empty 2
  run load_path $current.id
dr_pepper:
  clear_dialog
  "A can of Dr. Pepper."
  "I didn't know they grew in forests."
Yaml file for paths
forest.yaml, which contains the actual data for all the paths
forest:
  id: Forest Entrance
  contents:
    - bg: entrance1
      backAllowed: false
    - bg: entrance2
    - bg: entrance3
      pics:
        - img: dr_pepper.jpeg
          x: 100
          y: 800
          label: dr_pepper
          ratio: 16/9
      paths:
        - id: entrance-fork-1
          x: 200
          y: 500
          arrow: left
        - id: step-2
          x: 1500
          y: 300
          arrow: forward
entrance-fork-1:
  id: Entrance Fork 1
  contents:
    - bg: entrance-fork-1
      back: forest
step-2:
  id: Step 2
  contents:
    - bg: step-2-1
      back: forest
    - bg: step-2-2
Sprites data
common_sprites.yaml, which contains a list of sprites info for easily adding commonly needed sprites:
left:
  background: img/ui/left.webp
  x: 100
  y: 250
  width: 200
  height: 200
  anchor:
    x: 0.5
    y: 0.5
  onClick: choose_left
right:
  background: img/ui/right.webp
  x: 770
  y: 250
  width: 200
  height: 200
  anchor:
    x: 0.5
    y: 0.5
  onClick: choose_right
forward:
  background: img/ui/front.webp
  x: 440
  y: 120
  width: 200
  height: 200
  anchor:
    x: 0.5
    y: 0.5
  onClick: choose_front
back:
  background: img/ui/back.png
  x: 440
  y: 380
  width: 200
  height: 200
  anchor:
    x: 0.5
    y: 0.5
  onClick: choose_back
big_overlay:
  cssClass: big-overlay
  layer: 1
  x: 0
  y: 0
  width: 1920
  height: 1080
  anchor:
    x: 0
    y: 0
  onClick: close_pic
CSS file
And the main.css file:
/*  See https://docs.narrat.dev/guides/theming-the-game-and-ui for info on how to customise your game with CSS. */
html {
  font-size: 20px;
}
#app {
  /* Customise CSS variables here. They will override the existing narrat ones. You can also add your own variables */
  --bg-color: #131720;
  --text-color: #d9e1f2;
  --primary: hsl(255, 30%, 55%);
  --focus: hsl(210, 90%, 50%);
  --secondary: #42b983;
}
/* Add any other CSS you want here */
.big-overlay {
  background: rgba(0, 0, 0, 0.5);
}
.pic-preview {
  background-color: white;
  /* Polaroid style image */
  border-radius: 10px;
  border-top: 6px solid white;
  border-left: 6px solid white;
  border-right: 6px solid white;
  border-bottom: 6px solid white;
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.8), 0 6px 20px 0 rgba(0, 0, 0, 0.8);
}
.full-pic {
  border-top: 30px solid white;
  border-left: 10px solid white;
  border-right: 10px solid white;
  border-bottom: 10px solid white;
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.8), 0 6px 20px 0 rgba(0, 0, 0, 0.8);
}
.dialog.override {
  border-radius: 20px;
}

