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
|
|
@ -90,6 +90,7 @@ function rt_screen_w() -> int { return rt_fbw }
|
|||
function rt_screen_h() -> int { return rt_fbh }
|
||||
|
||||
function rt_clear(c: int) -> void {
|
||||
rt_camera_tick() # a timed Camera.shake_for advances once per frame
|
||||
let n = rt_fbw * rt_fbh
|
||||
for i in 0 .. n {
|
||||
rt_fb[i] = c
|
||||
|
|
@ -328,6 +329,24 @@ function rt_camera_shake(amount: int) -> void {
|
|||
rt_shake_y = rt_rng_range(0 - amount, amount)
|
||||
}
|
||||
|
||||
# Camera.shake_for(amount, frames): shake by up to +/- amount pixels for `frames`
|
||||
# frames, then stop — the engine re-rolls the offset at the start of every frame
|
||||
# (rt_camera_tick, from the frame clear) so no handler has to count it down. A
|
||||
# later call restarts the shake; a bigger amount wins over a smaller one in flight.
|
||||
var rt_shake_amount: int = 0
|
||||
var rt_shake_left: int = 0
|
||||
function rt_camera_shake_for(amount: int, frames: int) -> void {
|
||||
if amount >= rt_shake_amount { rt_shake_amount = amount; rt_shake_left = frames }
|
||||
}
|
||||
function rt_camera_tick() -> void {
|
||||
if rt_shake_left > 0 {
|
||||
rt_shake_left = rt_shake_left - 1
|
||||
rt_camera_shake(rt_shake_amount)
|
||||
return
|
||||
}
|
||||
if rt_shake_amount > 0 { rt_shake_amount = 0; rt_camera_shake(0) }
|
||||
}
|
||||
|
||||
# Restrict drawing to a screen-space rectangle (x, y, width, height).
|
||||
function rt_clip(x: int, y: int, width: int, height: int) -> void {
|
||||
rt_clip_x0 = x
|
||||
|
|
@ -601,6 +620,139 @@ function rt_map_row(y: int, s: string) -> void {
|
|||
}
|
||||
}
|
||||
|
||||
# ---- the cell API: a game edits the grid in place instead of keeping its own copy
|
||||
function rt_map_set(x: int, y: int, glyph: int) -> void {
|
||||
if (x < 0) or (y < 0) or (x >= rt_mapw) or (y >= rt_maph) { return }
|
||||
rt_map[y * 96 + x] = glyph
|
||||
}
|
||||
function rt_map_fill(glyph: int) -> void {
|
||||
var y = 0
|
||||
while y < rt_maph { var x = 0; while x < rt_mapw { rt_map[y * 96 + x] = glyph; x = x + 1 }; y = y + 1 }
|
||||
}
|
||||
# every cell of the rectangle (x, y, w, h)
|
||||
function rt_map_rect(x: int, y: int, w: int, h: int, glyph: int) -> void {
|
||||
var yy = y
|
||||
while yy < y + h { var xx = x; while xx < x + w { rt_map_set(xx, yy, glyph); xx = xx + 1 }; yy = yy + 1 }
|
||||
}
|
||||
# the outermost ring of cells
|
||||
function rt_map_border(glyph: int) -> void {
|
||||
rt_map_rect(0, 0, rt_mapw, 1, glyph)
|
||||
rt_map_rect(0, rt_maph - 1, rt_mapw, 1, glyph)
|
||||
rt_map_rect(0, 0, 1, rt_maph, glyph)
|
||||
rt_map_rect(rt_mapw - 1, 0, 1, rt_maph, glyph)
|
||||
}
|
||||
# a random cell holding `glyph` (seeded RNG): random tries, then a sweep; (-1, -1) if none
|
||||
function rt_map_random_cell(glyph: int) -> IVec2 {
|
||||
var tries = 0
|
||||
while tries < 64 {
|
||||
let x = rt_rng_range(0, rt_mapw - 1)
|
||||
let y = rt_rng_range(0, rt_maph - 1)
|
||||
if rt_tile(x, y) == glyph { return IVec2.make(x, y) }
|
||||
tries = tries + 1
|
||||
}
|
||||
var y2 = 0
|
||||
while y2 < rt_maph { var x2 = 0; while x2 < rt_mapw { if rt_tile(x2, y2) == glyph { return IVec2.make(x2, y2) }; x2 = x2 + 1 }; y2 = y2 + 1 }
|
||||
return IVec2.make(0 - 1, 0 - 1)
|
||||
}
|
||||
# a random cell holding `glyph` at least `min_tiles` from `from` (tiles); falls back to any such cell
|
||||
function rt_map_random_cell_far(glyph: int, from: IVec2, min_tiles: int) -> IVec2 {
|
||||
var tile = rt_map_random_cell(glyph)
|
||||
var tries = 0
|
||||
while tries < 40 {
|
||||
if not IVec2.within(tile, from, min_tiles - 1) { return tile }
|
||||
tile = rt_map_random_cell(glyph)
|
||||
tries = tries + 1
|
||||
}
|
||||
return tile
|
||||
}
|
||||
# the solid glyphs, as the move system read them from the Solids config (0 = none)
|
||||
var rt_map_solid1: int = 0
|
||||
var rt_map_solid2: int = 0
|
||||
var rt_map_tile_px: int = 16
|
||||
function rt_map_is_solid(x: int, y: int) -> bool {
|
||||
if rt_map_solid1 == 0 { return false } # no Solids config: nothing is solid
|
||||
let g = rt_tile(x, y)
|
||||
if (x < 0) or (y < 0) or (x >= rt_mapw) or (y >= rt_maph) { return true }
|
||||
if (rt_map_solid1 != 0) and (g == rt_map_solid1) { return true }
|
||||
if (rt_map_solid2 != 0) and (g == rt_map_solid2) { return true }
|
||||
return false
|
||||
}
|
||||
function rt_map_is_solid_at(px: int, py: int) -> bool { return rt_map_is_solid(px / rt_map_tile_px, py / rt_map_tile_px) }
|
||||
function rt_map_to_tile(pixel: IVec2) -> IVec2 { return IVec2.make(pixel.x / rt_map_tile_px, pixel.y / rt_map_tile_px) }
|
||||
function rt_map_width() -> int { return rt_mapw }
|
||||
function rt_map_height() -> int { return rt_maph }
|
||||
|
||||
# IVec2.heading / along / step and Angle.diff_degrees — integer-degree geometry
|
||||
function rt_ivec_heading(a: IVec2, b: IVec2) -> int {
|
||||
return floor(Math.rad_to_deg(Math.atan2(fixed(b.y - a.y), fixed(b.x - a.x))))
|
||||
}
|
||||
function rt_ivec_along(origin: IVec2, degrees: int, distance: int) -> IVec2 {
|
||||
let r = Math.deg_to_rad(fixed(degrees))
|
||||
return IVec2.make(origin.x + floor(Math.cos(r) * fixed(distance)), origin.y + floor(Math.sin(r) * fixed(distance)))
|
||||
}
|
||||
function rt_ivec_step(degrees: int) -> IVec2 {
|
||||
let p = rt_ivec_along(IVec2.zero(), degrees, 10)
|
||||
return IVec2.make(Math.sign(p.x), Math.sign(p.y))
|
||||
}
|
||||
function rt_angle_diff_degrees(a: int, b: int) -> int {
|
||||
var d = (b - a) % 360
|
||||
if d > 180 { d = d - 360 }
|
||||
if d <= 0 - 180 { d = d + 360 }
|
||||
return d
|
||||
}
|
||||
|
||||
# Screen.bar: a filled meter — `value` of `max` in `color` over a `back` track
|
||||
function rt_bar(x: int, y: int, w: int, h: int, value: int, max: int, color: int, back: int) -> void {
|
||||
rt_fill_rect(x, y, w, h, back)
|
||||
var filled = 0
|
||||
if max > 0 { filled = clamp(value, 0, max) * w / max }
|
||||
if filled > 0 { rt_fill_rect(x, y, filled, h, color) }
|
||||
}
|
||||
|
||||
# Random.weighted(weights): an index drawn in proportion to its weight (0 = never);
|
||||
# -1 when every weight is 0. Deterministic, from the seeded RNG.
|
||||
function rt_rng_weighted(weights: []int) -> int {
|
||||
var total = 0
|
||||
var i = 0
|
||||
while i < len(weights) { if weights[i] > 0 { total = total + weights[i] }; i = i + 1 }
|
||||
if total <= 0 { return 0 - 1 }
|
||||
var roll = rt_rng_range(0, total - 1)
|
||||
i = 0
|
||||
while i < len(weights) {
|
||||
if weights[i] > 0 {
|
||||
if roll < weights[i] { return i }
|
||||
roll = roll - weights[i]
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return 0 - 1
|
||||
}
|
||||
|
||||
# List.sample(pool, count): `count` picks from an int slice, distinct while the
|
||||
# pool has enough, repeating a valid pick when it does not; empty in -> zeros
|
||||
function rt_list_sample(pool: []int, count: int) -> []int {
|
||||
let out = new []int
|
||||
let n = len(pool)
|
||||
var i = 0
|
||||
while i < count {
|
||||
if n == 0 { push(out, 0); i = i + 1; continue }
|
||||
var pick = rt_rng_range(0, n - 1)
|
||||
var distinct = n > i
|
||||
var tries = 0
|
||||
while distinct and (tries < 64) {
|
||||
var seen = false
|
||||
var j = 0
|
||||
while j < len(out) { if out[j] == pool[pick] { seen = true }; j = j + 1 }
|
||||
if not seen { break }
|
||||
pick = rt_rng_range(0, n - 1)
|
||||
tries = tries + 1
|
||||
}
|
||||
push(out, pool[pick])
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function rt_tile(x: int, y: int) -> int {
|
||||
if x < 0 { return 35 }
|
||||
if y < 0 { return 35 }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue