Cookbook

On this page

Find what you want to make, copy the YAML into a file under data/ (or into game.yaml where the recipe says so), and change the names. Every block on this page is loaded by the tests over a small game, so it works as written; each recipe links to the full reference on the Schema page and, where it has the same feature, to the Go Zen sample. The gozen commands the recipes use are collected at the end, in Commands used by the recipes.

The recipes refer to a few IDs they do not define: the actor and class walker, the class mender, the skills attack and guard, the item key, the enemy mote, the troop wolves and the locations field, ridge, shop and gate. Use your own in their place.

See Planning screens and narrative checks for rosters, work, scene dependencies, materials and optional character voice checks.

By task #

You want toUses
Start a new gamestart in game.yaml, an opening event
Open a shopshops, items with a price, the shop command
Run an inn and a place to saveactions, if on gold, heal: all, save: true
Write talk with choices and branchespeople, say, choice, if
Remember things with flags and conditionsset, flag.* and var.*, when, trigger: auto
Let the player roll back and save in a sceneui.rollback, a choice’s id, chose.*
Stage portraits and effectsstand, effect, music fades, wait and sfx that hold
Add voicessay.voice, actor voices, ui.voice
Add a companion who joinsactors, join, leave
Grow by level and learn skillsclass stats formulas, exp_for, skills by level
Make items and equipmentitem use, kind, slot, stats, class equip
Add a status effectstates, add_state, remove_state
Set up troops and enemy AIenemies, troops, encounters, AI rules
Give the party tacticsactor ai, shared ai sets, tactic catalogs and presets
Stage a boss fightboss: true, troop events, battle with on_win
Send the party home instead of game overgame_over.event, battle.retry
Draw a dungeon by handdungeon.map, tiles
Put a chest in a dungeononce and after tiles
Lock a door with a keya solid tile with when, take
Make a one-way passageslide tiles
Generate dungeon floorsgenerate, place, dive
Let strong foes roam a floorroamers
Change a dungeon’s looklooks by floor, textures, darkness
Show a map with points and roadsmap, points, roads
Make a campaignregions, factions, campaign, orders, clash, gozen sim campaign
Mix a campaign with a dungeonan order with move, a dungeon’s way out, a.atk in clash
Paint a scene with people and balloonsstage, spots, dolls, bubbles
Give a character faces and a moving portraitfaces, say.face, puppet
Set the face iconsface_box, face_shade, a doll’s icon
Show event pictures and fill the galleryimage, gallery.cgs, gallery.scenes
Keep a character bookprofiles, ui.profiles, open: profiles
Keep a monster bookenemies.<id>.lore, ui.bestiary, bestiary.<id>.seen
Collect, raise and breed monstersrecruit, monster_families, breeding, open: ranch
Play music for places, battles and moodsmusic.songs, music, battle_music, mood rules
Change the language and the engine’s wordinglanguage, text, terms
Offer the game in two languageswritten_in, languages, lang/<code>.yaml
Test-play with notes, reload and quick startsmemo, --dev, F5, F9, --start
Measure battles with sim battle and sim divegozen sim battle, gozen sim dive
Check and measure in CIsetup-gozen, gozen check, gozen sim in GitHub Actions
Cut a release and a trialtrial.yaml, gozen release, gozen trial
Wear stone sets and fuse stonesstone_sets, fusions, equip_stones, stone_suggest, open: fusion
Set a retry point with a checkpointcheckpoint, restore_checkpoint
Insert a thought after a speaker’s linesasides
Dress the title screentitle_background, title_art, title_figures
Review pictures and scenes while writingcg_review, scene_review, --review-log
Tune a place, the windows and the battle paceno_menu, outfit, resume_to, talk_on_enter, ui.hide_gold, battle.tempo, battle.passives
Change states, duels and skill points from an eventaffect, fighters, reset_tree, sp_suggest, open_wall

Start a new game #

start in game.yaml says where a new game begins, with whom, and what they carry; start.event runs once before the player gets control.

# game.yaml
title: Lantern Trail
start:
  location: village
  party: [walker]
  gold: 50
  items: {key: 1}
  event: opening
locations:
  village: {name: Village, background: village.jpg, exits: [{to: field, label: Out to the field}]}
events:
  opening:
    do:
      - say: The lantern by the gate has gone out again.

Reference: Start, Game.

Open a shop #

A shop is a list of goods with prices. Open it from an action, an exit or anyone’s talk with the shop command; selling to it pays half the price.

items:
  tonic: {name: Tonic, price: 8, desc: Restores 30 HP., use: {scope: one_ally, effects: [{heal: 30}]}}
  lamp_oil: {name: Lamp oil, price: 3}
shops:
  general:
    name: General store
    greeting: Take a look around.
    keeper: keeper.png
    goods: [tonic, lamp_oil]
locations:
  market:
    name: Market
    actions:
      - {label: Shop, do: [{shop: general}]}
    exits: [{to: field, label: Out to the field}]

sell: false makes a shop that only sells, and spent: var.NAME adds every purchase to a variable. Reference: Shop, Item.

Run an inn and a place to save #

An inn is an action that takes gold and heals everyone; a save point is an action with save: true. safe: true also lets the player save from the menu here.

locations:
  inn:
    name: Inn
    safe: true
    actions:
      - label: Stay the night (10G)
        do:
          - if: gold >= 10
            then: [{gold: -10}, {heal: all}, {say: You wake up rested.}]
            else: [{say: You cannot pay for a room.}]
      - {label: Save, do: [{save: true}]}
    exits: [{to: field, label: Leave}]

Reference: Location, Action. Go Zen’s inn, The Dry Cup, has a rest and a save point.

Write talk with choices and branches #

A person’s talk is a list of commands. say shows a line, choice asks a question with its own commands for each answer, and if branches on a condition.

items:
  letter: {name: Letter, kind: key}
locations:
  well:
    name: Old well
    people:
      - name: Keeper
        image: keeper.png
        talk:
          - say: {who: Keeper, text: Will you carry a letter to the ridge?}
          - choice:
              - label: I will
                do:
                  - give: {item: letter}
                  - say: {who: Keeper, text: Thank you. Mind the wolves.}
              - label: Not now
                do: [{say: {who: Keeper, text: Come back if you change your mind.}}]
      - name: Guard
        talk:
          - if: item.letter > 0
            then: [{say: {who: Guard, text: A letter for the ridge? Go on.}}]
            else: [{say: {who: Guard, text: Nobody passes without business.}}]

A choice answer can have its own when, and prompt puts a line above the answers. Reference: Person, Say, ChoiceOption, Command.

Remember things with flags and conditions #

set writes flags (flag.NAME, true or false) and variables (var.NAME, numbers); anything with when appears only while its condition holds. An event with trigger: auto runs as soon as its when holds.

locations:
  crossing:
    name: Crossing
    people:
      - name: Ferryman
        when: "!flag.ferry_gone"
        talk:
          - set: {var.asked: "var.asked + 1"}
          - say: {who: Ferryman, text: Ask me again tomorrow.}
    exits:
      - {to: ridge, label: Wade across, when: flag.ferry_gone}
events:
  ferry_leaves:
    trigger: auto
    when: "var.asked >= 3 && !flag.ferry_gone"
    once: true
    do:
      - say: The ferry has left for the season. The river is low enough to wade.
      - set: {flag.ferry_gone: true}

To catch typos in names, declare the variables in game.yaml and turn on strict:

# game.yaml
state:
  strict: true
  names:
    var.asked: {type: number, initial: 0}
    flag.ferry_gone: {type: bool, initial: false}

Reference: Command (set, if), Event, StateNames, and the expressions on Writing a game. Go Zen opens its dungeon gate with flags.

Let the player roll back and save in a scene #

In the novel window, the player can step back a line with the “Back” button, Q or the mouse wheel: the flags, variables, items and the answers given go back too, and a choice comes back to be answered again. Read lines, the gallery and the settings stay. Rolling back stops at a battle, a shop, a save command, a screen, a move or the end, and reaches up to 8 earlier events; ui.rollback changes that number, and 0 turns rolling back off. “Save” and “Load” on the band save in the middle of a scene: loading begins the event again from its start and reads on to the line, with the same answers and the same random numbers.

# game.yaml
schema_version: 11
ui:
  message: novel
  rollback: 16

A choice with an id remembers the option taken, read as chose.<id>: the option’s id, or its number from 0. It is part of the state, so it rolls back and is saved.

events:
  dinner:
    do:
      - say: Soup or bread?
      - choice:
          - {id: soup, label: Soup}
          - {id: bread, label: Bread}
        id: dinner
      - if: chose.dinner == "soup"
        then: [{say: Warm.}]
        else: [{say: Crisp.}]

The game resumes a save made in the middle of an event at its saved line. Use a separate --save-dir when testing save and load through gozen run. Reference: UIStyle, Command (id).

Stage portraits and effects #

In the novel window, portraits come and go with the speakers by themselves. An event that uses stand places them as written instead: who stands where, how they come in (fade or slide) and when they leave. A line’s face still changes the expression of a portrait standing. effect shakes one portrait or the screen, plays a reaction of a moving portrait, or fades the screen through a color; music fades in and out; a wait with skip: false holds even in skip mode, and a sfx with wait: true holds until the sound ends.

# game.yaml
schema_version: 11
ui:
  message: novel
music:
  songs:
    rain: {file: rain.ogg}
events:
  meeting:
    do:
      - stand: {who: Keeper, face: keeper.png, at: left}
      - say: {who: Keeper, face: keeper.png, text: You came back.}
      - stand: {who: Guard, face: guard.png, at: right, enter: slide}
      - effect: {name: shake, who: Guard, seconds: 0.4}
      - say: {who: Guard, face: guard.png, text: Stand where you are.}
      - music: {song: rain, fade: 2}
      - wait: {seconds: 0.8, skip: false}
      - sfx: {name: door, wait: true}
      - stand: {who: Guard, leave: fade}
      - effect: {name: fade, color: "#000000", seconds: 1, hold: 0.5}
      - stand: none
      - music: {stop: true, fade: 3}

at is left, center, right or a fraction of the screen’s width. A name that is not an actor needs face, a picture under assets/; an actor’s face is one of its faces. The headless session sees stand as an effect (Effect.Kind stand, with the actor), so a test can check who stands in a scene. Every map form needs schema_version: 4; the old string forms work as before. Reference: Stand, EffectCue, WaitCue, SFXCue, MusicCue.

Add voices #

A line’s voice names its recording. For an actor with voices, a folder under assets/, it is a name in that folder, and the extension (.ogg or .wav) may be left out; for anyone else it is a path under assets/. A voice or a sound effect lasts at most 60 seconds; a longer file is skipped with a line in the log.

# game.yaml
schema_version: 11
ui:
  message: novel
  voice:
    cut: true        # the next line stops a voice still playing (the default)
    auto_wait: true  # auto waits for the voice to end (the default)
actors:
  keeper: {name: Keeper, class: walker, voices: keeper/voice}
events:
  welcome:
    do:
      # plays assets/keeper/voice/welcome_001.ogg
      - say: {who: keeper, voice: welcome_001, text: Come in out of the cold.}
      - say: {who: Traveler, voice: voice/traveler_001.ogg, text: Thank you.}

A voice plays when its line opens and stops at the next line. Skip mode, Ctrl over a line read, scene skip and the replay of rolling back pass the voices of the lines they pass; the line they land on speaks. The settings have a voice volume and a volume for each actor with voices. gozen check reports a voice file that is not there and counts the lines of a voiced actor without one (GZN0109). The headless session sees each voice as an effect (Effect.Kind voice, with the file), and a trace has a voice line for each voice played. Reference: Say (voice), Actor (voices), VoiceStyle.

Add a companion who joins #

A companion is an actor. join adds them to the party and leave takes them out; a flag keeps them from offering twice.

actors:
  sela:
    name: Sela
    class: mender
    faces: sela              # assets/sela/normal.png
    back: true               # starts in the back row
locations:
  hut:
    name: Hut
    people:
      - name: Sela
        image: sela/normal.png
        when: "!flag.sela_joined"
        talk:
          - say: {who: sela, text: You are going to the ridge? I will walk with you.}
          - join: sela
          - set: {flag.sela_joined: true}

join_exp in game.yaml raises a newcomer’s experience to the party’s. Reference: Actor, Command. Go Zen’s Ox, Tilly and Kes join this way.

Grow by level and learn skills #

A class gives each stat as a formula of level, the experience each level needs, and the skills learned on the way.

skills:
  spark: {name: Spark, mp: 3, effects: [{damage: a.mat * 2 - b.mdf}]}
  mend: {name: Mend, mp: 4, scope: one_ally, anim: heal, effects: [{heal: 10 + a.mat}]}
classes:
  scholar:
    name: Scholar
    stats: {mhp: "18 + level * 5", mmp: "6 + level * 2", atk: 3, def: 2, mat: "4 + level * 2", mdf: 4, agi: 5, luk: 2}
    exp_for: level * 25
    skills:
      - {level: 1, skill: spark}
      - {level: 4, skill: mend}
actors:
  ione: {name: Ione, class: scholar}

Experience comes from enemies’ exp and the exp command; learn teaches a skill at any time. A class tree adds a skill tree paid with SP.

reset_actor puts one actor back to the start, for a new game plus where only some companions begin again. It resets the level, experience, learnt skills, skill tree and SP, restores HP and MP and clears states. Equipment, the name, the row and whether the actor has joined stay. Add level to start from another level (1 to the class’s max); without it the level in the actors file is used. An unknown actor ID is a gozen check error, and an actor who has not joined yet is already at the start.

events:
  new_game_plus:
    do:
      - reset_actor: ione
      - reset_actor: ione
        level: 5

Reference: Class, Learning, Skill, Command.

Make items and equipment #

An item with use can be used from the menu or in battle. Equipment has a kind, the slot it goes into and the stats it adds; a class lists the kinds it can wear.

items:
  tonic:
    name: Tonic
    price: 8
    desc: Restores 30 HP.
    use: {scope: one_ally, effects: [{heal: 30}]}
  bronze_blade: {name: Bronze blade, kind: sword, slot: weapon, price: 60, stats: {atk: 4}}
  leather_vest: {name: Leather vest, kind: vest, slot: armor, price: 40, stats: {def: 3}}
classes:
  fencer:
    name: Fencer
    equip: [sword, vest]
    stats: {mhp: "20 + level * 6", mmp: 2, atk: "5 + level", def: 3, mat: 1, mdf: 2, agi: 6, luk: 2}
    exp_for: level * 20
actors:
  fen: {name: Fen, class: fencer, equip: {weapon: bronze_blade}}

The slots are weapon, armor and accessory unless equip_slots in game.yaml says otherwise. Reference: Item, Usage, Slot.

Add a status effect #

A state is put on with add_state and taken off with remove_state (or bad for every bad state). on_turn_end runs its effects at the end of each turn.

states:
  poison:
    name: Poison
    short: PSN
    bad: true
    turns: 5
    on_turn_end: [{damage: 3}]
    messages: {add: "{target} is poisoned!", remove: "{target} feels better."}
skills:
  sting: {name: Sting, effects: [{damage: a.atk}, {add_state: poison, chance: 0.4}]}
items:
  antidote: {name: Antidote, price: 6, use: {scope: one_ally, effects: [{remove_state: poison}]}}

restrict: skip makes a state cost turns (sleep), stats changes the holder’s stats (a buff), and persist: true keeps it after the battle. Reference: State, Effect.

Set up troops and enemy AI #

An enemy needs a picture, all eight stats and its AI: rules tried by priority, each with an optional condition and target. A troop is the group met together; encounters sets how often troops are met here.

skills:
  bite: {name: Bite, effects: [{damage: a.atk + 2}]}
enemies:
  bat:
    name: Bat
    image: bat.png
    stats: {mhp: 14, mmp: 0, atk: 4, def: 1, mat: 0, mdf: 1, agi: 7, luk: 2}
    exp: 4
    gold: 3
    drops: [{item: key, chance: 0.1}]
    ai:
      - {use: bite, when: "self.hp_rate < 0.5", priority: 1}
      - {use: attack, target: weakest}
troops:
  bats: {members: [bat, bat]}
  mixed: {members: [bat, mote]}
locations:
  cave_mouth:
    name: Cave mouth
    encounters:
      rate: 0.25
      troops: [{troop: bats, weight: 3}, {troop: mixed}]

Reference: Enemy, Rule, Target, Troop, Encounter. To see how hard a troop is, measure it.

Give the party tactics #

Party members on Auto follow their own ai, written inline or as a shared set under ai. The tactics screen lets the player change them; the catalogs say which conditions, targets and actions it offers.

skills:
  mend: {name: Mend, mp: 3, scope: one_ally, anim: heal, effects: [{heal: 12}]}
ai:
  healer:
    - {use: mend, when: "allies.hp_rate < 0.7", target: ally_lowest_hp_rate, priority: 1}
    - {use: attack}
actors:
  sela: {name: Sela, class: mender, ai: healer}
tactic_conditions:
  always: {name: Always, kind: always}
  hurt: {name: An ally is hurt, kind: hp_below, subject: allies}
tactic_targets:
  foe: {name: A foe, scope: one_enemy}
  weakest: {name: The most hurt ally, scope: one_ally, selector: {pick: min, by: t.hp_rate}}
tactic_actions:
  strike: {name: Attack, use: attack, target: foe}
  heal: {name: Mend, use: mend, target: weakest}
# game.yaml
battle:
  tactic_presets:
    careful:
      name: Careful
      description: Heal first, then attack.
      rows:
        healer:                # the AI the rows replace
          - {enabled: true, condition: hurt, percent: 60, action: heal}
          - {enabled: true, condition: always, action: strike}

The rows are keyed by the AI they replace. In an event, set: {actor.sela.tactics: careful} picks a preset and auto or manual switches the mode. Reference: Actor, Rule, TacticPreset, TacticCondition, BattleCfg. Go Zen’s party has AI rules and three presets.

Stage a boss fight #

A boss is an enemy with boss: true. Its troop can forbid running away, say a line at the start, and run commands mid-battle; the battle command decides what winning and losing do.

skills:
  quake: {name: Quake, scope: all_enemies, effects: [{damage: 8}]}
enemies:
  warden:
    name: Stone warden
    title: Keeper of the gate
    image: warden.png
    boss: true
    stats: {mhp: 120, mmp: 0, atk: 9, def: 4, mat: 0, mdf: 3, agi: 3, luk: 1}
    exp: 60
    gold: 40
    ai:
      - {use: quake, when: "turn % 3 == 0", priority: 1}
      - {use: attack}
troops:
  warden:
    members: [warden]
    can_escape: false
    intro: The statue in the gate opens its eyes.
    events:
      - {when: "turn == 4", do: [{say: Cracks run across the warden's shell.}]}
locations:
  gate_hall:
    name: Gate hall
    actions:
      - label: Face the warden
        when: "!flag.warden_beaten"
        do:
          - battle: warden
            on_win: [{set: {flag.warden_beaten: true}}, {say: The way north is open.}]
    exits:
      - {to: gate, label: Go north, when: flag.warden_beaten}

level_scale on the troop keeps an over-leveled party from crushing it, and music gives it its own song. Reference: Enemy, Troop, TroopEvent, LevelScale.

Send the party home instead of game over #

game_over.event runs after the game over screen, in place of the return to the title: the screen shows, and on the next press the party is recovered and the event runs. battle.retry offers to fight a lost battle again.

# game.yaml
game_over: {event: carried_home}
battle: {retry: true}
locations:
  home: {name: Home, safe: true, exits: [{to: field, label: Out to the field}]}
events:
  carried_home:
    do:
      - say: A passing carter brings you home. Half your gold is gone.
      - gold: -floor(gold / 2)
      - heal: all
      - move: home

Reference: GameOver, BattleCfg.

Draw a dungeon by hand #

A location with dungeon is walked in first person. Each character of map is a cell: # a wall, . a floor, D a door, and any character you define in tiles.

locations:
  crypt:
    name: Crypt
    battle_background: crypt.jpg
    encounters: {rate: 0.08, troops: [{troop: wolves}]}
    dungeon:
      wall: wall.jpg
      door: door.jpg
      floor: "#8a8f99"
      ceiling: "#2a2d33"
      map: |
        #########
        U.S.#...#
        ###.D.#.#
        #.....#.#
        #########
      tiles:
        S: {start: true, facing: east}
        U: {name: Stairs up, look: door, symbol: U, do: [{move: field}]}

Lead into it with an exit like any place (exits: [{to: crypt}]). Reference: Dungeon, Tile. Go Zen’s first two floors are hand-drawn mazes.

Put a chest in a dungeon #

A tile with once: true runs its commands a single time and then becomes its after character, here an open chest.

locations:
  storeroom:
    name: Storeroom
    dungeon:
      map: |
        #######
        #S...T#
        #######
      tiles:
        S: {start: true, facing: east}
        T:
          name: Old chest
          picture: chest.png
          solid: "true"
          once: true
          after: t
          mark: "#edbe70"
          do: [{gold: 20}, {say: Inside the chest are 20 gold coins.}]
        t: {name: Open chest, picture: chest_open.png, solid: "true"}

Reference: Tile. Go Zen’s floors have the same chests.

Lock a door with a key #

A tile is there only while its when holds; otherwise it becomes its else character, a floor by default. Walking into a solid tile runs its commands.

locations:
  vault:
    name: Vault
    dungeon:
      map: |
        #########
        #S..L..E#
        #########
      tiles:
        S: {start: true, facing: east}
        L:
          name: Locked door
          look: door
          solid: "true"
          when: "!flag.vault_open"
          do:
            - if: item.key > 0
              then: [{take: {item: key}}, {set: {flag.vault_open: true}}, {say: The key turns in the lock.}]
              else: [{say: The door is locked.}]
        E: {name: Way out, look: door, do: [{move: gate}]}

The key itself can come from a chest, a shop or a person. Reference: Tile. Go Zen’s Cistern Hall has three brass gates and three brass keys.

Make a one-way passage #

A slide tile carries the party on in one direction, so the passage can be crossed only that way.

locations:
  river_cave:
    name: River cave
    dungeon:
      map: |
        #########
        #S.>>>..#
        #########
      tiles:
        S: {start: true, facing: east}
        ">": {name: Fast water, slide: east, hint: (one way), mark: "#6fa8dc", symbol: ">"}

slide: facing makes ice, and spin turns the party around when it stops. Reference: Tile.

Generate dungeon floors #

With generate instead of map, every floor is built anew: a maze of rooms and corridors with the characters of place put in it. dive goes down; var.floor is the floor number. Inside generate and roamers (such as a count), plain floor is that number too; in the commands of a tile it is the floor() function, so write var.floor there. The size of generate and the count of place read floor and nothing else (no var, flag, rand or chance), because a loaded save builds the same floor again from its seed; a roamer’s count may read the rest.

locations:
  depths:
    name: The depths
    encounters: {rate: 0.1, troops: [{troop: wolves}]}
    dungeon:
      wall: wall.jpg
      generate:
        width: 15
        height: 15
        rooms: 3
        start: S
        place:
          - {char: ">", far: true}
          - {char: T, count: "floor < 5 ? 2 : 1"}
      tiles:
        S: {start: true}
        ">": {name: Stairs down, symbol: v, do: [{dive: depths, floor: var.floor + 1}]}
        T: {name: Chest, picture: chest.png, solid: "true", once: true, after: ".", do: [{gold: var.floor * 5}]}

Enter it with a dive: depths command. seed gives every save the same maps, and floors sets a seed or a hand-drawn map for one floor, such as a boss floor. Reference: Generate, Put, FloorPlan. Look at the floors and play a dive before you tune them.

Let strong foes roam a floor #

A roamer walks a generated floor and starts its battle when it touches the party; once beaten it is gone.

locations:
  catacombs:
    name: Catacombs
    dungeon:
      generate: {width: 13, height: 13, rooms: 2, start: S, place: [{char: ">", far: true}]}
      tiles:
        S: {start: true}
        ">": {name: Stairs down, do: [{dive: catacombs, floor: var.floor + 1}]}
      roamers:
        - name: Ghoul
          picture: ghoul.png
          move: chase
          sight: 5
          count: "floor >= 3 ? 1 : 0"
          troops: [{troop: wolves}]

move is chase, wander, guard or hunt, and meet runs commands instead of a battle. Reference: Roamer.

Change a dungeon’s look #

looks replace the walls, colors, darkness, music and the area name while their when holds, for example by floor (var.floor). The first that holds wins.

locations:
  tower:
    name: Tower
    dungeon:
      wall: brick.jpg
      floor: "#9c8f7a"
      floor_texture: stone.jpg
      darkness: 0.4
      generate: {width: 11, height: 11, start: S, place: [{char: ">", far: true}]}
      tiles:
        S: {start: true}
        ">": {name: Stairs up, do: [{dive: tower, floor: var.floor + 1}]}
      looks:
        - {when: "var.floor >= 6", area: Upper floors, wall: marble.jpg, floor: "#c8c8d0", darkness: 0.2}
        - {when: "var.floor >= 3", area: Middle floors, wall: mossy.jpg, ceiling_texture: vines.jpg}

A look can also set battle_background, music, battle_music and no_map. Reference: DungeonLook, Dungeon.

Use a dungeon theme #

A theme is a wall, a floor, a ceiling and a door picture with matching colors. gozen theme list names the thirteen that come with the gozen binary (cave, ice, ruins, castle, crystal, temple, sewer, forest, tomb, keep, dream, mine, vine); gozen theme add copies one into the game, with no network.

gozen theme add game --name cave
gozen theme add game --name ice

Then name it in the dungeon, and in a look for the deeper floors. What the YAML writes itself (wall, door, floor, ceiling, floor_texture, ceiling_texture) wins over the theme, and a look with a theme replaces the dungeon’s theme for what the look does not write.

locations:
  tower:
    name: Tower
    dungeon:
      theme: cave
      generate: {width: 11, height: 11, start: S, place: [{char: ">", far: true}]}
      tiles:
        S: {start: true}
        ">": {name: Stairs up, do: [{dive: tower, floor: floor + 1}]}
      looks:
        - {when: "var.floor >= 3", area: Deeper floors, theme: ice, door: gate.jpg}

The thirteen themes in the same corridor, cave, ice, ruins, castle, crystal, temple, sewer, forest, tomb, keep, dream, mine and vine:

The thirteen dungeon themes seen in one corridor

gozen check fails with GZN1111 when a named theme is not in the game. Reference: Dungeon, DungeonLook.

Show a map with points and roads #

A location with map is a picture with points on it (schema_version: 3). The party walks the open roads from point to point; a point with to leads into that location, and one with only do runs its commands there.

locations:
  valley:
    name: Valley
    map:
      picture: maps/valley.jpg        # point positions are in this picture's pixels
      unknown: dim
      points:
        home:   {at: [300, 620], to: field, start: true}
        store:  {at: [620, 480], to: shop}
        shrine: {at: [880, 600], label: Shrine, do: [{heal: all}, {say: You feel refreshed.}]}
        pass:   {at: [1100, 260], to: ridge, when: flag.pass_open, locked: Snow blocks the pass.}
      roads:
        - {between: [home, store], steps: 2}
        - {between: [store, shrine], via: [[760, 520]]}
        - {between: [shrine, pass], steps: 3, encounters: {rate: "0.2", troops: [{troop: wolves}]}}

Paint the roads in the picture: the lines in the YAML are what the token walks, drawn only with draw_roads: true. Reference: Atlas, MapPoint, Road, and Maps and painted scenes.

Make a campaign #

A campaign is a war of regions over turns (schema_version: 5): factions hold regions, the player gives a few orders a turn with the generals (actors of the game), and the other factions move by their ai rules when the turn ends. Its place is a map whose points are named like the regions.

schema_version: 11
campaign:
  location: war
  player: crown
  turns: 10
  calendar: week                  # the band says "Year 1, week 1"
  count: orders                   # each order once a turn, then End turn
  generals:
    walker: {troops: 500, might: 3, tactic: guard, skills: [volley]}
  clash:
    units: 2                      # at most two units a side (1: a duel)
    per_soldier: 30               # one soldier in the ranks for 30 troops (the default is 50)
    # or, instead of the ranks, an army by kind: poses of one soldier, lined up by the engine
    # (the kind follows the leader: generals.<id>.army, enemies.<id>.army)
    # armies: {men: [soldiers/men_1.png, soldiers/men_2.png], monsters: [soldiers/orc_1.png]}
    # max_soldiers: 4            # most soldiers one unit of an army shows
    # layout: front               # or below (the default): large cards of the leaders along the bottom
    backgrounds: {field: bg/field.jpg, castle: bg/castle.jpg}
    skills:
      volley: {label: Volley, charge: 3, all: true, power: "a.troops * 0.12"}
  orders:
    attack: {label: Attack, general: ready, target: border, march: true, fight: clash_view, generals: 2,
             on_win: [{say: {who: walker, text: The ford is ours.}}]}
    draft: {label: Draft, per_turn: 1, when: "gold >= 50", general: any, do: [{gold: -50}, {set: {general.troops: "general.troops + 150"}}]}
factions:
  crown: {name: Crown, color: "#3060c0", capital: vale}
  raven:
    name: Raven
    color: "#c03030"
    capital: well
    ai:
      - {draft: "150"}
      - {march: true, keep: 800, where: "s.troops * 0.6 > t.troops * 1.25", pick: min, by: t.troops}
regions:
  vale: {name: Ashvale, next: [ford], owner: crown, troops: 600, terrain: castle, area: [[0, 0], [600, 0], [600, 1080], [0, 1080]]}
  ford: {name: Brookford, next: [vale, well], troops: 350, terrain: field, defend: wolves, area: [[600, 0], [1300, 0], [1300, 1080], [600, 1080]]}
  well: {name: Deepwell, next: [ford], owner: raven, troops: 1500, terrain: castle, defend: wolves, area: [[1300, 0], [1920, 0], [1920, 1080], [1300, 1080]]}
locations:
  war:
    name: The war
    map:
      picture: maps/land.jpg          # the areas are in this picture's pixels
      points:
        vale: {at: [300, 540], start: true}
        ford: {at: [950, 540]}
        well: {at: [1600, 540]}
      roads:
        - {between: [vale, ford]}
        - {between: [ford, well]}

OK on a region lists the orders it can be given. Attack asks for the target and opens the sortie screen: the generals go in the front and back slots, each with a tactic (the engine’s front, weakest, strongest, leader and guard, or your clash.tactics). fight: clash_view plays the march as two armies on the terrain’s picture: the soldiers in the ranks are the troops, a skill fires when charged, and a table tells the losses. Leave out fight and the march is decided by the numbers of clash (power against defense); fight: battle fights the region’s defend troop as a battle of the game. An area fills a region in its owner’s colour; without one the region shows as a flag. On a map of one picture only the region chosen gets its roads and outline; region_roads: always and region_borders: always on the map draw them all. A region may also stand a cut-out landmark (its city or fort) on its point; it is optional.

Measure it before you tune it: gozen sim campaign game plays 200 campaigns and prints the win rate, the turns and the regions each side held turn by turn; --tactic guard marches every general with one tactic to compare them. Reference: Campaign, Region, Faction, Clash, Order, and Campaigns. Five Banners is a complete one (play it in the browser).

Mix a campaign with a dungeon #

A campaign shares its game with the towns and dungeons: the generals are the party’s actors, and move goes between the war map and any other place. Here an order sends the party into a mine whose way out leads back to the war, and the clash reads the general’s attack, so levels won in the mine make the marches stronger.

schema_version: 11
campaign:
  location: war
  player: crown
  turns: 8
  generals:
    walker: {troops: 400}
  clash:
    power: "a.troops * (1 + a.atk / 20) * (0.8 + rand() * 0.4)"   # the actor's attack counts
  orders:
    attack: {label: Attack, general: ready, target: border, march: true}
    delve: {label: Delve into the mine, cost: 0, do: [{move: mine}]}
factions:
  crown: {name: Crown, capital: home}
  raven: {name: Raven, capital: keep}
regions:
  home: {name: Home, next: [keep], owner: crown, troops: 300}
  keep: {name: Keep, next: [home], owner: raven, troops: 600}
locations:
  war:
    name: The war
    map:
      picture: maps/land.jpg
      points:
        home: {at: [400, 540], start: true}
        keep: {at: [1500, 540]}
      roads: [{between: [home, keep]}]
  mine:
    name: Old mine
    dungeon:
      wall: walls/rock.png
      floor: "#6a6258"
      map: |
        #####
        #S.E#
        #####
      tiles:
        S: {start: true, facing: east}
        E: {name: Way out, look: door, do: [{move: war}]}
    encounters: {rate: "0.2", troops: [{troop: wolves}]}

Gold, items, flags, experience and HP are one state: what the party finds in the mine is there on the map, and generals.walker.troops is there in the mine. Reference: Campaigns and Make a campaign.

Paint a scene with people and balloons #

A location with stage shows its background as a painting the party walks as a small doll (schema_version: 3). The stage says where one can walk, how tall people are at two depths, where the location’s people stand, and what can be looked at; bubbles are lines said now and then in balloons.

locations:
  square:
    name: Square
    background: bg/square.jpg          # painted from a fixed camera
    stage:
      scale: [{at: [640, 700], height: 160}, {at: [640, 430], height: 70}]
      walk: [[[200, 420], [1080, 420], [1260, 715], [20, 715]]]
      spots:
        start: {at: [640, 680], facing: north}
      exits: [{to: field, area: [[560, 420], [720, 420], [720, 440], [560, 440]]}]
      people:
        - {id: baker, at: [420, 600], facing: east, doll: {hair: bun, parts: [{kind: apron, color: "#c08040"}]}}
        - {id: runner, path: [[800, 500], [1000, 520], [900, 640]], speed: 0.9}
      things:
        - {id: well, name: Well, area: [[900, 600], [960, 600], [960, 650], [900, 650]], do: [{say: The water is cold and clear.}], once: true}
    people:
      - id: baker
        name: Baker
        image: baker.png
        talk: [{say: Fresh bread every morning.}]
        bubbles: [Bread, fresh bread!, {text: Back so soon?, when: flag.met_baker}]
      - {id: runner, name: Runner, image: runner.png, talk: [{say: Cannot stop now!}]}
    exits:
      - {to: field, label: Out to the field}

fronts hide people behind pillars and stalls, depth does the same from a depth picture, and ui.balloon in game.yaml sizes the balloons. Reference: Stage, Spot, StagePerson, Thing, Doll, Bubble.

Give a character faces and a moving portrait #

faces is a directory with one picture per expression; a line picks one with face. A puppet makes the talk portrait blink, move its mouth while the text runs and sway; its faces use the same names.

actors:
  guide:
    name: Guide
    class: walker
    faces: walker             # assets/walker/normal.png, assets/walker/smile.png
    puppet: walker/puppet.yaml
events:
  greeting:
    do:
      - say: {who: guide, text: The trail starts here.}
      - say: {who: guide, face: smile, text: Ready when you are.}

The puppet, assets/walker/puppet.yaml (picture paths are relative to it):

base: normal.png
face: normal               # the first face
faces:
  normal: {}
  smile: {image: smile.png}
eyes:
  closed: {image: eyes_closed.png, at: [166, 56]}
mouth:
  frames: [face, {image: mouth_open.png, at: [180, 92]}]
head: {center: [205, 85], radius: 75, neck: [205, 175]}
hair:
  - rect: [130, 70, 50, 90]

Check where each piece lies with gozen puppet. Reference: Actor, Say, FaceRule, and the puppet format. Go Zen’s Maren has a puppet. In a novel game, ui.stage_puppets: true lets the standing portraits move too (Move the standing portraits).

Set the face icons #

Menus and battle cards show a round face icon cut from the standing picture. face_box says where the face is (the center as fractions of the width and height, then the size as a fraction of the width), and face_shade darkens the icon.

actors:
  ione:
    name: Ione
    class: mender
    faces: ione                  # assets/ione/normal.png
    face_box: [0.5, 0.17, 0.32]
    face_shade: 0.15
locations:
  square:
    name: Square
    background: bg/square.jpg
    stage:
      scale: [{at: [640, 700], height: 160}, {at: [640, 430], height: 70}]
      walk: [[[200, 420], [1080, 420], [1260, 715], [20, 715]]]
      spots: {start: {at: [640, 680]}}
      people:
        - {id: baker, at: [420, 600], doll: {icon: icons/baker.png}}   # a square face for balloons
    people:
      - {id: baker, name: Baker, image: baker.png, bubbles: [Fresh bread!]}

In a painted scene, a balloon’s face comes from the doll’s icon, then face_at, then the face boxes that gozen faces finds. Reference: Actor, Doll.

image shows a full-screen picture under the lines that follow, and image: "" takes it away. The gallery on the title screen opens a picture once it has been shown, and a scene once its flag is set.

events:
  sunrise:
    do:
      - image: {file: cg/sunrise.jpg, fade: 1}
      - say: The sun comes up over the ridge.
      - image: ""
      - set: {flag.saw_sunrise: true}
# game.yaml
gallery:
  cgs:
    - {title: Sunrise, file: cg/sunrise.jpg}
  scenes:
    - {title: Sunrise on the ridge, event: sunrise, flag: saw_sunrise, location: ridge}

files shows several pictures in turn, and look: dream blurs the edges for a memory. Reference: CG, Gallery, GalleryCG, GalleryScene.

Keep a character book #

profiles gives each character a page on the Characters screen: the standing picture with its faces, a few lines of your own labels, an introduction and how they see the others. A profile whose ID is an actor’s shows that actor; picture shows anyone else. Under --dev the title and the menu offer Characters, F6 opens it anywhere and every page is open, so you can check your characters while you write. Run gozen faces once to center the list’s face icons on the faces. Every text is translated through lang/ like the lines.

To open a page when the player meets the character, write met: true (schema 10): the page opens the first time a line of the actor is shown, and stays open in later plays. A face with seen: true is offered once a line shows the actor with it, and label names its button. ui.profile_book: {title: Who's who, layout: grid} names the book and lays it out as a grid of face cards grouped by affiliation.

# game.yaml
schema_version: 11
ui:
  profiles: true    # Characters in the title, the menu and the novel bar for players too
actors:
  mira:
    name: Mira
    class: walker
    faces: mira             # assets/mira/normal.png, assets/mira/smile.png
profiles:
  mira:
    title: The courier of the pass
    affiliation: Pass couriers
    faces: [normal, smile]  # the faces the screen switches between (Z)
    when: flag.met_mira     # a shadow named ??? until then; once open, it stays open
    fields:
      - {label: Role, value: Courier}
      - {label: Likes, value: Hot cider}
    text: Mira has carried letters over the pass for {var.mira_years} winters.
    relations:
      - {who: walker, kind: friend, calls: Boss, text: Trusts them with the map.}
      - {who: you, kind: other, calls: Stranger}
  keeper:
    name: The keeper
    picture: people/keeper.png
events:
  meet_mira:
    do:
      - set: {flag.met_mira: true}
      - open: profiles

open: profiles opens the book from an event, and a test reads profile.mira.unlocked. Reference: Profile, ProfileField, ProfileRelation, UIStyle.

Press Tab in the book for the chart, use arrows to select a face, then Z to focus it. Its incoming and outgoing neighbours gather around it; other faces and lines fade. Press Z again to read its page, X to animate back to the full chart, or use the wheel to zoom. The kind colour is chosen by the author; love has no engine rule. A line to a locked profile is hidden until it opens. Run gozen faces <game-dir> to generate face boxes, then gozen check <game-dir> to catch unknown targets and kinds.

Keep a monster book #

Add lore to each enemy that can appear in an encounter and set ui.bestiary: true. Seeing an enemy in battle reveals its name, picture and lore; defeating it reveals its record and increments bestiary.<id>.defeated. Expressions can read bestiary.<id>.seen and bestiary.<id>.defeated. Under --dev, F4 opens every page.

# game.yaml
schema_version: 11
ui:
  bestiary: true
# data/enemies.yaml
enemies:
  mote:
    name: Mote
    image: enemies/mote.png
    lore: "Motes drift through the old stone tunnels and gather near quiet water."
    stats: {mhp: 42, mmp: 0, atk: 9, def: 2, mat: 0, mdf: 1, agi: 5, luk: 1}

Use open: bestiary to open the book from an event. gozen check warns with GZN0115 when an enemy named by a troop has no lore, and gozen book prints the enemy pages. Reference: Enemy, UIStyle.

Collect, raise and breed monsters #

Use schema version 6. Give each species a family, a positive rank and a class that defines its growth and level skills. Add recruit only to species that may ask to join after a won battle. This short data file assumes attack and guard are already defined, and that the two pictures exist under assets/.

monster_families: {slime: Slime}
classes:
  slime:
    name: Slime
    exp_for: level * level * 10
    stats: {mhp: "30 + level * 6", mmp: "4 + level", atk: "5 + level * 2", def: "2 + level", mat: "3 + level", mdf: "2 + level", agi: "5 + level", luk: 4}
    skills: [{level: 1, skill: attack}, {level: 5, skill: guard}]
enemies:
  dewdrop:
    name: Dewdrop
    image: enemies/dewdrop.png
    family: slime
    rank: 1
    monster_class: slime
    sex: any
    sex_ratio: 0.5
    recruit: {chance: 0.2, level: 1}
    stats: {mhp: 30, mmp: 0, atk: 6, def: 2, mat: 1, mdf: 2, agi: 5, luk: 4}
  silverdrop:
    name: Silverdrop
    image: enemies/silverdrop.png
    family: slime
    rank: 2
    monster_class: slime
    stats: {mhp: 60, mmp: 5, atk: 12, def: 5, mat: 6, mdf: 5, agi: 7, luk: 4}
breeding:
  min_level: 5
  skill_slots: 2
  staging: {rare_rank: 4, legend_rank: 7}
  keeper: {name: Mara, text: "Bring me two monsters and I will join them."}
  pairs: [{parents: [dewdrop, dewdrop], child: silverdrop}]
  families: [{parents: [slime, slime], children: [dewdrop, silverdrop]}]

At a safe location, add an action such as - {label: Monster ranch, do: [{open: ranch}]}. Each monster instance has a saved male or female sex (♂ or ♀); only opposite-sex parents can breed. A species can specify sex: male or sex: female, or use any with an optional male sex_ratio. The ranch shows active and reserve members, swaps them, asks before release, previews a child (? until seen), lets the player choose inherited skills and stages the fusion. A full party sends a new recruit to the reserve. reserve: {in: monster_2, out: monster_1}, release: monster_2 and breed: {parents: [monster_1, monster_2], skills: [attack]} are commands usable by YAML events and development scripts; the IDs refer to saved instances, not species. Expressions read reserve.size, monster.monster_1.family, monster.monster_1.plus, monster.monster_1.sex and bestiary.dewdrop.caught.

To require recruits to travel with the active group, use recruit: {chance: 0.2, level: 1, admission: party_only}. The default is admission: reserve. Capacity is checked when each offer is accepted, including after loading a pending choice. A full party gets a rejection message, with no new instance, caught-count increase or recruitment reward. Breeding still replaces its parents normally.

An event can recruit a specific species directly and react to a full party:

- capture: dewdrop
- if: 'var.capture_result == "party_full"'
  then:
    - say: Make room in the active party before returning.

capture requires a non-boss species with recruit settings and schema version 6 or later. It attempts admission directly, without rolling the post-battle invitation chance. The saved var.capture_result is party, reserve, party_full or unavailable; each attempt replaces the previous result.

Run gozen check game, gozen sim breed game --format json, gozen book game and battle and dive simulations while tuning. GZN1112 identifies invalid rules; GZN0116 warns of non-boss species unreachable from capturable monsters. A specific pair wins over a family rule, and family offspring are chosen by the mean parent rank.

The monster book shows the shortest recipe first as a picture tree of up to seven monsters: the current species, its parents and its grandparents. Captured leaves show their locations; family rules name the families and example parents. Arrows move between pictures, Z places the chosen species at the top, X returns along the breadcrumb, Tab changes the top species’ recipe, and Q opens the chosen known species’ page. A parent with a deeper recipe has a more mark. An unseen species is shown as ?; --dev reveals all species. gozen book game --format json supplies the same tree as structured data. In Go Zen, Yorn at the Breeding Altar opens the ranch. Whisper Imp comes from a male Incense Bloom and Pale Sylph, Vampire from Night Bat and Ghoul, and those two children can produce Succubus. gozen sim breed examples/go-zen/game reports that two-generation path.

Go Zen’s two-generation family tree The author’s breeding illustration

Transform an enemy #

Use an HP trigger for a change that happens immediately after damage. The resulting enemy keeps the slot and its share of HP.

enemies:
  guardian:
    image: enemies/guardian.png
    hp_triggers: [{below: 0.5, transform: guardian_open, hp: ratio, clear: true}]
  guardian_open:
    image: enemies/guardian_open.png

Use schema_version: 7. The Tollkeeper in Go Zen uses this form change.

Merge enemies #

Give a self-scoped skill a merge effect. Its from list names the eligible sources; count includes the user. The new enemy occupies their middle slot.

skills:
  gather: {name: Gather, scope: self, effects: [{merge: {into: king_slime, from: [drip_slime], count: 2, hp: sum}}]}
enemies:
  drip_slime:
    ai: [{use: gather, when: "allies.kind_drip_slime >= 2"}]
  king_slime:
    name: King Slime
    image: enemies/king_slime.png

Change pictures by HP #

List picture thresholds from high to low. An enemy names image files; an actor names faces in its face directory. Temporary hit reactions still take priority.

enemies:
  mote:
    image: enemies/mote.png
    images: [{below: 0.5, file: enemies/mote_hurt.png}]
actors:
  scout:
    faces: scout
    images: [{below: 0.35, face: worried}]

Move a portrait with soft sway #

Set puppet on an enemy or actor and define its motion in the puppet YAML. The same soft_sway region accepts rect: [x, y, width, height] or ellipse: [x, y, width, height]. follow: body makes it trail breathing and action or hit reactions. Coordinates are pixels of the base picture.

enemies:
  sprite:
    image: enemies/sprite.png
    puppet: sprite/puppet.yaml

In assets/sprite/puppet.yaml:

base: base.png
eyes:
  closed: {image: eyes-closed.png, at: [40, 30]}
head: {center: [55, 40], radius: 25, neck: [55, 70]}
soft_sway:
  - {ellipse: [20, 75, 70, 60], root: top, follow: body, axis: y, amount: 1.2, limit: 6}
motion:
  breath: {depth: 0.012, period: 4}

Run gozen puppet assets/sprite/puppet.yaml to inspect the picture parts.

Play music for places, battles and moods #

A song is a file, or a modular song with one module per mood. A location plays its music, ordinary battles there play battle_music, and a troop’s music comes first. Mood rules switch the mood of a modular song by the moment.

music:
  songs:
    town: {file: music/town.ogg, loop_start: 4.2}
    battle: {file: music/battle.ogg}
    boss: {file: music/boss.ogg}
    cave:                          # assets/music/cave/calm.ogg, assets/music/cave/tense.ogg
      dir: music/cave
      bpm: 96
      moods: {calm: calm, tense: tense}
  rules:
    field:
      - {song: cave, when: "party.min_hp_rate < 0.3", mood: tense}
locations:
  plaza: {name: Plaza, music: town}
  caves: {name: Caves, music: cave/calm, battle_music: battle}
troops:
  champion: {members: [mote], music: boss}
events:
  ambush:
    do:
      - mood: tense
      - battle: wolves
# game.yaml
title_music: town
battle: {music: battle, boss_win_music: town}

A song named battle is the battle music by default. music: town in an event changes the song. Reference: Music, Song, MusicCue, MoodRule.

Change the language and the engine’s wording #

language picks the engine’s own words (menus, battle messages, settings): en or ja. text replaces any of them, keyed by the engine’s Japanese source text; terms names the money and the stats.

# game.yaml
language: en
terms: {currency: coins, level: Lv., hp: HP, mp: MP, exp: EXP}
text:
  はじめから: New journey

The English catalog lists every key, and gozen check warns when the font lacks letters of the chosen words. Reference: Game, Terms.

Offer the game in two languages #

Write data/ in one language and translate it in lang/<code>.yaml: a map text from each text as written to its translation, with the {expressions} kept. languages lets the player choose in the settings; the choice is kept with the settings.

# game.yaml
schema_version: 11
language: en          # shown at first
written_in: en        # the language of data/
languages: [en, ja]   # the settings offer both
events:
  dinner:
    do:
      - say: {who: Keeper, text: "Soup or bread? You have {gold} gold."}
      - choice: [{label: Soup}, {label: Bread}]
# lang/ja.yaml
text:
  "Soup or bread? You have {gold} gold.": "Supu to pan, dochira ni? Shojikin wa {gold} G."
  "Keeper": "Yado no shujin"
  "Soup": "Supu"
  "Bread": "Pan"

Lines, speakers, choices, prompts and notices are translated as they are shown, and the names and descriptions of places, people, actions, exits, items, skills, states, enemies, classes, shops, the title and the gallery’s scenes when the language changes. Read lines are kept by the text as written, so they stay read in every language. One text has one translation: when the same words must be translated differently in two places, write them differently. The same map may also replace the engine’s own words in that language, as text in game.yaml does.

gozen check reports a translation that drops or changes an {expression}, warns of a key that is no text of the game (GZN0110) and of every text not translated yet (GZN0111, each with its path; --format json lists them for a script or an agent to fill the file), and lays out the lines in each language. A test of the Japanese game loads the same files with game.yaml changed to language: ja (an internal integration test overlay); its requests are in Japanese. Reference: Game (written_in, languages).

Test-play with notes, reload and quick starts #

Run your game with --dev: F5 reloads the YAML without losing your place, F9 writes a note (a screenshot, the state and your comment), and --start and its friends begin anywhere. memo in game.yaml sets the kinds of note and the numbers kept with each.

# game.yaml
memo:
  kinds:
    - {id: text, label: Wording}
    - {id: balance, label: Too hard or too easy}
    - {id: bug, label: Bug}
  vars:
    - {name: gold, value: gold}
    - {name: steps, value: steps}

The commands are in Test play. Reference: MemoCfg, and The edit–play loop for every key and option.

Measure battles with sim battle and sim dive #

gozen sim battle fights a troop many times on AUTO and prints the win rate; gozen sim dive takes the party down a generated dungeon floor by floor, with its levels, gold and boss win rates. Change a stat, an AI rule or a troop, and measure again. The F7 panel shows the same numbers while you play.

The commands are in Check and measure. Reference: Commands.

Check and measure in CI #

Let every push run the check and the numbers. In GitHub Actions, setup-gozen installs the release binary of the version in your go.mod, checked against the release checksums:

# .github/workflows/game.yml
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: nao1215/setup-gozen@1c378fc446f62ecb29a7bcd40aea6dff8565d698 # v0.1.1
      - run: gozen check game
      - run: gozen sim battle game --troop boss --format json   # boss: a troop id of your game
      - run: gozen sim campaign game --format json      # for a campaign

gozen check exits 1 on any error, so the job fails on a broken reference. Without the action, go run github.com/nao1215/gozen/cmd/gozen check game builds the version in go.mod. Reference: Install, Commands.

Cut a release and a trial #

gozen release copies the game with only approved pictures; gozen trial keeps the part of the game within the range of trial.yaml, a demo. The selector removes definitions and assets it can prove are outside the trial range. Analysis is conservative: unresolved branches and retained files can still contain later content. Inspect the output before distribution; this is not encryption or an asset-secrecy guarantee. -n counts what would be kept, and --list writes what was kept and what was dropped, to check before you ship. Here the demo stops at the third floor of the grotto and leaves out the ending event:

# game/trial.yaml
dive_max: {grotto: 3}      # dives below the third floor run blocked instead
cut: [finish]              # the ending; calls to it run blocked instead
blocked:
  - say: This is as far as the trial goes. Thank you for playing!

The release reads which pictures are approved from the game’s asset-policy.yaml and the inspection records:

# game/asset-policy.yaml
overlay: ../overlay
report: approvals.json
extensions: [.png]
inspection: {file: file, source: source, required: required, approved: approved}
records: {file: file, source: source, required: required, approved: approved, output: output}

A trial built without --overlay keeps the game’s asset-policy.yaml, so gozen export refuses it until its assets are approved; with --overlay and --inspect the trial is an approved tree. Either way it holds the game’s translations whole and its license notices.

The commands are in Release and trial. Reference: Commands, Commands.

Commands used by the recipes #

The examples use game as the game directory, lesson as a troop and grotto as a generated dungeon. Commands has every option.

Check and measure #

gozen check game                                       # references, expressions, pictures, text layout
gozen sim battle game --troop lesson -n 200 --level 5          # win rate of the party at level 5
gozen sim battle game --troop lesson --party scout --seed 7 -v # the messages of one battle
gozen sim floors game --location grotto --from 1 --to 3           # the generated maps
gozen sim dive game --location grotto --floors 5 --boss 5:lesson  # a whole dive: levels, gold and wins per floor
gozen book game > book.md                              # the whole game as one Markdown file

gozen faces finds the faces of the standing pictures for balloons; it needs a detection model:

gozen models fetch face
gozen faces game

Test play #

Use the game directory and enable the author tools:

gozen run game --dev --memos notes           # F5 reloads game/, F9 writes notes/
gozen run game --dev --start cave_mouth --level 5 --battle bats
gozen run game --dev --start village --flags sela_joined,var.asked=3 --gold 500
gozen run game --dev --event greeting --line 2
gozen run game --dev --from-memo notes/2026-01-01/001.json

Collect the notes into one HTML page:

gozen notes notes -o notes-html

Moving portraits #

gozen puppet streamer/puppet.yaml                        # check it and show where each piece lies
gozen puppet streamer/puppet.yaml --out streamer-packed  # write it with small difference pictures

Release and trial #

gozen release game --out release --overlay overlay --inspect inspect.jsonl
gozen trial game -n                               # count what the trial keeps
gozen trial game --out trial --list trial.txt           # build it, and list what was kept and dropped

A speaker who remembers a greeting #

Create a novel with gozen new my-story --template novel. In game.yaml, add reactions: {rowan: rowan.yaml}. Save this reaction book as assets/rowan.yaml:

people:
  - id: rowan
    rules:
      - {id: repeated, when: tag.repeat > 0, say: [Welcome back.]}
      - {id: welcome, say: [Hello.]}

In a location’s on_enter or an event’s do, use - respond: {who: rowan, text: Hello}. The first greeting produces “Hello.”; a repeated greeting produces “Welcome back.”. Saving and loading preserves that memory. Keep ordinary scripted lines as say; use respond only where rule-based variation is useful. A speaker needs no battle class or stats.

Check the contents of a trial #

Trial analysis is conservative: a condition it cannot prove false retains its branch. gozen trial game -n reports the count of unresolved conditions; --list trial.txt includes their paths as well as retained definitions and assets. Do not infer that all later story content was removed from a successful build.

Use forbid in trial.yaml to fail if selected definitions or assets remain:

cut: [ending]
forbid: [events.ending, assets/ending/*]

cut replaces calls to the named event with the configured blocked sequence. forbid checks retained IDs in events, troops, enemies, locations, skills, ai, shops and songs, plus asset paths, using the same glob syntax as the other selection patterns; it reports an error before writing the output. Inspect the resulting game and test its routes before distribution.

Record filled stream lines #

For a stream game, set voices: voice in the streams file and an optional voice_fallback: Thank you. on each channel (schema version 9). Record that translated phrase as Ogg Vorbis in the channel’s voice folder. Lookup tries the filled text, then the translated unfilled template, then the channel fallback for dynamic lines, then no recording. The subtitle keeps the full filled line. Without a recording, the browser uses speech synthesis by default. Player tip quotations also use the fallback. Static lines keep their usual behavior.

Export the recording inventory with gozen book GAME --section voices --format json. The command result’s data contains version: 1 and lines. Each line carries channel, language, text and file. Generate the exact text as Ogg Vorbis at that game-relative path, deduplicating by path. Records with fields are templates: their file hashes the translated text with placeholders intact. Record these without speaking the name or other runtime values, and save at that path. Set voices before exporting paths.

The inventory expands NPC names for their authored question options and candidate bot-tip reactions from crowd names and tip lines. Player names and input, visit counts, reaction mood, matched slots and remembered values remain runtime values. Reaction candidates may include rules that will not run for a particular tip. Translations are applied before filling and hashing. Existing recordings named after translated unfilled templates remain usable. gozen check checks template and expanded keys, missing fallback recordings (GZN0119) and audio format (GZN0120). The browser checks its file manifest before loading a voice; an absent recording does not incur a network request.

To keep unvoiced lines silent, write missing_voice: captions beside voices in the file named by game.yaml’s streams. This optional schema version 9 field accepts speech (the default when omitted) or captions; other values fail gozen check with GZN1104. It applies to absent recordings, decode errors and fetch timeouts, including an unavailable channel fallback.

Captions mode uses the same caption duration and mouth timing as desktop playback without speech synthesis, including the existing loading deadline and question-answer hold. It does not change recording playback or timeout logs. The speaker ON/OFF button controls recorded audio when voices is configured; without recorded audio it is hidden. The captions button remains independent.

Wear stone sets and fuse stones #

A stone is an item with a slot; an item of slot stone fits the equip slots stone1, stone2 and so on. A stone_sets entry names stones worn together by one member: while all of them are worn, the member has the set’s stats and its skill. for lists the members a set suits, and only: true makes it work for those members alone.

# game.yaml
schema_version: 11
equip_slots:
  - {id: weapon, name: Weapon}
  - {id: stone1, name: Stone 1, manual: true}
  - {id: stone2, name: Stone 2, manual: true}
classes:
  gemwright:
    name: Gemwright
    stats: {mhp: 20, mmp: 4, atk: 4, def: 2, mat: 2, mdf: 2, agi: 4, luk: 1}
    exp_for: level * 20
    equip: [stone]
actors:
  sage: {name: Sage, class: gemwright}
items:
  amber: {name: Amber, kind: stone, slot: stone, stats: {def: 2}}
  jet: {name: Jet, kind: stone, slot: stone, stats: {mat: 3}}
  flint: {name: Flint, kind: stone, slot: stone, price: 6, stats: {atk: 1}}
  spark: {name: Spark, kind: stone, slot: stone, stats: {atk: 4}}
stone_sets:
  dusk: {name: Dusk, desc: Amber and jet together., stones: [amber, jet], stats: {mdf: 4}, for: [sage]}
fusions:
  - {from: [flint, flint], to: spark, price: 10}
locations:
  workshop:
    name: Workshop
    safe: true
    actions:
      - {label: Fuse stones, do: [{open: fusion}]}
      - {label: Wear Dusk, do: [{equip_stones: {actor: '"sage"', set: '"dusk"'}}]}
      - label: Ask for advice
        do:
          - stone_suggest: tip
          - if: var.tip > 0
            then:
              - say: "{var.tip_page1}"
              - stone_apply: tip
            else:
              - say: Your stones are already at their best.
    exits: [{to: field, label: Out to the field}]

manual: true keeps the menu’s best-equipment choice and its offers away from a slot, which suits stones: a set is worth more than the numbers of its stones. equip_stones takes the stones from the bag, or from members not using them; actor and set are expressions, so a fixed ID is quoted. stone_suggest: tip looks for the strongest way to wear every owned stone and writes var.tip, the number of members whose stones would change (0 when the party already wears the best), with one text per member in var.tip_page1, var.tip_page2 and so on; stone_apply: tip then re-equips everyone that way. stone_choose, stone_wearer and stone_decline let the player pick a set and its wearer, or refuse the advice until the owned stones change.

open: fusion opens the screen of the fusions: a recipe takes the stones of from and the price, and gives the stone to. Reference: StoneSet, Fusion, EquipStones, Slot.

Set a retry point with a checkpoint #

checkpoint: EVENT keeps the state and the random numbers of that moment as the retry point, replacing the one before (schema version 9). restore_checkpoint: true gives up what happened since, goes back to that state and runs the event the checkpoint named. A save holds the retry point, so restoring also works after loading.

# game.yaml
schema_version: 11
locations:
  trailhead:
    name: Trailhead
    on_enter:
      - checkpoint: back_at_the_trailhead
    actions:
      - {label: Go back to the trailhead, do: [{restore_checkpoint: true}]}
    exits: [{to: field, label: Up the trail}]
events:
  back_at_the_trailhead:
    do:
      - say: You are back where the trail begins.

Everything changed after the checkpoint is undone, flags and variables included: keep what must survive a retry (a count of attempts, say) in the checkpoint’s event. Reference: Command.

Insert a thought after a speaker’s lines #

asides in game.yaml insert an event right after a line by a given speaker is shown, while when holds; the conversation goes on when the inserted event ends. Name the speaker by actor, by the line’s name, or by a pattern for its face.

# game.yaml
asides:
  - {actor: walker, when: "!flag.walker_wondered", call: walker_wonders, skip: ["finale*"]}
events:
  walker_wonders:
    do:
      - say: (Why does the trail feel shorter today?)
      - set: {flag.walker_wondered: true}

An aside is not inserted during a battle, inside another aside, or while an event matching skip (or one it calls) runs. How often it comes is yours to write: the engine does not count, so set a flag or a variable in the event and test it in when. Reference: Aside.

Dress the title screen #

title_background is the picture behind the title, and title_art a logo or key art drawn over it. title_figures stand pictures on the title screen instead, the later ones in front; with figures, title_art is not drawn.

# game.yaml
title_background: title/sky.jpg
title_figures:
  - {file: title/guide.png, x: 0.3}
  - {file: title/walker.png, x: 0.62, height: 0.8, bottom: 0.98}

x is the horizontal center as a fraction of the screen width; height (default 0.95) and bottom (default 1, the bottom edge) are fractions of the screen height. Reference: Game, TitleFigure.

Review pictures and scenes while writing #

cg_review and scene_review in game.yaml set up the tags of two review tools for authors. Start the game with --review-log FILE: with cg_review written, pictures can be tagged, and with --gallery-all too the gallery gains a column that lists the gallery.scenes to tag. gozen run game --dev --review --review-log review.jsonl starts by showing the pictures of every event one by one.

# game.yaml
gallery:
  cgs:
    - {title: Sunrise, file: cg/sunrise.jpg}
cg_review:
  tags:
    - {id: ok, label: OK, ok: true}
    - {id: redraw, label: Redraw}
  extra: [cg/draft_*.jpg]
  groups:
    - {label: Sketches, files: [cg/sketch_*.jpg]}
scene_review:
  title: Scene check
  tags:
    - {id: ok, label: OK, ok: true}
    - {id: pacing, label: Pacing}

The first ten tags are attached with the number keys. A tag with ok: true marks a picture as done, and the filter that hides finished pictures leaves it out. extra adds pictures the gallery does not list, and a groups entry is a set of pictures that is left out until it is chosen before the review. Picture tags are written to the review log as JSON Lines; scene tags go to scene-review.jsonl beside it. Reference: CGReviewCfg, SceneReviewCfg.

Tune a place, the windows and the battle pace #

A few single keys change how a place, the windows or every battle behave.

# game.yaml
schema_version: 11
battle:
  tempo: 1.5          # battle messages and moves go by half as fast again
  back_weight: 0.5    # the weight of one back-row member as a target (default 1)
  passives:
    - {actor: walker, state: steady, when: flag.trained}
ui:
  battle_message: fixed    # always three lines tall; fit (the default) is as tall as its lines
  choice_position: center  # the novel window's choices in the middle of the screen
  field_notices: false     # no small notices on the field
  hide_gold: [field, map]  # no money window in these kinds of place
states:
  steady: {name: Steady, stats: {def: 1.2}}
locations:
  snow_town:
    name: Snow town
    outfit: winter          # actors talking here wear <faces>/winter/<face>.png when there is one
    no_menu: true           # the menu cannot be opened here
    talk_on_enter: true     # one person's talk plays on each entry instead of a list of people
    people:
      - {name: Porter, talk: [{say: Mind the ice.}]}
    exits: [{to: snow_inn, label: The inn}]
  snow_inn:
    name: Snow inn
    safe: true
    resume_to: snow_town    # a save made here starts from the town when it is loaded
    actions:
      - {label: Train, do: [{set: {flag.trained: true}}]}
    exits: [{to: snow_town, label: Back outside}]

A passives state is put on the member at the start of each battle while its when holds, and does not last after the battle. An enemy aims at one front-row member with weight 4 and at one back-row member with weight 1 unless front_weight and back_weight say otherwise. hide_gold takes field, dungeon, map and stage. With talk_on_enter, people who have something new to say come first. Reference: BattleCfg, Passive, UIStyle, Location.

Change states, duels and skill points from an event #

These commands reach what the menus and battles usually own.

states:
  chilled: {name: Chilled, turns: 3, stats: {agi: 0.8}}
actors:
  guide: {name: Guide, class: mender}
locations:
  pass:
    name: Mountain pass
    on_enter:
      - affect: {who: all, add: chilled, resist: true}   # each member's state resistance may keep it off
    actions:
      - {label: Warm up, do: [{affect: {who: all, remove: chilled}}]}
      - label: Duel the mote alone
        do:
          - battle: wolves
            fighters: [walker]     # only these actors fight, not the whole party
            on_win: [{say: The mote backs away.}]
            on_lose: [{say: You step back to the fire.}]
      - {label: Unlearn, do: [{reset_tree: all}]}
      - label: Ask how to spend skill points
        do:
          - sp_suggest: plan
          - if: var.plan > 0
            then: [{say: "{var.plan_page1}"}, {sp_apply: plan}]
    exits: [{to: field, label: Down to the field}]

affect puts a state on party members outside a battle (who is an actor’s ID or all), for the state’s own turns unless turns is given, or takes one off with remove (remove: bad takes off every bad state). A battle with on_lose goes on with those commands after a defeat instead of the game over. reset_tree gives back the SP spent in the skill tree of one actor or of all. sp_suggest: plan plans how each member’s spare SP would be spent and writes var.plan, the number of members with a plan, and a text per member in var.plan_page1 and so on, without spending anything; sp_apply: plan spends it.

In a dungeon, open_wall: H turns the wall beside the party’s cell (the one it faces first) into the tile H and turns the party to it; the floor and saves keep it. Written under the dungeon’s complete, it makes a stair appear once every cell has been seen:

locations:
  cellar:
    name: Cellar
    dungeon:
      map: |
        #####
        #S..#
        #####
      tiles:
        S: {start: true, facing: east}
        H: {name: Hidden stair, look: door, solid: "true", do: [{move: field}]}
      complete:
        - open_wall: H

Reference: Affect, Command, Dungeon.