feat(stdlib): Tiled P4 — objects, custom props/types, templates, opt-in spawn (#72)
- Object layers: all shapes (rectangle/ellipse/point/polygon/polyline/text) and custom properties parse and are queryable — Tiled.object_count / Tiled.object / Tiled.object_shape, and Tiled.prop / prop_int / prop_type over any object / tile / layer / map. - Custom types: Tiled.load_types reads an objecttypes.xml project custom-type table so a class property resolves its default; enum values resolve as strings. - Templates: Tiled.template reads a .tx/.tj, and Tiled.load merges each object's `template` reference under the instance's overrides (field inheritance). - Opt-in spawning: Tiled.spawn / Tiled.spawn_layer map an object's class + properties onto Ludic components through the reflection ABI (Position from x/y, each property to a like-named field). Off by default — no gameplay coupling in the core load. Split into tiled_spawn.ludic, spliced only for a Tiled *game* (the world table exists only under has_ecs), so a plain map-reading tool never links against the reflection ABI. Proven by library/tiled_p4.ludic (18 assertions) incl. the design's named orthogonal-outside.tmx object shapes. x test: 95 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
58e1105f2d
commit
b8c71d2a8e
22 changed files with 18278 additions and 17081 deletions
8
assets/tiled-fixtures/p4_enemy.tx
Normal file
8
assets/tiled-fixtures/p4_enemy.tx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<template>
|
||||||
|
<object type="Enemy" width="16" height="16">
|
||||||
|
<properties>
|
||||||
|
<property name="hp" type="int" value="25"/>
|
||||||
|
</properties>
|
||||||
|
</object>
|
||||||
|
</template>
|
||||||
19
assets/tiled-fixtures/p4_map.tmx
Normal file
19
assets/tiled-fixtures/p4_map.tmx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Hand-authored P4 fixture: object shapes, custom types, templates (issue #72). CC0. -->
|
||||||
|
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="16" height="16" tilewidth="16" tileheight="16" infinite="0" nextlayerid="3" nextobjectid="13">
|
||||||
|
<objectgroup id="1" name="shapes">
|
||||||
|
<object id="1" x="10" y="10" width="20" height="30"/>
|
||||||
|
<object id="2" x="40" y="10" width="20" height="20"><ellipse/></object>
|
||||||
|
<object id="3" x="70" y="10"><point/></object>
|
||||||
|
<object id="4" x="90" y="10"><polygon points="0,0 16,0 16,16 0,16"/></object>
|
||||||
|
<object id="5" x="120" y="10"><polyline points="0,0 10,10 20,0"/></object>
|
||||||
|
<object id="6" x="10" y="60" width="80" height="20"><text pixelsize="12" halign="center">Hello</text></object>
|
||||||
|
</objectgroup>
|
||||||
|
<objectgroup id="2" name="spawns">
|
||||||
|
<object id="10" type="Enemy" x="32" y="48" width="16" height="16">
|
||||||
|
<properties><property name="hp" type="int" value="99"/></properties>
|
||||||
|
</object>
|
||||||
|
<object id="11" type="Enemy" x="64" y="48" width="16" height="16"/>
|
||||||
|
<object id="12" template="p4_enemy.tx" x="80" y="48"/>
|
||||||
|
</objectgroup>
|
||||||
|
</map>
|
||||||
8
assets/tiled-fixtures/p4_types.xml
Normal file
8
assets/tiled-fixtures/p4_types.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<objecttypes>
|
||||||
|
<objecttype name="Enemy" color="#ff0000">
|
||||||
|
<property name="hp" type="int" default="10"/>
|
||||||
|
<property name="speed" type="int" default="3"/>
|
||||||
|
<property name="boss" type="bool" default="false"/>
|
||||||
|
</objecttype>
|
||||||
|
</objecttypes>
|
||||||
|
|
@ -8,3 +8,4 @@ type: feat
|
||||||
- **P1 core load + render (#69)** — the runtime `rt_tmap` model (heap-allocated to `w·h`, lifting the old `96×64` cap), the GID resolver (`Tiled.resolve` → tileset / local id / H·V·D flips), image-backed rendering (`Tiled.draw`, flips applied at blit), and the legacy-tilemap compatibility projection so `Grid.*`/`Path.*`/`esys_move` keep working. `Tiled.load` reads either format, resolves external tilesets + images, and auto-projects a `collision` layer. The Kenney sample loads and renders pixel-identically from `.tmx` and `.tmj`; the `grid` and `physics_tiles` demos now run off a loaded map.
|
- **P1 core load + render (#69)** — the runtime `rt_tmap` model (heap-allocated to `w·h`, lifting the old `96×64` cap), the GID resolver (`Tiled.resolve` → tileset / local id / H·V·D flips), image-backed rendering (`Tiled.draw`, flips applied at blit), and the legacy-tilemap compatibility projection so `Grid.*`/`Path.*`/`esys_move` keep working. `Tiled.load` reads either format, resolves external tilesets + images, and auto-projects a `collision` layer. The Kenney sample loads and renders pixel-identically from `.tmx` and `.tmj`; the `grid` and `physics_tiles` demos now run off a loaded map.
|
||||||
- **P2 collision & grid (#70)** — normalise three collision sources into the byte tilemap `esys_move`/`Grid.*`/`Path.*` read, in the design's priority order: per-tile `<objectgroup>` hitboxes, the `solid`/`oneway`/`trigger` property convention (`Tiled.collision_kind`/`Tiled.tile_shapes`), and the designated collision layer (`Tiled.project`, any non-zero GID solid) — or drive collision from a visual layer's per-tile metadata alone (`Tiled.collide`). The property convention and the collision-layer fallback produce the same feed; `Path.a_star` over a loaded map matches the hand-authored baseline.
|
- **P2 collision & grid (#70)** — normalise three collision sources into the byte tilemap `esys_move`/`Grid.*`/`Path.*` read, in the design's priority order: per-tile `<objectgroup>` hitboxes, the `solid`/`oneway`/`trigger` property convention (`Tiled.collision_kind`/`Tiled.tile_shapes`), and the designated collision layer (`Tiled.project`, any non-zero GID solid) — or drive collision from a visual layer's per-tile metadata alone (`Tiled.collide`). The property convention and the collision-layer fallback produce the same feed; `Path.a_star` over a loaded map matches the hand-authored baseline.
|
||||||
- **P3 animated tiles + tile objects (#71)** — a tileset `<animation>` advances deterministically as a pure function of the fixed 60/s engine frame clock (`Tiled.frame_gid`/`Tiled.animated`), so an animated GID resolves at draw to the current frame's GID with its flip flags preserved and reproduces frame-for-frame across runs; `Tiled.draw_anim` draws a map with animations advanced. Object-layer entries with a `gid` render the tile image (with their own flips), bottom-anchored, as placeable sprites.
|
- **P3 animated tiles + tile objects (#71)** — a tileset `<animation>` advances deterministically as a pure function of the fixed 60/s engine frame clock (`Tiled.frame_gid`/`Tiled.animated`), so an animated GID resolves at draw to the current frame's GID with its flip flags preserved and reproduces frame-for-frame across runs; `Tiled.draw_anim` draws a map with animations advanced. Object-layer entries with a `gid` render the tile image (with their own flips), bottom-anchored, as placeable sprites.
|
||||||
|
- **P4 objects, properties, templates, spawning (#72)** — all object shapes (rectangle / ellipse / point / polygon / polyline / text) and custom properties parse and are queryable (`Tiled.object`, `Tiled.object_shape`, `Tiled.prop`/`Tiled.prop_int`/`Tiled.prop_type`); class properties resolve their defaults against a project custom-type table (`Tiled.load_types` over `objecttypes.xml`); template instances inherit their `.tx`/`.tj` template's fields; and an object maps onto Ludic components on demand (`Tiled.spawn`/`Tiled.spawn_layer`, off by default, via the reflection ABI).
|
||||||
|
|
|
||||||
14
docs/language/tiled/tiled-load_types.md
Normal file
14
docs/language/tiled/tiled-load_types.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: tiled-load_types
|
||||||
|
name: Tiled.load_types
|
||||||
|
category: tiled
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Tiled.load_types
|
||||||
|
sig: Tiled.load_types(path: string) -> int
|
||||||
|
tip: Load a project custom-type table.
|
||||||
|
order: 26
|
||||||
|
ns: Tiled
|
||||||
|
member: load_types
|
||||||
|
---
|
||||||
|
|
||||||
|
Loads an <code>objecttypes.xml</code> custom-type table so class/enum properties resolve their defaults; returns the number of types loaded. Each <code><objecttype></code> maps to its default <code><property></code> list.
|
||||||
14
docs/language/tiled/tiled-object.md
Normal file
14
docs/language/tiled/tiled-object.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: tiled-object
|
||||||
|
name: Tiled.object
|
||||||
|
category: tiled
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Tiled.object
|
||||||
|
sig: Tiled.object(map, layer: int, index: int) -> value
|
||||||
|
tip: An object on an object layer by index.
|
||||||
|
order: 21
|
||||||
|
ns: Tiled
|
||||||
|
member: object
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the object <a href="value"><code>Value</code></a> at <code>index</code> on object <code>layer</code> — its <code>x</code>/<code>y</code>/<code>width</code>/<code>height</code>, shape, <code>gid</code>, <code>type</code> (class) and <code>properties</code>.
|
||||||
14
docs/language/tiled/tiled-object_count.md
Normal file
14
docs/language/tiled/tiled-object_count.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: tiled-object_count
|
||||||
|
name: Tiled.object_count
|
||||||
|
category: tiled
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Tiled.object_count
|
||||||
|
sig: Tiled.object_count(map, layer: int) -> int
|
||||||
|
tip: How many objects an object layer holds.
|
||||||
|
order: 20
|
||||||
|
ns: Tiled
|
||||||
|
member: object_count
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the number of objects on object <code>layer</code> (0 for a non-object layer).
|
||||||
14
docs/language/tiled/tiled-object_shape.md
Normal file
14
docs/language/tiled/tiled-object_shape.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: tiled-object_shape
|
||||||
|
name: Tiled.object_shape
|
||||||
|
category: tiled
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Tiled.object_shape
|
||||||
|
sig: Tiled.object_shape(object) -> string
|
||||||
|
tip: Classify an object's shape.
|
||||||
|
order: 22
|
||||||
|
ns: Tiled
|
||||||
|
member: object_shape
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns an object's shape: <code>tile</code> (a gid object), <code>point</code>, <code>ellipse</code>, <code>polygon</code>, <code>polyline</code>, <code>text</code>, or <code>rectangle</code> (the default).
|
||||||
14
docs/language/tiled/tiled-prop.md
Normal file
14
docs/language/tiled/tiled-prop.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: tiled-prop
|
||||||
|
name: Tiled.prop
|
||||||
|
category: tiled
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Tiled.prop
|
||||||
|
sig: Tiled.prop(container, name: string) -> string
|
||||||
|
tip: A custom property's value, with class-default fallback.
|
||||||
|
order: 23
|
||||||
|
ns: Tiled
|
||||||
|
member: prop
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the string value of custom property <code>name</code> on <code>container</code> (an object / tile / layer / map), falling back to the container class's default from the project custom-type table (<a href="tiled-load_types"><code>Tiled.load_types</code></a>). <code>""</code> when absent everywhere.
|
||||||
14
docs/language/tiled/tiled-prop_int.md
Normal file
14
docs/language/tiled/tiled-prop_int.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: tiled-prop_int
|
||||||
|
name: Tiled.prop_int
|
||||||
|
category: tiled
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Tiled.prop_int
|
||||||
|
sig: Tiled.prop_int(container, name: string) -> int
|
||||||
|
tip: A custom property parsed as an integer.
|
||||||
|
order: 24
|
||||||
|
ns: Tiled
|
||||||
|
member: prop_int
|
||||||
|
---
|
||||||
|
|
||||||
|
Like <a href="tiled-prop"><code>Tiled.prop</code></a> but parsed as an integer (<code>true</code>/<code>false</code> → 1/0), with the same class-default fallback.
|
||||||
14
docs/language/tiled/tiled-prop_type.md
Normal file
14
docs/language/tiled/tiled-prop_type.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: tiled-prop_type
|
||||||
|
name: Tiled.prop_type
|
||||||
|
category: tiled
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Tiled.prop_type
|
||||||
|
sig: Tiled.prop_type(container, name: string) -> string
|
||||||
|
tip: A custom property's declared type.
|
||||||
|
order: 25
|
||||||
|
ns: Tiled
|
||||||
|
member: prop_type
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the declared type of custom property <code>name</code> (<code>string</code>/<code>int</code>/<code>float</code>/<code>bool</code>/<code>color</code>/<code>file</code>/<code>object</code>/<code>class</code>/an enum), with the class-default fallback.
|
||||||
14
docs/language/tiled/tiled-spawn.md
Normal file
14
docs/language/tiled/tiled-spawn.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: tiled-spawn
|
||||||
|
name: Tiled.spawn
|
||||||
|
category: tiled
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Tiled.spawn
|
||||||
|
sig: Tiled.spawn(map, object, model: string) -> int
|
||||||
|
tip: Spawn a Ludic entity from an object.
|
||||||
|
order: 28
|
||||||
|
ns: Tiled
|
||||||
|
member: spawn
|
||||||
|
---
|
||||||
|
|
||||||
|
Opt-in: spawns an entity of <code>model</code> from an <code>object</code> — its <code>Position</code> (if the model has one) takes the object's x/y, and each object custom property sets a like-named int/bool component field through the reflection ABI. Returns the entity, or -1 if the model is unknown. The core load never calls this.
|
||||||
14
docs/language/tiled/tiled-spawn_layer.md
Normal file
14
docs/language/tiled/tiled-spawn_layer.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: tiled-spawn_layer
|
||||||
|
name: Tiled.spawn_layer
|
||||||
|
category: tiled
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Tiled.spawn_layer
|
||||||
|
sig: Tiled.spawn_layer(map, layer: int, model: string) -> int
|
||||||
|
tip: Spawn every object on a layer.
|
||||||
|
order: 29
|
||||||
|
ns: Tiled
|
||||||
|
member: spawn_layer
|
||||||
|
---
|
||||||
|
|
||||||
|
Spawns every object on object <code>layer</code> as <code>model</code> (see <a href="tiled-spawn"><code>Tiled.spawn</code></a>); returns how many were spawned.
|
||||||
14
docs/language/tiled/tiled-template.md
Normal file
14
docs/language/tiled/tiled-template.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
id: tiled-template
|
||||||
|
name: Tiled.template
|
||||||
|
category: tiled
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Tiled.template
|
||||||
|
sig: Tiled.template(path: string) -> value
|
||||||
|
tip: Read a template file into an object value.
|
||||||
|
order: 27
|
||||||
|
ns: Tiled
|
||||||
|
member: template
|
||||||
|
---
|
||||||
|
|
||||||
|
Reads a <code>.tx</code>/<code>.tj</code> template file and returns its object <a href="value"><code>Value</code></a> — the reusable object definition an instance inherits. <a href="tiled-load"><code>Tiled.load</code></a> resolves each object's <code>template</code> reference automatically, merging the template's fields under the instance's overrides.
|
||||||
78
examples/library/tiled_p4.ludic
Normal file
78
examples/library/tiled_p4.ludic
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
# tiled_p4.ludic — Tiled P4 objects, custom properties/types, templates, opt-in
|
||||||
|
# spawning (issue #72). All object shapes (rect/ellipse/point/polygon/polyline/
|
||||||
|
# text) parse and are queryable; class properties resolve their defaults against
|
||||||
|
# the project custom-type table (objecttypes.xml); a template instance inherits
|
||||||
|
# its template's fields; and an object maps onto Ludic components on demand.
|
||||||
|
#
|
||||||
|
# A full run prints: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
||||||
|
program TiledP4 {
|
||||||
|
property Position { x: int = 0, y: int = 0 }
|
||||||
|
property Health { hp: int = 0 }
|
||||||
|
model Enemy { Position, Health }
|
||||||
|
|
||||||
|
handler Boot phase Start {
|
||||||
|
let m = Tiled.load("assets/tiled-fixtures/p4_map.tmx")
|
||||||
|
Tiled.load_types("assets/tiled-fixtures/p4_types.xml")
|
||||||
|
|
||||||
|
# --- all object shapes parse and are queryable (layer 0 "shapes") ---
|
||||||
|
if Tiled.object_count(m, 0) == 6 { print(1) }
|
||||||
|
if Tiled.object_shape(Tiled.object(m, 0, 0)) == "rectangle" { print(2) }
|
||||||
|
if Tiled.object_shape(Tiled.object(m, 0, 1)) == "ellipse" { print(3) }
|
||||||
|
if Tiled.object_shape(Tiled.object(m, 0, 2)) == "point" { print(4) }
|
||||||
|
if Tiled.object_shape(Tiled.object(m, 0, 3)) == "polygon" { print(5) }
|
||||||
|
if Tiled.object_shape(Tiled.object(m, 0, 4)) == "polyline" { print(6) }
|
||||||
|
if Tiled.object_shape(Tiled.object(m, 0, 5)) == "text" { print(7) }
|
||||||
|
# polygon points + text content, via the value tree
|
||||||
|
let poly = Tiled.object(m, 0, 3)
|
||||||
|
if Value.count(Value.get(poly, "polygon")) == 4 { print(8) }
|
||||||
|
let txt = Tiled.object(m, 0, 5)
|
||||||
|
if Value.as_str(Value.get(Value.get(txt, "text"), "text")) == "Hello" { print(9) }
|
||||||
|
|
||||||
|
# --- custom properties + class defaults (layer 1 "spawns") ---
|
||||||
|
let e0 = Tiled.object(m, 1, 0) # id 10, hp override = 99
|
||||||
|
if Tiled.prop_int(e0, "hp") == 99 { print(10) }
|
||||||
|
let e1 = Tiled.object(m, 1, 1) # id 11, no hp -> Enemy default 10
|
||||||
|
if Tiled.prop_int(e1, "hp") == 10 { print(11) }
|
||||||
|
if Tiled.prop_int(e1, "speed") == 3 { print(12) } # class default
|
||||||
|
if Tiled.prop_type(e1, "hp") == "int" { print(13) }
|
||||||
|
|
||||||
|
# --- template instance inherits its template's fields (id 12) ---
|
||||||
|
let e2 = Tiled.object(m, 1, 2)
|
||||||
|
if Value.as_str(Value.get(e2, "type")) == "Enemy" { print(14) } # type inherited from p4_enemy.tx
|
||||||
|
if Tiled.prop_int(e2, "hp") == 25 { print(15) } # hp inherited from p4_enemy.tx
|
||||||
|
|
||||||
|
# --- opt-in spawn maps an object's class/properties -> components ---
|
||||||
|
let ent = Tiled.spawn(m, e0, "Enemy")
|
||||||
|
let pp = World.prop_id("Position")
|
||||||
|
let ph = World.prop_id("Health")
|
||||||
|
let ok_pos = World.get(ent, pp, World.field_id(pp, "x")) == 32
|
||||||
|
let ok_hp = World.get(ent, ph, World.field_id(ph, "hp")) == 99
|
||||||
|
if ok_pos and ok_hp { print(16) }
|
||||||
|
|
||||||
|
# --- the design's named fixture: orthogonal-outside.tmx object shapes ---
|
||||||
|
let og = Tiled.load("assets/tiled-fixtures/orthogonal-outside.tmx")
|
||||||
|
var olayer = 0 - 1
|
||||||
|
var oli = 0
|
||||||
|
while oli < Tiled.layer_count(og) {
|
||||||
|
if Tiled.object_count(og, oli) > 0 { olayer = oli }
|
||||||
|
oli = oli + 1
|
||||||
|
}
|
||||||
|
var nell = 0
|
||||||
|
var npt = 0
|
||||||
|
var npg = 0
|
||||||
|
var npl = 0
|
||||||
|
var oi = 0
|
||||||
|
while oi < Tiled.object_count(og, olayer) {
|
||||||
|
let sh = Tiled.object_shape(Tiled.object(og, olayer, oi))
|
||||||
|
if sh == "ellipse" { nell = nell + 1 }
|
||||||
|
if sh == "point" { npt = npt + 1 }
|
||||||
|
if sh == "polygon" { npg = npg + 1 }
|
||||||
|
if sh == "polyline" { npl = npl + 1 }
|
||||||
|
oi = oi + 1
|
||||||
|
}
|
||||||
|
if nell == 1 and npt == 1 and npg == 1 { print(17) }
|
||||||
|
if npl == 2 { print(18) }
|
||||||
|
}
|
||||||
|
|
||||||
|
handler Run phase Update { quit() }
|
||||||
|
}
|
||||||
|
|
@ -106,6 +106,7 @@ function tmx_object_to_value(ob: Xml) -> Val {
|
||||||
if xml_has(ob, "name") == 1 { value_put(o, "name", value_str(xml_attr(ob, "name"))) }
|
if xml_has(ob, "name") == 1 { value_put(o, "name", value_str(xml_attr(ob, "name"))) }
|
||||||
if xml_has(ob, "type") == 1 { value_put(o, "type", value_str(xml_attr(ob, "type"))) }
|
if xml_has(ob, "type") == 1 { value_put(o, "type", value_str(xml_attr(ob, "type"))) }
|
||||||
if xml_has(ob, "class") == 1 { value_put(o, "type", value_str(xml_attr(ob, "class"))) }
|
if xml_has(ob, "class") == 1 { value_put(o, "type", value_str(xml_attr(ob, "class"))) }
|
||||||
|
if xml_has(ob, "template") == 1 { value_put(o, "template", value_str(xml_attr(ob, "template"))) }
|
||||||
value_put(o, "x", value_int(xml_attr_int(ob, "x", 0)))
|
value_put(o, "x", value_int(xml_attr_int(ob, "x", 0)))
|
||||||
value_put(o, "y", value_int(xml_attr_int(ob, "y", 0)))
|
value_put(o, "y", value_int(xml_attr_int(ob, "y", 0)))
|
||||||
value_put(o, "width", value_int(xml_attr_int(ob, "width", 0)))
|
value_put(o, "width", value_int(xml_attr_int(ob, "width", 0)))
|
||||||
|
|
@ -807,6 +808,7 @@ function tmap_add_layer(m: Tmap, lv: Val) -> void {
|
||||||
function tiled_load(path: pointer) -> Tmap {
|
function tiled_load(path: pointer) -> Tmap {
|
||||||
let tree = tiled_read(path)
|
let tree = tiled_read(path)
|
||||||
let m = tmap_build(tree, Path.dir(path))
|
let m = tmap_build(tree, Path.dir(path))
|
||||||
|
tmap_resolve_templates(m, Path.dir(path)) # #72: fill template-instance objects
|
||||||
let c = tmap_find_collision(m)
|
let c = tmap_find_collision(m)
|
||||||
if c >= 0 { tmap_project(m, c) }
|
if c >= 0 { tmap_project(m, c) }
|
||||||
return m
|
return m
|
||||||
|
|
@ -916,3 +918,194 @@ function tmap_draw(m: Tmap, camx: int, camy: int) -> void { tmap_draw_full(m, ca
|
||||||
# animated draw at engine `frame` (pass Time.frame): animated tiles advance,
|
# animated draw at engine `frame` (pass Time.frame): animated tiles advance,
|
||||||
# deterministically and frame-identically across runs.
|
# deterministically and frame-identically across runs.
|
||||||
function tmap_draw_anim(m: Tmap, camx: int, camy: int, frame: int) -> void { tmap_draw_full(m, camx, camy, frame, 1) }
|
function tmap_draw_anim(m: Tmap, camx: int, camy: int, frame: int) -> void { tmap_draw_full(m, camx, camy, frame, 1) }
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# P4 (#72) — object layers (shapes + text), custom properties/types, templates,
|
||||||
|
# and opt-in entity spawning. Objects, shapes and properties already parse into
|
||||||
|
# the intermediate tree (P0.5); this phase exposes them, resolves custom-type
|
||||||
|
# defaults and template inheritance, and maps an object onto Ludic components.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# ---- object accessors ------------------------------------------------------
|
||||||
|
function tmap_object_layer(m: Tmap, layer: int) -> Val {
|
||||||
|
if layer < 0 or layer >= len(m.layers) { return value_null() }
|
||||||
|
return m.layers[layer].data
|
||||||
|
}
|
||||||
|
function tmap_object_count(m: Tmap, layer: int) -> int {
|
||||||
|
return value_count(value_get(tmap_object_layer(m, layer), "objects"))
|
||||||
|
}
|
||||||
|
function tmap_object(m: Tmap, layer: int, i: int) -> Val {
|
||||||
|
return value_at(value_get(tmap_object_layer(m, layer), "objects"), i)
|
||||||
|
}
|
||||||
|
|
||||||
|
# the shape of an object: "tile" (a gid object), "point", "ellipse", "polygon",
|
||||||
|
# "polyline", "text", or "rectangle" (the default).
|
||||||
|
function tiled_object_shape(obj: Val) -> pointer {
|
||||||
|
if value_has(obj, "gid") == 1 { return "tile" }
|
||||||
|
if value_has(obj, "point") == 1 { return "point" }
|
||||||
|
if value_has(obj, "ellipse") == 1 { return "ellipse" }
|
||||||
|
if value_has(obj, "polygon") == 1 { return "polygon" }
|
||||||
|
if value_has(obj, "polyline") == 1 { return "polyline" }
|
||||||
|
if value_has(obj, "text") == 1 { return "text" }
|
||||||
|
return "rectangle"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- custom properties -----------------------------------------------------
|
||||||
|
# the {name,type,value} property node named `name` in a container's "properties"
|
||||||
|
# list (an object / tile / layer / map), or a null node.
|
||||||
|
function tiled_prop_node(container: Val, name: pointer) -> Val {
|
||||||
|
let props = value_get(container, "properties")
|
||||||
|
if value_kind(props) != 5 { return value_null() }
|
||||||
|
var i = 0
|
||||||
|
while i < value_count(props) {
|
||||||
|
let p = value_at(props, i)
|
||||||
|
if value_as_str(value_get(p, "name")) == name { return p }
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
return value_null()
|
||||||
|
}
|
||||||
|
|
||||||
|
# a property's raw string value, with a fallback to the object's custom-type
|
||||||
|
# default (objecttypes.xml, via the type table). "" when absent everywhere.
|
||||||
|
function tiled_prop_str(container: Val, name: pointer) -> pointer {
|
||||||
|
let p = tiled_prop_node(container, name)
|
||||||
|
if value_kind(p) == 6 { return value_as_str(value_get(p, "value")) }
|
||||||
|
# fall back to the container's class default
|
||||||
|
let cls = value_as_str(value_get(container, "type"))
|
||||||
|
if cls != "" {
|
||||||
|
let d = tiled_type_default(cls, name)
|
||||||
|
if value_kind(d) == 6 { return value_as_str(value_get(d, "value")) }
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# a property parsed as an integer ("true"/"false" -> 1/0), with the same default
|
||||||
|
# fallback.
|
||||||
|
function tiled_prop_int(container: Val, name: pointer) -> int {
|
||||||
|
let s = tiled_prop_str(container, name)
|
||||||
|
if s == "true" { return 1 }
|
||||||
|
if s == "false" { return 0 }
|
||||||
|
return xml_atoi(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
function tiled_prop_type(container: Val, name: pointer) -> pointer {
|
||||||
|
let p = tiled_prop_node(container, name)
|
||||||
|
if value_kind(p) == 6 { return value_as_str(value_get(p, "type")) }
|
||||||
|
let cls = value_as_str(value_get(container, "type"))
|
||||||
|
if cls != "" {
|
||||||
|
let d = tiled_type_default(cls, name)
|
||||||
|
if value_kind(d) == 6 { return value_as_str(value_get(d, "type")) }
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- custom types (objecttypes.xml) ----------------------------------------
|
||||||
|
# The project custom-type table: an object mapping a type/class name to its list
|
||||||
|
# of {name,type,value(default)} property definitions. class/enum properties then
|
||||||
|
# resolve their defaults against it.
|
||||||
|
var tiled_type_table: Val = null
|
||||||
|
|
||||||
|
function tiled_types() -> Val {
|
||||||
|
if tiled_type_table == null { tiled_type_table = value_object() }
|
||||||
|
return tiled_type_table
|
||||||
|
}
|
||||||
|
|
||||||
|
# load an objecttypes.xml file into the type table. Each <objecttype name=..> maps
|
||||||
|
# to its <property name= type= default=> list.
|
||||||
|
function tiled_load_types(path: pointer) -> int {
|
||||||
|
let text = Fs.read_text(path)
|
||||||
|
if text == null { return 0 }
|
||||||
|
if text == "" { return 0 }
|
||||||
|
let root = xml_parse(text)
|
||||||
|
let tbl = tiled_types()
|
||||||
|
var n = 0
|
||||||
|
var i = 0
|
||||||
|
while i < xml_child_count(root) {
|
||||||
|
let ot = xml_child(root, i)
|
||||||
|
if xml_tag(ot) == "objecttype" {
|
||||||
|
let props = value_list()
|
||||||
|
var j = 0
|
||||||
|
while j < xml_child_count(ot) {
|
||||||
|
let pr = xml_child(ot, j)
|
||||||
|
if xml_tag(pr) == "property" {
|
||||||
|
let o = value_object()
|
||||||
|
value_put(o, "name", value_str(xml_attr(pr, "name")))
|
||||||
|
var ty = xml_attr(pr, "type")
|
||||||
|
if ty == "" { ty = "string" }
|
||||||
|
value_put(o, "type", value_str(ty))
|
||||||
|
value_put(o, "value", value_str(xml_attr(pr, "default")))
|
||||||
|
push(props.kids, o)
|
||||||
|
}
|
||||||
|
j = j + 1
|
||||||
|
}
|
||||||
|
value_put(tbl, xml_attr(ot, "name"), props)
|
||||||
|
n = n + 1
|
||||||
|
}
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
# the default {name,type,value} property node for a custom type, or a null node.
|
||||||
|
function tiled_type_default(typename: pointer, propname: pointer) -> Val {
|
||||||
|
let tbl = tiled_types()
|
||||||
|
let props = value_get(tbl, typename)
|
||||||
|
if value_kind(props) != 5 { return value_null() }
|
||||||
|
var i = 0
|
||||||
|
while i < value_count(props) {
|
||||||
|
let p = value_at(props, i)
|
||||||
|
if value_as_str(value_get(p, "name")) == propname { return p }
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
return value_null()
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- templates (.tx / .tj) -------------------------------------------------
|
||||||
|
# read a template file -> its object Value (the <object> a .tx wraps, or the
|
||||||
|
# "object" of a .tj). External reusable object definitions.
|
||||||
|
function tiled_read_template(path: pointer) -> Val {
|
||||||
|
let text = Fs.read_text(path)
|
||||||
|
if text == null { return value_null() }
|
||||||
|
if text == "" { return value_null() }
|
||||||
|
if tiled_first_byte(text) == 60 { # XML .tx
|
||||||
|
let root = xml_parse(text) # <template>
|
||||||
|
let ob = xml_find(root, "object")
|
||||||
|
if xml_tag(ob) == "object" { return tmx_object_to_value(ob) }
|
||||||
|
return value_null()
|
||||||
|
}
|
||||||
|
let j = json_parse(text) # JSON .tj
|
||||||
|
return value_get(j, "object")
|
||||||
|
}
|
||||||
|
|
||||||
|
# merge a template's fields into an instance object: the instance keeps every key
|
||||||
|
# it already sets; the template fills in the rest (so an instance inherits the
|
||||||
|
# template's gid / shape / size / properties, overriding per field).
|
||||||
|
function tiled_merge_template(inst: Val, tmpl: Val) -> void {
|
||||||
|
if value_kind(tmpl) != 6 { return }
|
||||||
|
var i = 0
|
||||||
|
while i < value_count(tmpl) {
|
||||||
|
let k = value_key_at(tmpl, i)
|
||||||
|
if value_has(inst, k) == 0 { value_put(inst, k, value_at(tmpl, i)) }
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# resolve every object that references a `template`, loading + merging it. Paths
|
||||||
|
# are relative to the map file (`basedir`).
|
||||||
|
function tmap_resolve_templates(m: Tmap, basedir: pointer) -> void {
|
||||||
|
var li = 0
|
||||||
|
while li < len(m.layers) {
|
||||||
|
let l = m.layers[li]
|
||||||
|
if l.kind == 1 {
|
||||||
|
let objs = value_get(l.data, "objects")
|
||||||
|
var oi = 0
|
||||||
|
while oi < value_count(objs) {
|
||||||
|
let ob = value_at(objs, oi)
|
||||||
|
let tp = value_as_str(value_get(ob, "template"))
|
||||||
|
if tp != "" { tiled_merge_template(ob, tiled_read_template(tiled_join(basedir, tp))) }
|
||||||
|
oi = oi + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li = li + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
55
runtime/native/tiled_spawn.ludic
Normal file
55
runtime/native/tiled_spawn.ludic
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# ============================================================================
|
||||||
|
# tiled_spawn.ludic — Tiled P4 (#72) opt-in entity spawning. Split from
|
||||||
|
# tiled.ludic because it stands on the reflection ABI (world_spawn / world_set /
|
||||||
|
# world_field_id …), which the compiler emits only for a game (has_ecs); so this
|
||||||
|
# fragment is spliced only when a Tiled program is itself a game. A plain tool
|
||||||
|
# that reads maps never references the world table.
|
||||||
|
# ============================================================================
|
||||||
|
# ---- opt-in entity spawning ------------------------------------------------
|
||||||
|
# set an int field named `fname` on entity e wherever it exists (any prop the
|
||||||
|
# entity carries) — the by-name bridge from object properties to components.
|
||||||
|
function tiled_apply_field(e: int, fname: pointer, v: int) -> void {
|
||||||
|
let np = world_prop_count()
|
||||||
|
var pi = 0
|
||||||
|
while pi < np {
|
||||||
|
if world_has(e, pi) == 1 {
|
||||||
|
let fid = world_field_id(pi, fname)
|
||||||
|
if fid >= 0 { world_set(e, pi, fid, v) }
|
||||||
|
}
|
||||||
|
pi = pi + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# spawn a Ludic entity of `model_name` from an object: its Position (if the model
|
||||||
|
# has one) takes the object's x/y, and each object custom property sets a like-
|
||||||
|
# named int/bool component field. Returns the entity, or -1 if the model is
|
||||||
|
# unknown. Opt-in — the core load never calls this.
|
||||||
|
function tiled_spawn_object(m: Tmap, obj: Val, model_name: pointer) -> int {
|
||||||
|
let mid = world_model_id(model_name)
|
||||||
|
if mid < 0 { return 0 - 1 }
|
||||||
|
let e = world_spawn(mid)
|
||||||
|
let pp = world_prop_id("Position")
|
||||||
|
if pp >= 0 and world_has(e, pp) == 1 {
|
||||||
|
let fx = world_field_id(pp, "x")
|
||||||
|
let fy = world_field_id(pp, "y")
|
||||||
|
if fx >= 0 { world_set(e, pp, fx, value_as_int(value_get(obj, "x"))) }
|
||||||
|
if fy >= 0 { world_set(e, pp, fy, value_as_int(value_get(obj, "y"))) }
|
||||||
|
}
|
||||||
|
let props = value_get(obj, "properties")
|
||||||
|
var i = 0
|
||||||
|
while i < value_count(props) {
|
||||||
|
let p = value_at(props, i)
|
||||||
|
tiled_apply_field(e, value_as_str(value_get(p, "name")), tiled_prop_int(obj, value_as_str(value_get(p, "name"))))
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
# spawn every object on `layer` as `model_name` (opt-in). Returns the count.
|
||||||
|
function tiled_spawn_layer(m: Tmap, layer: int, model_name: pointer) -> int {
|
||||||
|
var n = 0
|
||||||
|
let c = tmap_object_count(m, layer)
|
||||||
|
var i = 0
|
||||||
|
while i < c { if tiled_spawn_object(m, tmap_object(m, layer, i), model_name) >= 0 { n = n + 1 }; i = i + 1 }
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
@ -630,6 +630,17 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
||||||
if (meth == "tree") { bare = "tmap_tree"; push(labels, "map") }
|
if (meth == "tree") { bare = "tmap_tree"; push(labels, "map") }
|
||||||
if (meth == "tile_prop") { bare = "tmap_tile_prop"; push(labels, "map"); push(labels, "gid"); push(labels, "name") }
|
if (meth == "tile_prop") { bare = "tmap_tile_prop"; push(labels, "map"); push(labels, "gid"); push(labels, "name") }
|
||||||
if (meth == "tile_shapes") { bare = "tmap_tile_has_shapes"; push(labels, "map"); push(labels, "gid") }
|
if (meth == "tile_shapes") { bare = "tmap_tile_has_shapes"; push(labels, "map"); push(labels, "gid") }
|
||||||
|
# P4 (#72): objects / properties / custom types / templates / opt-in spawn
|
||||||
|
if (meth == "object_count") { bare = "tmap_object_count"; push(labels, "map"); push(labels, "layer") }
|
||||||
|
if (meth == "object") { bare = "tmap_object"; push(labels, "map"); push(labels, "layer"); push(labels, "index") }
|
||||||
|
if (meth == "object_shape") { bare = "tiled_object_shape"; push(labels, "object") }
|
||||||
|
if (meth == "prop") { bare = "tiled_prop_str"; push(labels, "container"); push(labels, "name") }
|
||||||
|
if (meth == "prop_int") { bare = "tiled_prop_int"; push(labels, "container"); push(labels, "name") }
|
||||||
|
if (meth == "prop_type") { bare = "tiled_prop_type"; push(labels, "container"); push(labels, "name") }
|
||||||
|
if (meth == "load_types") { bare = "tiled_load_types"; push(labels, "path") }
|
||||||
|
if (meth == "template") { bare = "tiled_read_template"; push(labels, "path") }
|
||||||
|
if (meth == "spawn") { bare = "tiled_spawn_object"; push(labels, "map"); push(labels, "object"); push(labels, "model") }
|
||||||
|
if (meth == "spawn_layer") { bare = "tiled_spawn_layer"; push(labels, "map"); push(labels, "layer"); push(labels, "model") }
|
||||||
}
|
}
|
||||||
# #62: a package-provided namespace (declared with @Namespace(Foo)) that none
|
# #62: a package-provided namespace (declared with @Namespace(Foo)) that none
|
||||||
# of the hardcoded core blocks matched — alias Foo.method to the bare function
|
# of the hardcoded core blocks matched — alias Foo.method to the bare function
|
||||||
|
|
|
||||||
|
|
@ -744,6 +744,11 @@ function maybe_splice_runtime() -> void {
|
||||||
do_import("runtime/native/base64.ludic")
|
do_import("runtime/native/base64.ludic")
|
||||||
do_import("runtime/native/value.ludic")
|
do_import("runtime/native/value.ludic")
|
||||||
do_import("runtime/native/tiled.ludic")
|
do_import("runtime/native/tiled.ludic")
|
||||||
|
# #72 opt-in spawning stands on the reflection ABI, which the compiler emits
|
||||||
|
# only for a game (emit_decl: has_ecs -> emit_world_table). So splice the
|
||||||
|
# spawn helpers only for a Tiled game; a plain map-reading tool never links
|
||||||
|
# against the world table.
|
||||||
|
if has_ecs() { do_import("runtime/native/tiled_spawn.ludic") }
|
||||||
cur_dir = saved
|
cur_dir = saved
|
||||||
}
|
}
|
||||||
# Reflect.serialize/apply add the reflection serializer, which stands on both
|
# Reflect.serialize/apply add the reflection serializer, which stands on both
|
||||||
|
|
|
||||||
34828
selfhost/ludicc.seed.ll
34828
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -701,6 +701,16 @@
|
||||||
"tiled-tile_shapes",
|
"tiled-tile_shapes",
|
||||||
"tiled-draw_anim",
|
"tiled-draw_anim",
|
||||||
"tiled-frame_gid",
|
"tiled-frame_gid",
|
||||||
"tiled-animated"
|
"tiled-animated",
|
||||||
|
"tiled-object_count",
|
||||||
|
"tiled-object",
|
||||||
|
"tiled-object_shape",
|
||||||
|
"tiled-prop",
|
||||||
|
"tiled-prop_int",
|
||||||
|
"tiled-prop_type",
|
||||||
|
"tiled-load_types",
|
||||||
|
"tiled-template",
|
||||||
|
"tiled-spawn",
|
||||||
|
"tiled-spawn_layer"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,7 @@ function cmd_test() -> int {
|
||||||
feat_case("library/tiled_p1", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "tiled_p1.ludic (Tiled P1: rt_tmap model + GID resolver/flip decode + render; loads+draws Kenney map identically from .tmx and .tmj; issue #69)")
|
feat_case("library/tiled_p1", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "tiled_p1.ludic (Tiled P1: rt_tmap model + GID resolver/flip decode + render; loads+draws Kenney map identically from .tmx and .tmj; issue #69)")
|
||||||
feat_case("library/tiled_p2", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "tiled_p2.ludic (Tiled P2: per-tile objectgroup / property-convention / collision-layer -> Solids feed; A* baseline; issue #70)")
|
feat_case("library/tiled_p2", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "tiled_p2.ludic (Tiled P2: per-tile objectgroup / property-convention / collision-layer -> Solids feed; A* baseline; issue #70)")
|
||||||
feat_case("library/tiled_p3", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "tiled_p3.ludic (Tiled P3: animated tiles -> deterministic frame clock + tile objects w/ flips; beach_tileset 33 anims; issue #71)")
|
feat_case("library/tiled_p3", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "tiled_p3.ludic (Tiled P3: animated tiles -> deterministic frame clock + tile objects w/ flips; beach_tileset 33 anims; issue #71)")
|
||||||
|
feat_case("library/tiled_p4", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "tiled_p4.ludic (Tiled P4: object shapes/text + custom props/types + templates + opt-in spawn; issue #72)")
|
||||||
feat_case("library/render", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "render.ludic (Screen pixel/oval/camera/clip/blend_mode/measure_text + Camera set/follow/shake, verified by pixel readback)")
|
feat_case("library/render", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "render.ludic (Screen pixel/oval/camera/clip/blend_mode/measure_text + Camera set/follow/shake, verified by pixel readback)")
|
||||||
feat_case("library/lighting", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "lighting.ludic (Light ambient/point radial falloff + occluder hard shadows — 2D light accumulation, verified by pixel readback)")
|
feat_case("library/lighting", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "lighting.ludic (Light ambient/point radial falloff + occluder hard shadows — 2D light accumulation, verified by pixel readback)")
|
||||||
feat_case("library/light_tiers", "", "1 1 1 1 1 1 1 1 1", "light_tiers.ludic (Light spot/falloff/soft/gel/normal/time_of_day — render-quality tiers 3-4; issue #49)")
|
feat_case("library/light_tiers", "", "1 1 1 1 1 1 1 1 1", "light_tiers.ludic (Light spot/falloff/soft/gel/normal/time_of_day — render-quality tiers 3-4; issue #49)")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue