Tycho.dev
The dev tools, for the people making a game; players never see them. Testing switches kept in their own storage key (never in the save), flags read from the address, a log any game can write to, and the dev drawer: a small, dense, dark console docked at the side while the game runs on behind it and stays in sight. The drawer was Merryfolk’s (its ` key, its tabs, its overlays row and frame line, its events), made for any game; the White Dragon’s dev tools are on it.
- The address and the flags
- Notes
- The drawer
- Its tabs
- The pieces
- Settings that change play
- Overlays
- The events
- A mind in the drawer
- Its look
- Merryfolk’s drawer, on these options
The address and the flags
query(name) says whether ?name is in the address (?nogl, ?perf). new Flags(key, defaults).load() keeps testing switches in their own storage key: s holds them, save() keeps them, shown() is true with ?dev in the address or once it has been. The drawer keeps its own state in the same flags (flags.s.drawer: open or not, the tab, the overlays, the events’ filter and find box).
Notes
What happened under the hood, for the events. Any game can write to one, with a drawer or without (Lodestar’s log for the events by impact will be one):
const notes = new Tycho.dev.Notes({ most: 600, clock: () => Game.time });
notes.note('rift', 'a rift opened by the mill'); // newest first in notes.list, the last `most` kept
notes.paused = true; // nothing's noted
notes.rewind(t); // what's later than t goes (the clock went back)
notes.clear();
An event is { t, kind, text }, with who, place, impact (0 to 1) and data if the game has them; count rises with every note, for knowing when there’s something new. A drawer has its own (d.notes, and d.note(kind, text)).
The drawer
const flags = new Tycho.dev.Flags('mygame.dev', { speed: 1, freeze: false });
const drawer = new Tycho.dev.Drawer({
flags, side: 'right', title: 'DEV',
button: () => document.getElementById('bDev'), // the game's own button (or true for one of its own)
tabs: [
{ id: 'mind', every: 0.5, html: (d) => mindHtml(d) },
{ id: 'events', events: true },
{ id: 'world', html: (d) => worldHtml(d) }, // every 0.4 s by default
{ id: 'tools', every: 0, html: (d) => toolsHtml(d), bind: (body, d) => {} },
],
overlays: [{ id: 'names', on: true }, { id: 'boxes' }, { id: 'land', options: ['off', 'height', 'soil'] }],
play: [{ key: 'speed', def: 1, word: (v) => 'run ×' + v }, { key: 'freeze', def: false, word: () => 'clock stopped' }],
acts: { cr: () => { money += 1000; }, go: (id) => goTo(id) },
pick: (x, y) => whoIsAt(x, y), selectTab: 'mind',
events: { clock: () => minutes(), when: (t) => clock(t), from: () => worldLogAsEvents(), apart: ['moves'], find: true },
});
flags.load(); drawer.start(); // once the page is ready: the key, the button, the tag, picking
drawer.frame(dt); // every frame, with the real seconds since the last
drawer.time('sim', ms); // a frame time measured, smoothed, for the header's line
It opens and closes with the ` key (or ~; keys), never while typing in a field, and with its button; it opens at all only with ?dev in the address, or once it has been (flags.shown()), unless always. Its head is its title, a button a tab and a close button; under it a row of the overlays’ switches and the frame line (frame(d) gives it, else the frame rate and the times reported with time); then the tab. It remembers whether it’s open, the tab and the overlays across a reload. While it’s open the page’s body has a class (bodyClass, <prefix>-on), and the page carries CSS variables of how much it covers (--<prefix>-left, --<prefix>-right, --<prefix>-bottom, in px; also d.covered()), so the game’s own panels can make room for it and its camera can keep the player in the part left showing. Keys typed into its fields never reach the game.
Docked at the left (the default) or the right, at most 440 px wide or 46% of the screen; on a phone held sideways that leaves the game more than half the screen, and its text gets smaller on a short screen. On a screen held upright it’s a sheet at the foot instead.
| Call | What it does |
|---|---|
start() |
Wires the key, the button, the tag and picking; opens it again if it was open |
open(tab), close(), toggleOpen(force), on |
Opening and closing it |
show(tab), current(), addTab(tab, at) |
The tabs |
refresh(full, reset) |
The tab drawn again: full whatever’s going on, reset from the top |
frame(dt), time(name, ms), fps, frameText() |
Every frame; the frame times and the default line |
select(id, tab), sel |
Whoever’s picked (the game’s id) |
set(key, value), store(key, value) |
A setting changed (by the game’s set, or into the flags) |
note(kind, text, more), notes, evAll() |
The events |
overlay(id), overlays(), layer() |
The overlays, and a canvas over the game to draw them on |
covered(), allowed, st |
How much of the screen it covers, whether it may open, its own state |
Its tabs
A tab is { id, label, html(d), every, bind(body, d) }: its body’s HTML, how often it’s drawn again while it’s showing (seconds, 0.4 by default; 0 only when something’s done in it, as a tools tab wants), and what to wire after each draw. { id, events: true } is the drawer’s own events.
Drawing a tab again never loses your place: where it’s scrolled to (and anything inside it that scrolls, given data-keep), what has focus and where the caret is in it, what’s been typed into a field, and which details are open (each field or section known by its id, name or data-keep). It isn’t drawn again while a finger or the mouse is down in it, for a moment after it’s scrolled, or while a choice is open, so a button is never swept away under a tap.
What’s done in a tab goes by data attributes, with the drawer’s prefix:
| Attribute | |
|---|---|
data-<prefix>act (and data-<prefix>arg) |
Runs acts[name](arg, el, d), then the tab again (unless it returns false) |
data-<prefix>set and data-<prefix>to |
A setting: its key, and its new value as JSON (the steppers, toggles and choices make them) |
data-<prefix>sel |
Picks someone (to selectTab) |
data-<prefix>do |
A mind’s button: close lets go of whoever’s picked; the rest are acts[name](sel, el, d) |
The game’s own attributes go to click, change and input, each a table by the attribute’s name (click: { v: (id) => pickVillager(id) }, input: { findwho: (v, el) => filter(el.value) }). A button’s focus is let go after its tap, so the game’s keys are the game’s again.
The pieces
What every tab is made of, as HTML with the drawer’s prefix (a Bits of a prefix gives them without a drawer):
d.section('Clock', 'day 3') // <h5>, with a small note
d.kv('day 3 · 08:15 · raining') // a line
d.hint('Tap someone to read their mind.')
d.chips(d.button('cr', '+1000 cr') + d.button('go', 'Go there', { arg: id }))
d.bar(0.62, { hot: true }) // warm over 0.4, hot over 0.7, or a color
d.bars([['hunger', 0.34], ['energy', 0.8, { hot: true }]]) // a label, a bar and a number each
d.table(['who', 'doing'], [[name, 'pay'], { cells: [name2, 'eat'], cls: 'cur' }], { nums: [] })
d.who(id, 'Ines') // a link that picks them
d.stepper({ key: 'speed', label: 'Run speed', value: s.speed, values: [0.5, 0.75, 1, 1.5, 2], fmt: (v) => '×' + v })
d.stepper({ key: 'time', label: 'Time', value: hour, step: [0.25, 1], fmt: clock, stepWord: (h) => (h >= 1 ? h + 'h' : h * 60 + 'm') })
d.toggle({ key: 'freeze', on: s.freeze, label: 'Stop the clock' })
d.choice({ key: 'weather', label: 'Weather', value: s.weather, options: [['auto', 'by the clock'], ['rain', 'rain'], ['dry', 'dry']] })
A stepper goes through a list of values, or by step (several steps make a pair of buttons each, the biggest outermost) between min and max (landing on a bound before it stops), or round them with wrap. Steppers, never sliders, for anything that changes play: a thumb scrolling past can’t move one. A stepper, a toggle or a choice off its default (def, or the play table’s) is marked, and a stepper has ↺ to put it back. num(v, places) and esc, attr are there too.
Settings that change play
play lists them, each with its default (read from the flags by its key, or by get()) and how to say it (word(v)). While any is off its default, a small tag is on screen, the drawer open or not (“run ×0.5 · clock stopped”); a tap on it opens the drawer at tagTab. A run speed left at half by a slip of the thumb can’t go unnoticed.
Overlays
overlays are switches in the drawer’s head for what the game draws over itself: a checkbox each, or a choice of options (the first meaning off). d.overlay(id) is true (or the choice’s value) while it shows, which is only while the drawer’s open unless keepOverlays; they’re kept in the flags. With layer: true the drawer has a canvas over the game for them: d.layer() each frame gives { g, w, h }, cleared and scaled to CSS px, while any overlay shows, and null (the canvas cleared away) when none does.
pick(x, y, e), given, makes a tap on the game while the drawer’s open ask who’s there (CSS px): someone found is picked (select, to selectTab) and the tap goes no further, so it doesn’t move the game’s stick; nobody there, and the game has the tap.
The events
A tab with events: true shows what happened, newest first: the drawer’s notes, and what events.from() gives (another log, such as Tyrell’s, turned into events; oldest first is cheapest, and it’s asked only while the events show), merged by time. Above the list, a chip a kind with its count (the kinds in events.kinds first, then any others as they come), the kinds in apart left out of “all” (each shown when its chip is picked: the many walks and arrivals of a city), pause and clear (cleared, only what comes after shows), a box to find words in them (find), and a line by the filter (hint). Each line is its time (when(t)), its kind (in colors[kind], or the game’s CSS: .<prefix>-ev.k-<kind>) and its words. It’s drawn again only when there’s something new.
events.most (600) are kept and shown (250) shown. An event with an impact gets a column for it and the list a least impact to show: room for the events by impact (events.md), which will be a view here.
A mind in the drawer
A mind from Tycho.inspect.mind with the drawer’s prefix is styled to match, and its buttons work in the drawer (data-<prefix>sel picks someone they know, data-<prefix>do is an act with whoever’s picked). For a drawer it can be dense (inspect.md): each need’s number beside its bar (numbers), the chance each option had of being drawn (chances), the options as a table (table), and what they’d choose if they chose now, every option weighed again without changing anything, with what’s ruled out and why (now).
tabs: [{ id: 'mind', every: 0.5, html: (d) => d.sel ? Tycho.inspect.mind(world, d.sel, { prefix: d.P, numbers: true, chances: true, table: true, now: true }) : pickList(d) }]
Its look
Compact, dark and monospace, put first in the page’s head (so the game’s own CSS overrides it) unless style: false; drawerCss(prefix) is the stylesheet. Every class and id carries the prefix (dev by default: #dev, .dev-head, .dev-tabs, .dev-overlays, #dev-fps, .dev-body, .dev-chips, .dev-bar, .dev-ev, #dev-tag, #dev-layer). Its colors are CSS variables on it, for a game to give its own: --dv-bg, --dv-fg, --dv-dim, --dv-line, --dv-btn, --dv-acc (tabs, links, what’s on), --dv-hi (titles, and what’s off its default), --dv-up, --dv-down, and its width --dv-w.
Merryfolk’s drawer, on these options
Merryfolk keeps its own drawer until its harm work lands; every part of it is one of these options, so it can move without changing:
| Merryfolk’s | The drawer’s |
|---|---|
The ` key, the Dev button (#btn-dev, active while open), body.dev-on, always there |
keys (the default), button: () => $('btn-dev'), bodyClass (dev-on, the default), always: true |
<aside id="dev"> in #game, “Developer mode” |
prefix: 'dev' (the default), parent: () => $('game'), label |
| Brain, Events, World, Tools; Brain and World every 0.4 s, Events when there’s something new, Tools only when something’s done | tabs (every 0.4 by default, events: true, every: 0) |
| The overlays’ row (labels, paths, grid & lots, cliffs, chunks, the land’s choice), drawn only while it’s open | overlays (a checkbox each, options for the land), overlay(id) |
| The frame line (fps, sim, paint, draw, the folk, the speed, the chunks, the buckets, a red ! while an error’s been caught) | frame(d), with time('sim', ms) and the rest from the loop |
devLog(cat, text), the last 600, 250 shown, Game.time, d1 08:23, the chips by kind with their counts, pause and clear, a hint for decide and talk, the kinds’ colors |
note, events.most, shown, clock, when, kinds (DEV_CATS), hint, colors (or its CSS on .dev-ev.k-<kind> .dev-k) |
A villager’s link (data-v) picks them and shows the Brain; data-act runs a tool; data-tune changes a number in TUNE |
click: { v } (or data-devsel with selectTab: 'brain'), acts (or click: { act }), change: { tune } |
Its settings under merryfolk-dev |
flags (the drawer’s own state under flags.s.drawer) |
| Its look and its phone layout (a sheet at the foot under 720 px, the panel and the bottom bar stepping aside) | Its own CSS over the drawer’s (on #dev, body.dev-on) |