update for tiled stuff

This commit is contained in:
bryanthaboi
2026-07-28 20:32:29 -04:00
parent 370ddea0b8
commit 52e3a0d23f
8 changed files with 1535 additions and 0 deletions
+4
View File
@@ -35,3 +35,7 @@ mobile/ios/build/
# Legacy manual convenience-copy location (superseded by /dist/android/)
mobile/dist/
# Tiled map-editing workspace (tools/tiled_export.py). Derived from the imported
# ROM cache exactly like data/generated/, so it is never committable.
/build/tiled/
+5
View File
@@ -150,6 +150,11 @@ reference — lives on the
Shipped example mods, one per kind of author, live in `[mods/](mods/)`.
Maps can be edited in our own build of [Tiled](https://www.mapeditor.org),
[bryanthaboi/tiled_gen1recomp](https://github.com/bryanthaboi/tiled_gen1recomp/releases),
and exported back out as a mod; see
[docs/tiled-map-editing.md](docs/tiled-map-editing.md).
## Bugs and Ideas
Found a bug? A warp dropping you somewhere it shouldn't, a battle doing math
+17
View File
@@ -18,6 +18,23 @@ Regenerate the reference straight into a wiki checkout:
luajit tools/gen_registry_docs.lua ../gen1recomp.wiki
```
## Editing maps in Tiled
Maps are data, not assets, so they can be authored in a real map editor and
exported as a mod. `tools/tiled_export.py` builds a
[Tiled](https://www.mapeditor.org) workspace out of the imported ROM cache:
```sh
python3 tools/tiled_export.py # -> build/tiled/ (gitignored)
```
Open `build/tiled/gen1.tiled-project`, edit any of the 222 maps (or
`kanto.world` for the stitched overworld), and export with the
`gen1-mod-export` extension — one map file, or a whole loadable mod folder.
An edited vanilla map becomes a `mod.content.maps:patch` carrying only the
fields that moved; a new map becomes a `:register`. See
`docs/new-features.md` and the extension's own README.
## Rendering pipelines
Most registries hand the engine *content*. `render_pipelines` hands it
+6
View File
@@ -338,3 +338,9 @@ second, relabelling itself to `Confirm?` in between.
A validation pill in the tab rail mirrors what the running game would
quarantine on load; clicking it jumps to the tab holding the first problem.
## Tiled map editing (mod authoring)
`tools/tiled_export.py` turns the imported ROM cache into a Tiled workspace,
so maps can be edited in a real map editor and exported back out as a mod.
It has its own document: docs/tiled-map-editing.md.
+65
View File
@@ -0,0 +1,65 @@
# Tiled map editing (mod authoring)
`tools/tiled_export.py` turns the imported ROM cache into a
[Tiled](https://www.mapeditor.org) workspace, so maps can be edited in a
real map editor and exported back out as a mod. The original had no map
editor at all; the port's own map data is plain Lua, which is what makes
this a data path rather than an asset path.
Editing is done in our own Tiled build,
[bryanthaboi/tiled_gen1recomp](https://github.com/bryanthaboi/tiled_gen1recomp/releases),
which ships the `gen1-mod-export` extension the workspace relies on. Grab it
from that repo's releases; upstream Tiled opens the workspace but cannot
export a mod out of it.
```sh
python3 tools/tiled_export.py # -> build/tiled/ (gitignored)
```
Then open `build/tiled/gen1.tiled-project` in that build of Tiled.
- **The overworld is one surface.** All 222 maps become `maps/*.tmj`, and
`kanto.world` places the 36 connected overworld maps at their real
connection offsets. That world is pre-loaded (seeded into the workspace's
Tiled session), so opening any one overworld map draws its neighbors around
it and you scroll and edit straight across the seams. Everything else is a
double-click away in Tiled's project panel.
- **Extending Kanto wires both ends.** A connection lives on both maps, so
hooking a new map onto a base map also emits the return connection as a
patch on that base map, keeping its other directions intact. The return
offset is derived, not guessed: all 78 vanilla reciprocal pairs satisfy
`back.offset == -offset`.
- **A Tiled tile is a gen1 block.** Each of the 24 tilesets becomes a Tiled
tileset whose tiles are its 32x32 blocks, composited from the 8x8 sheet,
so a tile layer *is* the map's `blocks` array. Warps, signs and objects
sit on the 16px cell grid in object layers, which is the grid the engine
addresses them on.
- **Collision is visible.** View > Show Tile Collision Shapes draws the real
walkability: a rectangle covers each cell whose feet tile is not in the
tileset's `walkable` list, which is the rule `src/world/Map.lua` applies.
- **Maps are shown in their real colors.** Each map is atlased in the SGB
palette it renders with, so Cerulean is blue and Lavender is purple in the
editor exactly as in game. Vanilla resolves that through a cascade with
interiors inheriting the last outdoor map, so the workspace mirrors the
cascade and walks the warp graph to colour interiors. Changing a map's
`palette` exports `palette = "..."` on the record, which beats the cascade,
and the editor offers the real palette names as a dropdown.
- **New blocks and new tilesets.** `blocksets/*.tmj` show a tileset's blocks
as raw 8x8 tiles, four by four, so new blocks can be composed there;
per-tile flags on `tilesets/tiles_*.tsj` become `walkable`, `waterTiles`,
`doorTiles` and the rest.
- **Export is a diff, not a fork of the data.** The `gen1-mod-export`
extension (shipped in `tiled_gen1recomp`) writes either one map file or a whole
loadable mod folder. An edited vanilla map diffs against the imported data
and emits `mod.content.maps:patch` carrying *only* the fields that moved, so
a mod covers the parts it changes and leaves the rest to the base game; a
new map gets `:register` at an index of 1000 or above. An unchanged map
exports nothing at all. Exports pass `tools/modkit.py validate` and `lint`.
- **Or the whole record, on request.** Ticking `exactExport` on a map switches
it to `mod.content.maps:override`, pinning the map to exactly what the
editor shows. It is off by default because an override wins outright over
any other mod patching that map, where a patch composes.
No ROM-derived art travels into an exported mod: a tileset still drawing on
the player's own imported sheet references that path rather than shipping the
pixels, and only a sheet the author supplied is copied in.
+6
View File
@@ -493,6 +493,12 @@ R.maps = {
width = f.int(1), height = f.int(1),
blocks = f.list(f.int(0, 255)),
borderBlock = f.opt(f.int(0, 255)),
-- A named SGB palette, which wins over the field.palettes cascade
-- (OverworldController.lua:506 reads map.def.palette first). Deliberately
-- a plain string rather than f.id("palettes"): the ROM-free fixture base
-- carries no palettes at all, so an id reference would fail validation for
-- a perfectly good mod wherever there is no imported dataset.
palette = f.opt(f.str),
warps = f.opt(f.list(f.rec{ x = f.int(0), y = f.int(0),
destMap = f.str, destWarp = f.int(0) })),
objects = f.opt(f.list(f.any)),
+102
View File
@@ -0,0 +1,102 @@
-- Dump one generated data table as JSON.
--
-- luajit tools/lua_to_json.lua data/generated/maps.lua
--
-- The generated tables are plain data (the extractor serializes them with no
-- functions or cycles), so a straight recursive encode is enough. This exists
-- so Python tooling -- tools/tiled_export.py -- can read the same records the
-- engine reads without carrying a Lua parser of its own, which is the usual
-- way those two drift apart.
local path = ...
if not path then
io.stderr:write("usage: luajit tools/lua_to_json.lua <data/generated/x.lua>\n")
os.exit(2)
end
local chunk, err = loadfile(path)
if not chunk then
io.stderr:write("cannot load " .. path .. ": " .. tostring(err) .. "\n")
os.exit(1)
end
local root = chunk()
local ESCAPES = {
['"'] = '\\"', ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t",
}
local function encodeString(s)
return '"' .. s:gsub('[%c"\\]', function(c)
return ESCAPES[c] or string.format("\\u%04x", c:byte())
end) .. '"'
end
local function encodeNumber(n)
if n ~= n or n == math.huge or n == -math.huge then
error("non-finite number in " .. path)
end
-- integers must not pick up a ".0": tile and block ids are compared as
-- integers on the Python side and in the exported Lua
if n % 1 == 0 then return string.format("%d", n) end
return string.format("%.17g", n)
end
-- A table is an array when its keys are exactly 1..#t. Empty tables encode as
-- [] and the reader coerces; nothing in the generated data distinguishes an
-- empty list from an empty map.
local function isArray(t)
local count = 0
for k in pairs(t) do
if type(k) ~= "number" or k % 1 ~= 0 or k < 1 then return false end
count = count + 1
end
return count == #t
end
local out = {}
local function emit(s) out[#out + 1] = s end
local function encode(value)
local kind = type(value)
if value == nil then
emit("null")
elseif kind == "boolean" then
emit(value and "true" or "false")
elseif kind == "number" then
emit(encodeNumber(value))
elseif kind == "string" then
emit(encodeString(value))
elseif kind == "table" then
if isArray(value) then
emit("[")
for i = 1, #value do
if i > 1 then emit(",") end
encode(value[i])
end
emit("]")
else
-- sort keys so a regenerated dump is byte-stable and diffable
local keys = {}
for k in pairs(value) do keys[#keys + 1] = k end
table.sort(keys, function(a, b)
if type(a) == type(b) then return a < b end
return type(a) == "number"
end)
emit("{")
for i, k in ipairs(keys) do
if i > 1 then emit(",") end
emit(encodeString(tostring(k)))
emit(":")
encode(value[k])
end
emit("}")
end
else
error("cannot encode a " .. kind .. " in " .. path)
end
end
encode(root)
io.write(table.concat(out))
io.write("\n")
File diff suppressed because it is too large Load Diff