feat(assets): #81 namespaced spritesheet/atlas API (Sprite.* / Assets.*)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m24s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 29s

Sprite loading was a bare png_load — one file per 16x16 sprite, no way to load
one sheet and address a cell by grid coords or name. Adds a Sprite.*/Assets.*
runtime (atlas.ludic) over the variable-size image loader, so a cell is a
sub-rect of the kept image and is NOT restricted to the 16x16 sprite table:
Sprite.sheet(path,cw,ch), Sprite.cell(sheet,col,row),
Sprite.cell_span(sheet,col,row,cols,rows) (a sprite may span >1 cell),
Sprite.define/named (name + lookup), Sprite.draw/draw_scaled (through
camera/zoom/clip like Screen.sprite), Sprite.width/height, and
Assets.image/load/get. Spliced on demand (Sprite.sheet/… or Assets.*), so a
program using neither is byte-identical.

Example examples/library/atlas.ludic (verified against a real 12x11 Kenney
sheet, incl. a 2x3 multi-cell span and named lookup). 14 docs pages. Full
suite 114/0, goldens byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-02 07:58:13 +03:00
parent ab4546e365
commit f3f1336882
21 changed files with 34542 additions and 32915 deletions

View file

@ -0,0 +1,3 @@
bump: minor
type: feat
Namespaced spritesheet / atlas API (#81). Sprite loading was a bare `png_load("floor0.png")` — one file per 16x16 sprite, with no way to load one sheet and address a cell by grid coords or name. Adds a `Sprite.*` / `Assets.*` runtime (over the variable-size image loader, so a cell is a sub-rect of the kept image and is **not** restricted to the 16x16 sprite table): `Sprite.sheet(path, cellw, cellh)` -> handle, `Sprite.cell(sheet, col, row)` and `Sprite.cell_span(sheet, col, row, cols, rows)` -> id (a sprite may span more than one cell — a tall character, a wide object), `Sprite.define(name, …)` / `Sprite.named(name)` to name and look up a cell, `Sprite.draw` / `Sprite.draw_scaled` (through the camera / zoom / clip, like `Screen.sprite`), `Sprite.width` / `height`, and `Assets.image(path)` / `Assets.load` / `Assets.get(name)`. Spliced on demand; a program that uses neither compiles byte-identically. Example: `examples/library/atlas.ludic`.

View file

@ -0,0 +1,7 @@
---
id: assets
title: Assets
order: 34
---
Loading and lookup for whole-file assets, paired with the <a href="sprite-sheet"><code>Sprite.*</code></a> atlas API. <a href="assets-image"><code>Assets.image</code></a> (and its alias <a href="assets-load"><code>Assets.load</code></a>) loads a whole image file as one sprite; <a href="assets-get"><code>Assets.get</code></a> looks a named sprite up.

View file

@ -0,0 +1,14 @@
---
id: assets-get
name: Assets.get
category: assets
kind: namespace-method
tokens: Assets.get
sig: Assets.get(name) -> int
tip: Look up a named sprite's id (alias of Sprite.named).
order: 3
ns: Assets
member: get
---
Alias of <a href="sprite-named"><code>Sprite.named</code></a>: returns the sprite id registered under <code>name</code>, or <code>-1</code>.

View file

@ -0,0 +1,14 @@
---
id: assets-image
name: Assets.image
category: assets
kind: namespace-method
tokens: Assets.image
sig: Assets.image(path) -> int
tip: Load a whole image file as one sprite; returns its id.
order: 1
ns: Assets
member: image
---
Loads the image at <code>path</code> as a single atlas sprite the size of the image, returning its id — the namespaced replacement for a bare <code>png_load</code>. Draw it with <a href="sprite-draw"><code>Sprite.draw</code></a>.

View file

@ -0,0 +1,14 @@
---
id: assets-load
name: Assets.load
category: assets
kind: namespace-method
tokens: Assets.load
sig: Assets.load(path) -> int
tip: Alias of Assets.image.
order: 2
ns: Assets
member: load
---
Alias of <a href="assets-image"><code>Assets.image</code></a>: loads a whole image file as one sprite and returns its id.

View file

@ -0,0 +1,7 @@
---
id: sprite
title: Sprite
order: 33
---
A namespaced spritesheet / atlas API. <a href="sprite-sheet"><code>Sprite.sheet</code></a> loads one image and remembers its cell grid; <a href="sprite-cell"><code>Sprite.cell</code></a> addresses a cell by grid coords and <a href="sprite-cell_span"><code>Sprite.cell_span</code></a> a sprite that spans more than one cell; <a href="sprite-define"><code>Sprite.define</code></a> / <a href="sprite-named"><code>Sprite.named</code></a> name and look up a cell; <a href="sprite-draw"><code>Sprite.draw</code></a> / <a href="sprite-draw_scaled"><code>Sprite.draw_scaled</code></a> blit it (through the camera / zoom / clip). It stands on the variable-size image loader, so a cell can be any size — not just 16x16. (Distinct from the <code>Sprite</code> engine component of the sprite-render system.)

View file

@ -0,0 +1,14 @@
---
id: sprite-cell
name: Sprite.cell
category: sprite
kind: namespace-method
tokens: Sprite.cell
sig: Sprite.cell(sheet, col, row) -> int
tip: One cell of a sheet, by grid coords; returns a sprite id.
order: 2
ns: Sprite
member: cell
---
Returns a sprite id for the cell at column <code>col</code>, row <code>row</code> of <code>sheet</code> (the sub-rect <code>(col*cellw, row*cellh, cellw, cellh)</code>). Draw it with <a href="sprite-draw"><code>Sprite.draw</code></a>.

View file

@ -0,0 +1,14 @@
---
id: sprite-cell_span
name: Sprite.cell_span
category: sprite
kind: namespace-method
tokens: Sprite.cell_span
sig: Sprite.cell_span(sheet, col, row, cols, rows) -> int
tip: A sprite spanning cols x rows cells (tall/wide art).
order: 3
ns: Sprite
member: cell_span
---
Returns a sprite id for a sub-rect spanning <code>cols x rows</code> cells from <code>(col, row)</code> — for a sprite that covers more than one cell (a tall character, a wide object). Same as <a href="sprite-cell"><code>Sprite.cell</code></a> but sized to the span.

View file

@ -0,0 +1,14 @@
---
id: sprite-define
name: Sprite.define
category: sprite
kind: namespace-method
tokens: Sprite.define
sig: Sprite.define(name, sheet, col, row) -> int
tip: Name a cell for later lookup; returns its sprite id.
order: 4
ns: Sprite
member: define
---
Creates the cell at <code>(col, row)</code> of <code>sheet</code> and registers it under <code>name</code>, returning its sprite id. Look it up later with <a href="sprite-named"><code>Sprite.named</code></a> — content data (item/tile names) can drive rendering without hard-coded ids.

View file

@ -0,0 +1,14 @@
---
id: sprite-draw
name: Sprite.draw
category: sprite
kind: namespace-method
tokens: Sprite.draw
sig: Sprite.draw(id, x, y)
tip: Blit an atlas sprite at (x, y) through the camera/zoom/clip.
order: 6
ns: Sprite
member: draw
---
Draws atlas sprite <code>id</code> with its top-left at <code>(x, y)</code>. Sufficiently-opaque pixels are drawn through <code>rt_put_px</code>, so the camera, zoom and clip apply exactly like <a href="screen-sprite"><code>Screen.sprite</code></a>.

View file

@ -0,0 +1,14 @@
---
id: sprite-draw_scaled
name: Sprite.draw_scaled
category: sprite
kind: namespace-method
tokens: Sprite.draw_scaled
sig: Sprite.draw_scaled(id, x, y, scale)
tip: Blit an atlas sprite scaled by an integer factor.
order: 7
ns: Sprite
member: draw_scaled
---
Draws atlas sprite <code>id</code> at <code>(x, y)</code> scaled by the integer <code>scale</code> (each source pixel becomes a <code>scale x scale</code> block). For sub-integer / camera zoom, use <a href="camera-zoom"><code>Camera.zoom</code></a> around a plain <a href="sprite-draw"><code>Sprite.draw</code></a>.

View file

@ -0,0 +1,14 @@
---
id: sprite-height
name: Sprite.height
category: sprite
kind: namespace-method
tokens: Sprite.height
sig: Sprite.height(id) -> int
tip: The atlas sprite's height in pixels.
order: 9
ns: Sprite
member: height
---
Returns the pixel height of atlas sprite <code>id</code> (its sub-rect height).

View file

@ -0,0 +1,14 @@
---
id: sprite-named
name: Sprite.named
category: sprite
kind: namespace-method
tokens: Sprite.named
sig: Sprite.named(name) -> int
tip: Look up a named sprite's id, or -1.
order: 5
ns: Sprite
member: named
---
Returns the sprite id registered under <code>name</code> by <a href="sprite-define"><code>Sprite.define</code></a>, or <code>-1</code> if none. Names compare by content.

View file

@ -0,0 +1,14 @@
---
id: sprite-sheet
name: Sprite.sheet
category: sprite
kind: namespace-method
tokens: Sprite.sheet
sig: Sprite.sheet(path, cellw, cellh) -> int
tip: Load a spritesheet and remember its cell grid; returns a sheet handle.
order: 1
ns: Sprite
member: sheet
---
Loads the PNG at <code>path</code> and records that its cells are <code>cellw x cellh</code> px, returning a sheet <em>handle</em>. Address its cells with <a href="sprite-cell"><code>Sprite.cell</code></a> / <a href="sprite-cell_span"><code>Sprite.cell_span</code></a>. The whole image is kept (variable size), so cells are not restricted to the 16x16 sprite table.

View file

@ -0,0 +1,14 @@
---
id: sprite-width
name: Sprite.width
category: sprite
kind: namespace-method
tokens: Sprite.width
sig: Sprite.width(id) -> int
tip: The atlas sprite's width in pixels.
order: 8
ns: Sprite
member: width
---
Returns the pixel width of atlas sprite <code>id</code> (its sub-rect width) — handy for centering and layout.

View file

@ -0,0 +1,40 @@
# atlas.ludic — the namespaced spritesheet / atlas API (#81): load one sheet and
# address a cell by grid coords or by name, including sprites that span more than
# one cell, instead of one png_load per 16x16 file. Cells are sub-rects of the kept
# image, so they are not restricted to the 16x16 sprite table. Draws go through the
# camera / zoom / clip.
#
# Deterministic; a full run prints: 16 16 32 48 1 1 1 16 1
program AtlasDemo {
# a model so the program runs the ECS and links the core runtime / framebuffer.
property Cam { z: fixed = 1.0 }
model V { Cam }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
entry {
let sheet = Sprite.sheet("assets/kenney/tiny-dungeon/Tilemap/tilemap_packed.png", 16, 16)
let cell = Sprite.cell(sheet, 0, 0)
print(Sprite.width(cell)) # 16
print(Sprite.height(cell)) # 16
let span = Sprite.cell_span(sheet, 0, 0, 2, 3) # a sprite spanning 2x3 cells
print(Sprite.width(span)) # 32
print(Sprite.height(span)) # 48
let d = Sprite.define("hero", sheet, 2, 3) # name a cell
print(bi(Sprite.named("hero") == d)) # 1 — looked up by name
print(bi(Assets.get("hero") == d)) # 1 — Assets.get is the same lookup
print(bi(Sprite.named("nope") == (0 - 1))) # 1 — an unknown name is -1
let whole = Assets.image("assets/kenney/tiny-dungeon/Tiles/tile_0096.png")
print(Sprite.width(whole)) # 16 — a whole tile as one sprite
# draw verification: clear, draw the tile, count non-transparent pixels drawn.
Screen.clear(0)
Sprite.draw(whole, 10, 10)
var n = 0
for yy in 10 .. 26 { for xx in 10 .. 26 { if Screen.pixel(xx, yy) != 0 { n = n + 1 } } }
print(bi(n > 0)) # 1 — the sprite drew content
}
}

212
runtime/native/atlas.ludic Normal file
View file

@ -0,0 +1,212 @@
# ============================================================================
# atlas.ludic — a namespaced spritesheet / atlas API (#81).
#
# Sprite loading used to be a bare png_load("assets/sprites/floor0.png") — one
# file per 16x16 sprite, and no way to load one sheet and address a cell by grid
# coords or by name. This adds:
#
# Sprite.sheet(path, cellw, cellh) -> handle load a sheet, remember its grid
# Sprite.cell(sheet, col, row) -> id one cell, addressed by grid coords
# Sprite.cell_span(sheet, col, row, cols, rows) a sprite spanning >1 cell (tall/wide)
# Sprite.define(name, sheet, col, row) -> id name a cell for later lookup
# Sprite.named(name) -> id look a named sprite up
# Sprite.draw(id, x, y) / draw_scaled(id,x,y,sc) blit an atlas sprite
# Sprite.width(id) / height(id) its pixel size
# Assets.image(path) -> id a whole file as one sprite
# Assets.load(path) -> id alias of Assets.image
# Assets.get(name) -> id alias of Sprite.named
#
# It stands on the variable-size image loader in image.ludic (rt_image_load keeps
# the whole decoded PNG; a cell is a sub-rect of it), so a sprite is NOT restricted
# to the 16x16 sprite table — a cell (and a multi-cell span) can be any size. Draws
# go through rt_put_px, so the camera, zoom (#78) and clip all apply, exactly like
# Screen.sprite. Spliced on demand when a program uses Sprite.sheet / Assets.*.
# ============================================================================
const ATLAS_MAX_SHEET: int = 32
const ATLAS_MAX_SPR: int = 1024
const ATLAS_MAX_NAME: int = 512
var at_sheet_img: words = null # image id per sheet
var at_sheet_cw: words = null # cell width per sheet
var at_sheet_ch: words = null # cell height per sheet
var at_nsheet: int = 0
var at_spr_sheet: words = null # owning sheet per atlas sprite
var at_spr_sx: words = null # source x/y (px) of the sub-rect
var at_spr_sy: words = null
var at_spr_w: words = null # sub-rect width/height (px)
var at_spr_h: words = null
var at_nspr: int = 0
var at_name_str: pointers = null # name per named sprite
var at_name_id: words = null # atlas id per name
var at_nname: int = 0
function atlas_init() -> void {
if at_sheet_img != null { return }
at_sheet_img = words(ATLAS_MAX_SHEET)
at_sheet_cw = words(ATLAS_MAX_SHEET)
at_sheet_ch = words(ATLAS_MAX_SHEET)
at_spr_sheet = words(ATLAS_MAX_SPR)
at_spr_sx = words(ATLAS_MAX_SPR)
at_spr_sy = words(ATLAS_MAX_SPR)
at_spr_w = words(ATLAS_MAX_SPR)
at_spr_h = words(ATLAS_MAX_SPR)
at_name_str = bytes(ATLAS_MAX_NAME * 8) # a pointer (8 bytes) per name slot
at_name_id = words(ATLAS_MAX_NAME)
}
# load a spritesheet PNG whose cells are cw x ch px; returns a sheet handle (>= 0).
function atlas_sheet(path: pointer, cw: int, ch: int) -> int {
atlas_init()
if at_nsheet >= ATLAS_MAX_SHEET { return 0 - 1 }
let img = rt_image_load(path)
if img < 0 { return 0 - 1 }
let h = at_nsheet
at_sheet_img[h] = img
at_sheet_cw[h] = cw
at_sheet_ch[h] = ch
at_nsheet = at_nsheet + 1
return h
}
# register an atlas sprite for a sub-rect (px) of a sheet's image.
function atlas_make(sheet: int, sx: int, sy: int, w: int, h: int) -> int {
atlas_init()
if at_nspr >= ATLAS_MAX_SPR { return 0 - 1 }
let id = at_nspr
at_spr_sheet[id] = sheet
at_spr_sx[id] = sx
at_spr_sy[id] = sy
at_spr_w[id] = w
at_spr_h[id] = h
at_nspr = at_nspr + 1
return id
}
# one cell (col, row) of a sheet, addressed by grid coords.
function atlas_cell(sheet: int, col: int, row: int) -> int {
if (sheet < 0) or (sheet >= at_nsheet) { return 0 - 1 }
let cw = at_sheet_cw[sheet]
let ch = at_sheet_ch[sheet]
return atlas_make(sheet, col * cw, row * ch, cw, ch)
}
# a sprite spanning cols x rows cells from (col, row) — some sprites cover more
# than one cell (a tall character, a wide object).
function atlas_cell_span(sheet: int, col: int, row: int, cols: int, rows: int) -> int {
if (sheet < 0) or (sheet >= at_nsheet) { return 0 - 1 }
let cw = at_sheet_cw[sheet]
let ch = at_sheet_ch[sheet]
return atlas_make(sheet, col * cw, row * ch, cols * cw, rows * ch)
}
# name a cell so a game can look it up by name; returns the atlas id.
function atlas_define(name: pointer, sheet: int, col: int, row: int) -> int {
atlas_init()
let id = atlas_cell(sheet, col, row)
if id < 0 { return 0 - 1 }
if at_nname < ATLAS_MAX_NAME {
at_name_str[at_nname] = name
at_name_id[at_nname] = id
at_nname = at_nname + 1
}
return id
}
# look up a named sprite's atlas id (names compare by content), or -1.
function atlas_named(name: pointer) -> int {
atlas_init()
var i = 0
while i < at_nname {
if at_name_str[i] == name { return at_name_id[i] }
i = i + 1
}
return 0 - 1
}
# load a whole image file as one atlas sprite (a 1x1 sheet the size of the image).
function atlas_image(path: pointer) -> int {
atlas_init()
if at_nsheet >= ATLAS_MAX_SHEET { return 0 - 1 }
let img = rt_image_load(path)
if img < 0 { return 0 - 1 }
let w = img_w[img]
let h = img_h[img]
let sh = at_nsheet
at_sheet_img[sh] = img
at_sheet_cw[sh] = w
at_sheet_ch[sh] = h
at_nsheet = at_nsheet + 1
return atlas_make(sh, 0, 0, w, h)
}
# blit an atlas sprite at (dx, dy) through rt_put_px (camera / zoom / clip apply).
# Only sufficiently-opaque pixels are drawn (the sprite's transparent border is
# skipped), matching Screen.sprite.
function atlas_draw(id: int, dx: int, dy: int) -> void {
atlas_init()
if (id < 0) or (id >= at_nspr) { return }
let sheet = at_spr_sheet[id]
let img = at_sheet_img[sheet]
let iw = img_w[img]
let ih = img_h[img]
let s: words = img_px[img]
let sx = at_spr_sx[id]
let sy = at_spr_sy[id]
let w = at_spr_w[id]
let h = at_spr_h[id]
var y = 0
while y < h {
let srcy = sy + y
if (srcy >= 0) and (srcy < ih) {
var x = 0
while x < w {
let srcx = sx + x
if (srcx >= 0) and (srcx < iw) {
let px = s[srcy * iw + srcx]
if ((px >> 24) & 255) >= 128 { rt_put_px(dx + x, dy + y, px & 16777215) }
}
x = x + 1
}
}
y = y + 1
}
}
# blit an atlas sprite scaled by an integer factor (through rt_fill_rect blocks).
function atlas_draw_scaled(id: int, dx: int, dy: int, sc: int) -> void {
atlas_init()
if (id < 0) or (id >= at_nspr) { return }
if sc < 1 { return }
let sheet = at_spr_sheet[id]
let img = at_sheet_img[sheet]
let iw = img_w[img]
let ih = img_h[img]
let s: words = img_px[img]
let sx = at_spr_sx[id]
let sy = at_spr_sy[id]
let w = at_spr_w[id]
let h = at_spr_h[id]
var y = 0
while y < h {
let srcy = sy + y
if (srcy >= 0) and (srcy < ih) {
var x = 0
while x < w {
let srcx = sx + x
if (srcx >= 0) and (srcx < iw) {
let px = s[srcy * iw + srcx]
if ((px >> 24) & 255) >= 128 { rt_fill_rect(dx + x * sc, dy + y * sc, sc, sc, px & 16777215) }
}
x = x + 1
}
}
y = y + 1
}
}
# an atlas sprite's pixel size — handy for centering / layout.
function atlas_width(id: int) -> int { atlas_init(); if (id < 0) or (id >= at_nspr) { return 0 }; return at_spr_w[id] }
function atlas_height(id: int) -> int { atlas_init(); if (id < 0) or (id >= at_nspr) { return 0 }; return at_spr_h[id] }

View file

@ -176,6 +176,27 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if (meth == "measure_text") { bare = "measure_text"; push(labels, "text") } if (meth == "measure_text") { bare = "measure_text"; push(labels, "text") }
if (meth == "pixel") { bare = "get_px"; push(labels, "x"); push(labels, "y") } if (meth == "pixel") { bare = "get_px"; push(labels, "x"); push(labels, "y") }
} }
# Sprite.* / Assets.* — the namespaced spritesheet / atlas API (#81), a runtime
# in atlas.ludic over the variable-size image loader. A cell (or multi-cell span)
# is a sub-rect of a loaded sheet; draws go through rt_put_px so camera/zoom/clip
# apply. Distinct from the `Sprite` engine component (#85) — same name, different
# namespace (a namespaced builtin, never an entity).
if (ns == "Sprite") {
if (meth == "sheet") { bare = "atlas_sheet"; push(labels, "path"); push(labels, "cellw"); push(labels, "cellh") }
if (meth == "cell") { bare = "atlas_cell"; push(labels, "sheet"); push(labels, "col"); push(labels, "row") }
if (meth == "cell_span") { bare = "atlas_cell_span"; push(labels, "sheet"); push(labels, "col"); push(labels, "row"); push(labels, "cols"); push(labels, "rows") }
if (meth == "define") { bare = "atlas_define"; push(labels, "name"); push(labels, "sheet"); push(labels, "col"); push(labels, "row") }
if (meth == "named") { bare = "atlas_named"; push(labels, "name") }
if (meth == "draw") { bare = "atlas_draw"; push(labels, "id"); push(labels, "x"); push(labels, "y") }
if (meth == "draw_scaled") { bare = "atlas_draw_scaled"; push(labels, "id"); push(labels, "x"); push(labels, "y"); push(labels, "scale") }
if (meth == "width") { bare = "atlas_width"; push(labels, "id") }
if (meth == "height") { bare = "atlas_height"; push(labels, "id") }
}
if (ns == "Assets") {
if (meth == "image") { bare = "atlas_image"; push(labels, "path") }
if (meth == "load") { bare = "atlas_image"; push(labels, "path") } # alias
if (meth == "get") { bare = "atlas_named"; push(labels, "name") } # alias of Sprite.named
}
# Camera.* — the world-space camera: a draw offset threaded through the render # Camera.* — the world-space camera: a draw offset threaded through the render
# path (runtime/native/core.ludic). set/follow move it; shake jitters it from # path (runtime/native/core.ludic). set/follow move it; shake jitters it from
# the seeded RNG, so a replay shakes identically. # the seeded RNG, so a replay shakes identically.

View file

@ -186,6 +186,10 @@ function p_postfix() -> Node {
if e.a.kind == E_ID and e.a.s == "Xml" { g_uses_xml = true } # splice the XML reader on demand (Tiled #67) if e.a.kind == E_ID and e.a.s == "Xml" { g_uses_xml = true } # splice the XML reader on demand (Tiled #67)
if e.a.kind == E_ID and e.a.s == "Base64" { g_uses_base64 = true } # splice base64 codec + inflate on demand (Tiled #67) if e.a.kind == E_ID and e.a.s == "Base64" { g_uses_base64 = true } # splice base64 codec + inflate on demand (Tiled #67)
if e.a.kind == E_ID and e.a.s == "Tiled" { g_uses_tiled = true } # splice the Tiled map runtime on demand (#69) if e.a.kind == E_ID and e.a.s == "Tiled" { g_uses_tiled = true } # splice the Tiled map runtime on demand (#69)
# #81 — Sprite.sheet/cell/… or Assets.* splice the spritesheet/atlas runtime
# (it reads the framebuffer + the image loader, so it links against core).
if e.a.kind == E_ID and e.a.s == "Sprite" and (e.s == "sheet" or e.s == "cell" or e.s == "cell_span" or e.s == "define" or e.s == "named" or e.s == "draw" or e.s == "draw_scaled" or e.s == "width" or e.s == "height") { g_uses_atlas = true }
if e.a.kind == E_ID and e.a.s == "Assets" { g_uses_atlas = true }
if e.a.kind == E_ID and e.a.s == "Reflect" and (e.s == "serialize" or e.s == "apply") { g_uses_value = true; g_uses_reflect_io = true } # Reflect.serialize/apply -> value tree + world table if e.a.kind == E_ID and e.a.s == "Reflect" and (e.s == "serialize" or e.s == "apply") { g_uses_value = true; g_uses_reflect_io = true } # Reflect.serialize/apply -> value tree + world table
# Input.* action-map / record-replay methods (#7) -> splice input.ludic. # Input.* action-map / record-replay methods (#7) -> splice input.ludic.
# Input.key stays bare (no runtime), so gate on the new methods only. # Input.key stays bare (no runtime), so gate on the new methods only.
@ -442,6 +446,7 @@ var g_uses_value: bool = false # Value.*/Json.*/Reflect.serialize -> splice t
var g_uses_xml: bool = false # Xml.* -> splice the XML reader (Tiled #67) var g_uses_xml: bool = false # Xml.* -> splice the XML reader (Tiled #67)
var g_uses_base64: bool = false # Base64.* -> splice the base64 codec + inflate (Tiled #67) var g_uses_base64: bool = false # Base64.* -> splice the base64 codec + inflate (Tiled #67)
var g_uses_tiled: bool = false # Tiled.* -> splice the Tiled map runtime (#69) var g_uses_tiled: bool = false # Tiled.* -> splice the Tiled map runtime (#69)
var g_uses_atlas: bool = false # Sprite.sheet/cell/… or Assets.* -> splice the spritesheet/atlas runtime (#81)
var g_uses_reflect_io: bool = false # Reflect.serialize/apply -> splice the reflection serializer var g_uses_reflect_io: bool = false # Reflect.serialize/apply -> splice the reflection serializer
var g_uses_esys: bool = false # an engine-owned system component (SpriteAnim/Motion/Light2D) is declared -> splice systems.ludic + force the reflection ABI var g_uses_esys: bool = false # an engine-owned system component (SpriteAnim/Motion/Light2D) is declared -> splice systems.ludic + force the reflection ABI
var g_uses_input: bool = false # a program used Input.bind/down/poll/… (action maps + record/replay) -> splice input.ludic var g_uses_input: bool = false # a program used Input.bind/down/poll/… (action maps + record/replay) -> splice input.ludic
@ -784,6 +789,15 @@ function maybe_splice_runtime() -> void {
do_import("runtime/native/xml.ludic") do_import("runtime/native/xml.ludic")
cur_dir = saved cur_dir = saved
} }
# Sprite.sheet/cell/… or Assets.* (#81) — the spritesheet / atlas runtime. It
# reads the framebuffer and the image loader (rt_image_load / img_px in
# image.ludic), so it links against core.ludic (do_import dedupes).
if g_uses_atlas {
cur_dir = ""
do_import("runtime/native/core.ludic")
do_import("runtime/native/atlas.ludic")
cur_dir = saved
}
# Base64.* (#67) — base64 codec; pull in the DEFLATE inflater alongside it, so # Base64.* (#67) — base64 codec; pull in the DEFLATE inflater alongside it, so
# a plain tool can run the full base64 -> zlib/gzip decode chain (inflate.ludic # a plain tool can run the full base64 -> zlib/gzip decode chain (inflate.ludic
# is self-contained; do_import dedupes when a game already linked it via core). # is self-contained; do_import dedupes when a game already linked it via core).
@ -942,6 +956,7 @@ function parse_program() -> void {
g_uses_xml = false g_uses_xml = false
g_uses_base64 = false g_uses_base64 = false
g_uses_tiled = false g_uses_tiled = false
g_uses_atlas = false
g_uses_reflect_io = false g_uses_reflect_io = false
g_tests = new []Node g_tests = new []Node
g_mod_sys_fn = new []pointer g_mod_sys_fn = new []pointer

File diff suppressed because it is too large Load diff

View file

@ -241,6 +241,7 @@ function cmd_test() -> int {
feat_case("library/grid", "", "1 2 3 4 5 6 7 8 9 10 11 12 13", "grid.ludic (Grid line/flood/line_of_sight + A* pathfinding over the tilemap)") feat_case("library/grid", "", "1 2 3 4 5 6 7 8 9 10 11 12 13", "grid.ludic (Grid line/flood/line_of_sight + A* pathfinding over the tilemap)")
feat_case("library/anim", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34", "anim.ludic (Anim frame/once/pingpong/cell + Tween progress/loop/yoyo/ease/number/round/point/tint)") feat_case("library/anim", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34", "anim.ludic (Anim frame/once/pingpong/cell + Tween progress/loop/yoyo/ease/number/round/point/tint)")
feat_case("library/anim_sugar", "", "4 8 2 1 0 100 100 0 0 1 20 20 30 0 1", "anim_sugar.ludic (Anim.clip/play/on_frame/fired + Motion.to + fluent Tween.to/chain/delay/parallel handles; issue #48)") feat_case("library/anim_sugar", "", "4 8 2 1 0 100 100 0 0 1 20 20 30 0 1", "anim_sugar.ludic (Anim.clip/play/on_frame/fired + Motion.to + fluent Tween.to/chain/delay/parallel handles; issue #48)")
feat_case("library/atlas", "", "16 16 32 48 1 1 1 16 1", "atlas.ludic (#81 Sprite.sheet/cell/cell_span/define/named + Assets.image/get — namespaced spritesheet/atlas with multi-cell sprites)")
feat_case("library/camera_zoom", "", "1 0 0 1 1", "camera_zoom.ludic (Camera.zoom deterministic Q16.16 render-time zoom about the screen centre, verified by pixel readback; issue #78)") feat_case("library/camera_zoom", "", "1 0 0 1 1", "camera_zoom.ludic (Camera.zoom deterministic Q16.16 render-time zoom about the screen centre, verified by pixel readback; issue #78)")
feat_case("library/clear_color", "q", "1 1", "clear_color.ludic (@ClearColor: the Render phase auto-clears to the declared colour + auto-presents, no Screen.clear/show in the handler; issue #86)") feat_case("library/clear_color", "q", "1 1", "clear_color.ludic (@ClearColor: the Render phase auto-clears to the declared colour + auto-presents, no Screen.clear/show in the handler; issue #86)")
feat_case("library/query", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "query.ludic (Query count/first/nearest/within — ECS spatial queries over the reflection ABI)") feat_case("library/query", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "query.ludic (Query count/first/nearest/within — ECS spatial queries over the reflection ABI)")