Commit graph

504 commits

Author SHA1 Message Date
d2cd775309 feat(physics): phys_heightfield_smooth - the ground as a renderer draws a cubic B-spline height map, smoothed in C
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:27:15 +03:00
c7a18fd4f0 feat(physics): a walker (Jolt CharacterVirtual with an inner body) and the verbs a boat and a guest's copy need - force, torque, angular impulse, spin, set_pose
Five new tests: stand and walk at the asked speed, a low stone is a step and a tall one a wall, a
jump up and back, a crate shoved aside, a hull turned by a torque and carried by a force.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:24:47 +03:00
b44b88e00e merge lang/foundations into lang/native-jolt (seeds regenerated)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:22:50 +03:00
54f5efb23f feat(migrate): 0.R5 - ludic migrate state --tighten takes mut off every state parameter nothing down the chain writes; --root DIR lets the edits reach a directory without running its programs; a write through a local holding part of a mut state counts as a write to it
Maroon Lake: 149 parameters became read-only. What stays mut is a real write - in the packages mostly a
lazy start inside a question (things_all, gear__ensure), which is what to take out next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:20:44 +03:00
e53f3197ac refactor(render3d): phase 16 - the collider store is gone; still things are bodies in ludic.physics
collide.ludic (circles in a cell grid that could not be removed) and its Render3dState fields are
deleted, and the reload example stops asserting colliders. A game still calling col_* must move to
ludic.physics first (Maroon Lake's src/solid).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:08:36 +03:00
d4fc9bbb91 fix(physics): push keeps the old collider contract exactly - a thing wholly under the feet or over the head is not in the way, anything else pushes out sideways (Jolt's axis when side-on, else radially by the overlap across)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:08:36 +03:00
28c54e4b39 feat(physics): phys_resolve (two passes, the place kept) - a contact more up than across is ground under the feet, not a wall
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:47:29 +03:00
1c814464c5 feat(physics): phys_step_top - the highest top a foot could step up to, walls left out (what CharacterGround.top_at asks)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:43:40 +03:00
204ec93c5a chore(physics): Jolt Physics' MIT licence ships with the package (native/LICENSE-JoltPhysics)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:41:47 +03:00
72dbdca1f3 feat(physics): phase 16 - ludic.physics over Jolt Physics v5.6.0
Our own shim (native/shim/jph_shim.cpp) over Jolt built from the pinned tag with cross-platform
determinism and no fp contraction: shapes as handles (box, sphere, capsule, cylinder, dome,
offset, heightfield, mesh), still/kinematic/dynamic bodies by id with real removal, rays,
top_at and push (what CharacterGround asks), overlaps, buoyancy against the PhysWater port,
contacts recorded in C and drained as PHYS_HIT / PHYS_SPLASH facts. Fixed 1/60 steps, at most four
a frame. Nine tests against the real library, including a pile run twice to the bit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:41:37 +03:00
5d3a799e33 feat(pkg): phase 15 - a package can carry a native library
native "<target>" "<path>" in a package's package.ludic; the compiler records the libraries of
every package a program imports and writes them into the IR (; ludic-native:), so ludicc -o,
ludic build, ludic test and ludic bundle all link one list. macOS: an rpath to the package and to
Contents/Frameworks, where ludic bundle copies and signs each library and drops the build
machine's rpath. Windows: the import library, the .dll copied beside the exe (--natives-out for
the bundle). tools/native/lib.sh builds from a pinned, checksummed source with clang on both
machines; ludic.nativeecho is the worked example; the shim rules are in packages/README.md.
Linked at build time rather than dlopen (docs/PACKAGES.md says why). Reseeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:35:45 +03:00
40a91f39ed fix(ui): the pointer in the renderer's pixels - macOS reports it in window points, so on Retina every hit landed at half the cursor's position
The old kit read Input.mouse_x() * gl_scale; ludic.ui read it raw against a layout sized in
drawable pixels. A backend says how many screen pixels one pointer unit is (pointer_scale);
render3d's is gl_pixel_scale(), 1 on Windows and headless.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:31:47 +03:00
5df0747642 feat(cli): 0.R5 - ludic deps says what a function can come to change (widest_write_reach, --wreach N); a port member and a registry field reach only themselves
A reach through Port.member() follows that member's binding (or its default), and Registry[i].field -
or a local holding Registry[i] - follows that field in each entry, so a question asked of a port or a
table that also holds verbs no longer reaches the verbs. In Maroon Lake that took the valley's
'what is this Thing called' from 47 states it could change to 1. examples/state/write_reach.ludic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:19:54 +03:00
c64f8750e2 feat(lang): 0.R5 - a reducer writes one state and may read others, declared between its state and the action
What only becomes known inside the drain - a value another reducer just set, the map in play - no longer
has to be faked into the action, so a verb that reads several systems while it changes one is a reducer
instead of an act handed its states. A second state to write is still refused, and the message says the
way out. examples/actions/reads.ludic; reseeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:23:48 +03:00
dfcc851d2a docs: bootstrapping ludic-dev by hand needs --unsafe --globals (the toolchain's own programs keep their module vars)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 13:23:04 +03:00
259b4d9b35 fix(ui): 0.R4 - an action a UI button dispatches is reduced before the frame is presented
ludic.ui runs a frame's presses after drawing it, and what they dispatched waited for the end of the
phase - after the host had presented - so a button's change showed a frame late. Decided: drain, not a
phase per action. ui_show and ui_press drain the queue once the presses have run, so the code after
them and the frame presented next see the change; a host that runs presses some other way calls
drain_actions() before it presents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 07:08:26 +03:00
d46f0adb5e fix(lang): 0.R4 - ludic.ui's UiAct is its own, and a name that meets a package's export says whose it is
A game's exported UiAct collided with ludic.ui's, which nothing outside ludic.ui uses: it is private
to ludic.ui now, and a private record of one spelling in two modules never clashed. A real clash - a
type named like one a package exports - is still refused, and the message names the package and the
way out (`ludic_ui exports it, and exported names are one namespace - rename this one, or declare it
without export inside a module of your own`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 06:57:35 +03:00
eb1e780733 feat(cli): 0.R4 - ludic deps sees through fn values (widest_reach) and lists the widest functions (--widest N, --reach N)
A step list or a registry of fn values takes no state and still reaches every state its steps take.
The compiler now writes `reach <n> <function>` - every state a function can come to by a call, a
`fn f` it writes or a global holding fn values it reads, to a fixed point - and ludic deps reports
widest_reach beside widest_function, with how many of those states the function does not take
(Maroon Lake: app_boot, 72, all 72 through fn values). --widest N lists the N functions that take the
most states with what each reaches; --reach N orders them by reach. A baseline without widest_reach
does not hold it until rewritten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 06:24:46 +03:00
2eefae0618 fix(check): 0.R4 - a misspelled type in a parameter, a result or a field is refused where it is written
`function kind_of(f: CharFact)` for a CharacterFact was taken on trust and failed in the code writer
as "member access on non-aggregate". A capitalised type - plain, in a slice, or a generic's argument -
must name a declared property, record, state, event, enum, action or packed value type, or a type
parameter of its declaration: `kind_of's parameter f: there is no type CharFact`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 05:58:35 +03:00
c35481f344 fix(migrate): 0.R4 - --prune keeps a state declared after a plain parameter; a parameter named twice is refused
On Maroon Lake the prune gave home_keep_records(base_app_st, home_st, r: RunRecords, save_app_st) a
second save_app_st at the front, and every call a second argument: a state declared after a plain
parameter was not counted as declared. It is now, and a call to such a function is never given the
state again. `ludic build --check` let the duplicate through and clang refused it; the checker now
refuses a function that names two parameters alike (`add names two parameters n`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 05:32:08 +03:00
71735b10a2 feat(base): 0.R4 - a queue keeps its own count, so every package verb takes only its own state; ludic migrate state --prune
ludic.base's Queue<T> carries a QueueTag (its name and pending count): queue_new(name), q_push(q, v),
q_drain(q), q_clear(q) take no BaseState, and core_undrained(tags) names the given queues still holding
facts. A reducer on a package's state can now call that package's verbs (wallet_earn(wallet_st, n)).

ludic migrate state --prune (ludicc --migrate-prune) takes out each state parameter a function no
longer uses, nor anything it calls, and the argument that fills it - including an argument for a
parameter the callee has dropped, which is taken out before the call is checked, so a generic's T is
told by the argument that says it. A reducer keeps its state. --dry-run now counts the edits it would
make. Every package was moved with it: 608 base_st parameters and their arguments, 1889 edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 04:19:56 +03:00
8cf4b3fed6 fix(lang): the action drain is the program's; --check runs every check a build makes; a registry key named count is refused; name lookups are tables
- the function that calls every reducer (and the action queue) is written in the program's own
  file: in the first action's file it belonged to that module, depended on every module with a
  reducer, and joined a game's modules into one 69-module cycle (examples/actions/modules and a
  ludic deps case hold it); the state instances, the queue and the reducers make no deps edges
- ludicc --check / ludic build --check lower the program too and write nothing, so the code
  writer's refusals are in it: a bind to a function that is gone, an unknown name (and the checker
  now refuses fn <missing> itself); rejects bind_missing_fn, unknown_name, registry_count_key
- def R count is refused: its constant would be PREFIX_COUNT, the registry's size
- a file's module, package, trust and numbers-float are tables, and from the check on the lookups
  of functions, enums, records, globals and externs are too (tagged enums kept as a list): Maroon
  Lake's check-only build went from about 20 s to 7 s including lowering, its IR from about 2
  minutes to under 10 s; duplicate declarations are found by table, not a pair of loops
- threads.ludic's pool check gives each call a little work, so a busy machine cannot run them all
  on the caller before a worker wakes (it failed one run in three under load)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 23:34:43 +03:00
2fdefa6040 fix(test): expect_eq on strings compares their text and prints both on a failure
It lowered to an i32 compare of two pointers, which the IR refused. Two strings with the same text
are equal now, a null only to a null, and a failure says expect_eq failed (got "camp", want
"lake"). examples/library/testing_strings.ludic (two tests fail on purpose, and the output is
checked); ludic.base's actions_test uses it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:23:38 +03:00
55c1de8734 feat(cli): 0.R2 - ludic deps reports the widest function and ratchets it
widest_function: the most states any function or entry point of the program's own takes, with
which one; --check holds it like the other numbers and --baseline writes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:13:46 +03:00
808c4a6f7a feat(lang): 0.R1 - actions and reducers
action Name { fields } is a typed record; reducer State on Action(s: mut State, a: Action) { ... }
in the module that owns the state takes exactly that state and the action (a second state is
refused); dispatch Action { fields } queues one from anywhere, the queue supplied by the runtime.
The queue is drained at the end of every phase of the frame loop, after every phase of ludic.base's
core_tick_all, and by drain_actions(): in dispatch order, each action's reducers in the order of
their states' names, an action a reducer dispatches queued behind, a queue still growing after 64
rounds stopped with the action named. Examples actions/pack, phases, runaway; rejects for a second
state, a reducer on a non-action and an unknown dispatch; ludic.base's actions_test; LANGUAGE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:13:46 +03:00
3a87d02696 packages: ludic.ui's render3d backend threads Render3dState; render3d's settable vars (r3d_dem_path, post_*, grass_*, wt_wade_*, ...) are Render3dState's fields again, not lets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:11:15 +03:00
ce80e64b24 feat(lang): 0.S - components take states in their header; migrate writes them, never inside a name, never outside the programs given, never into a package's state
- component Name (a: mut A, b: B) { ... }: every getter, default, function and event takes the
  header's states before the instance; a member's call to another passes them on; the glue is
  supplied them; the template never sees them; a read-only one is read-only in every member
- ludic migrate state: a component's members' needs go into its header (added to an existing one,
  mut added where now changed); a field read or a member call inside a component is the compiler's,
  so nothing is written inside a name and no ', )' is left; an entry point that declares states
  already gets the rest after them
- a program's module named like a package gets <Name>AppState; a program's own file its own state;
  a friend module's files go by directory; a package's settable var stays state
- it writes only under the programs and directories given (and runtime/ with --runtime), and
  refuses the whole run naming any other file that would have to change
- a name a package already moved into its state is rewritten through it; a read of the runtime's
  var through the runtime function that answers it (gl_w: gl_width())
- a state's instance supplied by the runtime is not a uses reference
- tests: state/component, rejected/state_component_ro, rendering/ui_render3d, migrate component
  and foreign cases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:11:15 +03:00
c167ecc714 feat(lang): 0.S - a reference out of a read-only state is read-only (named so), a program's type named like the runtime's is refused (PadButton), tests for both, the migrate case covers lets; changeset
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:17:03 +03:00
02448e176c wip(0.S3): the runtime migrated - ludic migrate state --runtime <every program>: 331 vars into 25 states (RtInputState, RtGlState, ...), 2 lets; its states are made before it boots; no module-level var is let through outside --globals
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:02:12 +03:00
7b17b4a1b9 wip(0.S3): outside the runtime, its drawable size, scale, screen and tile size are read and set through its functions; a migration reports such a reference instead of threading the runtime's state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:47:21 +03:00
d70b00f30d wip(0.S3): calls into the runtime get its states supplied; the emitter's own calls to runtime functions reach their thunks; the migration names the runtime's states per file
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:44:50 +03:00
a476ec7976 docs(0.S): the doc fences migrated by ludic migrate state; LANGUAGE.md's state section says what the tool and the checker do now; no module-level var left in the docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:37:29 +03:00
19fcf60599 wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run
ludic migrate state packages <every example program> packages/ludic.lab/example/plate.ludic
  1804 vars into 126 states, 64 into lets; 23498 edits in 460 files

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:31:34 +03:00
5ffe50ed02 wip(0.S2): migrate - a var nothing writes becomes a let; a state is keyed by its module, directory or program, so every program's plan agrees; paths normalized; program states named SceneDemoState / scene_demo_st; the LSP reads state, mut and entry/handler parameters
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:31:34 +03:00
3eda72e8c2 wip(0.S3): packages and examples migrated in one run - ludic migrate state packages <every example program> packages/ludic.lab/example/plate.ludic
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:02:45 +03:00
d490d4f9f1 wip(0.S2): a migration makes a declared read-only state mut where it is now changed; net_sync builds --unsafe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:02:45 +03:00
e7f8ee6b91 wip(0.S3): ui blocks, scene on enter/exit, hooks, machines over a state's field, namespace and engine-system injection; a reference out of a read-only state or a module let is read-only; deps counts writes into another module's state; the rejected examples hold states
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:49:03 +03:00
4919c9c2fc examples: net_sync's raw buffer inside unsafe, now that a query's body is checked
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:43:23 +03:00
253d8d3632 ui: a renderer installs itself through the UiRenderers registry, not a global's initializer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:31:48 +03:00
07505e7ef2 wip(0.S3): the packages migrated by ludic migrate state packages - every package test green
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:02:21 +03:00
1e8b5b0523 wip(0.S1, 0.S2): state records, mut and read-only state parameters, entry injection, module var refused; ludic migrate state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:45:09 +03:00
63a1fa1378 feat(cli): ludic deps --writes warns about writes through a local alias
A local bound straight from another module's global (let t =
thing_cur), or from such a local, is followed within its function, and
a write through its field or element is listed as a warning after the
counted writes. A reference from a function's result is not followed.
Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:22:37 +03:00
8892f51096 feat(cli): ludic build --check / ludicc --check - check without building
The parse, the types and the module rules (export, uses, layers, ports,
registries) run and nothing is emitted or linked: about three seconds
on Maroon Lake. In this mode the checker asks vis_check at each
reference it resolves, with a global's initializer and a registry's
entries seen from the files the emitter would use. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:13:42 +03:00
7ef6c7ec75 feat(lang): bind a port member that takes nothing to a variable
bind Purse { money: g_money } writes the getter bind_Purse_money in the
bind's own file, so a one-line wrapper per member is not needed; a
member that takes something is refused a variable. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:02:01 +03:00
7b8c134c21 feat(lang): layers - module flow in layer app uses base, items
The modules of one layer use each other freely and may go round;
anything outside the layer is held to the module's uses, and a layered
module with no uses reaches nothing outside it. The cycle check walks
the graph with each layer as one node, so a cycle leaving a layer is
refused. ludic deps shows the layers and counts the largest cycle
without their own edges. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:54:15 +03:00
e3219f17dd feat(test): ludic test -j N runs tests side by side
Every file is compiled, linked and every test block run as up to N jobs
at once (xargs -P; default the CPU count), each test with a TMPDIR of
its own, and the report is read back in file order. Windows keeps the
sequential path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:38:09 +03:00
e738741521 feat(cli): ludic deps - the module graph as the compiler resolved it
With LUDIC_DEPS=<file> the compiler records every reference the
visibility pass resolves (from module, to module) and every assignment
to another module's global. ludic deps prints the modules,
dependencies, largest cycle, cross writes and globals written from
outside, with --graph, --dot, --writes, --uses MOD, --check FILE and
--baseline FILE. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:16:34 +03:00
69352babfd feat(lang): a module's private records and events are its own
A property or event a module does not export no longer collides with
another module's of the same spelling: each private one is renamed for
its module, with the types, new, emit and @On written in that module.
Exported ones stay one namespace; two events of one spelling are now
refused. Generic records, entity components and component or view
records stay global. Reseed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:02:48 +03:00
029a1ebba2 feat(packages): ludic.npc - the other people in a place: kinds, spots, acts, names and lines as open registries (a kind's routine is steps by the hour - a spot, an act, how long - with its own start and plan as function values, readable from .lres), a census on the half hour by weight (after dark only who stays out) with an extra the place asks for, walking round what is in the way and never into the water or up a scramble, no act where the place says nobody stops, facing a player within notice and back to the errand, leaving only out of every player's sight, unique names, posts that never walk (a vendor), lines by prio and chance with the story's words through a port and choices, a guest's copy of a host's people; greeted, talked and arrived as facts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 09:59:07 +03:00
73a376ac6c feat(packages): ludic.minimap - a map's logic: the explored fog over a grid of cells round an origin (revealed within a reach, a version for rebaking, explored as a share of the land over a core square, the grid as text for an older save), the player's marks (a cap, a symbol, a colour, the day, a name; the open one and the followed one, rubbing out, the last symbol reused, the nearest for a compass) and the view (world to window and back, zoom in steps and about a point, a drag, pick and place, the camp, you, a focus that never pulls back out, a scale bar, bounds); the world through a port; seen and marks in its own save section by key
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 09:45:59 +03:00