• v0.3.0 e9c2c51bc3

    v0.3.0
    All checks were successful
    bootstrap / cfree-fixpoint (push) Successful in 23s
    ci / build-and-test (push) Successful in 2m28s
    commit-lint / conventional-commits (push) Successful in 5s
    docs / build-and-deploy (push) Successful in 29s
    Pre-release

    orkun released this 2026-09-02 07:37:20 +02:00 | 1008 commits to main since this release

    v0.3.0 — 2026-09-02

    • feat: Tiled map support (#67–#74) — load and draw Tiled maps (TMX/TSX/TX and TMJ/TSJ/TJ), the design record from #66. - P0 parsing primitives (#67) — a minimal pure-Ludic XML reader (Xml.*) for the element/attribute/CDATA subset TMX/TSX/TX use; standard base64 decode/encode (Base64.*, RFC 4648), whose decoder ignores the whitespace Tiled wraps into <data>; and gzip framing (z_gunzip, RFC 1952) wrapping the existing DEFLATE inflater. zlib and the JSON reader already shipped. A curated, attributed golden corpus lands under assets/tiled-fixtures/. - P0.5 TMX/TSX reader (#68) — Tiled.read / Tiled.read_tsx map the native XML formats onto the same intermediate the JSON path produces — a Value tree in Tiled's JSON schema, with every tile layer's data decoded to a dense GID list (CSV, base64, base64+zlib, base64+gzip). A CSV .tmx and a base64+zlib .tmj of the same map read structurally identically; the in-repo Kenney sampleMap.tmx + external sampleSheet.tsx load with no manual JSON re-export. - 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. - 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). - P5 breadth (#73) — image layers (parallax + repeat) and group layers (flattened, with recursive offset/opacity/tint/visible; Tiled.layer_kind/Tiled.layer_offsetx/Tiled.layer_tint); the isometric / staggered / hexagonal orientation coordinate transforms (Tiled.cell_x/Tiled.cell_y, driving the tile draw so cells land at the correct screen coords); and Wang-set GID resolution through the standard resolver (the terrain-corner authoring concept is editor-side and ignored). - P6 scale (#74) — infinite/chunked maps: <chunk> (TMX) and JSON chunks[] decode and flatten into the dense layer; .world stitching (Tiled.world/Tiled.world_count/Tiled.world_map) lists member maps at their offsets; and a self-contained pure-Ludic zstd decompressor (z_zstd, RFC 8878) for base64+zstd layers — frame + raw/RLE/compressed blocks, raw/RLE/direct-weight-Huffman literals, and the full FSE sequence path — decoding the low-entropy GID streams a tilemap produces (a high-entropy FSE-compressed-Huffman-weights block fails cleanly with -1 rather than emitting wrong bytes).
    • feat: Builtin NPC AI (#61) — the source package ludic.npcai, a perception → decision → action stack that plugs into the other controllers instead of re-implementing movement. The AI never moves a body directly: it writes the SAME intent fields the player controllers read (want_x/want_y/want_fire, want_jump), so an enemy gunner reuses the shooter's weapon/projectile/auto-aim systems verbatim (set the body's TopDown.aim_mode = 3) and a companion reuses the mover — friendly vs enemy is faction + goal, not different code. Perception (Vision + Memory, throttled esys_perception with faction filtering and optional Grid line-of-sight) remembers the nearest hostile and emits TargetSpotted/TargetLost. Decision offers three models writing one Brain intent — a finite-state machine (patrol/chase/attack/flee), a utility scorer, and a canonical behaviour tree — each decision veto-able via cancellable DecisionMade. Steering adds Reynolds flocking (separate/cohere), and a Follower component gives companion stances. Reuses ludic.gameplay Faction (who is hostile) + Stats (hp for flee). Fully deterministic: perception + replan are frame-throttled and fixed-order. Example: examples/games/npcai_demo.ludic — one enemy perceives, chases and shoots a target through the shooter controller, flees at low hp under the utility model, and a companion follows its leader.
    • feat: Builtin Platformer controller (#58) — the reference implementation of the six-lever extensibility contract, shipped as the source package ludic.platformer. Movement feel is all defaulted POD data (Platformer { move_speed, jump_height, apex_frames, fall_gravity_mul, coyote_frames, jump_buffer_frames, air_jumps, policy, … }); the controller is decomposed into small engine-owned sub-systems — input (Input phase), move/gravity/jump (FixedUpdate, before the shared esys_move sweep) and animation-state (LateUpdate, after it) — each independently switch-off-able with disable system <fn>. It owns movement policy only and reuses the engine Body/Collider swept-AABB collision. Jump feel derives gravity + impulse from height/apex, with coyote time, jump buffering, variable jump height and multi-jump; every jump decision emits a cancellable JumpRequested / JumpPerformed / Landed / StateChanged event, and a gravity policy enum (asymmetric / symmetric / floaty) is the formula hook. Ships the opt-in game-loop layer too (scaffolding.ludic): moving & crumbling platform blocks with rider carry, collectibles + Score, springs, hazards + a light Life/i-frames model, and checkpoint/goal triggers. Deterministic integer Q16.16 throughout. Examples: examples/games/platformer_demo.ludic, examples/games/platformer_scaffolding.ludic. Also fixes a latent codegen bug in ludic_sweep_entity (an SSA register name collided once a program declared ≥11 events).
    • feat: Builtin RPG systems suite (#59) — the source package ludic.rpg, seven independently-usable modules on the six-lever contract, with name-keyed data registries so a game or mod adds content with zero code. A Movement — one Mover with a mode selector (grid / free / grid-tween) and 4/8-axis, tilemap walkability, and cancellable MoveRequested (locked doors/ice) / TileEntered (encounters) / Interacted (the action-button raycast). B Inventory — a name-keyed item registry, per-owner counts, gold, ItemUse veto, and Equipment whose bonuses flow through the gameplay Stats modifier stack. C Crafting — a data-driven recipe + ingredient registry; Craft.can/Craft.make consume from the inventory. D Quests — quests + objectives whose progress is driven by Quest.notify (route any gameplay signal in), auto-completing when met, plus a global flag store for branching. E Dialog — an Ink/Yarn-style graph registry (nodes + choices) with a per-speaker Dialog component and cancellable DialogChoice for skill-check gating. F Puzzles — Sokoban Pushable + a switch / pressure-plate / gate signal graph (logic puzzles with no code). G Status — over-time poison/regen effects routed through the shared Combat pipeline. Everything is integer-deterministic, so save/load (world_save) and rollback hold. Example: examples/games/rpg_demo.ludic (21 self-checks across all seven modules).
    • feat: Builtin top-down Shooter controller (#60) — the source package ludic.shooter, conforming to the six-lever contract and built on the engine Body/Collider + ludic.gameplay Faction/Combat/Stats. TopDown decouples movement from aim (aim_mode: mouse / right-stick / move-direction / nearest-enemy auto-aim, with a turn_rate for tank-style rotation). Weapons are a name-keyed registry (Weapon.def("shotgun", …) — add a gun with zero code) with per-weapon fire-rate, damage, speed, spread, pellet count, pattern (single / spread cone / ring / spiral), plus data-driven pierce (Weapon.set_pierce) and homing (Weapon.set_homing); esys_weapon reads a want_fire intent so the same weapon fires for a player (input) and an NPC (AI). Projectile + esys_projectile is a self-contained deterministic pool: integrate, TTL, faction-filtered hit through Combat.damage, pierce, and homing that curves onto the nearest enemy — every step observable via ProjectileSpawned (mutable/veto) / ProjectileHit / ProjectileExpired. A budgeted Spawner wave director emits SpawnRequested / WaveCleared. Example: examples/games/shooter_demo.ludic (11 self-checks: movement, aim, faction damage, friendly-fire immunity, spread, kill, ring, homing, waves).
    • feat: Canonical engine-ABI components (#77) — the shared Position / Body / Collider / Solids bundles the engine-owned movement system (esys_move, #65) reads by name are now shipped from a base source package, ludic.core, instead of being re-declared by hand in every game and example. A game import "ludic.core/components.ludic" and the engine moves and collides its entities for free; extend by composition (attach your own components on the same model). AOT means the properties compile straight into the consumer's compile-time ECS with no ABI seam, and everything stays integer + Q16.16 deterministic (lockstep / replay / world_save hold). Example: examples/library/core_components.ludic.
    • feat: Cursor capture (#89) — Input.cursor_mode(mode) hides / locks / confines the OS mouse for a windowed game: 0 normal (visible, free), 1 hidden (hide the OS cursor while focused so a game draws its own reticle), 2 locked (hidden + dissociated — the mouse feeds relative motion through Input.mouse_dx/dy, and Input.mouse_x/y becomes a clamped virtual cursor, the FPS / twin-stick aim mode), 3 confined (dissociated but visible; the mouse cannot leave the window). The platform auto-releases (shows + reconnects the cursor) while the window is not key (Cmd-Tab) and on close, so the cursor is never left captured. Adds the native macOS implementation in cocoa.ll ([NSCursor hide]/[unhide], ref-counted and toggled only on change; CGAssociateMouseAndMouseCursorPosition; CGGetLastMouseDelta for the relative virtual cursor) behind a new win_cursor_mode intrinsic; headless / non-windowed it is a no-op (DCE'd). Example: examples/library/cursor_capture.ludic.
    • feat: Declarative Render clear + present (#86) — @ClearColor(0xRRGGBB) makes the engine own the per-frame clear and flip: at the top of the Render phase it clears the framebuffer to the declared colour, and after the Render handlers run it presents the frame, so a game's Render handler no longer repeats Screen.clear(color) / Screen.show() and the clear colour is configured declaratively rather than in the handler body. Opt-in and backward-compatible: a program with no @ClearColor is byte-for-byte identical (it clears/presents itself, or the light system owns the present). Example: examples/library/clear_color.ludic.
    • feat: Deterministic camera zoom (#78) — Camera.zoom(scale) scales the whole view about the screen centre by a Q16.16 factor (1.0 = none, 2.0 = 2x in, 0.5 = out). It rides on the same two framebuffer chokepoints (rt_put_px / rt_fill_rect) that already carry the camera offset, so it composes with Camera.set/follow/shake, and it is a render-time transform — the world coordinate types stay integer pixels + Q16.16 velocity, so lockstep, replay and world_save are untouched, and the zoom itself is deterministic. Gated by an internal rt_cam_zoomed flag so a game that never zooms renders byte-for-byte identically (golden renders unchanged); Camera.zoom(1.0) turns it back off. This is the concrete outcome of the #78 position-types investigation (docs/RFC-POSITION-TYPES.md), which rejected hardware floats for the deterministic coordinate core and identified zoom as the one genuinely-missing render feature. Example: examples/library/camera_zoom.ludic (pixel-readback verified).
    • feat: Directional int input (#79) — Input.axis_i(neg, pos) -> int returns a -1/0/1 movement intent (+1 positive key held, -1 negative, 0 neither or both) read from the multi-key device set, so turning WASD into movement no longer needs the ki(key_down('d')) - ki(key_down('a')) bool-to-int glue and feeds an int mover directly: dx = Input.axis_i('a', 'd'), dy = Input.axis_i('w', 's'). Complements Input.axis (fixed) / Input.vector (normalized). Example: examples/library/input_movement.ludic.
    • feat: Engine sprite-render system (#85) — the engine already auto-ticks SpriteAnim and Motion; it now auto-draws too. Declare a Sprite component (id + optional offx/offy/scale/flip/tint/hidden, shipped from ludic.core) on an entity with a Position and the engine draws it each Render frame — no hand-written Render handler querying positions and calling draw_sprite per entity, and no hand animation (when the entity also carries SpriteAnim, the current frame is added to the base id). Registered on the compile-time engine-system registry for the Render phase and spliced only when a game declares Sprite, so a game that never declares it compiles byte-identically; a game wanting a custom draw omits Sprite (or disable system esys_sprite). Also deprecates the bare draw_sprite / draw_sprite_scaled globals in favour of the namespaced Screen.sprite / Screen.sprite_scaled: a direct bare call now emits a one-time compile-time deprecation note (the bare form still lowers, since Screen.sprite uses it), and the in-repo chronorift demo is migrated to the namespaced calls. Example: examples/library/sprite_render.ludic.
    • feat: Entity-pool stats (#80). Ludic's ECS is already pool-based: the allocator recycles freed entity slots through a freelist (a despawned slot is reused by the next spawn before any new slot is taken), and component storage is fixed per-entity arrays — so spawning and despawning many entities per frame (bullet-hell / horde) does no per-spawn heap allocation and cannot fragment. Exposes that with a Pool.* namespace so a game can watch the reuse and budget against the cap: Pool.live() (entities alive now), Pool.free() (freed slots waiting to be reused), Pool.reserved() (high-water — slots ever allocated; stays flat across a steady spawn/despawn loop, the proof that slots are pooled not reallocated), and Pool.capacity() (the fixed entity cap). Zero-cost — they read the existing allocator counters inline. Example: examples/library/pool.ludic.
    • feat: Gameplay-controller foundation (#57) — the shared, cross-genre building blocks the builtin controllers stand on, shipped as the source package ludic.gameplay: a deterministic Cooldown frame timer (engine-ticked), a Stats attribute bundle with an unbounded timed modifier stack (Stats.total computes base+flat then percent on demand; expired modifiers self-despawn), a Faction friend/enemy/neutral relationship table (same-id-friendly / different-hostile by default), and a Combat damage pipeline whose cancellable DamageAboutToApply hook lets a game veto a hit or rewrite the amount (Combat.set_amount) and which emits Damaged/Died/Healed. Everything is integer-only so lockstep, replay and world_save snapshots hold. Also adds extensibility lever 5 to the language: disable system <esys_fn> drops exactly one engine-owned system's tick at compile time, so a game can carry a well-known component but tick it with its own handler (byte-identical when nothing is disabled; the C-free bootstrap fixpoint is untouched). Example: examples/library/gameplay_foundation.ludic.
    • feat: Incremental asset preloading (#82). Assets used to load synchronously inside Boot/Start (png_load, Audio.load), stalling the first frame(s) as content grows, with no built-in loading phase. Adds an Assets.* preload queue: Assets.enqueue(name, path) queues a named image without loading it, Assets.pump(max) loads up to max queued assets per frame (returning how many it loaded), and Assets.total / loaded / ready / progress (0..100) drive a progress bar. A loading scene pumps a few assets per frame, draws Assets.progress(), and becomes the play scene once Assets.ready(), so the game shows a responsive loading screen and only enters play once content is ready — the deterministic, no-threads form of async preloading (the work is spread across frames instead of stalling one, and the same enqueue+pump order loads identically every run). Loaded assets are reachable by name via Assets.get / Sprite.named. Builds on the #81 atlas. Example: examples/library/preload.ludic.
    • feat: Input Manager + automatic device-layer drive (#83). The generated frame loop now commits the input device layer itself — when a game uses any Input action-map / device method it calls input_poll each frame (reading the live key, recording/replaying, and rebuilding the held-key/mouse/gamepad state), so Input.active / Input.key_down / the mouse and pads read live without the game calling Input.poll by hand (previously the loop fed only Input.key, and the device layer read empty unless the game polled at the top of its Input phase). A game that uses no Input runtime keeps the plain rt_poll path, byte-identical. Adds the Input-Manager API on top: Input.action(name, key) ships a default binding (kept if already bound, so a player's Input.rebind or a loaded key-map is not clobbered); Input.bind_pad(name, button) makes an action device-agnostic (fires from keyboard or gamepad); and Input.active / Input.just_pressed / Input.just_released read the whole multi-key device layer with clean on-press / on-release edges (the deterministic, dispatch-free equivalent of event handlers — a handler polls the edge and reacts, so a replay fires identically). Examples: examples/library/input_manager.ludic, examples/library/input_auto.ludic.
    • feat: Namespace block form (#76) — namespace Name { export function foo(…) … internal function bar(…) … } declares a Name.* namespace once and controls its public surface declaratively, instead of annotating every function with @Namespace(Name) one at a time. Inside the block each function short(…) is emitted as namelower_short; an export function (the default) is callable as Name.short(…), while an internal function is a private helper — emitted and callable by its short name from siblings in the block (calls are rewritten to the emitted name), but not part of the Name.* surface (Name.internalOne() is a compile error). It is the block sugar for the per-function @Namespace annotation, so a package's public API reads at a glance. A namespace declared the old per-function way is unchanged. Example: examples/library/namespace_block.ludic.
    • 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.
    • feat: Optional world boundaries (#84) — declare a single Bounds config entity (a rect x, y, w, h plus a policy, shipped from ludic.core) and the engine-owned world-bounds system keeps every moving Body inside the play area each frame, so a game no longer hand-clamps Position. Four policies: 0 clamp (walls), 1 wrap (toroidal), 2 bounce (clamp + flip the Body velocity on the axis that hit), 3 kill (despawn a body fully outside). It reads each body's Collider size so the whole box stays inside; off by default (no Bounds entity = open world). Registered on the engine-system registry for LateUpdate (after movement integrates) and spliced only when a game declares Bounds, so a game that never does compiles byte-identically. Also adds World.despawn(entity) — the reflective, by-id form of the despawn statement (runs @OnDespawn + frees the slot), which the kill policy uses and any system can call. Example: examples/library/world_bounds.ludic.
    • feat: Package manager (#63) — x add / x get / x update / x verify / x vendor bring third-party packages to Ludic with no new infrastructure. Dependencies are named by their git import path (URL-as-identity, no registry — a git tag vX.Y.Z is publishing), resolved by Go-style Minimum Version Selection, fetched into a content-addressed global store (~/.ludic/store, keyed by a file-content hash) and linked into each project under ludic_modules/. A package.ludic manifest declares dependencies, the provided Foo.* namespace(s), the kind (source or prebuilt) and, for prebuilt libs, the shipped targets; package.lock.ludic pins the resolved versions and content hashes for reproducible, verifiable builds. A source package's Ludic compiles into the consumer via a new module-root import fallback in the compiler (import "git.workshopsoft.io/user/pkg/foo.ludic" resolves against $LUDIC_MODULES, default ludic_modules/), so a package registers a namespace the same way the built-in stdlib does. Namespace collisions and missing prebuilt targets are hard errors. Existing programs compile byte-for-byte identically; the C-free bootstrap fixpoint is untouched. See docs/PACKAGES.md.
    • feat: Package-declarable namespaces & engine systems (#62) — the two hooks that made Foo.* stdlib namespaces and engine-owned systems compiler-hardcoded are now data-driven registries, so a package registers them with no compiler edit. @Namespace(Name) on a function opens a Name.method(…) namespace that dispatches to the bare name_method through the same generic path the built-in namespaces use (applied only after them, so it never shadows a core one). @EngineSystem(Component, Phase) registers an engine-owned system the frame loop runs each phase when the component is present — the package-declarable form of the built-in SpriteAnim/Motion/Light2D systems, reading components by name through the reflection ABI so an unused registration is byte-identical. Both annotations are keyword-free. The core stdlib keeps its optimized codegen (byte-identical output; the C-free bootstrap fixpoint is untouched) and packages ride the generic registry alongside it. This completes the packaging prerequisite for shipping gameplay-controller libraries (#58–#61) as real packages. See docs/PACKAGES.md.
    • feat: Prebuilt binary packages (#64) — a package can now ship a compiled artifact whose exported functions, systems and components a consumer uses without the source, over the stable reflection C-ABI. x build-lib <module.ludic> compiles a package's module to a per-target native dylib (lib/<target>/); a kind prebuilt dependency is fetched and linked like any other, and x link-flags prints the clang flags to link the module dylibs into a game (or x app does it in-repo). A module registers its dynamic components (world_register_prop) and its @System(Phase) functions with the host at load through a constructor, and the host dispatches every registered system each frame — the systems analogue of dynamic components. Binary packages are native-only and second-class ECS by design (source packages remain the portable, first-class, deterministic path); missing a build target is a hard error. See docs/PACKAGES.md.
    • feat: The engine runtime ships with the toolchain, not the project (#75). The compiler auto-splices runtime/native/* for any ECS game; a runtime/... import that is not found relative to the build is now resolved from the install root $LUDIC_HOME (default: the compiler binary's directory — where the platform .ll files already come from) before the package module root. So an external game that consumes the ludic.* packages no longer has to copy or symlink the engine runtime into its ludic_modules/; that directory holds only third-party packages, and the runtime is part of the toolchain install. In-repo builds are byte-identical (the runtime still resolves locally there, so the $LUDIC_HOME fallback never fires and the C-free bootstrap fixpoint is untouched).
    • fix: Correct the element type of a slice indexed by a member-access expression. emit_index_addr set the global g_addr_ty to the slice's element type before evaluating the index expression, so an index that was itself a struct-field access (slice[obj.field]) overwrote it — the load then came back typed as the field, and a following field access failed with "member access on non-aggregate". The slice branch now sets g_addr_ty last, matching the raw-pointer branches. Also de-duplicate the @strcmp declaration (centralised in the head prelude) so a program that pulls in both the world table and the filesystem prelude links.
    • fix: Input.key_pressed / key_released edges now fire (#87). In a frame-loop game the edges never triggered: the loop (since #83) commits the device layer once per frame via input_drive, but a game that also called Input.poll by hand committed a second time in the same frame, and input_device_commit copies in_held into in_prev at the top of every commit — so the second commit left in_prev == in_held and key_pressed (held && !prev) / key_released could never see a transition. Fixed with an in_have_frame_driver flag: the loop's input_drive sets it, and a manual Input.poll under the loop becomes a no-op that returns the frame's key instead of re-committing. An entry-driven harness has no loop, so the flag stays false and each Input.poll still commits a frame of input as before (record/replay and the #50 device tests are unchanged). Example: examples/library/input_edge.ludic (press edge on the down frame, release edge on the up frame).
    • fix: Windowed games no longer force-quit on Esc or 'q' (#88). The macOS platform layer (runtime/native/cocoa.ll win_poll) used to hard-code Escape (keycode 53) and the character 'q' as quit — storing W_running = 0 so a shipped windowed game died the instant a player pressed Esc (a universal pause key) or typed 'q'. Those dev-loop conveniences are removed for windowed builds: Escape is delivered to the game as key 27 and 'q' is an ordinary key, consistently across both the single per-frame key (@W_key) and the #50 held-key set (ev_keyval now maps Escape→27, not 'q'). A windowed game now owns Esc/pause and shuts down via quit() or the window close button (which still ends the run). The headless test driver (rt_poll in core.ludic) keeps its own 'q' = quit for scripted golden runs, so nothing headless changes. Also stops forwarding consumed key events to -sendEvent:, which was triggering AppKit's system "funk" beep on every keystroke.
    Downloads