Closes the two open issues and lands the pending unreleased batch: - #90: `Sprite { atlas: 1 }` routes esys_sprite through atlas_draw_ex (scale/flip/tint), so cell / cell_span / strip ids of any size draw through the engine sprite-render system. examples/library/sprite_atlas is the pixel-readback regression. - #91: `become` from an @On(Event) listener / global handler / plain function no longer segfaults the compiler; it emits @L_scene_leave() (a dispatch on the live scene id) so the leaving scene's on-exit runs. UI_* handles are readable from any code (widget table built on first use). examples/library/scene_menus covers it. - fix: a windowed `ludicc -o` build that reaches the audio runtime only through the atlas/Assets preload import now links audio.ll + AVFoundation (the audio backend link was gated on a game-level Audio.* call, so any windowed game declaring Sprite failed to link). - the hand-written "Unreleased" CHANGELOG section is converted to changesets under changes/ so `x release` generates it. - plus the batch: engine-driven retained UI + UiClicked event, Overlay phase, TileSkin tilemap-render system, Key.* constants, Font/Ui/File namespaces, Sprite.strip, prefabs, managers, countdown fields, enum-typed machines, layer @Queries, ludic.prefs / ludic.dungeon packages, Ai.seek pathing, Solids.solid2, cursor confine (mode 3) fix, shooter centre-aim fix, reserved-word function diagnostic. Verified: x test (124/124), x test-tools, check-impl, check-vocabulary, check-docs, docs-gen + docs-check, bootstrap-cfree (seed is a fixpoint). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
e9c2c51bc3
commit
ad548840c7
139 changed files with 58981 additions and 43671 deletions
46
examples/library/combat_kit.ludic
Normal file
46
examples/library/combat_kit.ludic
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# combat_kit.ludic — the shooter kit over ludic.gameplay: Dash (a dodge with i-frames),
|
||||
# Melee (an arc swing with knockback and MeleeHit), build stats on Stats applied by
|
||||
# Combat.damage (damage_pct, crit_pct, thorns; a Crit event), Stats.add, and the
|
||||
# Dungeon package (arena, exits, entry points). Prints:
|
||||
# 6846 153196 31 107110 6000 5002 1 80 97 11 10 700 110
|
||||
program CombatKit {
|
||||
import "ludic.core/components.ludic"
|
||||
import "ludic.shooter/shooter.ludic"
|
||||
import "ludic.dungeon/dungeon.ludic"
|
||||
import "ludic.prefs/prefs.ludic"
|
||||
property Foe { n: int = 0 }
|
||||
model Hero { Position, Body, Collider, TopDown, Weapon, Faction, Stats, Dash, Melee }
|
||||
model Creature { Position, Body, Collider, Faction, Stats, Foe }
|
||||
model WorldRules { Solids }
|
||||
var frames: int = 0
|
||||
var hero: int = -1
|
||||
@On(MeleeHit) handler H { print(5000 + target) }
|
||||
@On(Crit) handler C { print(6000) }
|
||||
handler Tick phase Update {
|
||||
frames += 1
|
||||
if frames == 1 {
|
||||
Map.size(20, 15)
|
||||
Dungeon.arena(Dungeon.random_style(), '#', '.')
|
||||
Dungeon.set_exit(Side.North, 'D')
|
||||
spawn WorldRules { Solids { tile: 16, wall: '#', solid2: 'D' } }
|
||||
print(Map.get(x: 9, y: 0) * 100 + Map.get(x: 9, y: 7)) # 6846: the sealed door, the open lane
|
||||
let e = Dungeon.entry_point(Side.South, 24, 14, 20)
|
||||
print(e.x * 1000 + e.y) # 153196
|
||||
print(Dungeon.opposite(Side.West) * 10 + Dungeon.at_edge(IVec2.make(0, 3))) # 31
|
||||
Weapon.def(name: "p", fire_rate: 10, damage: 10, speed: 8, spread: 0, pellets: 1, pattern: 0)
|
||||
hero = spawn Hero { Position { x: 100, y: 100 }, Collider { w: 14, h: 20 }, Body { policy: 1 }, TopDown { move_speed: 4, aim_mode: AimMode.Manual, aim_angle: 0 }, Faction { id: 1 },
|
||||
Stats { hp: 100, max_hp: 100 }, Dash { frames: 3, speed: 9, cooldown_frames: 5 }, Melee { range: 30, damage: 7, knockback: 4 } }
|
||||
Stats.add(hero, StatKind.CritPct, 100)
|
||||
Stats.add(hero, StatKind.DamagePct, 50)
|
||||
let c = Collider.center(hero); print(c.x * 1000 + c.y) # 107110
|
||||
spawn Creature { Position { x: 120, y: 100 }, Collider { w: 14, h: 20 }, Body { policy: 1 }, Faction { id: 2 }, Stats { hp: 100, max_hp: 100, thorns: 3 }, Foe { n: 1 } }
|
||||
Faction.set(1, 2, 2)
|
||||
print(Melee.swing(hero)) # 1 hit (after 6000 and 5002)
|
||||
for (s, f) in query [Stats, Foe] { print(s.hp) } # 80: 7 * 150% doubled by the crit
|
||||
print(Stats.of(hero).hp) # 97: thorns
|
||||
print(Dash.start(hero, 0, 0) * 10 + Dash.active(hero)) # 11
|
||||
print(Prefs.max("best", 5) * 10 + Prefs.max("best", 3)) # 10
|
||||
}
|
||||
if frames == 7 { print(700 + Dash.active(hero)); print(Position.of(hero).x); quit() } # 700 110
|
||||
}
|
||||
}
|
||||
57
examples/library/component_access.ludic
Normal file
57
examples/library/component_access.ludic
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# component_access.ludic — typed component access from an entity handle:
|
||||
# `Prop.of(e)` reads and writes fields, `Prop.has(e)` is safe for -1 and dead
|
||||
# handles, `Prop.count()` counts, `Prop.despawn_all()` clears; `countdown` fields
|
||||
# tick toward 0 each Update; `Enum.Variant` typed vars and machines. Prints:
|
||||
# 9 15 10 33 1 0 0 1 0 1 101 100 102 1 2 1 0
|
||||
program ComponentAccess {
|
||||
import "ludic.core/components.ludic"
|
||||
property Hero { roll_frames: int = 0, iframes: int = 0, flash: countdown = 0 }
|
||||
property Tag { n: int = 0 }
|
||||
model Player { Position, Hero }
|
||||
model Thing { Position, Tag }
|
||||
enum HeroState { Idle, Rolling, Swinging }
|
||||
var hero_state: HeroState = HeroState.Rolling
|
||||
var player: int = -1
|
||||
var frames: int = 0
|
||||
function step() -> void {
|
||||
machine hero_state {
|
||||
state Idle { print(100); become Swinging }
|
||||
state Rolling { print(101); become Idle }
|
||||
state Swinging { print(102); become Rolling }
|
||||
}
|
||||
}
|
||||
handler Tick phase Update {
|
||||
frames += 1
|
||||
if frames == 1 {
|
||||
player = spawn Player { Position { x: 3, y: 4 }, Hero { roll_frames: 9, flash: 3 } }
|
||||
spawn Thing { Position { x: 30, y: 40 }, Tag { n: 7 } }
|
||||
print(Hero.of(player).roll_frames) # 9
|
||||
Hero.of(player).iframes = 20
|
||||
Hero.of(player).iframes -= 5
|
||||
print(Hero.of(player).iframes) # 15
|
||||
let hero = Hero.of(player)
|
||||
hero.roll_frames = hero.roll_frames + 1
|
||||
print(Hero.of(player).roll_frames) # 10
|
||||
print(Position.of(player).x + Position.of(1).x) # 33
|
||||
print(Hero.has(player)) # 1
|
||||
print(Hero.has(1)) # 0
|
||||
print(Hero.has(-1)) # 0
|
||||
print(Tag.has(1)) # 1
|
||||
despawn 1
|
||||
print(Tag.has(1)) # 0
|
||||
print(Position.count()) # 1: the player
|
||||
spawn Thing { Position { x: 1 } }
|
||||
}
|
||||
if frames == 2 {
|
||||
step(); step(); step() # 101 100 102
|
||||
print(hero_state) # 1
|
||||
print(Position.count()) # 2: the player and the thing spawned after
|
||||
}
|
||||
if frames == 3 {
|
||||
print(Hero.of(player).flash) # ticked twice: 1
|
||||
Position.despawn_all()
|
||||
print(Position.count()) # 0
|
||||
quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
42
examples/library/managers.ludic
Normal file
42
examples/library/managers.ludic
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# managers.ludic — the engine managers: the Map cell API, Sprite facing / run strip /
|
||||
# flash / blink, IVec2 geometry, List.sample, Input.move_i, Fx, the audio bank,
|
||||
# Camera.shake_for, timed Assets. Deterministic; prints:
|
||||
# 35046 46 251 53 900 -1 20 324 0 10 102
|
||||
program Managers {
|
||||
import "ludic.core/components.ludic"
|
||||
import "ludic.shooter/shooter.ludic"
|
||||
property Tag { n: int = 0 }
|
||||
model Thing { Position, Body, Sprite, Tag, TopDown }
|
||||
model WorldRules { Solids }
|
||||
var frames: int = 0
|
||||
handler Tick phase Update {
|
||||
frames += 1
|
||||
if frames == 1 {
|
||||
Map.size(10, 8)
|
||||
Map.fill(glyph: '.')
|
||||
Map.border(glyph: '#')
|
||||
Map.rect(x: 4, y: 3, width: 2, height: 2, glyph: '#')
|
||||
spawn WorldRules { Solids { tile: 16, wall: '#' } }
|
||||
print(Map.get(x: 0, y: 0) * 1000 + Map.get(x: 5, y: 5)) # 35046: '#', '.'
|
||||
let cell = Map.random_cell(glyph: '.')
|
||||
print(Map.get(x: cell.x, y: cell.y)) # 46
|
||||
let a = IVec2.make(0, 0); let b = IVec2.make(3, 4)
|
||||
print(IVec2.distance2(a, b) * 10 + IVec2.within(a, b, 5)) # 251
|
||||
print(IVec2.heading(a, b)) # 53
|
||||
let p = IVec2.along(a, 0, 10); print(p.x * 100 + p.y) # 900 (the sine table rounds down)
|
||||
print(IVec2.step(180).x) # -1
|
||||
print(Angle.diff_degrees(a: 350, b: 10)) # 20
|
||||
let pool = new []int; push(pool, 7); push(pool, 8); push(pool, 9)
|
||||
let picks = List.sample(pool, 3); print(len(picks) * 100 + picks[0] + picks[1] + picks[2]) # 324
|
||||
let m = Input.move_i(); print(m.x + m.y) # 0
|
||||
Audio.define(name: "hit", path: "nope.wav")
|
||||
Audio.play(name: "hit")
|
||||
Camera.shake_for(amount: 3, frames: 2)
|
||||
Fx.sparks(x: 10, y: 10, color: 0xffffff, count: 4)
|
||||
Fx.number(x: 20, y: 20, value: 42, color: 0xffff00)
|
||||
spawn Thing { Position { x: 20, y: 20 }, Sprite { id: 5, move_id: 9, face: 1, flash: 3, blink: 6 }, TopDown { want_x: -1 } }
|
||||
}
|
||||
if frames == 3 { print(Map.is_solid(x: 0, y: 0) * 10 + Map.is_solid_at(x: 40, y: 40)) } # 10
|
||||
if frames == 5 { for (s, t) in query [Sprite, Tag] { print(s.flip * 100 + s.flash * 10 + s.blink) }; quit() } # 102
|
||||
}
|
||||
}
|
||||
25
examples/library/prefabs.ludic
Normal file
25
examples/library/prefabs.ludic
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# prefabs.ludic — prefabs: a model with preset fields, chained (`Grunt: Foe: Creature`),
|
||||
# spawned by name with overrides, `spawn` as an expression, `Prefab.spawn` / `spawn_at`
|
||||
# by a runtime name, and `self()` inside @OnSpawn. Deterministic; prints:
|
||||
# 230159 30 0 7 44 -1 2
|
||||
program Prefabs {
|
||||
import "ludic.core/components.ludic"
|
||||
property Enemy { kind: int = 0, hp: int = 10, team: int = 0 }
|
||||
model Creature { Position, Enemy }
|
||||
prefab Foe: Creature { Enemy { team: 2, hp: 1 }, Position { y: 9 } }
|
||||
prefab Grunt: Foe { Enemy { kind: 1, hp: 30 } }
|
||||
var last: int = -1
|
||||
@OnSpawn(Creature) handler Remember { last = self() }
|
||||
entry {
|
||||
let g = spawn Grunt { Position { x: 5 } } # Foe's presets, Grunt's, then this
|
||||
print(Enemy.of(g).team * 100000 + Enemy.of(g).hp * 1000 + Enemy.of(g).kind * 100 + Position.of(g).x * 10 + Position.of(g).y)
|
||||
let n = Prefab.spawn(name: "Grunt")
|
||||
print(Enemy.of(n).hp)
|
||||
print(g) # the first entity
|
||||
let placed = Prefab.spawn_at(name: "Foe", at: IVec2.make(7, 44))
|
||||
print(Position.of(placed).x)
|
||||
print(Position.of(placed).y)
|
||||
print(Prefab.spawn(name: "Nope")) # no such prefab
|
||||
print(last) # self() in the hook: the last spawn
|
||||
}
|
||||
}
|
||||
29
examples/library/scene_menus.ludic
Normal file
29
examples/library/scene_menus.ludic
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# scene_menus.ludic — scenes: `become` from an @On listener, @Queries handlers inside
|
||||
# a layer, `scene X shows Menu` (opened / closed by the engine), `lasts N then Y`,
|
||||
# `goto:` buttons, and scene-qualified handler names. Keys drive frames. Prints:
|
||||
# 100 200 201 1 202 300 6 7 8
|
||||
program SceneMenus {
|
||||
import "ludic.core/components.ludic"
|
||||
property Pos { x: int = 0 }
|
||||
model Thing { Pos }
|
||||
var menu_font: int = 0
|
||||
var frames: int = 0
|
||||
ui Menu {
|
||||
panel id: Root w: 100 pad: 4 bg: Color.Gunmetal border: Color.SlateGray align: center {
|
||||
button id: Go text: "Go" font: menu_font size: 16 w: 80 goto: Play
|
||||
}
|
||||
}
|
||||
scene Splash start lasts 1 then Title { on enter { print(100) } }
|
||||
scene Title shows Menu {
|
||||
on enter { print(200) }
|
||||
on exit { print(202) }
|
||||
layer L { handler Tick phase Update { frames += 1; if frames == 1 { print(201); emit UiClicked(id: UI_Go) } } }
|
||||
}
|
||||
@On(UiClicked) handler Report { print(id) } # UI_Go is widget 1 (the root panel is 0)
|
||||
scene Play {
|
||||
on enter { print(300); spawn Thing { Pos { x: 5 } } }
|
||||
layer Sim {
|
||||
@Queries(these: [Pos]) handler Tick phase Update { Pos.x += 1; print(Pos.x); if Pos.x >= 8 { quit() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
51
examples/library/sprite_atlas.ludic
Normal file
51
examples/library/sprite_atlas.ludic
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# sprite_atlas.ludic — the engine sprite-render system draws atlas ids (#90).
|
||||
# `Sprite { id, atlas: 1 }` marks `id` as a Sprite.cell / Sprite.cell_span sprite
|
||||
# (any size, here a 16x32 two-cell character) and esys_sprite routes it through
|
||||
# atlas_draw_ex with scale / flip / tint, so the #81 atlas API and the #85 engine
|
||||
# sprite system compose. atlas: 0 (the default) keeps `id` a 16x16 table sprite.
|
||||
# Verified by pixel readback after one frame.
|
||||
#
|
||||
# Driven headless by one stdin key ('q' quits after the frame renders).
|
||||
# printf 'q' | LUDIC_MODULES=packages bin/ludic examples/library/sprite_atlas.ludic -> 32 1 1 1 0
|
||||
program SpriteAtlas {
|
||||
import "ludic.core/components.ludic"
|
||||
|
||||
var TALL: int = 0
|
||||
|
||||
function bi(b: bool) -> int { if b { return 1 }; return 0 }
|
||||
|
||||
@OnStart handler Boot {
|
||||
let sheet = Sprite.sheet("assets/kenney/tiny-dungeon/Tilemap/tilemap_packed.png", 16, 16)
|
||||
TALL = Sprite.cell_span(sheet, 0, 0, 1, 2) # a 16x32 sprite: two cells tall
|
||||
spawn Tall { Position { x: 40, y: 40 }, Sprite { id: TALL, atlas: 1 } }
|
||||
spawn Tinted { Position { x: 100, y: 40 }, Sprite { id: TALL, atlas: 1, tint: 0xff0000 } }
|
||||
spawn Table { Position { x: 160, y: 40 }, Sprite { id: TALL } } # atlas: 0 -> the (empty) 16x16 table
|
||||
}
|
||||
|
||||
# An empty Render handler: the engine sprite system draws the Sprite entities.
|
||||
handler Draw phase Render { }
|
||||
|
||||
@OnQuit handler Report {
|
||||
print(Sprite.height(TALL)) # 32 — the span is two cells tall
|
||||
|
||||
var top = 0
|
||||
for yy in 40 .. 56 { for xx in 40 .. 56 { if Screen.pixel(xx, yy) != 0 { top = 1 } } }
|
||||
print(top) # 1 — the upper cell drew
|
||||
|
||||
var bottom = 0
|
||||
for yy in 56 .. 72 { for xx in 40 .. 56 { if Screen.pixel(xx, yy) != 0 { bottom = 1 } } }
|
||||
print(bottom) # 1 — the lower cell drew too (a table sprite stops at 16px)
|
||||
|
||||
var tinted = 1
|
||||
var seen = 0
|
||||
for yy in 40 .. 72 { for xx in 100 .. 116 {
|
||||
let c = Screen.pixel(xx, yy)
|
||||
if c != 0 { seen = 1; if c != 0xff0000 { tinted = 0 } }
|
||||
} }
|
||||
print(bi((seen == 1) and (tinted == 1))) # 1 — every opaque pixel took the tint
|
||||
|
||||
var table = 0
|
||||
for yy in 40 .. 72 { for xx in 160 .. 176 { if Screen.pixel(xx, yy) != 0 { table = 1 } } }
|
||||
print(table) # 0 — without atlas: 1 the id is a table id (nothing loaded there)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue