- Python 63.1%
- Lua 33.9%
- Makefile 3%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| .vscode | ||
| games | ||
| lib | ||
| tools | ||
| .gitattributes | ||
| .gitignore | ||
| .luacheckrc | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
tts-games
Games I build in Tabletop Simulator — the Lua that runs them, the custom UI XML, and the save files they ship as. One repo holding several games, plus the library they share.
Why one repo
TTS Lua has no package manager. require is not a runtime feature — it only
works because a bundler inlines the module before the script reaches the game,
and bundlers resolve modules by filesystem search path. Split these games
into separate repos and every shared helper becomes a git submodule or a
copy-paste, both of which rot. Keep them together and sharing code is one -p
flag in the build.
The rest follows: one .luacheckrc, one CI workflow, one clone to open in
VSCode, and a refactor of a shared helper lands in a single commit across every
game that uses it.
When to split a game out: it gets real players filing issues and wants its
own tracker, or a collaborator should see exactly one game. Extract it with
git filter-repo and pull lib/ back in as a submodule — history survives.
Merging repos later is the messier direction, which is why this starts unified.
Layout
lib/ shared modules; no globals, no side effects
games/
_template/ copy this to start a game
game.json manifest -- version here is the source of truth
src/Global.-1.ttslua Global script (-1 is TTS's reserved GUID)
src/<Name>.<GUID>.ttslua one file per scripted object
ui/Global.xml custom UI, if the game uses it
saves/TS_Save_N.json the exported mod
tools/normalize_save.py makes save diffs readable
Requirements
- Node — only for
npx luabundler; nothing here is an npm project. - Lua 5.2 + luacheck (
luarocks install luacheck) for linting. - Python 3.11+ for the save normalizer. Standard library only.
- VSCode with a Tabletop Simulator Lua extension, if you want to push scripts into a running game instead of pasting them. See .vscode/settings.json — the config key prefix differs between the two extensions in circulation, and the file says how to tell.
Working on a game
make new GAME=cosmic-frogs # copy the template
make lint # luacheck lib/ and games/
make bundle GAME=cosmic-frogs # -> build/cosmic-frogs/Global.lua
Paste the bundle, never the source. src/Global.-1.ttslua contains
require("log") calls that TTS cannot resolve on its own; they are inlined by
make bundle. A script that works in the editor and errors in game is almost
always this.
Save files
The save JSON is the actual artifact — it holds every object, asset URL, and snap point — so it stays committed. It is also machine-generated, tens of thousands of lines, and re-saving after nudging one card rewrites float positions across the whole file.
Run make normalize before committing a save. It sorts keys, rounds floats to
four decimals, and reindents — all semantically inert, and it turns a 30,000-line
diff back into the handful of lines that actually changed. CI enforces it.
It does not reorder ObjectStates by default. That array is spawn order and
decides what lands on top of what among coplanar objects; --sort-objects is
there if you want it, but check the table in game afterward.
Before you commit a save, know that it can carry more than the game:
- notebook tabs keep whatever was typed in them, including playtest notes;
- object descriptions and nicknames keep whatever anyone typed on them;
- asset URLs point at wherever you hosted the art.
Releases
Games version independently. The version in a game's game.json is the source
of truth; tags are prefixed with the slug:
cosmic-frogs/v1.2.0
Each game keeps its own CHANGELOG.md. The root CHANGELOG.md covers lib/
and the tooling only.
License
MIT — see LICENSE. The license covers the code in this repo. Art and other assets referenced by a save file are governed by whatever terms they shipped under, which is not necessarily this one.