- Rust 84.8%
- WGSL 14.8%
- Dockerfile 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| assets/shaders | ||
| dist | ||
| shots | ||
| src | ||
| .dockerignore | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| class.json | ||
| docker-compose.yml | ||
| Dockerfile | ||
| Dockerfile.ci | ||
| keybinds.json | ||
| README.md | ||
spc — space manipulation sandbox
A PvP-sandbox prototype where space itself is the weapon: spells compress, expand and twist the geometry of the arena. Travel by compressing 16 m into a perceived ~1.3 m, shear enemies with a rotating chunk of space, crush them in a compression ball — all of it one mathematical system, rendered by a geodesic raymarcher.
Run
cargo run # singleplayer sandbox
cargo run -- --host [port] # host a game (plays + serves, default port 42800)
cargo run -- --join host[:port] # join a host (hostname or IP)
# dedicated headless server (no window, no GPU, no render/UI code compiled in)
cargo run --no-default-features --bin spc-server -- [port]
Multiplayer is server-authoritative (bevy_replicon): spells, portals, projectiles,
dummies and all damage run on the host; each client is authoritative over its own
pose for now. Spells replicate as their tiny parametric structs and every peer
rebuilds the metric field locally, so the raymarcher needs no network awareness at
all. Other players are blue capsules; dying teleports you to a fresh spawn point.
SPC_NETPROBE=1 logs the replicated world once a second on any peer.
| Input | Action |
|---|---|
| WASD + mouse | move / look (LMB grabs cursor, Esc releases) |
| Shift / Space / LCtrl | sprint / fly up / fly down |
| hold Q | quick-access wheel — release on a slot to adopt that loadout (shape + effect + size) |
1–6 |
adopt the matching wheel slot directly, without opening the wheel |
| Tab | settings: pick a class, edit the wheel kit and its slot keys, rebind |
| wheel | resize the current effect (fan/rift: wedge angle; else radius / half-extent / aperture) |
| R | cycle the current effect's placement mode (the gestures it supports) |
| RMB (aim + click) | cast a targeted (crush/expand/twist/slow/haste) or aligned (compress/portal) effect at the crosshair |
| RMB press–drag–release | draw an A→B line: a surgery's cut line, or a volume/portal you span freehand |
| RMB hold–release | charge: hold to grow the size, release to drop it at the aim |
| LMB | alt-fire: cancel a draw / charge in flight, or slam an open fan shut |
| F2 | screenshot |
A spell offers one or more placement gestures; R cycles between them and the HUD shows the active one plus the alternatives. It's no longer one universal drag:
- Target (crush / expand / twist / slow / haste): aim and right-click; a point volume
drops at the crosshair, the wheel sets its size. Sphere: aim = center, size = radius.
Cylinder/cuboid: aim = base, the column rises
2·sizetall. - Environment (compress corridor, portals): aim and right-click; the placement aligns to your sightline — a corridor down the view, a portal from beside you to the aim.
- Draw (surgeries always; volumes/portals optionally): press an anchor near you (clamped to ≤ 10 m so casts originate near their caster), drag the line out to B, release to cast. For a surgery the drawn line is the cut; for a volume/portal you pick both endpoints yourself — the freehand, unguided placement.
- Charge (point volumes): hold the button to ramp the size up, release to drop the now-larger volume at the aim; the ghost grows as you hold.
A live HUD ghost previews the placement, price and parameters of the pending cast; for metric volumes the deformation front sweeps from A to B over the cast time — the warp visibly fills the volume, and the moving front is the damage transient you dodge.
Portals: aim and click — the entrance disk drops on the ground just ahead of you and the exit at the aim; the wheel sets the aperture. Both disks stand upright facing you; they are two-way, carry players / projectiles / dummies through with a pure-yaw view transport, and render the through-view natively (rays teleport mid-march). The golden rim is a cone singularity — standing on it hurts, and a portal closing on a body straddling it is a guillotine.
Seams: fault-line surgery — the topologist's cut. Drag draws a crack line along the ground (a crawling white-hot tip; harmless telegraph, and a slow-time zone dropped on the tip stalls it). The moment the line completes, the cut SNAPS: the two halves of the arena shear several metres along the fault — everything seen across the line slides sideways, and everything crossing it is displaced the same way (light, projectiles and bodies all route through the same identification, so what you see is what you get). Anyone straddling the line at the snap is torn apart; a settled fault is safe but splits the map — chases break, retreats land somewhere else. The cut's end edges are branch singularities (through ≠ around): standing at a crack tip of an open fault hurts. The fault heals shut when it decays (a second straddle-danger window). No metric, no lensing — pure cut-and-reglue, nearly free to render.
Fans: opened scissors — the seam's rotational twin, localized to a sector. Drag from a pivot outward to draw the closed scissors; the wheel sets how far they open. The cut crawls out from the pivot (harmless telegraph), then SNAPS: a second blade swings out of the drawn one by the wedge angle and the two blades are glued to each other — the pie slice between them is excised from space. From outside, the world closes seamlessly from one blade onto the other: walk or shoot "through" the fan and you cross exactly one blade, emerging from the other rotated by the wedge angle (view included — the deficit even double-images things behind the pivot, cosmic-string style). Anything caught inside the sector when it opens is pocketed — sealed behind the glued lips, alive, reachable only around the blade tip (r > len). Once open the scissor hangs as live area-denial until you slam it with LMB (the player-triggered guillotine): the jaws collapse on an extremely aggressive ease-in and cross past shut, so the blades snap through anything between them — a forgotten fan auto-slams after a few seconds. Because a rotation displaces a crossing by ≈ radius × angle, the blades are nearly still at the pivot and violent at the tip: a body straddling either blade — at the opening snap, or in the guillotine's crossing arc — is sheared, harder the farther out it stands, and the slam bites hardest of all. The draw is swept at constant tearing power (equal area per second), so a small fan completes fast while a big one crawls the further it reaches: quick cheap flicks vs slow deadly sweeps. The pivot axis is the disclination line (a cone singularity — standing on it hurts).
Rifts: the shear blade — the fan's single-cut sibling and the rawer surgery. One blade, same drag/wheel controls, but its two coincident lips are re-identified with the yaw directly (a wedge disclination): there is no excised sector — instead the whole far side of the cut renders and is rotated about the pivot. Same snap-straddle shear, same r·angle tip violence, same cone-singularity pivot; where the fan bites a local pocket out of the board, the rift swings half the arena.
Time zones (slow / haste): a lapse field τ(x) that multiplies the local rate of time. A slow zone (τ = 0.3) is a stasis trap — movement crawls, projectiles visibly decelerate mid-flight, damage accrues in slow motion; a haste zone (τ = 2.2) speeds all of that up. Time is orthogonal to space: it leaves the metric untouched (no lensing), so it reads purely as a tint — frost blue-white for slow, hot magenta for haste — and the geodesic marcher skips it entirely. Drop a slow zone over a compression corridor to throttle it, or over a twist to defuse the shear transient.
Scripted demo/verification: SPC_AUTOTEST=corridor|twist|crush|portal|portalwalk|mobius|dehn|seam|fan|rift|timeslow|timehaste cargo run
casts the spell, saves before/after screenshots (SPC_SHOT_DIR, default shots/; the
seam, fan and rift modes add a mid-draw shot), prints dummy HP, exits.
SPC_AUTOTEST=netcast casts through the full network round-trip (request → server
validation → replication back) — run it on a --join instance against a live host to
smoke-test the wire.
How it works
The world lives in one fixed coordinate chart (what the ECS stores). All spatial manipulation is a time-varying Riemannian metric g(x,t) on that chart:
- a spell is an effect × volume: the volume's signed distance drives a plateau weight
(exactly 1 in the interior, C²-falling to 0 in a thin shell at the surface), gated by
the sweeping front; the field is the convex blend g = (1−Σŵ)I + Σ ŵ_i g_i — always SPD,
smooth everywhere, exactly Euclidean outside spell regions (
src/space/spells.rs). Because the plateau core has constant metric, there is zero curvature inside: a compression cuboid is see-through (straight sightlines, axially magnified), and the renderer strides straight across cores at full sphere-trace steps (the constant-metric fast path). Twist is the exception by design: it uses a thick profile (shell = the volume's inradius) so the angle varies smoothly through the interior — a soft vortex with distributed shear and interior damage — with analytic ∇θ for sphere/cylinder; - movement converts proper (perceived) speed to chart speed via 1/‖dir‖_g, so compressed corridors are traversed 12× faster with zero special-case code;
- projectiles and light follow geodesics: the renderer
(
assets/shaders/raymarch.wgsl) sphere-traces where space is flat and integrates the geodesic ODE (RK2, finite-difference Christoffels) inside spell supports — the compression corridor genuinely lenses: distant targets appear near; - animating spells drag matter via per-spell flow fields (the "space wind");
- damage is strain: bodies sample g at six points; the strain rate ½ġ decomposes into
volumetric (crush/tear) and deviatoric (shear) parts plus a tidal gradient term
(
src/space/strain.rs). Static warped zones are safe; the cast/collapse transients and zone boundaries hurt; - topology change is surgery, not deformation (
src/space/portal.rs,src/space/seam.rs): no continuous family of metrics changes topology, so wormholes are two disks identified by an isometry σ whose aperture animates from 0 — the handle opens from measure zero, so there is never a torn sheet, only a growing rim (where the rim damage lives). A seam is the same surgery with the two glued faces coincident: a cut rectangle re-identified with a slide along itself (a dislocation in space, slip animated from 0), so crossing rays/bodies shear sideways and the arena visibly rips in two along the drawn line. A rift is the rotational version of that same surgery — a cut half-plane re-identified with a yaw about its pivot (a wedge disclination, angle animated from 0) instead of a slide, so a crossing at radius r is displaced ≈ r·angle: the deformation, and the damage, grow out the length of the blade. A fan takes the same yaw σ but glues two blades — the drawn one and its image under σ — to each other, which excises the sector between them and seals its contents in a cone pocket (src/space/fan.rs; the true "rotor pocket", contents rendering rotated with net-zero pass-through, is provably not an isometric surgery — displayed angles around the pivot must sum to 2π — so excision is the honest scissor); - time is a scalar lapse field τ(x) (
SpaceField::lapse, theDilateeffect) — a 3+1 split alongside the spatial metric, deliberately not a Lorentzian metric (no causality). τ multiplies the local rate of time for movement, projectiles and damage, blended convexly in log-space (so it is 1 outside, composes bounded, stays positive). It contributes nothing to g and is skipped by the geodesic marcher, so time zones don't lens — they show only as a tint. Space and time are cleanly orthogonal.
Keep src/space/spells.rs and assets/shaders/raymarch.wgsl in sync — the CPU and GPU
evaluate the same metric.
Tests
cargo test
covers: SPD-ness under overlap, compact support, corridor proper-length shortening, movement speedup, geodesic sanity, lifecycle continuity, flow direction, strain damage (transient vs static vs boundary).